Commit aa1f1eb680e4328c2cf619a770f1e90f74c41987

Authored by Dmitriy Zaporozhets
2 parents 097e6053 d839f6c5

Merge pull request #2746 from gitlabhq/features/teams

New feature: Teams
Showing 118 changed files with 2862 additions and 322 deletions   Show diff stats

Too many changes.

To preserve performance only 100 of 118 files displayed.

app/assets/javascripts/dashboard.js.coffee
@@ -4,11 +4,11 @@ window.dashboardPage = -> @@ -4,11 +4,11 @@ window.dashboardPage = ->
4 event.preventDefault() 4 event.preventDefault()
5 toggleFilter $(this) 5 toggleFilter $(this)
6 reloadActivities() 6 reloadActivities()
7 - 7 +
8 reloadActivities = -> 8 reloadActivities = ->
9 $(".content_list").html '' 9 $(".content_list").html ''
10 Pager.init 20, true 10 Pager.init 20, true
11 - 11 +
12 toggleFilter = (sender) -> 12 toggleFilter = (sender) ->
13 sender.parent().toggleClass "inactive" 13 sender.parent().toggleClass "inactive"
14 event_filters = $.cookie("event_filter") 14 event_filters = $.cookie("event_filter")
@@ -17,11 +17,11 @@ toggleFilter = (sender) -> @@ -17,11 +17,11 @@ toggleFilter = (sender) ->
17 event_filters = event_filters.split(",") 17 event_filters = event_filters.split(",")
18 else 18 else
19 event_filters = new Array() 19 event_filters = new Array()
20 - 20 +
21 index = event_filters.indexOf(filter) 21 index = event_filters.indexOf(filter)
22 if index is -1 22 if index is -1
23 event_filters.push filter 23 event_filters.push filter
24 else 24 else
25 event_filters.splice index, 1 25 event_filters.splice index, 1
26 - 26 +
27 $.cookie "event_filter", event_filters.join(",") 27 $.cookie "event_filter", event_filters.join(",")
app/assets/javascripts/merge_requests.js.coffee
1 # 1 #
2 # * Filter merge requests 2 # * Filter merge requests
3 -# 3 +#
4 @merge_requestsPage = -> 4 @merge_requestsPage = ->
5 $('#assignee_id').chosen() 5 $('#assignee_id').chosen()
6 $('#milestone_id').chosen() 6 $('#milestone_id').chosen()
@@ -8,16 +8,16 @@ @@ -8,16 +8,16 @@
8 $(this).closest('form').submit() 8 $(this).closest('form').submit()
9 9
10 class MergeRequest 10 class MergeRequest
11 - 11 +
12 constructor: (@opts) -> 12 constructor: (@opts) ->
13 this.$el = $('.merge-request') 13 this.$el = $('.merge-request')
14 @diffs_loaded = false 14 @diffs_loaded = false
15 @commits_loaded = false 15 @commits_loaded = false
16 - 16 +
17 this.activateTab(@opts.action) 17 this.activateTab(@opts.action)
18 - 18 +
19 this.bindEvents() 19 this.bindEvents()
20 - 20 +
21 this.initMergeWidget() 21 this.initMergeWidget()
22 this.$('.show-all-commits').on 'click', => 22 this.$('.show-all-commits').on 'click', =>
23 this.showAllCommits() 23 this.showAllCommits()
@@ -28,7 +28,7 @@ class MergeRequest @@ -28,7 +28,7 @@ class MergeRequest
28 28
29 initMergeWidget: -> 29 initMergeWidget: ->
30 this.showState( @opts.current_state ) 30 this.showState( @opts.current_state )
31 - 31 +
32 if this.$('.automerge_widget').length and @opts.check_enable 32 if this.$('.automerge_widget').length and @opts.check_enable
33 $.get @opts.url_to_automerge_check, (data) => 33 $.get @opts.url_to_automerge_check, (data) =>
34 this.showState( data.state ) 34 this.showState( data.state )
@@ -42,12 +42,12 @@ class MergeRequest @@ -42,12 +42,12 @@ class MergeRequest
42 bindEvents: -> 42 bindEvents: ->
43 this.$('.nav-tabs').on 'click', 'a', (event) => 43 this.$('.nav-tabs').on 'click', 'a', (event) =>
44 a = $(event.currentTarget) 44 a = $(event.currentTarget)
45 - 45 +
46 href = a.attr('href') 46 href = a.attr('href')
47 History.replaceState {path: href}, document.title, href 47 History.replaceState {path: href}, document.title, href
48 - 48 +
49 event.preventDefault() 49 event.preventDefault()
50 - 50 +
51 this.$('.nav-tabs').on 'click', 'li', (event) => 51 this.$('.nav-tabs').on 'click', 'li', (event) =>
52 this.activateTab($(event.currentTarget).data('action')) 52 this.activateTab($(event.currentTarget).data('action'))
53 53
app/assets/stylesheets/sections/projects.scss
@@ -7,6 +7,7 @@ @@ -7,6 +7,7 @@
7 @extend .right; 7 @extend .right;
8 8
9 .groups_box, 9 .groups_box,
  10 + .teams_box,
10 .projects_box { 11 .projects_box {
11 > .title { 12 > .title {
12 padding: 2px 15px; 13 padding: 2px 15px;
app/controllers/admin/application_controller.rb 0 → 100644
@@ -0,0 +1,11 @@ @@ -0,0 +1,11 @@
  1 +# Provides a base class for Admin controllers to subclass
  2 +#
  3 +# Automatically sets the layout and ensures an administrator is logged in
  4 +class Admin::ApplicationController < ApplicationController
  5 + layout 'admin'
  6 + before_filter :authenticate_admin!
  7 +
  8 + def authenticate_admin!
  9 + return render_404 unless current_user.is_admin?
  10 + end
  11 +end
app/controllers/admin/dashboard_controller.rb
1 -class Admin::DashboardController < AdminController 1 +class Admin::DashboardController < Admin::ApplicationController
2 def index 2 def index
3 @projects = Project.order("created_at DESC").limit(10) 3 @projects = Project.order("created_at DESC").limit(10)
4 @users = User.order("created_at DESC").limit(10) 4 @users = User.order("created_at DESC").limit(10)
app/controllers/admin/groups_controller.rb
1 -class Admin::GroupsController < AdminController 1 +class Admin::GroupsController < Admin::ApplicationController
2 before_filter :group, only: [:edit, :show, :update, :destroy, :project_update, :project_teams_update] 2 before_filter :group, only: [:edit, :show, :update, :destroy, :project_update, :project_teams_update]
3 3
4 def index 4 def index
app/controllers/admin/hooks_controller.rb
1 -class Admin::HooksController < AdminController 1 +class Admin::HooksController < Admin::ApplicationController
2 def index 2 def index
3 @hooks = SystemHook.all 3 @hooks = SystemHook.all
4 @hook = SystemHook.new 4 @hook = SystemHook.new
app/controllers/admin/logs_controller.rb
1 -class Admin::LogsController < AdminController 1 +class Admin::LogsController < Admin::ApplicationController
2 end 2 end
app/controllers/admin/projects/application_controller.rb 0 → 100644
@@ -0,0 +1,11 @@ @@ -0,0 +1,11 @@
  1 +# Provides a base class for Admin controllers to subclass
  2 +#
  3 +# Automatically sets the layout and ensures an administrator is logged in
  4 +class Admin::Projects::ApplicationController < Admin::ApplicationController
  5 +
  6 + protected
  7 +
  8 + def project
  9 + @project ||= Project.find_by_path(params[:project_id])
  10 + end
  11 +end
app/controllers/admin/projects/members_controller.rb 0 → 100644
@@ -0,0 +1,32 @@ @@ -0,0 +1,32 @@
  1 +class Admin::Projects::MembersController < Admin::Projects::ApplicationController
  2 + def edit
  3 + @member = team_member
  4 + @project = project
  5 + @team_member_relation = team_member_relation
  6 + end
  7 +
  8 + def update
  9 + if team_member_relation.update_attributes(params[:team_member])
  10 + redirect_to [:admin, project], notice: 'Project Access was successfully updated.'
  11 + else
  12 + render action: "edit"
  13 + end
  14 + end
  15 +
  16 + def destroy
  17 + team_member_relation.destroy
  18 +
  19 + redirect_to :back
  20 + end
  21 +
  22 + private
  23 +
  24 + def team_member
  25 + @member ||= project.users.find(params[:id])
  26 + end
  27 +
  28 + def team_member_relation
  29 + team_member.users_projects.find_by_project_id(project)
  30 + end
  31 +
  32 +end
app/controllers/admin/projects_controller.rb
1 -class Admin::ProjectsController < AdminController 1 +class Admin::ProjectsController < Admin::ApplicationController
2 before_filter :project, only: [:edit, :show, :update, :destroy, :team_update] 2 before_filter :project, only: [:edit, :show, :update, :destroy, :team_update]
3 3
4 def index 4 def index
@@ -29,7 +29,9 @@ class Admin::ProjectsController &lt; AdminController @@ -29,7 +29,9 @@ class Admin::ProjectsController &lt; AdminController
29 end 29 end
30 30
31 def update 31 def update
32 - status = Projects::UpdateContext.new(project, current_user, params).execute(:admin) 32 + project.creator = current_user unless project.creator
  33 +
  34 + status = ::Projects::UpdateContext.new(project, current_user, params).execute(:admin)
33 35
34 if status 36 if status
35 redirect_to [:admin, @project], notice: 'Project was successfully updated.' 37 redirect_to [:admin, @project], notice: 'Project was successfully updated.'
app/controllers/admin/resque_controller.rb
1 -class Admin::ResqueController < AdminController 1 +class Admin::ResqueController < Admin::ApplicationController
2 def show 2 def show
3 end 3 end
4 end 4 end
app/controllers/admin/team_members_controller.rb
@@ -1,22 +0,0 @@ @@ -1,22 +0,0 @@
1 -class Admin::TeamMembersController < AdminController  
2 - def edit  
3 - @admin_team_member = UsersProject.find(params[:id])  
4 - end  
5 -  
6 - def update  
7 - @admin_team_member = UsersProject.find(params[:id])  
8 -  
9 - if @admin_team_member.update_attributes(params[:team_member])  
10 - redirect_to [:admin, @admin_team_member.project], notice: 'Project Access was successfully updated.'  
11 - else  
12 - render action: "edit"  
13 - end  
14 - end  
15 -  
16 - def destroy  
17 - @admin_team_member = UsersProject.find(params[:id])  
18 - @admin_team_member.destroy  
19 -  
20 - redirect_to :back  
21 - end  
22 -end  
app/controllers/admin/teams/application_controller.rb 0 → 100644
@@ -0,0 +1,11 @@ @@ -0,0 +1,11 @@
  1 +# Provides a base class for Admin controllers to subclass
  2 +#
  3 +# Automatically sets the layout and ensures an administrator is logged in
  4 +class Admin::Teams::ApplicationController < Admin::ApplicationController
  5 +
  6 + private
  7 +
  8 + def user_team
  9 + @team = UserTeam.find_by_path(params[:team_id])
  10 + end
  11 +end
app/controllers/admin/teams/members_controller.rb 0 → 100644
@@ -0,0 +1,41 @@ @@ -0,0 +1,41 @@
  1 +class Admin::Teams::MembersController < Admin::Teams::ApplicationController
  2 + def new
  3 + @users = User.potential_team_members(user_team)
  4 + @users = UserDecorator.decorate @users
  5 + end
  6 +
  7 + def create
  8 + unless params[:user_ids].blank?
  9 + user_ids = params[:user_ids]
  10 + access = params[:default_project_access]
  11 + is_admin = params[:group_admin]
  12 + user_team.add_members(user_ids, access, is_admin)
  13 + end
  14 +
  15 + redirect_to admin_team_path(user_team), notice: 'Members was successfully added into Team of users.'
  16 + end
  17 +
  18 + def edit
  19 + team_member
  20 + end
  21 +
  22 + def update
  23 + options = {default_projects_access: params[:default_project_access], group_admin: params[:group_admin]}
  24 + if user_team.update_membership(team_member, options)
  25 + redirect_to admin_team_path(user_team), notice: "Membership for #{team_member.name} was successfully updated in Team of users."
  26 + else
  27 + render :edit
  28 + end
  29 + end
  30 +
  31 + def destroy
  32 + user_team.remove_member(team_member)
  33 + redirect_to admin_team_path(user_team), notice: "Member #{team_member.name} was successfully removed from Team of users."
  34 + end
  35 +
  36 + protected
  37 +
  38 + def team_member
  39 + @member ||= user_team.members.find(params[:id])
  40 + end
  41 +end
app/controllers/admin/teams/projects_controller.rb 0 → 100644
@@ -0,0 +1,41 @@ @@ -0,0 +1,41 @@
  1 +class Admin::Teams::ProjectsController < Admin::Teams::ApplicationController
  2 + def new
  3 + @projects = Project.scoped
  4 + @projects = @projects.without_team(user_team) if user_team.projects.any?
  5 + #@projects.reject!(&:empty_repo?)
  6 + end
  7 +
  8 + def create
  9 + unless params[:project_ids].blank?
  10 + project_ids = params[:project_ids]
  11 + access = params[:greatest_project_access]
  12 + user_team.assign_to_projects(project_ids, access)
  13 + end
  14 +
  15 + redirect_to admin_team_path(user_team), notice: 'Team of users was successfully assgned to projects.'
  16 + end
  17 +
  18 + def edit
  19 + team_project
  20 + end
  21 +
  22 + def update
  23 + if user_team.update_project_access(team_project, params[:greatest_project_access])
  24 + redirect_to admin_team_path(user_team), notice: 'Access was successfully updated.'
  25 + else
  26 + render :edit
  27 + end
  28 + end
  29 +
  30 + def destroy
  31 + user_team.resign_from_project(team_project)
  32 + redirect_to admin_team_path(user_team), notice: 'Team of users was successfully reassigned from project.'
  33 + end
  34 +
  35 + protected
  36 +
  37 + def team_project
  38 + @project ||= user_team.projects.find_with_namespace(params[:id])
  39 + end
  40 +
  41 +end
app/controllers/admin/teams_controller.rb 0 → 100644
@@ -0,0 +1,59 @@ @@ -0,0 +1,59 @@
  1 +class Admin::TeamsController < Admin::ApplicationController
  2 + def index
  3 + @teams = UserTeam.order('name ASC')
  4 + @teams = @teams.search(params[:name]) if params[:name].present?
  5 + @teams = @teams.page(params[:page]).per(20)
  6 + end
  7 +
  8 + def show
  9 + user_team
  10 + end
  11 +
  12 + def new
  13 + @team = UserTeam.new
  14 + end
  15 +
  16 + def edit
  17 + user_team
  18 + end
  19 +
  20 + def create
  21 + @team = UserTeam.new(params[:user_team])
  22 + @team.path = @team.name.dup.parameterize if @team.name
  23 + @team.owner = current_user
  24 +
  25 + if @team.save
  26 + redirect_to admin_team_path(@team), notice: 'Team of users was successfully created.'
  27 + else
  28 + render action: "new"
  29 + end
  30 + end
  31 +
  32 + def update
  33 + user_team_params = params[:user_team].dup
  34 + owner_id = user_team_params.delete(:owner_id)
  35 +
  36 + if owner_id
  37 + user_team.owner = User.find(owner_id)
  38 + end
  39 +
  40 + if user_team.update_attributes(user_team_params)
  41 + redirect_to admin_team_path(user_team), notice: 'Team of users was successfully updated.'
  42 + else
  43 + render action: "edit"
  44 + end
  45 + end
  46 +
  47 + def destroy
  48 + user_team.destroy
  49 +
  50 + redirect_to admin_user_teams_path, notice: 'Team of users was successfully deleted.'
  51 + end
  52 +
  53 + protected
  54 +
  55 + def user_team
  56 + @team ||= UserTeam.find_by_path(params[:id])
  57 + end
  58 +
  59 +end
app/controllers/admin/users_controller.rb
1 -class Admin::UsersController < AdminController 1 +class Admin::UsersController < Admin::ApplicationController
2 def index 2 def index
3 @admin_users = User.scoped 3 @admin_users = User.scoped
4 @admin_users = @admin_users.filter(params[:filter]) 4 @admin_users = @admin_users.filter(params[:filter])
app/controllers/admin_controller.rb
@@ -1,11 +0,0 @@ @@ -1,11 +0,0 @@
1 -# Provides a base class for Admin controllers to subclass  
2 -#  
3 -# Automatically sets the layout and ensures an administrator is logged in  
4 -class AdminController < ApplicationController  
5 - layout 'admin'  
6 - before_filter :authenticate_admin!  
7 -  
8 - def authenticate_admin!  
9 - return render_404 unless current_user.is_admin?  
10 - end  
11 -end  
app/controllers/application_controller.rb
@@ -94,6 +94,14 @@ class ApplicationController &lt; ActionController::Base @@ -94,6 +94,14 @@ class ApplicationController &lt; ActionController::Base
94 return access_denied! unless can?(current_user, :download_code, project) 94 return access_denied! unless can?(current_user, :download_code, project)
95 end 95 end
96 96
  97 + def authorize_manage_user_team!
  98 + return access_denied! unless user_team.present? && can?(current_user, :manage_user_team, user_team)
  99 + end
  100 +
  101 + def authorize_admin_user_team!
  102 + return access_denied! unless user_team.present? && can?(current_user, :admin_user_team, user_team)
  103 + end
  104 +
97 def access_denied! 105 def access_denied!
98 render "errors/access_denied", layout: "errors", status: 404 106 render "errors/access_denied", layout: "errors", status: 404
99 end 107 end
@@ -135,4 +143,5 @@ class ApplicationController &lt; ActionController::Base @@ -135,4 +143,5 @@ class ApplicationController &lt; ActionController::Base
135 def dev_tools 143 def dev_tools
136 Rack::MiniProfiler.authorize_request 144 Rack::MiniProfiler.authorize_request
137 end 145 end
  146 +
138 end 147 end
app/controllers/dashboard_controller.rb
@@ -18,6 +18,8 @@ class DashboardController &lt; ApplicationController @@ -18,6 +18,8 @@ class DashboardController &lt; ApplicationController
18 @projects 18 @projects
19 end 19 end
20 20
  21 + @teams = (UserTeam.with_member(current_user) + UserTeam.created_by(current_user)).uniq
  22 +
21 @projects = @projects.page(params[:page]).per(30) 23 @projects = @projects.page(params[:page]).per(30)
22 24
23 @events = Event.in_projects(current_user.authorized_projects.pluck(:id)) 25 @events = Event.in_projects(current_user.authorized_projects.pluck(:id))
app/controllers/projects/application_controller.rb 0 → 100644
@@ -0,0 +1,11 @@ @@ -0,0 +1,11 @@
  1 +class Projects::ApplicationController < ApplicationController
  2 +
  3 + before_filter :authorize_admin_team_member!
  4 +
  5 + protected
  6 +
  7 + def user_team
  8 + @team ||= UserTeam.find_by_path(params[:id])
  9 + end
  10 +
  11 +end
app/controllers/projects/teams_controller.rb 0 → 100644
@@ -0,0 +1,27 @@ @@ -0,0 +1,27 @@
  1 +class Projects::TeamsController < Projects::ApplicationController
  2 +
  3 + def available
  4 + @teams = current_user.is_admin? ? UserTeam.scoped : current_user.user_teams
  5 + @teams = @teams.without_project(project)
  6 + unless @teams.any?
  7 + redirect_to project_team_index_path(project), notice: "No avaliable teams for assigment."
  8 + end
  9 + end
  10 +
  11 + def assign
  12 + unless params[:team_id].blank?
  13 + team = UserTeam.find(params[:team_id])
  14 + access = params[:greatest_project_access]
  15 + team.assign_to_project(project, access)
  16 + end
  17 + redirect_to project_team_index_path(project)
  18 + end
  19 +
  20 + def resign
  21 + team = project.user_teams.find_by_path(params[:id])
  22 + team.resign_from_project(project)
  23 +
  24 + redirect_to project_team_index_path(project)
  25 + end
  26 +
  27 +end
app/controllers/projects_controller.rb
@@ -19,7 +19,7 @@ class ProjectsController &lt; ProjectResourceController @@ -19,7 +19,7 @@ class ProjectsController &lt; ProjectResourceController
19 end 19 end
20 20
21 def create 21 def create
22 - @project = Projects::CreateContext.new(current_user, params[:project]).execute 22 + @project = ::Projects::CreateContext.new(current_user, params[:project]).execute
23 23
24 respond_to do |format| 24 respond_to do |format|
25 flash[:notice] = 'Project was successfully created.' if @project.saved? 25 flash[:notice] = 'Project was successfully created.' if @project.saved?
@@ -35,7 +35,7 @@ class ProjectsController &lt; ProjectResourceController @@ -35,7 +35,7 @@ class ProjectsController &lt; ProjectResourceController
35 end 35 end
36 36
37 def update 37 def update
38 - status = Projects::UpdateContext.new(project, current_user, params).execute 38 + status = ::Projects::UpdateContext.new(project, current_user, params).execute
39 39
40 respond_to do |format| 40 respond_to do |format|
41 if status 41 if status
app/controllers/team_members_controller.rb
@@ -4,15 +4,16 @@ class TeamMembersController &lt; ProjectResourceController @@ -4,15 +4,16 @@ class TeamMembersController &lt; ProjectResourceController
4 before_filter :authorize_admin_project!, except: [:index, :show] 4 before_filter :authorize_admin_project!, except: [:index, :show]
5 5
6 def index 6 def index
  7 + @teams = UserTeam.scoped
7 end 8 end
8 9
9 def show 10 def show
10 - @team_member = project.users_projects.find(params[:id])  
11 - @events = @team_member.user.recent_events.where(:project_id => @project.id).limit(7) 11 + @user_project_relation = project.users_projects.find_by_user_id(member)
  12 + @events = member.recent_events.in_projects(project).limit(7)
12 end 13 end
13 14
14 def new 15 def new
15 - @team_member = project.users_projects.new 16 + @user_project_relation = project.users_projects.new
16 end 17 end
17 18
18 def create 19 def create
@@ -28,18 +29,18 @@ class TeamMembersController &lt; ProjectResourceController @@ -28,18 +29,18 @@ class TeamMembersController &lt; ProjectResourceController
28 end 29 end
29 30
30 def update 31 def update
31 - @team_member = project.users_projects.find(params[:id])  
32 - @team_member.update_attributes(params[:team_member]) 32 + @user_project_relation = project.users_projects.find_by_user_id(member)
  33 + @user_project_relation.update_attributes(params[:team_member])
33 34
34 - unless @team_member.valid? 35 + unless @user_project_relation.valid?
35 flash[:alert] = "User should have at least one role" 36 flash[:alert] = "User should have at least one role"
36 end 37 end
37 redirect_to project_team_index_path(@project) 38 redirect_to project_team_index_path(@project)
38 end 39 end
39 40
40 def destroy 41 def destroy
41 - @team_member = project.users_projects.find(params[:id])  
42 - @team_member.destroy 42 + @user_project_relation = project.users_projects.find_by_user_id(params[:id])
  43 + @user_project_relation.destroy
43 44
44 respond_to do |format| 45 respond_to do |format|
45 format.html { redirect_to project_team_index_path(@project) } 46 format.html { redirect_to project_team_index_path(@project) }
@@ -54,4 +55,10 @@ class TeamMembersController &lt; ProjectResourceController @@ -54,4 +55,10 @@ class TeamMembersController &lt; ProjectResourceController
54 55
55 redirect_to project_team_members_path(project), notice: notice 56 redirect_to project_team_members_path(project), notice: notice
56 end 57 end
  58 +
  59 + protected
  60 +
  61 + def member
  62 + @member ||= User.find(params[:id])
  63 + end
57 end 64 end
app/controllers/teams/application_controller.rb 0 → 100644
@@ -0,0 +1,13 @@ @@ -0,0 +1,13 @@
  1 +class Teams::ApplicationController < ApplicationController
  2 +
  3 + layout 'user_team'
  4 +
  5 + before_filter :authorize_manage_user_team!
  6 +
  7 + protected
  8 +
  9 + def user_team
  10 + @team ||= UserTeam.find_by_path(params[:team_id])
  11 + end
  12 +
  13 +end
app/controllers/teams/members_controller.rb 0 → 100644
@@ -0,0 +1,49 @@ @@ -0,0 +1,49 @@
  1 +class Teams::MembersController < Teams::ApplicationController
  2 +
  3 + skip_before_filter :authorize_manage_user_team!, only: [:index]
  4 +
  5 + def index
  6 + @members = user_team.members
  7 + end
  8 +
  9 + def new
  10 + @users = User.potential_team_members(user_team)
  11 + @users = UserDecorator.decorate @users
  12 + end
  13 +
  14 + def create
  15 + unless params[:user_ids].blank?
  16 + user_ids = params[:user_ids]
  17 + access = params[:default_project_access]
  18 + is_admin = params[:group_admin]
  19 + user_team.add_members(user_ids, access, is_admin)
  20 + end
  21 +
  22 + redirect_to team_members_path(user_team), notice: 'Members was successfully added into Team of users.'
  23 + end
  24 +
  25 + def edit
  26 + team_member
  27 + end
  28 +
  29 + def update
  30 + options = {default_projects_access: params[:default_project_access], group_admin: params[:group_admin]}
  31 + if user_team.update_membership(team_member, options)
  32 + redirect_to team_members_path(user_team), notice: "Membership for #{team_member.name} was successfully updated in Team of users."
  33 + else
  34 + render :edit
  35 + end
  36 + end
  37 +
  38 + def destroy
  39 + user_team.remove_member(team_member)
  40 + redirect_to team_path(user_team), notice: "Member #{team_member.name} was successfully removed from Team of users."
  41 + end
  42 +
  43 + protected
  44 +
  45 + def team_member
  46 + @member ||= user_team.members.find(params[:id])
  47 + end
  48 +
  49 +end
app/controllers/teams/projects_controller.rb 0 → 100644
@@ -0,0 +1,57 @@ @@ -0,0 +1,57 @@
  1 +class Teams::ProjectsController < Teams::ApplicationController
  2 +
  3 + skip_before_filter :authorize_manage_user_team!, only: [:index]
  4 +
  5 + def index
  6 + @projects = user_team.projects
  7 + @avaliable_projects = current_user.admin? ? Project.without_team(user_team) : current_user.owned_projects.without_team(user_team)
  8 + end
  9 +
  10 + def new
  11 + user_team
  12 + @avaliable_projects = current_user.owned_projects.scoped
  13 + @avaliable_projects = @avaliable_projects.without_team(user_team) if user_team.projects.any?
  14 +
  15 + redirect_to team_projects_path(user_team), notice: "No avalible projects." unless @avaliable_projects.any?
  16 + end
  17 +
  18 + def create
  19 + redirect_to :back if params[:project_ids].blank?
  20 +
  21 + project_ids = params[:project_ids]
  22 + access = params[:greatest_project_access]
  23 +
  24 + # Reject non-allowed projects
  25 + allowed_project_ids = current_user.owned_projects.map(&:id)
  26 + project_ids.select! { |id| allowed_project_ids.include?(id) }
  27 +
  28 + # Assign projects to team
  29 + user_team.assign_to_projects(project_ids, access)
  30 +
  31 + redirect_to team_projects_path(user_team), notice: 'Team of users was successfully assigned to projects.'
  32 + end
  33 +
  34 + def edit
  35 + team_project
  36 + end
  37 +
  38 + def update
  39 + if user_team.update_project_access(team_project, params[:greatest_project_access])
  40 + redirect_to team_projects_path(user_team), notice: 'Access was successfully updated.'
  41 + else
  42 + render :edit
  43 + end
  44 + end
  45 +
  46 + def destroy
  47 + user_team.resign_from_project(team_project)
  48 + redirect_to team_projects_path(user_team), notice: 'Team of users was successfully reassigned from project.'
  49 + end
  50 +
  51 + private
  52 +
  53 + def team_project
  54 + @project ||= user_team.projects.find_with_namespace(params[:id])
  55 + end
  56 +
  57 +end
app/controllers/teams_controller.rb 0 → 100644
@@ -0,0 +1,92 @@ @@ -0,0 +1,92 @@
  1 +class TeamsController < ApplicationController
  2 + # Authorize
  3 + before_filter :authorize_manage_user_team!
  4 + before_filter :authorize_admin_user_team!
  5 +
  6 + # Skip access control on public section
  7 + skip_before_filter :authorize_manage_user_team!, only: [:index, :show, :new, :destroy, :create, :search, :issues, :merge_requests]
  8 + skip_before_filter :authorize_admin_user_team!, only: [:index, :show, :new, :create, :search, :issues, :merge_requests]
  9 +
  10 + layout 'user_team', only: [:show, :edit, :update, :destroy, :issues, :merge_requests, :search]
  11 +
  12 + def index
  13 + @teams = current_user.user_teams.order('name ASC')
  14 + end
  15 +
  16 + def show
  17 + user_team
  18 + projects
  19 + @events = Event.in_projects(user_team.project_ids).limit(20).offset(params[:offset] || 0)
  20 + end
  21 +
  22 + def edit
  23 + user_team
  24 + end
  25 +
  26 + def update
  27 + if user_team.update_attributes(params[:user_team])
  28 + redirect_to team_path(user_team)
  29 + else
  30 + render action: :edit
  31 + end
  32 + end
  33 +
  34 + def destroy
  35 + user_team.destroy
  36 + redirect_to teams_path
  37 + end
  38 +
  39 + def new
  40 + @team = UserTeam.new
  41 + end
  42 +
  43 + def create
  44 + @team = UserTeam.new(params[:user_team])
  45 + @team.owner = current_user unless params[:owner]
  46 + @team.path = @team.name.dup.parameterize if @team.name
  47 +
  48 + if @team.save
  49 + redirect_to team_path(@team)
  50 + else
  51 + render action: :new
  52 + end
  53 + end
  54 +
  55 + # Get authored or assigned open merge requests
  56 + def merge_requests
  57 + projects
  58 + @merge_requests = MergeRequest.of_user_team(user_team)
  59 + @merge_requests = FilterContext.new(@merge_requests, params).execute
  60 + @merge_requests = @merge_requests.recent.page(params[:page]).per(20)
  61 + end
  62 +
  63 + # Get only assigned issues
  64 + def issues
  65 + projects
  66 + @issues = Issue.of_user_team(user_team)
  67 + @issues = FilterContext.new(@issues, params).execute
  68 + @issues = @issues.recent.page(params[:page]).per(20)
  69 + @issues = @issues.includes(:author, :project)
  70 + end
  71 +
  72 + def search
  73 + result = SearchContext.new(user_team.project_ids, params).execute
  74 +
  75 + @projects = result[:projects]
  76 + @merge_requests = result[:merge_requests]
  77 + @issues = result[:issues]
  78 + @wiki_pages = result[:wiki_pages]
  79 + @teams = result[:teams]
  80 + end
  81 +
  82 + protected
  83 +
  84 + def projects
  85 + @projects ||= user_team.projects.sorted_by_activity
  86 + end
  87 +
  88 + def user_team
  89 + @team ||= UserTeam.find_by_path(params[:id])
  90 + end
  91 +
  92 +end
app/decorators/user_decorator.rb
@@ -8,4 +8,8 @@ class UserDecorator &lt; ApplicationDecorator @@ -8,4 +8,8 @@ class UserDecorator &lt; ApplicationDecorator
8 def tm_of(project) 8 def tm_of(project)
9 project.team_member_by_id(self.id) 9 project.team_member_by_id(self.id)
10 end 10 end
  11 +
  12 + def name_with_email
  13 + "#{name} (#{email})"
  14 + end
11 end 15 end
app/helpers/admin/teams/members_helper.rb 0 → 100644
@@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
  1 +module Admin::Teams::MembersHelper
  2 + def member_since(team, member)
  3 + team.user_team_user_relationships.find_by_user_id(member).created_at
  4 + end
  5 +end
app/helpers/admin/teams/projects_helper.rb 0 → 100644
@@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
  1 +module Admin::Teams::ProjectsHelper
  2 + def assigned_since(team, project)
  3 + team.user_team_project_relationships.find_by_project_id(project).created_at
  4 + end
  5 +end
app/helpers/projects_helper.rb
@@ -3,8 +3,12 @@ module ProjectsHelper @@ -3,8 +3,12 @@ module ProjectsHelper
3 @project.users_projects.sort_by(&:project_access).reverse.group_by(&:project_access) 3 @project.users_projects.sort_by(&:project_access).reverse.group_by(&:project_access)
4 end 4 end
5 5
6 - def remove_from_team_message(project, member)  
7 - "You are going to remove #{member.user_name} from #{project.name}. Are you sure?" 6 + def grouper_project_teams(project)
  7 + @project.user_team_project_relationships.sort_by(&:greatest_access).reverse.group_by(&:greatest_access)
  8 + end
  9 +
  10 + def remove_from_project_team_message(project, user)
  11 + "You are going to remove #{user.name} from #{project.name} project team. Are you sure?"
8 end 12 end
9 13
10 def link_to_project project 14 def link_to_project project
app/helpers/user_teams_helper.rb 0 → 100644
@@ -0,0 +1,26 @@ @@ -0,0 +1,26 @@
  1 +module UserTeamsHelper
  2 + def team_filter_path(entity, options={})
  3 + exist_opts = {
  4 + status: params[:status],
  5 + project_id: params[:project_id],
  6 + }
  7 +
  8 + options = exist_opts.merge(options)
  9 +
  10 + case entity
  11 + when 'issue' then
  12 + issues_team_path(@team, options)
  13 + when 'merge_request'
  14 + merge_requests_team_path(@team, options)
  15 + end
  16 + end
  17 +
  18 + def grouped_user_team_members(team)
  19 + team.user_team_user_relationships.sort_by(&:permission).reverse.group_by(&:permission)
  20 + end
  21 +
  22 + def remove_from_user_team_message(team, member)
  23 + "You are going to remove #{member.name} from #{team.name}. Are you sure?"
  24 + end
  25 +
  26 +end
app/models/ability.rb
@@ -8,6 +8,7 @@ class Ability @@ -8,6 +8,7 @@ class Ability
8 when "Snippet" then snippet_abilities(object, subject) 8 when "Snippet" then snippet_abilities(object, subject)
9 when "MergeRequest" then merge_request_abilities(object, subject) 9 when "MergeRequest" then merge_request_abilities(object, subject)
10 when "Group", "Namespace" then group_abilities(object, subject) 10 when "Group", "Namespace" then group_abilities(object, subject)
  11 + when "UserTeam" then user_team_abilities(object, subject)
11 else [] 12 else []
12 end 13 end
13 end 14 end
@@ -110,6 +111,22 @@ class Ability @@ -110,6 +111,22 @@ class Ability
110 rules.flatten 111 rules.flatten
111 end 112 end
112 113
  114 + def user_team_abilities user, team
  115 + rules = []
  116 +
  117 + # Only group owner and administrators can manage group
  118 + if team.owner == user || team.admin?(user) || user.admin?
  119 + rules << [ :manage_user_team ]
  120 + end
  121 +
  122 + if team.owner == user || user.admin?
  123 + rules << [ :admin_user_team ]
  124 + end
  125 +
  126 + rules.flatten
  127 + end
  128 +
  129 +
113 [:issue, :note, :snippet, :merge_request].each do |name| 130 [:issue, :note, :snippet, :merge_request].each do |name|
114 define_method "#{name}_abilities" do |user, subject| 131 define_method "#{name}_abilities" do |user, subject|
115 if subject.author == user 132 if subject.author == user
app/models/concerns/issuable.rb
@@ -22,6 +22,7 @@ module Issuable @@ -22,6 +22,7 @@ module Issuable
22 scope :opened, where(closed: false) 22 scope :opened, where(closed: false)
23 scope :closed, where(closed: true) 23 scope :closed, where(closed: true)
24 scope :of_group, ->(group) { where(project_id: group.project_ids) } 24 scope :of_group, ->(group) { where(project_id: group.project_ids) }
  25 + scope :of_user_team, ->(team) { where(project_id: team.project_ids, assignee_id: team.member_ids) }
25 scope :assigned, ->(u) { where(assignee_id: u.id)} 26 scope :assigned, ->(u) { where(assignee_id: u.id)}
26 scope :recent, order("created_at DESC") 27 scope :recent, order("created_at DESC")
27 28
app/models/project.rb
@@ -33,28 +33,31 @@ class Project &lt; ActiveRecord::Base @@ -33,28 +33,31 @@ class Project &lt; ActiveRecord::Base
33 attr_accessor :error_code 33 attr_accessor :error_code
34 34
35 # Relations 35 # Relations
36 - belongs_to :group, foreign_key: "namespace_id", conditions: "type = 'Group'" 36 + belongs_to :creator, foreign_key: "creator_id", class_name: "User"
  37 + belongs_to :group, foreign_key: "namespace_id", conditions: "type = 'Group'"
37 belongs_to :namespace 38 belongs_to :namespace
38 39
39 - belongs_to :creator,  
40 - class_name: "User",  
41 - foreign_key: "creator_id"  
42 -  
43 - has_many :users, through: :users_projects  
44 - has_many :events, dependent: :destroy  
45 - has_many :merge_requests, dependent: :destroy  
46 - has_many :issues, dependent: :destroy, order: "closed, created_at DESC"  
47 - has_many :milestones, dependent: :destroy  
48 - has_many :users_projects, dependent: :destroy  
49 - has_many :notes, dependent: :destroy  
50 - has_many :snippets, dependent: :destroy  
51 - has_many :deploy_keys, dependent: :destroy, foreign_key: "project_id", class_name: "Key"  
52 - has_many :hooks, dependent: :destroy, class_name: "ProjectHook"  
53 - has_many :wikis, dependent: :destroy  
54 - has_many :protected_branches, dependent: :destroy  
55 has_one :last_event, class_name: 'Event', order: 'events.created_at DESC', foreign_key: 'project_id' 40 has_one :last_event, class_name: 'Event', order: 'events.created_at DESC', foreign_key: 'project_id'
56 has_one :gitlab_ci_service, dependent: :destroy 41 has_one :gitlab_ci_service, dependent: :destroy
57 42
  43 + has_many :events, dependent: :destroy
  44 + has_many :merge_requests, dependent: :destroy
  45 + has_many :issues, dependent: :destroy, order: "closed, created_at DESC"
  46 + has_many :milestones, dependent: :destroy
  47 + has_many :users_projects, dependent: :destroy
  48 + has_many :notes, dependent: :destroy
  49 + has_many :snippets, dependent: :destroy
  50 + has_many :deploy_keys, dependent: :destroy, class_name: "Key", foreign_key: "project_id"
  51 + has_many :hooks, dependent: :destroy, class_name: "ProjectHook"
  52 + has_many :wikis, dependent: :destroy
  53 + has_many :protected_branches, dependent: :destroy
  54 + has_many :user_team_project_relationships, dependent: :destroy
  55 +
  56 + has_many :users, through: :users_projects
  57 + has_many :user_teams, through: :user_team_project_relationships
  58 + has_many :user_team_user_relationships, through: :user_teams
  59 + has_many :user_teams_members, through: :user_team_user_relationships
  60 +
58 delegate :name, to: :owner, allow_nil: true, prefix: true 61 delegate :name, to: :owner, allow_nil: true, prefix: true
59 62
60 # Validations 63 # Validations
@@ -77,6 +80,8 @@ class Project &lt; ActiveRecord::Base @@ -77,6 +80,8 @@ class Project &lt; ActiveRecord::Base
77 # Scopes 80 # Scopes
78 scope :without_user, ->(user) { where("id NOT IN (:ids)", ids: user.authorized_projects.map(&:id) ) } 81 scope :without_user, ->(user) { where("id NOT IN (:ids)", ids: user.authorized_projects.map(&:id) ) }
79 scope :not_in_group, ->(group) { where("id NOT IN (:ids)", ids: group.project_ids ) } 82 scope :not_in_group, ->(group) { where("id NOT IN (:ids)", ids: group.project_ids ) }
  83 + scope :without_team, ->(team) { where("id NOT IN (:ids)", ids: team.projects.map(&:id)) }
  84 + scope :in_team, ->(team) { where("id IN (:ids)", ids: team.projects.map(&:id)) }
80 scope :in_namespace, ->(namespace) { where(namespace_id: namespace.id) } 85 scope :in_namespace, ->(namespace) { where(namespace_id: namespace.id) }
81 scope :sorted_by_activity, ->() { order("(SELECT max(events.created_at) FROM events WHERE events.project_id = projects.id) DESC") } 86 scope :sorted_by_activity, ->() { order("(SELECT max(events.created_at) FROM events WHERE events.project_id = projects.id) DESC") }
82 scope :personal, ->(user) { where(namespace_id: user.namespace_id) } 87 scope :personal, ->(user) { where(namespace_id: user.namespace_id) }
@@ -122,7 +127,7 @@ class Project &lt; ActiveRecord::Base @@ -122,7 +127,7 @@ class Project &lt; ActiveRecord::Base
122 end 127 end
123 128
124 def team 129 def team
125 - @team ||= Team.new(self) 130 + @team ||= ProjectTeam.new(self)
126 end 131 end
127 132
128 def repository 133 def repository
@@ -489,6 +494,11 @@ class Project &lt; ActiveRecord::Base @@ -489,6 +494,11 @@ class Project &lt; ActiveRecord::Base
489 http_url = [Gitlab.config.gitlab.url, "/", path_with_namespace, ".git"].join('') 494 http_url = [Gitlab.config.gitlab.url, "/", path_with_namespace, ".git"].join('')
490 end 495 end
491 496
  497 + def project_access_human(member)
  498 + project_user_relation = self.users_projects.find_by_user_id(member.id)
  499 + self.class.access_options.key(project_user_relation.project_access)
  500 + end
  501 +
492 # Check if current branch name is marked as protected in the system 502 # Check if current branch name is marked as protected in the system
493 def protected_branch? branch_name 503 def protected_branch? branch_name
494 protected_branches.map(&:name).include?(branch_name) 504 protected_branches.map(&:name).include?(branch_name)
app/models/project_team.rb 0 → 100644
@@ -0,0 +1,122 @@ @@ -0,0 +1,122 @@
  1 +class ProjectTeam
  2 + attr_accessor :project
  3 +
  4 + def initialize(project)
  5 + @project = project
  6 + end
  7 +
  8 + # Shortcut to add users
  9 + #
  10 + # Use:
  11 + # @team << [@user, :master]
  12 + # @team << [@users, :master]
  13 + #
  14 + def << args
  15 + users = args.first
  16 +
  17 + if users.respond_to?(:each)
  18 + add_users(users, args.second)
  19 + else
  20 + add_user(users, args.second)
  21 + end
  22 + end
  23 +
  24 + def get_tm user_id
  25 + project.users_projects.find_by_user_id(user_id)
  26 + end
  27 +
  28 + def add_user(user, access)
  29 + add_users_ids([user.id], access)
  30 + end
  31 +
  32 + def add_users(users, access)
  33 + add_users_ids(users.map(&:id), access)
  34 + end
  35 +
  36 + def add_users_ids(user_ids, access)
  37 + UsersProject.add_users_into_projects(
  38 + [project.id],
  39 + user_ids,
  40 + access
  41 + )
  42 + end
  43 +
  44 + # Remove all users from project team
  45 + def truncate
  46 + UsersProject.truncate_team(project)
  47 + end
  48 +
  49 + def members
  50 + project.users_projects
  51 + end
  52 +
  53 + def guests
  54 + members.guests.map(&:user)
  55 + end
  56 +
  57 + def reporters
  58 + members.reporters.map(&:user)
  59 + end
  60 +
  61 + def developers
  62 + members.developers.map(&:user)
  63 + end
  64 +
  65 + def masters
  66 + members.masters.map(&:user)
  67 + end
  68 +
  69 + def repository_readers
  70 + repository_members[UsersProject::REPORTER]
  71 + end
  72 +
  73 + def repository_writers
  74 + repository_members[UsersProject::DEVELOPER]
  75 + end
  76 +
  77 + def repository_masters
  78 + repository_members[UsersProject::MASTER]
  79 + end
  80 +
  81 + def repository_members
  82 + keys = Hash.new {|h,k| h[k] = [] }
  83 + UsersProject.select("keys.identifier, project_access").
  84 + joins(user: :keys).where(project_id: project.id).
  85 + each {|row| keys[row.project_access] << [row.identifier] }
  86 +
  87 + keys[UsersProject::REPORTER] += project.deploy_keys.pluck(:identifier)
  88 + keys
  89 + end
  90 +
  91 + def import(source_project)
  92 + target_project = project
  93 +
  94 + source_team = source_project.users_projects.all
  95 + target_team = target_project.users_projects.all
  96 + target_user_ids = target_team.map(&:user_id)
  97 +
  98 + source_team.reject! do |tm|
  99 + # Skip if user already present in team
  100 + target_user_ids.include?(tm.user_id)
  101 + end
  102 +
  103 + source_team.map! do |tm|
  104 + new_tm = tm.dup
  105 + new_tm.id = nil
  106 + new_tm.project_id = target_project.id
  107 + new_tm.skip_git = true
  108 + new_tm
  109 + end
  110 +
  111 + UsersProject.transaction do
  112 + source_team.each do |tm|
  113 + tm.save
  114 + end
  115 + target_project.update_repository
  116 + end
  117 +
  118 + true
  119 + rescue
  120 + false
  121 + end
  122 +end
app/models/team.rb
@@ -1,122 +0,0 @@ @@ -1,122 +0,0 @@
1 -class Team  
2 - attr_accessor :project  
3 -  
4 - def initialize(project)  
5 - @project = project  
6 - end  
7 -  
8 - # Shortcut to add users  
9 - #  
10 - # Use:  
11 - # @team << [@user, :master]  
12 - # @team << [@users, :master]  
13 - #  
14 - def << args  
15 - users = args.first  
16 -  
17 - if users.respond_to?(:each)  
18 - add_users(users, args.second)  
19 - else  
20 - add_user(users, args.second)  
21 - end  
22 - end  
23 -  
24 - def get_tm user_id  
25 - project.users_projects.find_by_user_id(user_id)  
26 - end  
27 -  
28 - def add_user(user, access)  
29 - add_users_ids([user.id], access)  
30 - end  
31 -  
32 - def add_users(users, access)  
33 - add_users_ids(users.map(&:id), access)  
34 - end  
35 -  
36 - def add_users_ids(user_ids, access)  
37 - UsersProject.add_users_into_projects(  
38 - [project.id],  
39 - user_ids,  
40 - access  
41 - )  
42 - end  
43 -  
44 - # Remove all users from project team  
45 - def truncate  
46 - UsersProject.truncate_team(project)  
47 - end  
48 -  
49 - def members  
50 - project.users_projects  
51 - end  
52 -  
53 - def guests  
54 - members.guests.map(&:user)  
55 - end  
56 -  
57 - def reporters  
58 - members.reporters.map(&:user)  
59 - end  
60 -  
61 - def developers  
62 - members.developers.map(&:user)  
63 - end  
64 -  
65 - def masters  
66 - members.masters.map(&:user)  
67 - end  
68 -  
69 - def repository_readers  
70 - repository_members[UsersProject::REPORTER]  
71 - end  
72 -  
73 - def repository_writers  
74 - repository_members[UsersProject::DEVELOPER]  
75 - end  
76 -  
77 - def repository_masters  
78 - repository_members[UsersProject::MASTER]  
79 - end  
80 -  
81 - def repository_members  
82 - keys = Hash.new {|h,k| h[k] = [] }  
83 - UsersProject.select("keys.identifier, project_access").  
84 - joins(user: :keys).where(project_id: project.id).  
85 - each {|row| keys[row.project_access] << [row.identifier] }  
86 -  
87 - keys[UsersProject::REPORTER] += project.deploy_keys.pluck(:identifier)  
88 - keys  
89 - end  
90 -  
91 - def import(source_project)  
92 - target_project = project  
93 -  
94 - source_team = source_project.users_projects.all  
95 - target_team = target_project.users_projects.all  
96 - target_user_ids = target_team.map(&:user_id)  
97 -  
98 - source_team.reject! do |tm|  
99 - # Skip if user already present in team  
100 - target_user_ids.include?(tm.user_id)  
101 - end  
102 -  
103 - source_team.map! do |tm|  
104 - new_tm = tm.dup  
105 - new_tm.id = nil  
106 - new_tm.project_id = target_project.id  
107 - new_tm.skip_git = true  
108 - new_tm  
109 - end  
110 -  
111 - UsersProject.transaction do  
112 - source_team.each do |tm|  
113 - tm.save  
114 - end  
115 - target_project.update_repository  
116 - end  
117 -  
118 - true  
119 - rescue  
120 - false  
121 - end  
122 -end  
app/models/user.rb
@@ -45,18 +45,27 @@ class User &lt; ActiveRecord::Base @@ -45,18 +45,27 @@ class User &lt; ActiveRecord::Base
45 attr_accessor :force_random_password 45 attr_accessor :force_random_password
46 46
47 # Namespace for personal projects 47 # Namespace for personal projects
48 - has_one :namespace, class_name: "Namespace", foreign_key: :owner_id, conditions: 'type IS NULL', dependent: :destroy  
49 - has_many :groups, class_name: "Group", foreign_key: :owner_id  
50 -  
51 - has_many :keys, dependent: :destroy  
52 - has_many :users_projects, dependent: :destroy  
53 - has_many :issues, foreign_key: :author_id, dependent: :destroy  
54 - has_many :notes, foreign_key: :author_id, dependent: :destroy  
55 - has_many :merge_requests, foreign_key: :author_id, dependent: :destroy  
56 - has_many :events, class_name: "Event", foreign_key: :author_id, dependent: :destroy  
57 - has_many :recent_events, class_name: "Event", foreign_key: :author_id, order: "id DESC"  
58 - has_many :assigned_issues, class_name: "Issue", foreign_key: :assignee_id, dependent: :destroy  
59 - has_many :assigned_merge_requests, class_name: "MergeRequest", foreign_key: :assignee_id, dependent: :destroy 48 + has_one :namespace, dependent: :destroy, foreign_key: :owner_id, class_name: "Namespace", conditions: 'type IS NULL'
  49 +
  50 + has_many :keys, dependent: :destroy
  51 + has_many :users_projects, dependent: :destroy
  52 + has_many :issues, dependent: :destroy, foreign_key: :author_id
  53 + has_many :notes, dependent: :destroy, foreign_key: :author_id
  54 + has_many :merge_requests, dependent: :destroy, foreign_key: :author_id
  55 + has_many :events, dependent: :destroy, foreign_key: :author_id, class_name: "Event"
  56 + has_many :assigned_issues, dependent: :destroy, foreign_key: :assignee_id, class_name: "Issue"
  57 + has_many :assigned_merge_requests, dependent: :destroy, foreign_key: :assignee_id, class_name: "MergeRequest"
  58 +
  59 + has_many :groups, class_name: "Group", foreign_key: :owner_id
  60 + has_many :recent_events, class_name: "Event", foreign_key: :author_id, order: "id DESC"
  61 +
  62 + has_many :projects, through: :users_projects
  63 +
  64 + has_many :user_team_user_relationships, dependent: :destroy
  65 +
  66 + has_many :user_teams, through: :user_team_user_relationships
  67 + has_many :user_team_project_relationships, through: :user_teams
  68 + has_many :team_projects, through: :user_team_project_relationships
60 69
61 validates :name, presence: true 70 validates :name, presence: true
62 validates :bio, length: { within: 0..255 } 71 validates :bio, length: { within: 0..255 }
@@ -80,6 +89,9 @@ class User &lt; ActiveRecord::Base @@ -80,6 +89,9 @@ class User &lt; ActiveRecord::Base
80 scope :blocked, where(blocked: true) 89 scope :blocked, where(blocked: true)
81 scope :active, where(blocked: false) 90 scope :active, where(blocked: false)
82 scope :alphabetically, order('name ASC') 91 scope :alphabetically, order('name ASC')
  92 + scope :in_team, ->(team){ where(id: team.member_ids) }
  93 + scope :not_in_team, ->(team){ where('users.id NOT IN (:ids)', ids: team.member_ids) }
  94 + scope :potential_team_members, ->(team) { team.members.any? ? active : active.not_in_team(team) }
83 95
84 # 96 #
85 # Class methods 97 # Class methods
app/models/user_team.rb 0 → 100644
@@ -0,0 +1,97 @@ @@ -0,0 +1,97 @@
  1 +class UserTeam < ActiveRecord::Base
  2 + attr_accessible :name, :owner_id, :path
  3 +
  4 + belongs_to :owner, class_name: User
  5 +
  6 + has_many :user_team_project_relationships, dependent: :destroy
  7 + has_many :user_team_user_relationships, dependent: :destroy
  8 +
  9 + has_many :projects, through: :user_team_project_relationships
  10 + has_many :members, through: :user_team_user_relationships, source: :user
  11 +
  12 + validates :name, presence: true, uniqueness: true
  13 + validates :owner, presence: true
  14 + validates :path, uniqueness: true, presence: true, length: { within: 1..255 },
  15 + format: { with: Gitlab::Regex.path_regex,
  16 + message: "only letters, digits & '_' '-' '.' allowed. Letter should be first" }
  17 +
  18 + scope :with_member, ->(user){ joins(:user_team_user_relationships).where(user_team_user_relationships: {user_id: user.id}) }
  19 + scope :with_project, ->(project){ joins(:user_team_project_relationships).where(user_team_project_relationships: {project_id: project})}
  20 + scope :without_project, ->(project){ where("user_teams.id NOT IN (:ids)", ids: (a = with_project(project); a.blank? ? 0 : a))}
  21 + scope :created_by, ->(user){ where(owner_id: user) }
  22 +
  23 + class << self
  24 + def search query
  25 + where("name LIKE :query OR path LIKE :query", query: "%#{query}%")
  26 + end
  27 +
  28 + def global_id
  29 + 'GLN'
  30 + end
  31 +
  32 + def access_roles
  33 + UsersProject.access_roles
  34 + end
  35 + end
  36 +
  37 + def to_param
  38 + path
  39 + end
  40 +
  41 + def assign_to_projects(projects, access)
  42 + projects.each do |project|
  43 + assign_to_project(project, access)
  44 + end
  45 + end
  46 +
  47 + def assign_to_project(project, access)
  48 + Gitlab::UserTeamManager.assign(self, project, access)
  49 + end
  50 +
  51 + def resign_from_project(project)
  52 + Gitlab::UserTeamManager.resign(self, project)
  53 + end
  54 +
  55 + def add_members(users, access, group_admin)
  56 + users.each do |user|
  57 + add_member(user, access, group_admin)
  58 + end
  59 + end
  60 +
  61 + def add_member(user, access, group_admin)
  62 + Gitlab::UserTeamManager.add_member_into_team(self, user, access, group_admin)
  63 + end
  64 +
  65 + def remove_member(user)
  66 + Gitlab::UserTeamManager.remove_member_from_team(self, user)
  67 + end
  68 +
  69 + def update_membership(user, options)
  70 + Gitlab::UserTeamManager.update_team_user_membership(self, user, options)
  71 + end
  72 +
  73 + def update_project_access(project, permission)
  74 + Gitlab::UserTeamManager.update_project_greates_access(self, project, permission)
  75 + end
  76 +
  77 + def max_project_access(project)
  78 + user_team_project_relationships.find_by_project_id(project).greatest_access
  79 + end
  80 +
  81 + def human_max_project_access(project)
  82 + self.class.access_roles.invert[max_project_access(project)]
  83 + end
  84 +
  85 + def default_projects_access(member)
  86 + user_team_user_relationships.find_by_user_id(member).permission
  87 + end
  88 +
  89 + def human_default_projects_access(member)
  90 + self.class.access_roles.invert[default_projects_access(member)]
  91 + end
  92 +
  93 + def admin?(member)
  94 + user_team_user_relationships.with_user(member).first.group_admin?
  95 + end
  96 +
  97 +end
app/models/user_team_project_relationship.rb 0 → 100644
@@ -0,0 +1,28 @@ @@ -0,0 +1,28 @@
  1 +class UserTeamProjectRelationship < ActiveRecord::Base
  2 + attr_accessible :greatest_access, :project_id, :user_team_id
  3 +
  4 + belongs_to :user_team
  5 + belongs_to :project
  6 +
  7 + validates :project, presence: true
  8 + validates :user_team, presence: true
  9 + validate :check_greatest_access
  10 +
  11 + scope :with_project, ->(project){ where(project_id: project.id) }
  12 +
  13 + def team_name
  14 + user_team.name
  15 + end
  16 +
  17 + private
  18 +
  19 + def check_greatest_access
  20 + errors.add(:base, :incorrect_access_code) unless correct_access?
  21 + end
  22 +
  23 + def correct_access?
  24 + return false if greatest_access.blank?
  25 + return true if UsersProject.access_roles.has_value?(greatest_access)
  26 + false
  27 + end
  28 +end
app/models/user_team_user_relationship.rb 0 → 100644
@@ -0,0 +1,19 @@ @@ -0,0 +1,19 @@
  1 +class UserTeamUserRelationship < ActiveRecord::Base
  2 + attr_accessible :group_admin, :permission, :user_id, :user_team_id
  3 +
  4 + belongs_to :user_team
  5 + belongs_to :user
  6 +
  7 + validates :user_team, presence: true
  8 + validates :user, presence: true
  9 +
  10 + scope :with_user, ->(user) { where(user_id: user.id) }
  11 +
  12 + def user_name
  13 + user.name
  14 + end
  15 +
  16 + def access_human
  17 + UsersProject.access_roles.invert[permission]
  18 + end
  19 +end
app/models/users_project.rb
@@ -39,7 +39,10 @@ class UsersProject &lt; ActiveRecord::Base @@ -39,7 +39,10 @@ class UsersProject &lt; ActiveRecord::Base
39 scope :reporters, where(project_access: REPORTER) 39 scope :reporters, where(project_access: REPORTER)
40 scope :developers, where(project_access: DEVELOPER) 40 scope :developers, where(project_access: DEVELOPER)
41 scope :masters, where(project_access: MASTER) 41 scope :masters, where(project_access: MASTER)
  42 +
42 scope :in_project, ->(project) { where(project_id: project.id) } 43 scope :in_project, ->(project) { where(project_id: project.id) }
  44 + scope :in_projects, ->(projects) { where(project_id: project_ids) }
  45 + scope :with_user, ->(user) { where(user_id: user.id) }
43 46
44 class << self 47 class << self
45 48
app/views/admin/projects/members/_form.html.haml 0 → 100644
@@ -0,0 +1,16 @@ @@ -0,0 +1,16 @@
  1 += form_for @team_member_relation, as: :team_member, url: admin_project_member_path(@project, @member) do |f|
  2 + -if @team_member_relation.errors.any?
  3 + .alert-message.block-message.error
  4 + %ul
  5 + - @team_member_relation.errors.full_messages.each do |msg|
  6 + %li= msg
  7 +
  8 + .clearfix
  9 + %label Project Access:
  10 + .input
  11 + = f.select :project_access, options_for_select(Project.access_options, @team_member_relation.project_access), {}, class: "project-access-select chosen span3"
  12 +
  13 + %br
  14 + .actions
  15 + = f.submit 'Save', class: "btn primary"
  16 + = link_to 'Cancel', :back, class: "btn"
app/views/admin/projects/members/edit.html.haml 0 → 100644
@@ -0,0 +1,8 @@ @@ -0,0 +1,8 @@
  1 +%p.slead
  2 + Edit access for
  3 + = link_to @member.name, admin_user_path(@member)
  4 + in
  5 + = link_to @project.name_with_namespace, admin_project_path(@project)
  6 +
  7 +%hr
  8 += render 'form'
app/views/admin/projects/show.html.haml
@@ -114,9 +114,9 @@ @@ -114,9 +114,9 @@
114 %h5 114 %h5
115 Team 115 Team
116 %small 116 %small
117 - (#{@project.users_projects.count}) 117 + (#{@project.users.count})
118 %br 118 %br
119 -%table.zebra-striped 119 +%table.zebra-striped.team_members
120 %thead 120 %thead
121 %tr 121 %tr
122 %th Name 122 %th Name
@@ -124,13 +124,13 @@ @@ -124,13 +124,13 @@
124 %th Repository Access 124 %th Repository Access
125 %th 125 %th
126 126
127 - - @project.users_projects.each do |tm| 127 + - @project.users.each do |tm|
128 %tr 128 %tr
129 %td 129 %td
130 - = link_to tm.user_name, admin_user_path(tm.user)  
131 - %td= tm.project_access_human  
132 - %td= link_to 'Edit Access', edit_admin_team_member_path(tm), class: "btn small"  
133 - %td= link_to 'Remove from team', admin_team_member_path(tm), confirm: 'Are you sure?', method: :delete, class: "btn danger small" 130 + = link_to tm.name, admin_user_path(tm)
  131 + %td= @project.project_access_human(tm)
  132 + %td= link_to 'Edit Access', edit_admin_project_member_path(@project, tm), class: "btn small"
  133 + %td= link_to 'Remove from team', admin_project_member_path(@project, tm), confirm: 'Are you sure?', method: :delete, class: "btn danger small"
134 134
135 %br 135 %br
136 %h5 Add new team member 136 %h5 Add new team member
app/views/admin/team_members/_form.html.haml
@@ -1,16 +0,0 @@ @@ -1,16 +0,0 @@
1 -= form_for @admin_team_member, as: :team_member, url: admin_team_member_path(@admin_team_member) do |f|  
2 - -if @admin_team_member.errors.any?  
3 - .alert-message.block-message.error  
4 - %ul  
5 - - @admin_team_member.errors.full_messages.each do |msg|  
6 - %li= msg  
7 -  
8 - .clearfix  
9 - %label Project Access:  
10 - .input  
11 - = f.select :project_access, options_for_select(Project.access_options, @admin_team_member.project_access), {}, class: "project-access-select chosen span3"  
12 -  
13 - %br  
14 - .actions  
15 - = f.submit 'Save', class: "btn primary"  
16 - = link_to 'Cancel', :back, class: "btn"  
app/views/admin/team_members/edit.html.haml
@@ -1,8 +0,0 @@ @@ -1,8 +0,0 @@
1 -%p.slead  
2 - Edit access for  
3 - = link_to @admin_team_member.user_name, admin_user_path(@admin_team_member)  
4 - in  
5 - = link_to @admin_team_member.project.name_with_namespace, admin_project_path(@admin_team_member)  
6 -  
7 -%hr  
8 -= render 'form'  
app/views/admin/teams/edit.html.haml 0 → 100644
@@ -0,0 +1,23 @@ @@ -0,0 +1,23 @@
  1 +%h3.page_title Rename Team
  2 +%hr
  3 += form_for @team, url: admin_team_path(@team), method: :put do |f|
  4 + - if @team.errors.any?
  5 + .alert-message.block-message.error
  6 + %span= @team.errors.full_messages.first
  7 + .clearfix.team_name_holder
  8 + = f.label :name do
  9 + Team name is
  10 + .input
  11 + = f.text_field :name, placeholder: "Example Team", class: "xxlarge"
  12 +
  13 + .clearfix.team_name_holder
  14 + = f.label :path do
  15 + %span.cred Team path is
  16 + .input
  17 + = f.text_field :path, placeholder: "example-team", class: "xxlarge danger"
  18 + %ul.cred
  19 + %li It will change web url for access team and team projects.
  20 +
  21 + .form-actions
  22 + = f.submit 'Rename team', class: "btn danger"
  23 + = link_to 'Cancel', admin_teams_path, class: "btn cancel-btn"
app/views/admin/teams/index.html.haml 0 → 100644
@@ -0,0 +1,38 @@ @@ -0,0 +1,38 @@
  1 +%h3.page_title
  2 + Teams
  3 + %small
  4 + simple Teams description
  5 +
  6 + = link_to 'New Team', new_admin_team_path, class: "btn small right"
  7 + %br
  8 +
  9 += form_tag admin_teams_path, method: :get, class: 'form-inline' do
  10 + = text_field_tag :name, params[:name], class: "xlarge"
  11 + = submit_tag "Search", class: "btn submit primary"
  12 +
  13 +%table
  14 + %thead
  15 + %tr
  16 + %th
  17 + Name
  18 + %i.icon-sort-down
  19 + %th Path
  20 + %th Projects
  21 + %th Members
  22 + %th Owner
  23 + %th.cred Danger Zone!
  24 +
  25 + - @teams.each do |team|
  26 + %tr
  27 + %td
  28 + %strong= link_to team.name, admin_team_path(team)
  29 + %td= team.path
  30 + %td= team.projects.count
  31 + %td= team.members.count
  32 + %td
  33 + = link_to team.owner.name, admin_user_path(team.owner_id)
  34 + %td.bgred
  35 + = link_to 'Rename', edit_admin_team_path(team), id: "edit_#{dom_id(team)}", class: "btn small"
  36 + = link_to 'Destroy', admin_team_path(team), confirm: "REMOVE #{team.name}? Are you sure?", method: :delete, class: "btn small danger"
  37 +
  38 += paginate @teams, theme: "admin"
app/views/admin/teams/members/_form.html.haml 0 → 100644
@@ -0,0 +1,20 @@ @@ -0,0 +1,20 @@
  1 += form_tag admin_team_member_path(@team, @member), method: :put do
  2 + -if @member.errors.any?
  3 + .alert-message.block-message.error
  4 + %ul
  5 + - @member.errors.full_messages.each do |msg|
  6 + %li= msg
  7 +
  8 + .clearfix
  9 + %label Default access for Team projects:
  10 + .input
  11 + = select_tag :default_project_access, options_for_select(UserTeam.access_roles, @team.default_projects_access(@member)), class: "project-access-select chosen span3"
  12 + .clearfix
  13 + %label Team admin?
  14 + .input
  15 + = check_box_tag :group_admin, true, @team.admin?(@member)
  16 +
  17 + %br
  18 + .actions
  19 + = submit_tag 'Save', class: "btn primary"
  20 + = link_to 'Cancel', :back, class: "btn"
app/views/admin/teams/members/edit.html.haml 0 → 100644
@@ -0,0 +1,16 @@ @@ -0,0 +1,16 @@
  1 +%h3
  2 + Edit access #{@member.name} in #{@team.name} team
  3 +
  4 +%hr
  5 +%table.zebra-striped
  6 + %tr
  7 + %td User:
  8 + %td= @member.name
  9 + %tr
  10 + %td Team:
  11 + %td= @team.name
  12 + %tr
  13 + %td Since:
  14 + %td= member_since(@team, @member).stamp("Nov 11, 2010")
  15 +
  16 += render 'form'
app/views/admin/teams/members/new.html.haml 0 → 100644
@@ -0,0 +1,29 @@ @@ -0,0 +1,29 @@
  1 +%h3.page_title
  2 + Team: #{@team.name}
  3 +
  4 +%fieldset
  5 + %legend Members (#{@team.members.count})
  6 + = form_tag admin_team_members_path(@team), id: "team_members", class: "bulk_import", method: :post do
  7 + %table#members_list
  8 + %thead
  9 + %tr
  10 + %th User name
  11 + %th Default project access
  12 + %th Team access
  13 + %th
  14 + - @team.members.each do |member|
  15 + %tr.member
  16 + %td
  17 + = link_to [:admin, member] do
  18 + = member.name
  19 + %small= "(#{member.email})"
  20 + %td= @team.human_default_projects_access(member)
  21 + %td= @team.admin?(member) ? "Admin" : "Member"
  22 + %td
  23 + %tr
  24 + %td= select_tag :user_ids, options_from_collection_for_select(@users , :id, :name_with_email), multiple: true, data: {placeholder: 'Select users'}, class: 'chosen span5'
  25 + %td= select_tag :default_project_access, options_for_select(Project.access_options), {class: "project-access-select chosen span3" }
  26 + %td
  27 + %span= check_box_tag :group_admin
  28 + %span Admin?
  29 + %td= submit_tag 'Add', class: "btn primary", id: :add_members_to_team
app/views/admin/teams/new.html.haml 0 → 100644
@@ -0,0 +1,19 @@ @@ -0,0 +1,19 @@
  1 +%h3.page_title New Team
  2 +%hr
  3 += form_for @team, url: admin_teams_path do |f|
  4 + - if @team.errors.any?
  5 + .alert-message.block-message.error
  6 + %span= @team.errors.full_messages.first
  7 + .clearfix
  8 + = f.label :name do
  9 + Team name is
  10 + .input
  11 + = f.text_field :name, placeholder: "Ex. OpenSource", class: "xxlarge left"
  12 + &nbsp;
  13 + = f.submit 'Create team', class: "btn primary"
  14 + %hr
  15 + .padded
  16 + %ul
  17 + %li All created teams are public (users can view who enter into team and which project are assigned for this team)
  18 + %li People within a team see only projects they have access to
  19 + %li You will be able to assign existing projects for team
app/views/admin/teams/projects/_form.html.haml 0 → 100644
@@ -0,0 +1,16 @@ @@ -0,0 +1,16 @@
  1 += form_tag admin_team_project_path(@team, @project), method: :put do
  2 + -if @project.errors.any?
  3 + .alert-message.block-message.error
  4 + %ul
  5 + - @project.errors.full_messages.each do |msg|
  6 + %li= msg
  7 +
  8 + .clearfix
  9 + %label Max access for Team members:
  10 + .input
  11 + = select_tag :greatest_project_access, options_for_select(UserTeam.access_roles, @team.max_project_access(@project)), class: "project-access-select chosen span3"
  12 +
  13 + %br
  14 + .actions
  15 + = submit_tag 'Save', class: "btn primary"
  16 + = link_to 'Cancel', :back, class: "btn"
app/views/admin/teams/projects/edit.html.haml 0 → 100644
@@ -0,0 +1,16 @@ @@ -0,0 +1,16 @@
  1 +%h3
  2 + Edit max access in #{@project.name} for #{@team.name} team
  3 +
  4 +%hr
  5 +%table.zebra-striped
  6 + %tr
  7 + %td Project:
  8 + %td= @project.name
  9 + %tr
  10 + %td Team:
  11 + %td= @team.name
  12 + %tr
  13 + %td Since:
  14 + %td= assigned_since(@team, @project).stamp("Nov 11, 2010")
  15 +
  16 += render 'form'
app/views/admin/teams/projects/new.html.haml 0 → 100644
@@ -0,0 +1,23 @@ @@ -0,0 +1,23 @@
  1 +%h3.page_title
  2 + Team: #{@team.name}
  3 +
  4 +%fieldset
  5 + %legend Projects (#{@team.projects.count})
  6 + = form_tag admin_team_projects_path(@team), id: "assign_projects", class: "bulk_import", method: :post do
  7 + %table#projects_list
  8 + %thead
  9 + %tr
  10 + %th Project name
  11 + %th Max access
  12 + %th
  13 + - @team.projects.each do |project|
  14 + %tr.project
  15 + %td
  16 + = link_to project.name_with_namespace, [:admin, project]
  17 + %td
  18 + %span= @team.human_max_project_access(project)
  19 + %td
  20 + %tr
  21 + %td= select_tag :project_ids, options_from_collection_for_select(@projects , :id, :name_with_namespace), multiple: true, data: {placeholder: 'Select projects'}, class: 'chosen span5'
  22 + %td= select_tag :greatest_project_access, options_for_select(Project.access_options), {class: "project-access-select chosen span3" }
  23 + %td= submit_tag 'Add', class: "btn primary", id: :assign_projects_to_team
app/views/admin/teams/show.html.haml 0 → 100644
@@ -0,0 +1,101 @@ @@ -0,0 +1,101 @@
  1 +%h3.page_title
  2 + Team: #{@team.name}
  3 +
  4 +%br
  5 +%table.zebra-striped
  6 + %thead
  7 + %tr
  8 + %th Team
  9 + %th
  10 + %tr
  11 + %td
  12 + %b
  13 + Name:
  14 + %td
  15 + = @team.name
  16 + &nbsp;
  17 + = link_to edit_admin_team_path(@team), class: "btn btn-small right" do
  18 + %i.icon-edit
  19 + Rename
  20 + %tr
  21 + %td
  22 + %b
  23 + Owner:
  24 + %td
  25 + = @team.owner.name
  26 + .right
  27 + = link_to "#", class: "btn btn-small change-owner-link" do
  28 + %i.icon-edit
  29 + Change owner
  30 +
  31 + %tr.change-owner-holder.hide
  32 + %td.bgred
  33 + %b.cred
  34 + New Owner:
  35 + %td.bgred
  36 + = form_for @team, url: admin_team_path(@team) do |f|
  37 + = f.select :owner_id, User.all.map { |user| [user.name, user.id] }, {}, {class: 'chosen'}
  38 + %div
  39 + = f.submit 'Change Owner', class: "btn danger"
  40 + = link_to "Cancel", "#", class: "btn change-owner-cancel-link"
  41 +
  42 +%fieldset
  43 + %legend
  44 + Members (#{@team.members.count})
  45 + %span= link_to 'Add members', new_admin_team_member_path(@team), class: "btn success small right", id: :add_members_to_team
  46 + - if @team.members.any?
  47 + %table#members_list
  48 + %thead
  49 + %tr
  50 + %th User name
  51 + %th Default project access
  52 + %th Team access
  53 + %th.cred.span3 Danger Zone!
  54 + - @team.members.each do |member|
  55 + %tr.member{ class: "user_#{member.id}"}
  56 + %td
  57 + = link_to [:admin, member] do
  58 + = member.name
  59 + %small= "(#{member.email})"
  60 + %td= @team.human_default_projects_access(member)
  61 + %td= @team.admin?(member) ? "Admin" : "Member"
  62 + %td.bgred
  63 + = link_to 'Edit', edit_admin_team_member_path(@team, member), class: "btn small"
  64 + &nbsp;
  65 + = link_to 'Remove', admin_team_member_path(@team, member), confirm: 'Remove member from team. Are you sure?', method: :delete, class: "btn danger small", id: "remove_member_#{member.id}"
  66 +
  67 +%fieldset
  68 + %legend
  69 + Projects (#{@team.projects.count})
  70 + %span= link_to 'Add projects', new_admin_team_project_path(@team), class: "btn success small right", id: :assign_projects_to_team
  71 + - if @team.projects.any?
  72 + %table#projects_list
  73 + %thead
  74 + %tr
  75 + %th Project name
  76 + %th Max access
  77 + %th.cred.span3 Danger Zone!
  78 + - @team.projects.each do |project|
  79 + %tr.project
  80 + %td
  81 + = link_to project.name_with_namespace, [:admin, project]
  82 + %td
  83 + %span= @team.human_max_project_access(project)
  84 + %td.bgred
  85 + = link_to 'Edit', edit_admin_team_project_path(@team, project), class: "btn small"
  86 + &nbsp;
  87 + = link_to 'Relegate', admin_team_project_path(@team, project), confirm: 'Remove project from team. Are you sure?', method: :delete, class: "btn danger small", id: "relegate_project_#{project.id}"
  88 +
  89 +:javascript
  90 + $(function(){
  91 + var modal = $('.change-owner-holder');
  92 + $('.change-owner-link').bind("click", function(){
  93 + $(this).hide();
  94 + modal.show();
  95 + });
  96 + $('.change-owner-cancel-link').bind("click", function(){
  97 + modal.hide();
  98 + $('.change-owner-link').show();
  99 + })
  100 + })
  101 +
app/views/dashboard/_groups.html.haml
1 -.groups_box 1 +.ui-box
2 %h5.title 2 %h5.title
3 Groups 3 Groups
4 %small 4 %small
@@ -13,8 +13,6 @@ @@ -13,8 +13,6 @@
13 %li 13 %li
14 = link_to group_path(id: group.path), class: dom_class(group) do 14 = link_to group_path(id: group.path), class: dom_class(group) do
15 %strong.well-title= truncate(group.name, length: 35) 15 %strong.well-title= truncate(group.name, length: 35)
16 - %span.arrow  
17 - &rarr;  
18 - %span.last_activity  
19 - %strong Projects:  
20 - %span= current_user.authorized_projects.where(namespace_id: group.id).count 16 + %span.right.light
  17 + - if group.owner == current_user
  18 + %i.icon-wrench
app/views/dashboard/_sidebar.html.haml
  1 +- if @teams.present?
  2 + = render "teams", teams: @teams
1 - if @groups.present? 3 - if @groups.present?
2 = render "groups", groups: @groups 4 = render "groups", groups: @groups
3 = render "projects", projects: @projects 5 = render "projects", projects: @projects
app/views/dashboard/_teams.html.haml 0 → 100644
@@ -0,0 +1,20 @@ @@ -0,0 +1,20 @@
  1 +.ui-box
  2 + %h5.title
  3 + Teams
  4 + %small
  5 + (#{@teams.count})
  6 + %span.right
  7 + = link_to new_team_path, class: "btn very_small info" do
  8 + %i.icon-plus
  9 + New Team
  10 + %ul.well-list
  11 + - @teams.each do |team|
  12 + %li
  13 + = link_to team_path(id: team.path), class: dom_class(team) do
  14 + %strong.well-title= truncate(team.name, length: 35)
  15 + %span.right.light
  16 + - if team.owner == current_user
  17 + %i.icon-wrench
  18 + - tm = current_user.user_team_user_relationships.find_by_user_team_id(team.id)
  19 + - if tm
  20 + = tm.access_human
app/views/layouts/admin.html.haml
@@ -10,6 +10,8 @@ @@ -10,6 +10,8 @@
10 = link_to "Stats", admin_root_path 10 = link_to "Stats", admin_root_path
11 = nav_link(controller: :projects) do 11 = nav_link(controller: :projects) do
12 = link_to "Projects", admin_projects_path 12 = link_to "Projects", admin_projects_path
  13 + = nav_link(controller: :teams) do
  14 + = link_to "Teams", admin_teams_path
13 = nav_link(controller: :groups) do 15 = nav_link(controller: :groups) do
14 = link_to "Groups", admin_groups_path 16 = link_to "Groups", admin_groups_path
15 = nav_link(controller: :users) do 17 = nav_link(controller: :users) do
app/views/layouts/group.html.haml
@@ -3,7 +3,7 @@ @@ -3,7 +3,7 @@
3 = render "layouts/head", title: "#{@group.name}" 3 = render "layouts/head", title: "#{@group.name}"
4 %body{class: "#{app_theme} application"} 4 %body{class: "#{app_theme} application"}
5 = render "layouts/flash" 5 = render "layouts/flash"
6 - = render "layouts/head_panel", title: "#{@group.name}" 6 + = render "layouts/head_panel", title: "group: #{@group.name}"
7 .container 7 .container
8 %ul.main_menu 8 %ul.main_menu
9 = nav_link(path: 'groups#show', html_options: {class: 'home'}) do 9 = nav_link(path: 'groups#show', html_options: {class: 'home'}) do
app/views/layouts/user_team.html.haml 0 → 100644
@@ -0,0 +1,40 @@ @@ -0,0 +1,40 @@
  1 +!!! 5
  2 +%html{ lang: "en"}
  3 + = render "layouts/head", title: "#{@team.name}"
  4 + %body{class: "#{app_theme} application"}
  5 + = render "layouts/flash"
  6 + = render "layouts/head_panel", title: "team: #{@team.name}"
  7 + .container
  8 + %ul.main_menu
  9 + = nav_link(path: 'teams#show', html_options: {class: 'home'}) do
  10 + = link_to "Home", team_path(@team), title: "Home"
  11 +
  12 + = nav_link(path: 'teams#issues') do
  13 + = link_to issues_team_path(@team) do
  14 + Issues
  15 + %span.count= Issue.opened.of_user_team(@team).count
  16 +
  17 + = nav_link(path: 'teams#merge_requests') do
  18 + = link_to merge_requests_team_path(@team) do
  19 + Merge Requests
  20 + %span.count= MergeRequest.opened.of_user_team(@team).count
  21 +
  22 + = nav_link(path: 'teams#search') do
  23 + = link_to "Search", search_team_path(@team)
  24 +
  25 + = nav_link(controller: [:members]) do
  26 + = link_to team_members_path(@team), class: "team-tab tab" do
  27 + Members
  28 +
  29 + - if can? current_user, :admin_user_team, @team
  30 + = nav_link(controller: [:projects]) do
  31 + = link_to team_projects_path(@team), class: "team-tab tab" do
  32 + %i.icon-briefcase
  33 + Projects
  34 +
  35 + = nav_link(path: 'teams#edit') do
  36 + = link_to edit_team_path(@team), class: "stat-tab tab " do
  37 + %i.icon-edit
  38 + Edit Team
  39 +
  40 + .content= yield
app/views/projects/_project_head.html.haml
@@ -3,7 +3,7 @@ @@ -3,7 +3,7 @@
3 = link_to project_path(@project), class: "activities-tab tab" do 3 = link_to project_path(@project), class: "activities-tab tab" do
4 %i.icon-home 4 %i.icon-home
5 Show 5 Show
6 - = nav_link(controller: :team_members) do 6 + = nav_link(controller: [:team_members, :teams]) do
7 = link_to project_team_index_path(@project), class: "team-tab tab" do 7 = link_to project_team_index_path(@project), class: "team-tab tab" do
8 %i.icon-user 8 %i.icon-user
9 Team 9 Team
app/views/projects/teams/available.html.haml 0 → 100644
@@ -0,0 +1,22 @@ @@ -0,0 +1,22 @@
  1 += render "projects/project_head"
  2 +
  3 +%h3.page_title
  4 + = "Assign project to team of users"
  5 +%hr
  6 +%p.slead
  7 + Read more about assign to team of users #{link_to "here", '#', class: 'vlink'}.
  8 += form_tag assign_project_teams_path(@project), method: 'post' do
  9 + %p.slead Choose Team of users you want to assign:
  10 + .padded
  11 + = label_tag :team_id, "Team"
  12 + .input= select_tag(:team_id, options_from_collection_for_select(@teams, :id, :name), prompt: "Select team", class: "chosen xxlarge", required: true)
  13 + %p.slead Choose greatest user acces in team you want to assign:
  14 + .padded
  15 + = label_tag :team_ids, "Permission"
  16 + .input= select_tag :greatest_project_access, options_for_select(UserTeam.access_roles), {class: "project-access-select chosen span3" }
  17 +
  18 +
  19 + .actions
  20 + = submit_tag 'Assign', class: "btn save-btn"
  21 + = link_to "Cancel", project_team_index_path(@project), class: "btn cancel-btn"
  22 +
app/views/team_members/_form.html.haml
1 %h3.page_title 1 %h3.page_title
2 = "New Team member(s)" 2 = "New Team member(s)"
3 %hr 3 %hr
4 -= form_for @team_member, as: :team_member, url: project_team_members_path(@project, @team_member) do |f|  
5 - -if @team_member.errors.any? 4 += form_for @user_project_relation, as: :team_member, url: project_team_members_path(@project) do |f|
  5 + -if @user_project_relation.errors.any?
6 .alert-message.block-message.error 6 .alert-message.block-message.error
7 %ul 7 %ul
8 - - @team_member.errors.full_messages.each do |msg| 8 + - @user_project_relation.errors.full_messages.each do |msg|
9 %li= msg 9 %li= msg
10 10
11 %h6 1. Choose people you want in the team 11 %h6 1. Choose people you want in the team
@@ -16,7 +16,7 @@ @@ -16,7 +16,7 @@
16 %h6 2. Set access level for them 16 %h6 2. Set access level for them
17 .clearfix 17 .clearfix
18 = f.label :project_access, "Project Access" 18 = f.label :project_access, "Project Access"
19 - .input= select_tag :project_access, options_for_select(Project.access_options, @team_member.project_access), class: "project-access-select chosen" 19 + .input= select_tag :project_access, options_for_select(Project.access_options, @user_project_relation.project_access), class: "project-access-select chosen"
20 20
21 .actions 21 .actions
22 = f.submit 'Save', class: "btn save-btn" 22 = f.submit 'Save', class: "btn save-btn"
app/views/team_members/_show.html.haml
1 - user = member.user 1 - user = member.user
2 - allow_admin = can? current_user, :admin_project, @project 2 - allow_admin = can? current_user, :admin_project, @project
3 -%li{id: dom_id(member), class: "team_member_row user_#{user.id}"} 3 +%li{id: dom_id(user), class: "team_member_row user_#{user.id}"}
4 .row 4 .row
5 .span6 5 .span6
6 - = link_to project_team_member_path(@project, member), title: user.name, class: "dark" do 6 + = link_to project_team_member_path(@project, user), title: user.name, class: "dark" do
7 = image_tag gravatar_icon(user.email, 40), class: "avatar s32" 7 = image_tag gravatar_icon(user.email, 40), class: "avatar s32"
8 - = link_to project_team_member_path(@project, member), title: user.name, class: "dark" do 8 + = link_to project_team_member_path(@project, user), title: user.name, class: "dark" do
9 %strong= truncate(user.name, lenght: 40) 9 %strong= truncate(user.name, lenght: 40)
10 %br 10 %br
11 %small.cgray= user.email 11 %small.cgray= user.email
@@ -13,7 +13,7 @@ @@ -13,7 +13,7 @@
13 .span5.right 13 .span5.right
14 - if allow_admin 14 - if allow_admin
15 .left 15 .left
16 - = form_for(member, as: :team_member, url: project_team_member_path(@project, member)) do |f| 16 + = form_for(member, as: :team_member, url: project_team_member_path(@project, member.user)) do |f|
17 = f.select :project_access, options_for_select(UsersProject.access_roles, member.project_access), {}, class: "medium project-access-select span2" 17 = f.select :project_access, options_for_select(UsersProject.access_roles, member.project_access), {}, class: "medium project-access-select span2"
18 .right 18 .right
19 - if current_user == user 19 - if current_user == user
@@ -23,6 +23,6 @@ @@ -23,6 +23,6 @@
23 - elsif user.blocked 23 - elsif user.blocked
24 %span.btn.disabled.blocked Blocked 24 %span.btn.disabled.blocked Blocked
25 - elsif allow_admin 25 - elsif allow_admin
26 - = link_to project_team_member_path(project_id: @project, id: member.id), confirm: remove_from_team_message(@project, member), method: :delete, class: "very_small btn danger" do 26 + = link_to project_team_member_path(@project, user), confirm: remove_from_project_team_message(@project, user), method: :delete, class: "very_small btn danger" do
27 %i.icon-minus.icon-white 27 %i.icon-minus.icon-white
28 28
app/views/team_members/_show_team.html.haml 0 → 100644
@@ -0,0 +1,15 @@ @@ -0,0 +1,15 @@
  1 +- team = team_rel.user_team
  2 +- allow_admin = can? current_user, :admin_team_member, @project
  3 +%li{id: dom_id(team), class: "user_team_row team_#{team.id}"}
  4 + .row
  5 + .span6
  6 + %strong= link_to team.name, team_path(team), title: team.name, class: "dark"
  7 + %br
  8 + %small.cgray Members: #{team.members.count}
  9 +
  10 + .span5.right
  11 + .right
  12 + - if allow_admin
  13 + .left
  14 + = link_to resign_project_team_path(@project, team), method: :delete, confirm: "Are you shure?", class: "btn danger small" do
  15 + %i.icon-minus.icon-white
app/views/team_members/_teams.html.haml 0 → 100644
@@ -0,0 +1,16 @@ @@ -0,0 +1,16 @@
  1 +- grouper_project_teams(@project).each do |access, teams|
  2 + .ui-box
  3 + %h5.title
  4 + = UserTeam.access_roles.key(access).pluralize
  5 + %small= teams.size
  6 + %ul.well-list
  7 + - teams.sort_by(&:team_name).each do |tofr|
  8 + = render(partial: 'team_members/show_team', locals: {team_rel: tofr})
  9 +
  10 +
  11 +:javascript
  12 + $(function(){
  13 + $('.repo-access-select, .project-access-select').live("change", function() {
  14 + $(this.form).submit();
  15 + });
  16 + })
app/views/team_members/create.js.haml
1 -- if @team_member.valid? 1 +- if @user_project_relation.valid?
2 :plain 2 :plain
3 $("#new_team_member").hide("slide", { direction: "right" }, 150, function(){ 3 $("#new_team_member").hide("slide", { direction: "right" }, 150, function(){
4 $("#team-table").show("slide", { direction: "left" }, 150, function() { 4 $("#team-table").show("slide", { direction: "left" }, 150, function() {
app/views/team_members/import.html.haml
@@ -4,7 +4,7 @@ @@ -4,7 +4,7 @@
4 = "Import team from another project" 4 = "Import team from another project"
5 %hr 5 %hr
6 %p.slead 6 %p.slead
7 - Read more about team import #{link_to "here", '#', class: 'vlink'}. 7 + Read more about project team import #{link_to "here", '#', class: 'vlink'}.
8 = form_tag apply_import_project_team_members_path(@project), method: 'post' do 8 = form_tag apply_import_project_team_members_path(@project), method: 'post' do
9 %p.slead Choose project you want to use as team source: 9 %p.slead Choose project you want to use as team source:
10 .padded 10 .padded
app/views/team_members/index.html.haml
1 = render "projects/project_head" 1 = render "projects/project_head"
2 %h3.page_title 2 %h3.page_title
3 Team Members 3 Team Members
4 - (#{@project.users_projects.count}) 4 + (#{@project.users.count})
5 %small 5 %small
6 Read more about project permissions 6 Read more about project permissions
7 %strong= link_to "here", help_permissions_path, class: "vlink" 7 %strong= link_to "here", help_permissions_path, class: "vlink"
@@ -10,11 +10,24 @@ @@ -10,11 +10,24 @@
10 %span.right 10 %span.right
11 = link_to import_project_team_members_path(@project), class: "btn small grouped", title: "Import team from another project" do 11 = link_to import_project_team_members_path(@project), class: "btn small grouped", title: "Import team from another project" do
12 Import team from another project 12 Import team from another project
  13 + = link_to available_project_teams_path(@project), class: "btn small grouped", title: "Assign project to team of users" do
  14 + Assign project to Team of users
13 = link_to new_project_team_member_path(@project), class: "btn success small grouped", title: "New Team Member" do 15 = link_to new_project_team_member_path(@project), class: "btn success small grouped", title: "New Team Member" do
14 New Team Member 16 New Team Member
15 -%hr  
16 17
  18 +%hr
17 19
18 .clearfix 20 .clearfix
19 %div.team-table 21 %div.team-table
20 = render partial: "team_members/team", locals: {project: @project} 22 = render partial: "team_members/team", locals: {project: @project}
  23 +
  24 +
  25 +%h3.page_title
  26 + Assigned teams
  27 + (#{@project.user_teams.count})
  28 +
  29 +%hr
  30 +
  31 +.clearfix
  32 +%div.team-table
  33 + = render partial: "team_members/teams", locals: {project: @project}
app/views/team_members/show.html.haml
1 - allow_admin = can? current_user, :admin_project, @project 1 - allow_admin = can? current_user, :admin_project, @project
2 -- user = @team_member.user  
3 2
4 .team_member_show 3 .team_member_show
5 - if can? current_user, :admin_project, @project 4 - if can? current_user, :admin_project, @project
6 - = link_to 'Remove from team', project_team_member_path(project_id: @project, id: @team_member.id), confirm: 'Are you sure?', method: :delete, class: "right btn danger" 5 + = link_to 'Remove from team', project_team_member_path(@project, @member), confirm: 'Are you sure?', method: :delete, class: "right btn danger"
7 .profile_avatar_holder 6 .profile_avatar_holder
8 - = image_tag gravatar_icon(user.email, 60), class: "borders" 7 + = image_tag gravatar_icon(@member.email, 60), class: "borders"
9 %h3.page_title 8 %h3.page_title
10 - = user.name  
11 - %small (@#{user.username}) 9 + = @member.name
  10 + %small (@#{@member.username})
12 11
13 %hr 12 %hr
14 .back_link 13 .back_link
@@ -21,34 +20,34 @@ @@ -21,34 +20,34 @@
21 %table.lite 20 %table.lite
22 %tr 21 %tr
23 %td Email 22 %td Email
24 - %td= mail_to user.email 23 + %td= mail_to @member.email
25 %tr 24 %tr
26 %td Skype 25 %td Skype
27 - %td= user.skype  
28 - - unless user.linkedin.blank? 26 + %td= @member.skype
  27 + - unless @member.linkedin.blank?
29 %tr 28 %tr
30 %td LinkedIn 29 %td LinkedIn
31 - %td= user.linkedin  
32 - - unless user.twitter.blank? 30 + %td= @member.linkedin
  31 + - unless @member.twitter.blank?
33 %tr 32 %tr
34 %td Twitter 33 %td Twitter
35 - %td= user.twitter  
36 - - unless user.bio.blank? 34 + %td= @member.twitter
  35 + - unless @member.bio.blank?
37 %tr 36 %tr
38 %td Bio 37 %td Bio
39 - %td= user.bio 38 + %td= @member.bio
40 .span6 39 .span6
41 %table.lite 40 %table.lite
42 %tr 41 %tr
43 %td Member since 42 %td Member since
44 - %td= @team_member.created_at.stamp("Aug 21, 2011") 43 + %td= @user_project_relation.created_at.stamp("Aug 21, 2011")
45 %tr 44 %tr
46 %td 45 %td
47 Project Access: 46 Project Access:
48 %small (#{link_to "read more", help_permissions_path, class: "vlink"}) 47 %small (#{link_to "read more", help_permissions_path, class: "vlink"})
49 %td 48 %td
50 - = form_for(@team_member, as: :team_member, url: project_team_member_path(@project, @team_member)) do |f|  
51 - = f.select :project_access, options_for_select(Project.access_options, @team_member.project_access), {}, class: "project-access-select", disabled: !allow_admin 49 + = form_for(@user_project_relation, as: :team_member, url: project_team_member_path(@project, @member)) do |f|
  50 + = f.select :project_access, options_for_select(Project.access_options, @user_project_relation.project_access), {}, class: "project-access-select", disabled: !allow_admin
52 %hr 51 %hr
53 = render @events 52 = render @events
54 :javascript 53 :javascript
app/views/team_members/update.js.haml
1 -- if @team_member.valid? 1 +- if @user_project_relation.valid?
2 :plain 2 :plain
3 - $("##{dom_id(@team_member)}").effect("highlight", {color: "#529214"}, 1000);; 3 + $("##{dom_id(@user_project_relation)}").effect("highlight", {color: "#529214"}, 1000);;
4 - else 4 - else
5 :plain 5 :plain
6 - $("##{dom_id(@team_member)}").effect("highlight", {color: "#D12F19"}, 1000);; 6 + $("##{dom_id(@user_project_relation)}").effect("highlight", {color: "#D12F19"}, 1000);;
app/views/teams/_filter.html.haml 0 → 100644
@@ -0,0 +1,33 @@ @@ -0,0 +1,33 @@
  1 += form_tag team_filter_path(entity), method: 'get' do
  2 + %fieldset.dashboard-search-filter
  3 + = search_field_tag "search", params[:search], { placeholder: 'Search', class: 'search-text-input' }
  4 + = button_tag type: 'submit', class: 'btn' do
  5 + %i.icon-search
  6 +
  7 + %fieldset
  8 + %legend Status:
  9 + %ul.nav.nav-pills.nav-stacked
  10 + %li{class: ("active" if !params[:status])}
  11 + = link_to team_filter_path(entity, status: nil) do
  12 + Open
  13 + %li{class: ("active" if params[:status] == 'closed')}
  14 + = link_to team_filter_path(entity, status: 'closed') do
  15 + Closed
  16 + %li{class: ("active" if params[:status] == 'all')}
  17 + = link_to team_filter_path(entity, status: 'all') do
  18 + All
  19 +
  20 + %fieldset
  21 + %legend Projects:
  22 + %ul.nav.nav-pills.nav-stacked
  23 + - @projects.each do |project|
  24 + - unless entities_per_project(project, entity).zero?
  25 + %li{class: ("active" if params[:project_id] == project.id.to_s)}
  26 + = link_to team_filter_path(entity, project_id: project.id) do
  27 + = project.name_with_namespace
  28 + %small.right= entities_per_project(project, entity)
  29 +
  30 + %fieldset
  31 + %hr
  32 + = link_to "Reset", team_filter_path(entity), class: 'btn right'
  33 +
app/views/teams/_projects.html.haml 0 → 100644
@@ -0,0 +1,22 @@ @@ -0,0 +1,22 @@
  1 +.projects_box
  2 + %h5.title
  3 + Projects
  4 + %small
  5 + (#{projects.count})
  6 + - if can? current_user, :manage_group, @group
  7 + %span.right
  8 + = link_to new_project_path(namespace_id: @group.id), class: "btn very_small info" do
  9 + %i.icon-plus
  10 + New Project
  11 + %ul.well-list
  12 + - if projects.blank?
  13 + %p.nothing_here_message This team has no projects yet
  14 + - projects.each do |project|
  15 + %li
  16 + = link_to project_path(project), class: dom_class(project) do
  17 + %strong.well-title= truncate(project.name, length: 25)
  18 + %span.arrow
  19 + &rarr;
  20 + %span.last_activity
  21 + %strong Last activity:
  22 + %span= project_last_activity(project)
app/views/teams/_team_head.html.haml 0 → 100644
app/views/teams/edit.html.haml 0 → 100644
@@ -0,0 +1,24 @@ @@ -0,0 +1,24 @@
  1 += render "team_head"
  2 +
  3 +%h3.page_title= "Edit Team #{@team.name}"
  4 +%hr
  5 += form_for @team, url: teams_path do |f|
  6 + - if @team.errors.any?
  7 + .alert-message.block-message.error
  8 + %span= @team.errors.full_messages.first
  9 + .clearfix
  10 + = f.label :name do
  11 + Team name is
  12 + .input
  13 + = f.text_field :name, placeholder: "Ex. OpenSource", class: "xxlarge left"
  14 +
  15 + .clearfix
  16 + = f.label :path do
  17 + Team path is
  18 + .input
  19 + = f.text_field :path, placeholder: "opensource", class: "xxlarge left"
  20 + .clearfix
  21 + .input.span3.center
  22 + = f.submit 'Save team changes', class: "btn primary"
  23 + .input.span3.center
  24 + = link_to 'Delete team', team_path(@team), method: :delete, confirm: "You are shure?", class: "btn danger"
app/views/teams/index.html.haml 0 → 100644
@@ -0,0 +1,38 @@ @@ -0,0 +1,38 @@
  1 +%h3.page_title
  2 + Teams
  3 + %small
  4 + list of all teams
  5 +
  6 + = link_to 'New Team', new_team_path, class: "btn success small right"
  7 + %br
  8 +
  9 += form_tag search_teams_path, method: :get, class: 'form-inline' do
  10 + = text_field_tag :name, params[:name], class: "xlarge"
  11 + = submit_tag "Search", class: "btn submit primary"
  12 +
  13 +%table.teams_list
  14 + %thead
  15 + %tr
  16 + %th
  17 + Name
  18 + %i.icon-sort-down
  19 + %th Path
  20 + %th Projects
  21 + %th Members
  22 + %th Owner
  23 + %th.cred Danger Zone!
  24 +
  25 + - @teams.each do |team|
  26 + %tr
  27 + %td
  28 + %strong= link_to team.name, team_path(team)
  29 + %td= team.path
  30 + %td= link_to team.projects.count, team_projects_path(team)
  31 + %td= link_to team.members.count, team_members_path(team)
  32 + %td= link_to team.owner.name, team_member_path(team, team.owner)
  33 + %td.bgred
  34 + - if current_user.can?(:manage_user_team, team)
  35 + = link_to "Edit", edit_team_path(team), class: "btn small"
  36 + - if current_user.can?(:admin_user_team, team)
  37 + = link_to "Destroy", team_path(team), method: :delete, confirm: "You are shure?", class: "danger btn small"
  38 + &nbsp;
app/views/teams/issues.html.haml 0 → 100644
@@ -0,0 +1,25 @@ @@ -0,0 +1,25 @@
  1 += render "team_head"
  2 +
  3 +%h3.page_title
  4 + Issues
  5 + %small (in Team projects assigned to Team members)
  6 + %small.right #{@issues.total_count} issues
  7 +
  8 +%hr
  9 +.row
  10 + .span3
  11 + = render 'filter', entity: 'issue'
  12 + .span9
  13 + - if @issues.any?
  14 + - @issues.group_by(&:project).each do |group|
  15 + %div.ui-box
  16 + - @project = group[0]
  17 + %h5.title
  18 + = link_to_project @project
  19 + %ul.well-list.issues_table
  20 + - group[1].each do |issue|
  21 + = render(partial: 'issues/show', locals: {issue: issue})
  22 + %hr
  23 + = paginate @issues, theme: "gitlab"
  24 + - else
  25 + %p.nothing_here_message Nothing to show here
app/views/teams/members/_form.html.haml 0 → 100644
@@ -0,0 +1,20 @@ @@ -0,0 +1,20 @@
  1 += form_tag admin_team_member_path(@team, @member), method: :put do
  2 + -if @member.errors.any?
  3 + .alert-message.block-message.error
  4 + %ul
  5 + - @member.errors.full_messages.each do |msg|
  6 + %li= msg
  7 +
  8 + .clearfix
  9 + %label Default access for Team projects:
  10 + .input
  11 + = select_tag :default_project_access, options_for_select(UserTeam.access_roles, @team.default_projects_access(@member)), class: "project-access-select chosen span3"
  12 + .clearfix
  13 + %label Team admin?
  14 + .input
  15 + = check_box_tag :group_admin, true, @team.admin?(@member)
  16 +
  17 + %br
  18 + .actions
  19 + = submit_tag 'Save', class: "btn primary"
  20 + = link_to 'Cancel', :back, class: "btn"
app/views/teams/members/_show.html.haml 0 → 100644
@@ -0,0 +1,31 @@ @@ -0,0 +1,31 @@
  1 +- user = member.user
  2 +- allow_admin = can? current_user, :manage_user_team, @team
  3 +%li{id: dom_id(member), class: "team_member_row user_#{user.id}"}
  4 + .row
  5 + .span5
  6 + = link_to user_path(user.username), title: user.name, class: "dark" do
  7 + = image_tag gravatar_icon(user.email, 40), class: "avatar s32"
  8 + = link_to user_path(user.username), title: user.name, class: "dark" do
  9 + %strong= truncate(user.name, lenght: 40)
  10 + %br
  11 + %small.cgray= user.email
  12 +
  13 + .span6.right
  14 + - if allow_admin
  15 + .left.span2
  16 + = form_for(member, as: :team_member, url: team_member_path(@team, user)) do |f|
  17 + = f.select :permission, options_for_select(UsersProject.access_roles, @team.default_projects_access(user)), {}, class: "medium project-access-select span2"
  18 + .left.span2
  19 + %span
  20 + Admin access
  21 + = check_box_tag :group_admin
  22 + .right
  23 + - if current_user == user
  24 + %span.btn.disabled This is you!
  25 + - if @team.owner == user
  26 + %span.btn.disabled.success Owner
  27 + - elsif user.blocked
  28 + %span.btn.disabled.blocked Blocked
  29 + - elsif allow_admin
  30 + = link_to team_member_path(@team, user), confirm: remove_from_user_team_message(@team, user), method: :delete, class: "very_small btn danger" do
  31 + %i.icon-minus.icon-white
app/views/teams/members/_team.html.haml 0 → 100644
@@ -0,0 +1,16 @@ @@ -0,0 +1,16 @@
  1 +- grouped_user_team_members(@team).each do |access, members|
  2 + .ui-box
  3 + %h5.title
  4 + = Project.access_options.key(access).pluralize
  5 + %small= members.size
  6 + %ul.well-list
  7 + - members.sort_by(&:user_name).each do |up|
  8 + = render(partial: 'teams/members/show', locals: {member: up})
  9 +
  10 +
  11 +:javascript
  12 + $(function(){
  13 + $('.repo-access-select, .project-access-select').live("change", function() {
  14 + $(this.form).submit();
  15 + });
  16 + })
app/views/teams/members/edit.html.haml 0 → 100644
@@ -0,0 +1,18 @@ @@ -0,0 +1,18 @@
  1 += render "teams/team_head"
  2 +
  3 +%h3
  4 + Edit access #{@member.name} in #{@team.name} team
  5 +
  6 +%hr
  7 +%table.zebra-striped
  8 + %tr
  9 + %td User:
  10 + %td= @member.name
  11 + %tr
  12 + %td Team:
  13 + %td= @team.name
  14 + %tr
  15 + %td Since:
  16 + %td= member_since(@team, @member).stamp("Nov 11, 2010")
  17 +
  18 += render 'form'
app/views/teams/members/index.html.haml 0 → 100644
@@ -0,0 +1,19 @@ @@ -0,0 +1,19 @@
  1 += render "teams/team_head"
  2 +
  3 +%h3.page_title
  4 + Team Members
  5 + (#{@members.count})
  6 + %small
  7 + Read more about project permissions
  8 + %strong= link_to "here", help_permissions_path, class: "vlink"
  9 +
  10 + - if can? current_user, :manage_user_team, @team
  11 + %span.right
  12 + = link_to new_team_member_path(@team), class: "btn success small grouped", title: "New Team Member" do
  13 + New Team Member
  14 +%hr
  15 +
  16 +
  17 +.clearfix
  18 +%div.team-table
  19 + = render partial: "teams/members/team", locals: {project: @team}
app/views/teams/members/new.html.haml 0 → 100644
@@ -0,0 +1,30 @@ @@ -0,0 +1,30 @@
  1 += render "teams/team_head"
  2 +
  3 +%h3.page_title
  4 + Team: #{@team.name}
  5 +
  6 +%fieldset
  7 + %legend Members (#{@team.members.count})
  8 + = form_tag team_members_path(@team), id: "team_members", class: "bulk_import", method: :post do
  9 + %table#members_list
  10 + %thead
  11 + %tr
  12 + %th User name
  13 + %th Default project access
  14 + %th Team access
  15 + %th
  16 + - @team.members.each do |member|
  17 + %tr.member
  18 + %td
  19 + = member.name
  20 + %small= "(#{member.email})"
  21 + %td= @team.human_default_projects_access(member)
  22 + %td= @team.admin?(member) ? "Admin" : "Member"
  23 + %td
  24 + %tr
  25 + %td= select_tag :user_ids, options_from_collection_for_select(@users , :id, :name_with_email), multiple: true, data: {placeholder: 'Select users'}, class: 'chosen span5'
  26 + %td= select_tag :default_project_access, options_for_select(Project.access_options), {class: "project-access-select chosen span3" }
  27 + %td
  28 + %span= check_box_tag :group_admin
  29 + %span Admin?
  30 + %td= submit_tag 'Add', class: "btn primary", id: :add_members_to_team
app/views/teams/members/show.html.haml 0 → 100644
@@ -0,0 +1,62 @@ @@ -0,0 +1,62 @@
  1 += render "teams/team_head"
  2 +
  3 +- allow_admin = can? current_user, :admin_project, @project
  4 +- user = @team_member.user
  5 +
  6 +.team_member_show
  7 + - if can? current_user, :admin_project, @project
  8 + = link_to 'Remove from team', project_team_member_path(project_id: @project, id: @team_member.id), confirm: 'Are you sure?', method: :delete, class: "right btn danger"
  9 + .profile_avatar_holder
  10 + = image_tag gravatar_icon(user.email, 60), class: "borders"
  11 + %h3.page_title
  12 + = user.name
  13 + %small (@#{user.username})
  14 +
  15 + %hr
  16 + .back_link
  17 + %br
  18 + = link_to project_team_index_path(@project), class: "" do
  19 + &larr; To team list
  20 + %br
  21 + .row
  22 + .span6
  23 + %table.lite
  24 + %tr
  25 + %td Email
  26 + %td= mail_to user.email
  27 + %tr
  28 + %td Skype
  29 + %td= user.skype
  30 + - unless user.linkedin.blank?
  31 + %tr
  32 + %td LinkedIn
  33 + %td= user.linkedin
  34 + - unless user.twitter.blank?
  35 + %tr
  36 + %td Twitter
  37 + %td= user.twitter
  38 + - unless user.bio.blank?
  39 + %tr
  40 + %td Bio
  41 + %td= user.bio
  42 + .span6
  43 + %table.lite
  44 + %tr
  45 + %td Member since
  46 + %td= @team_member.created_at.stamp("Aug 21, 2011")
  47 + %tr
  48 + %td
  49 + Project Access:
  50 + %small (#{link_to "read more", help_permissions_path, class: "vlink"})
  51 + %td
  52 + = form_for(@team_member, as: :team_member, url: project_team_member_path(@project, @team_member)) do |f|
  53 + = f.select :project_access, options_for_select(Project.access_options, @team_member.project_access), {}, class: "project-access-select", disabled: !allow_admin
  54 + %hr
  55 + = render @events
  56 +:javascript
  57 + $(function(){
  58 + $('.repo-access-select, .project-access-select').live("change", function() {
  59 + $(this.form).submit();
  60 + });
  61 + })
  62 +
app/views/teams/merge_requests.html.haml 0 → 100644
@@ -0,0 +1,26 @@ @@ -0,0 +1,26 @@
  1 += render "team_head"
  2 +
  3 +%h3.page_title
  4 + Merge Requests
  5 + %small (authored by or assigned to Team members)
  6 + %small.right #{@merge_requests.total_count} merge requests
  7 +
  8 +%hr
  9 +.row
  10 + .span3
  11 + = render 'filter', entity: 'merge_request'
  12 + .span9
  13 + - if @merge_requests.any?
  14 + - @merge_requests.group_by(&:project).each do |group|
  15 + .ui-box
  16 + - @project = group[0]
  17 + %h5.title
  18 + = link_to_project @project
  19 + %ul.well-list
  20 + - group[1].each do |merge_request|
  21 + = render(partial: 'merge_requests/merge_request', locals: {merge_request: merge_request})
  22 + %hr
  23 + = paginate @merge_requests, theme: "gitlab"
  24 +
  25 + - else
  26 + %h3.nothing_here_message Nothing to show here
app/views/teams/new.html.haml 0 → 100644
@@ -0,0 +1,19 @@ @@ -0,0 +1,19 @@
  1 +%h3.page_title New Team
  2 +%hr
  3 += form_for @team, url: teams_path do |f|
  4 + - if @team.errors.any?
  5 + .alert-message.block-message.error
  6 + %span= @team.errors.full_messages.first
  7 + .clearfix
  8 + = f.label :name do
  9 + Team name is
  10 + .input
  11 + = f.text_field :name, placeholder: "Ex. Ruby Developers", class: "xxlarge left"
  12 + &nbsp;
  13 + = f.submit 'Create team', class: "btn primary"
  14 + %hr
  15 + .padded
  16 + %ul
  17 + %li All created teams are public (users can view who enter into team and which project are assigned for this team)
  18 + %li People within a team see only projects they have access to
  19 + %li You will be able to assign existing projects for team
app/views/teams/projects/_form.html.haml 0 → 100644
@@ -0,0 +1,16 @@ @@ -0,0 +1,16 @@
  1 += form_tag team_project_path(@team, @project), method: :put do
  2 + -if @project.errors.any?
  3 + .alert-message.block-message.error
  4 + %ul
  5 + - @project.errors.full_messages.each do |msg|
  6 + %li= msg
  7 +
  8 + .clearfix
  9 + %label Max access for Team members:
  10 + .input
  11 + = select_tag :greatest_project_access, options_for_select(UserTeam.access_roles, @team.max_project_access(@project)), class: "project-access-select chosen span3"
  12 +
  13 + %br
  14 + .actions
  15 + = submit_tag 'Save', class: "btn primary"
  16 + = link_to 'Cancel', :back, class: "btn"
app/views/teams/projects/edit.html.haml 0 → 100644
@@ -0,0 +1,18 @@ @@ -0,0 +1,18 @@
  1 += render "teams/team_head"
  2 +
  3 +%h3
  4 + Edit max access in #{@project.name} for #{@team.name} team
  5 +
  6 +%hr
  7 +%table.zebra-striped
  8 + %tr
  9 + %td Project:
  10 + %td= @project.name
  11 + %tr
  12 + %td Team:
  13 + %td= @team.name
  14 + %tr
  15 + %td Since:
  16 + %td= assigned_since(@team, @project).stamp("Nov 11, 2010")
  17 +
  18 += render 'form'
app/views/teams/projects/index.html.haml 0 → 100644
@@ -0,0 +1,34 @@ @@ -0,0 +1,34 @@
  1 += render "teams/team_head"
  2 +
  3 +%h3.page_title
  4 + Assigned projects (#{@team.projects.count})
  5 + %small
  6 + Read more about project permissions
  7 + %strong= link_to "here", help_permissions_path, class: "vlink"
  8 +
  9 + - if current_user.can?(:manage_user_team, @team) && @avaliable_projects.any?
  10 + %span.right
  11 + = link_to new_team_project_path(@team), class: "btn success small grouped", title: "New Team Member" do
  12 + Assign project to Team
  13 +
  14 +%hr
  15 +
  16 +%table.projects-table
  17 + %thead
  18 + %tr
  19 + %th Project name
  20 + %th Max access
  21 + - if current_user.can?(:admin_user_team, @team)
  22 + %th.span3
  23 +
  24 + - @team.projects.each do |project|
  25 + %tr.project
  26 + %td
  27 + = link_to project.name_with_namespace, project_path(project)
  28 + %td
  29 + %span= @team.human_max_project_access(project)
  30 +
  31 + - if current_user.can?(:admin_user_team, @team)
  32 + %td.bgred
  33 + = link_to 'Edit max access', edit_team_project_path(@team, project), class: "btn small"
  34 + = link_to 'Relegate', team_project_path(@team, project), confirm: 'Remove project from team and move to global namespace. Are you sure?', method: :delete, class: "btn danger small"
app/views/teams/projects/new.html.haml 0 → 100644
@@ -0,0 +1,25 @@ @@ -0,0 +1,25 @@
  1 += render "teams/team_head"
  2 +
  3 +%h3.page_title
  4 + Team: #{@team.name}
  5 +
  6 +%fieldset
  7 + %legend Projects (#{@team.projects.count})
  8 + = form_tag team_projects_path(@team), id: "assign_projects", class: "bulk_import", method: :post do
  9 + %table#projects_list
  10 + %thead
  11 + %tr
  12 + %th Project name
  13 + %th Max access
  14 + %th
  15 + - @team.projects.each do |project|
  16 + %tr.project
  17 + %td
  18 + = link_to project.name_with_namespace, team_project_path(@team, project)
  19 + %td
  20 + %span= @team.human_max_project_access(project)
  21 + %td
  22 + %tr
  23 + %td= select_tag :project_ids, options_from_collection_for_select(@avaliable_projects , :id, :name_with_namespace), multiple: true, data: {placeholder: 'Select projects'}, class: 'chosen span5'
  24 + %td= select_tag :greatest_project_access, options_for_select(UserTeam.access_roles), {class: "project-access-select chosen span3" }
  25 + %td= submit_tag 'Add', class: "btn primary", id: :assign_projects_to_team
app/views/teams/search.html.haml 0 → 100644
@@ -0,0 +1,11 @@ @@ -0,0 +1,11 @@
  1 += render "team_head"
  2 +
  3 += form_tag search_team_path(@team), method: :get, class: 'form-inline' do |f|
  4 + .padded
  5 + = label_tag :search do
  6 + %strong Looking for
  7 + .input
  8 + = search_field_tag :search, params[:search], placeholder: "issue 143", class: "input-xxlarge search-text-input", id: "dashboard_search"
  9 + = submit_tag 'Search', class: "btn primary wide"
  10 +- if params[:search].present?
  11 + = render 'search/result'
app/views/teams/show.html.haml 0 → 100644
@@ -0,0 +1,30 @@ @@ -0,0 +1,30 @@
  1 += render "team_head"
  2 +
  3 +.projects
  4 + .activities.span8
  5 + = link_to dashboard_path, class: 'btn very_small' do
  6 + &larr; To dashboard
  7 + &nbsp;
  8 + %span.cgray Events and projects are filtered in scope of team
  9 + %hr
  10 + - if @events.any?
  11 + .content_list
  12 + - else
  13 + %p.nothing_here_message Projects activity will be displayed here
  14 + .loading.hide
  15 + .side.span4
  16 + = render "projects", projects: @projects
  17 + %div
  18 + %span.rss-icon
  19 + = link_to dashboard_path(:atom, { private_token: current_user.private_token }) do
  20 + = image_tag "rss_ui.png", title: "feed"
  21 + %strong News Feed
  22 +
  23 + %hr
  24 + .gitlab-promo
  25 + = link_to "Homepage", "http://gitlabhq.com"
  26 + = link_to "Blog", "http://blog.gitlabhq.com"
  27 + = link_to "@gitlabhq", "https://twitter.com/gitlabhq"
  28 +
  29 +:javascript
  30 + $(function(){ Pager.init(20, true); });
config/routes.rb
@@ -21,7 +21,7 @@ Gitlab::Application.routes.draw do @@ -21,7 +21,7 @@ Gitlab::Application.routes.draw do
21 project_root: Gitlab.config.gitolite.repos_path, 21 project_root: Gitlab.config.gitolite.repos_path,
22 upload_pack: Gitlab.config.gitolite.upload_pack, 22 upload_pack: Gitlab.config.gitolite.upload_pack,
23 receive_pack: Gitlab.config.gitolite.receive_pack 23 receive_pack: Gitlab.config.gitolite.receive_pack
24 - }), at: '/', constraints: lambda { |request| /[-\/\w\.-]+\.git\//.match(request.path_info) } 24 + }), at: '/', constraints: lambda { |request| /[-\/\w\.]+\.git\//.match(request.path_info) }
25 25
26 # 26 #
27 # Help 27 # Help
@@ -56,6 +56,7 @@ Gitlab::Application.routes.draw do @@ -56,6 +56,7 @@ Gitlab::Application.routes.draw do
56 put :unblock 56 put :unblock
57 end 57 end
58 end 58 end
  59 +
59 resources :groups, constraints: { id: /[^\/]+/ } do 60 resources :groups, constraints: { id: /[^\/]+/ } do
60 member do 61 member do
61 put :project_update 62 put :project_update
@@ -63,18 +64,31 @@ Gitlab::Application.routes.draw do @@ -63,18 +64,31 @@ Gitlab::Application.routes.draw do
63 delete :remove_project 64 delete :remove_project
64 end 65 end
65 end 66 end
66 - resources :projects, constraints: { id: /[a-zA-Z.\/0-9_\-]+/ }, except: [:new, :create] do  
67 - member do  
68 - get :team  
69 - put :team_update 67 +
  68 + resources :teams, constraints: { id: /[^\/]+/ } do
  69 + scope module: :teams do
  70 + resources :members, only: [:edit, :update, :destroy, :new, :create]
  71 + resources :projects, only: [:edit, :update, :destroy, :new, :create], constraints: { id: /[a-zA-Z.\/0-9_\-]+/ }
70 end 72 end
71 end 73 end
72 - resources :team_members, only: [:edit, :update, :destroy] 74 +
73 resources :hooks, only: [:index, :create, :destroy] do 75 resources :hooks, only: [:index, :create, :destroy] do
74 get :test 76 get :test
75 end 77 end
  78 +
76 resource :logs, only: [:show] 79 resource :logs, only: [:show]
77 resource :resque, controller: 'resque', only: [:show] 80 resource :resque, controller: 'resque', only: [:show]
  81 +
  82 + resources :projects, constraints: { id: /[a-zA-Z.\/0-9_\-]+/ }, except: [:new, :create] do
  83 + member do
  84 + get :team
  85 + put :team_update
  86 + end
  87 + scope module: :projects, constraints: { id: /[^\/]+/ } do
  88 + resources :members, only: [:edit, :update, :destroy]
  89 + end
  90 + end
  91 +
78 root to: "dashboard#index" 92 root to: "dashboard#index"
79 end 93 end
80 94
@@ -108,7 +122,6 @@ Gitlab::Application.routes.draw do @@ -108,7 +122,6 @@ Gitlab::Application.routes.draw do
108 get "dashboard/issues" => "dashboard#issues" 122 get "dashboard/issues" => "dashboard#issues"
109 get "dashboard/merge_requests" => "dashboard#merge_requests" 123 get "dashboard/merge_requests" => "dashboard#merge_requests"
110 124
111 -  
112 # 125 #
113 # Groups Area 126 # Groups Area
114 # 127 #
@@ -122,6 +135,24 @@ Gitlab::Application.routes.draw do @@ -122,6 +135,24 @@ Gitlab::Application.routes.draw do
122 end 135 end
123 end 136 end
124 137
  138 + #
  139 + # Teams Area
  140 + #
  141 + resources :teams, constraints: { id: /[^\/]+/ } do
  142 + member do
  143 + get :issues
  144 + get :merge_requests
  145 + get :search
  146 + end
  147 + scope module: :teams do
  148 + resources :members, only: [:index, :new, :create, :edit, :update, :destroy]
  149 + resources :projects, only: [:index, :new, :create, :edit, :update, :destroy], constraints: { id: /[a-zA-Z.0-9_\-\/]+/ }
  150 + end
  151 + collection do
  152 + get :search
  153 + end
  154 + end
  155 +
125 resources :projects, constraints: { id: /[^\/]+/ }, only: [:new, :create] 156 resources :projects, constraints: { id: /[^\/]+/ }, only: [:new, :create]
126 157
127 devise_for :users, controllers: { omniauth_callbacks: :omniauth_callbacks, registrations: :registrations } 158 devise_for :users, controllers: { omniauth_callbacks: :omniauth_callbacks, registrations: :registrations }
@@ -238,6 +269,18 @@ Gitlab::Application.routes.draw do @@ -238,6 +269,18 @@ Gitlab::Application.routes.draw do
238 end 269 end
239 end 270 end
240 271
  272 + scope module: :projects do
  273 + resources :teams, only: [] do
  274 + collection do
  275 + get :available
  276 + post :assign
  277 + end
  278 + member do
  279 + delete :resign
  280 + end
  281 + end
  282 + end
  283 +
241 resources :notes, only: [:index, :create, :destroy] do 284 resources :notes, only: [:index, :create, :destroy] do
242 collection do 285 collection do
243 post :preview 286 post :preview
db/migrate/20121219183753_create_user_teams.rb 0 → 100644
@@ -0,0 +1,11 @@ @@ -0,0 +1,11 @@
  1 +class CreateUserTeams < ActiveRecord::Migration
  2 + def change
  3 + create_table :user_teams do |t|
  4 + t.string :name
  5 + t.string :path
  6 + t.integer :owner_id
  7 +
  8 + t.timestamps
  9 + end
  10 + end
  11 +end
db/migrate/20121220064104_create_user_team_project_relationships.rb 0 → 100644
@@ -0,0 +1,11 @@ @@ -0,0 +1,11 @@
  1 +class CreateUserTeamProjectRelationships < ActiveRecord::Migration
  2 + def change
  3 + create_table :user_team_project_relationships do |t|
  4 + t.integer :project_id
  5 + t.integer :user_team_id
  6 + t.integer :greatest_access
  7 +
  8 + t.timestamps
  9 + end
  10 + end
  11 +end