Commit 30d2d2381f608b7c926747d61a9231aa417ea5da

Authored by Dmitriy Zaporozhets
2 parents cc5440e8 60bb3516

Merge branch 'feature/users_groups' into 6-0-dev

Showing 41 changed files with 396 additions and 252 deletions   Show diff stats
app/assets/javascripts/dispatcher.js.coffee
... ... @@ -32,6 +32,8 @@ class Dispatcher
32 32 new Wall(project_id)
33 33 when 'teams:members:index'
34 34 new TeamMembers()
  35 + when 'groups:people'
  36 + new GroupMembers()
35 37  
36 38 switch path.first()
37 39 when 'admin' then new Admin()
... ...
app/assets/javascripts/groups.js.coffee 0 → 100644
... ... @@ -0,0 +1,6 @@
  1 +class GroupMembers
  2 + constructor: ->
  3 + $('li.users_group').bind 'ajax:success', ->
  4 + $(this).fadeOut()
  5 +
  6 +@GroupMembers = GroupMembers
... ...
app/assets/stylesheets/gitlab_bootstrap/lists.scss
... ... @@ -16,6 +16,12 @@
16 16 color: #888;
17 17 }
18 18  
  19 + &.unstyled {
  20 + &:hover {
  21 + background: none;
  22 + }
  23 + }
  24 +
19 25 &.smoke { background-color: #f5f5f5; }
20 26  
21 27 &:hover {
... ...
app/controllers/admin/groups_controller.rb
... ... @@ -66,14 +66,12 @@ class Admin::GroupsController < Admin::ApplicationController
66 66 end
67 67  
68 68 def project_teams_update
69   - @group.add_users_to_project_teams(params[:user_ids].split(','), params[:project_access])
  69 + @group.add_users(params[:user_ids].split(','), params[:group_access])
70 70  
71 71 redirect_to [:admin, @group], notice: 'Users were successfully added.'
72 72 end
73 73  
74 74 def destroy
75   - @group.truncate_teams
76   -
77 75 @group.destroy
78 76  
79 77 redirect_to admin_groups_path, notice: 'Group was successfully deleted.'
... ...
app/controllers/dashboard_controller.rb
... ... @@ -34,11 +34,12 @@ class DashboardController < ApplicationController
34 34 @projects
35 35 end
36 36  
37   - @projects = @projects.tagged_with(params[:label]) if params[:label].present?
38 37 @projects = @projects.search(params[:search]) if params[:search].present?
39   - @projects = @projects.page(params[:page]).per(30)
40 38  
41 39 @labels = Project.where(id: @projects.map(&:id)).tags_on(:labels)
  40 +
  41 + @projects = @projects.tagged_with(params[:label]) if params[:label].present?
  42 + @projects = @projects.page(params[:page]).per(30)
42 43 end
43 44  
44 45 # Get authored or assigned open merge requests
... ...
app/controllers/graphs_controller.rb
... ... @@ -10,7 +10,7 @@ class GraphsController < ProjectResourceController
10 10 format.js do
11 11 @repo = @project.repository
12 12 @stats = Gitlab::Git::GitStats.new(@repo.raw, @repo.root_ref)
13   - @log = @stats.parsed_log.to_json
  13 + @log = @stats.parsed_log.to_json rescue []
14 14 end
15 15 end
16 16 end
... ...
app/controllers/groups_controller.rb
1 1 class GroupsController < ApplicationController
2 2 respond_to :html
3   - before_filter :group, except: [:new, :create]
  3 + before_filter :group, except: [:new, :create, :people]
4 4  
5 5 # Authorize
6 6 before_filter :authorize_read_group!, except: [:new, :create]
... ... @@ -63,19 +63,8 @@ class GroupsController &lt; ApplicationController
63 63  
64 64 def people
65 65 @project = group.projects.find(params[:project_id]) if params[:project_id]
66   - @users = @project ? @project.users : group.users
67   - @users.sort_by!(&:name)
68   -
69   - if @project
70   - @team_member = @project.users_projects.new
71   - else
72   - @team_member = UsersProject.new
73   - end
74   - end
75   -
76   - def team_members
77   - @group.add_users_to_project_teams(params[:user_ids].split(','), params[:project_access])
78   - redirect_to people_group_path(@group), notice: 'Users were successfully added.'
  66 + @members = group.users_groups.order('group_access DESC')
  67 + @users_group = UsersGroup.new
79 68 end
80 69  
81 70 def edit
... ... @@ -83,7 +72,7 @@ class GroupsController &lt; ApplicationController
83 72  
84 73 def update
85 74 group_params = params[:group].dup
86   - owner_id =group_params.delete(:owner_id)
  75 + owner_id = group_params.delete(:owner_id)
87 76  
88 77 if owner_id
89 78 @group.owner = User.find(owner_id)
... ...
app/controllers/issues_controller.rb
... ... @@ -23,7 +23,7 @@ class IssuesController &lt; ProjectResourceController
23 23  
24 24 assignee_id, milestone_id = params[:assignee_id], params[:milestone_id]
25 25  
26   - @assignee = @project.users.find(assignee_id) if assignee_id.present? && !assignee_id.to_i.zero?
  26 + @assignee = @project.team.find(assignee_id) if assignee_id.present? && !assignee_id.to_i.zero?
27 27 @milestone = @project.milestones.find(milestone_id) if milestone_id.present? && !milestone_id.to_i.zero?
28 28  
29 29 respond_to do |format|
... ...
app/controllers/team_members_controller.rb
... ... @@ -4,10 +4,8 @@ class TeamMembersController &lt; ProjectResourceController
4 4 before_filter :authorize_admin_project!, except: [:index, :show]
5 5  
6 6 def index
7   - @team = @project.users_projects.scoped
8   - @team = @team.send(params[:type]) if %w(masters developers reporters guests).include?(params[:type])
9   - @team = @team.sort_by(&:project_access).reverse.group_by(&:project_access)
10   -
  7 + @group = @project.group
  8 + @users_projects = @project.users_projects.order('project_access DESC')
11 9 @assigned_teams = @project.user_team_project_relationships
12 10 end
13 11  
... ...
app/controllers/users_controller.rb
... ... @@ -3,7 +3,7 @@ class UsersController &lt; ApplicationController
3 3  
4 4 def show
5 5 @user = User.find_by_username!(params[:username])
6   - @projects = @user.authorized_projects.where('projects.id in (?)', current_user.authorized_projects.map(&:id))
  6 + @projects = @user.authorized_projects.where('projects.id in (?)', current_user.authorized_projects.map(&:id)).order('namespace_id DESC')
7 7 @events = @user.recent_events.where(project_id: @projects.map(&:id)).limit(20)
8 8  
9 9 @title = @user.name
... ...
app/controllers/users_groups_controller.rb 0 → 100644
... ... @@ -0,0 +1,40 @@
  1 +class UsersGroupsController < ApplicationController
  2 + before_filter :group
  3 +
  4 + # Authorize
  5 + before_filter :authorize_admin_group!
  6 +
  7 + layout 'group'
  8 +
  9 + def create
  10 + @group.add_users(params[:user_ids].split(','), params[:group_access])
  11 +
  12 + redirect_to people_group_path(@group), notice: 'Users were successfully added.'
  13 + end
  14 +
  15 + def update
  16 + # TODO: implement
  17 + end
  18 +
  19 + def destroy
  20 + @users_group = @group.users_groups.find(params[:id])
  21 + @users_group.destroy
  22 +
  23 + respond_to do |format|
  24 + format.html { redirect_to people_group_path(@group), notice: 'User was successfully removed from group.' }
  25 + format.js { render nothing: true }
  26 + end
  27 + end
  28 +
  29 + protected
  30 +
  31 + def group
  32 + @group ||= Group.find_by_path(params[:group_id])
  33 + end
  34 +
  35 + def authorize_admin_group!
  36 + unless can?(current_user, :manage_group, group)
  37 + return render_404
  38 + end
  39 + end
  40 +end
... ...
app/helpers/groups_helper.rb
... ... @@ -14,4 +14,8 @@ module GroupsHelper
14 14 merge_requests_group_path(@group, options)
15 15 end
16 16 end
  17 +
  18 + def remove_user_from_group_message(group, user)
  19 + "You are going to remove #{user.name} from #{group.name} Group. Are you sure?"
  20 + end
17 21 end
... ...
app/models/ability.rb
... ... @@ -50,6 +50,10 @@ class Ability
50 50 rules << project_admin_rules
51 51 end
52 52  
  53 + if project.group && project.group.owners.include?(user)
  54 + rules << project_admin_rules
  55 + end
  56 +
53 57 rules.flatten
54 58 end
55 59  
... ... @@ -132,7 +136,7 @@ class Ability
132 136 rules = []
133 137  
134 138 # Only group owner and administrators can manage group
135   - if group.owner == user || user.admin?
  139 + if group.owners.include?(user) || user.admin?
136 140 rules << [
137 141 :manage_group,
138 142 :manage_namespace
... ...
app/models/group.rb
... ... @@ -13,26 +13,28 @@
13 13 #
14 14  
15 15 class Group < Namespace
  16 + has_many :users_groups, dependent: :destroy
  17 + has_many :users, through: :users_groups
16 18  
17   - def add_users_to_project_teams(user_ids, project_access)
18   - UsersProject.add_users_into_projects(
19   - projects.map(&:id),
20   - user_ids,
21   - project_access
22   - )
  19 + after_create :add_owner
  20 +
  21 + def human_name
  22 + name
23 23 end
24 24  
25   - def users
26   - users = User.joins(:users_projects).where(users_projects: {project_id: project_ids})
27   - users = users << owner
28   - users.uniq
  25 + def owners
  26 + @owners ||= (users_groups.owners.map(&:user) << owner)
29 27 end
30 28  
31   - def human_name
32   - name
  29 + def add_users(user_ids, group_access)
  30 + user_ids.compact.each do |user_id|
  31 + self.users_groups.create(user_id: user_id, group_access: group_access)
  32 + end
33 33 end
34 34  
35   - def truncate_teams
36   - UsersProject.truncate_teams(project_ids)
  35 + private
  36 +
  37 + def add_owner
  38 + self.add_users([owner.id], UsersGroup::OWNER)
37 39 end
38 40 end
... ...
app/models/project_team.rb
... ... @@ -21,6 +21,16 @@ class ProjectTeam
21 21 end
22 22 end
23 23  
  24 + def find user_id
  25 + user = project.users.find_by_id(user_id)
  26 +
  27 + if group
  28 + user ||= group.users.find_by_id(user_id)
  29 + end
  30 +
  31 + user
  32 + end
  33 +
24 34 def get_tm user_id
25 35 project.users_projects.find_by_user_id(user_id)
26 36 end
... ... @@ -47,23 +57,23 @@ class ProjectTeam
47 57 end
48 58  
49 59 def members
50   - project.users_projects
  60 + fetch_members
51 61 end
52 62  
53 63 def guests
54   - members.guests.map(&:user)
  64 + @guests ||= fetch_members(:guests)
55 65 end
56 66  
57 67 def reporters
58   - members.reporters.map(&:user)
  68 + @reporters ||= fetch_members(:reporters)
59 69 end
60 70  
61 71 def developers
62   - members.developers.map(&:user)
  72 + @developers ||= fetch_members(:developers)
63 73 end
64 74  
65 75 def masters
66   - members.masters.map(&:user)
  76 + @masters ||= fetch_members(:masters)
67 77 end
68 78  
69 79 def import(source_project)
... ... @@ -96,4 +106,22 @@ class ProjectTeam
96 106 rescue
97 107 false
98 108 end
  109 +
  110 + private
  111 +
  112 + def fetch_members(level = nil)
  113 + project_members = project.users_projects
  114 + group_members = group ? group.users_groups : []
  115 +
  116 + if level
  117 + project_members = project_members.send(level)
  118 + group_members = group_members.send(level) if group
  119 + end
  120 +
  121 + (project_members + group_members).map(&:user).uniq
  122 + end
  123 +
  124 + def group
  125 + project.group
  126 + end
99 127 end
... ...
app/models/user.rb
... ... @@ -71,7 +71,9 @@ class User &lt; ActiveRecord::Base
71 71 has_many :keys, dependent: :destroy
72 72  
73 73 # Groups
74   - has_many :groups, class_name: "Group", foreign_key: :owner_id
  74 + has_many :own_groups, class_name: "Group", foreign_key: :owner_id
  75 + has_many :users_groups, dependent: :destroy
  76 + has_many :groups, through: :users_groups
75 77  
76 78 # Teams
77 79 has_many :own_teams, dependent: :destroy, class_name: "UserTeam", foreign_key: :owner_id
... ... @@ -230,7 +232,7 @@ class User &lt; ActiveRecord::Base
230 232  
231 233 # Groups where user is an owner
232 234 def owned_groups
233   - groups
  235 + own_groups
234 236 end
235 237  
236 238 def owned_teams
... ... @@ -239,14 +241,14 @@ class User &lt; ActiveRecord::Base
239 241  
240 242 # Groups user has access to
241 243 def authorized_groups
242   - @group_ids ||= (groups.pluck(:id) + authorized_projects.pluck(:namespace_id))
  244 + @group_ids ||= (groups.pluck(:id) + own_groups.pluck(:id) + authorized_projects.pluck(:namespace_id))
243 245 Group.where(id: @group_ids)
244 246 end
245 247  
246 248  
247 249 # Projects user has access to
248 250 def authorized_projects
249   - @project_ids ||= (owned_projects.pluck(:id) + projects.pluck(:id)).uniq
  251 + @project_ids ||= (owned_projects.pluck(:id) + groups.map(&:projects).flatten.map(&:id) + projects.pluck(:id)).uniq
250 252 Project.where(id: @project_ids)
251 253 end
252 254  
... ...
app/models/users_group.rb 0 → 100644
... ... @@ -0,0 +1,42 @@
  1 +class UsersGroup < ActiveRecord::Base
  2 + GUEST = 10
  3 + REPORTER = 20
  4 + DEVELOPER = 30
  5 + MASTER = 40
  6 + OWNER = 50
  7 +
  8 + def self.group_access_roles
  9 + {
  10 + "Guest" => GUEST,
  11 + "Reporter" => REPORTER,
  12 + "Developer" => DEVELOPER,
  13 + "Master" => MASTER,
  14 + "Owner" => OWNER
  15 + }
  16 + end
  17 +
  18 + attr_accessible :group_access, :user_id
  19 +
  20 + belongs_to :user
  21 + belongs_to :group
  22 +
  23 + scope :guests, -> { where(group_access: GUEST) }
  24 + scope :reporters, -> { where(group_access: REPORTER) }
  25 + scope :developers, -> { where(group_access: DEVELOPER) }
  26 + scope :masters, -> { where(group_access: MASTER) }
  27 + scope :owners, -> { where(group_access: OWNER) }
  28 +
  29 + scope :with_group, ->(group) { where(group_id: group.id) }
  30 + scope :with_user, ->(user) { where(user_id: user.id) }
  31 +
  32 + validates :group_access, inclusion: { in: UsersGroup.group_access_roles.values }, presence: true
  33 + validates :user_id, presence: true
  34 + validates :group_id, presence: true
  35 + validates :user_id, uniqueness: { scope: [:group_id], message: "already exists in group" }
  36 +
  37 + delegate :name, :username, :email, to: :user, prefix: true
  38 +
  39 + def human_access
  40 + UsersGroup.group_access_roles.key(self.group_access)
  41 + end
  42 +end
... ...
app/models/users_project.rb
... ... @@ -129,9 +129,7 @@ class UsersProject &lt; ActiveRecord::Base
129 129 Project.access_options.key(self.project_access)
130 130 end
131 131  
132   - def repo_access_human
133   - self.class.access_roles.invert[self.project_access]
134   - end
  132 + alias_method :human_access, :project_access_human
135 133  
136 134 def skip_git?
137 135 !!@skip_git
... ...
app/services/system_hooks_service.rb
... ... @@ -43,7 +43,7 @@ class SystemHooksService
43 43 project_id: model.project_id,
44 44 user_name: model.user.name,
45 45 user_email: model.user.email,
46   - project_access: model.repo_access_human
  46 + project_access: model.project_access_human
47 47 })
48 48 end
49 49 end
... ...
app/views/admin/groups/show.html.haml
... ... @@ -49,10 +49,23 @@
49 49 %strong
50 50 = @group.created_at.stamp("March 1, 1999")
51 51  
  52 + .ui-box
  53 + %h5.title
  54 + Projects
  55 + %small
  56 + (#{@group.projects.count})
  57 + %ul.well-list
  58 + - @group.projects.sort_by(&:name).each do |project|
  59 + %li
  60 + %strong
  61 + = link_to project.name_with_namespace, [:admin, project]
  62 + %span.pull-right.light
  63 + %span.monospace= project.path_with_namespace + ".git"
52 64  
  65 + .span6
53 66 .ui-box
54 67 %h5.title
55   - Add user to Group projects:
  68 + Add user(s):
56 69 .ui-box-body.form-holder
57 70 %p.light
58 71 Read more about project permissions
... ... @@ -64,30 +77,18 @@
64 77 %div.prepend-top-10
65 78 = select_tag :project_access, options_for_select(Project.access_options), {class: "project-access-select chosen span2"}
66 79 %hr
67   - = submit_tag 'Add user to projects in group', class: "btn btn-create"
  80 + = submit_tag 'Add users into group', class: "btn btn-create"
68 81 .ui-box
69 82 %h5.title
70   - Users from Group projects
  83 + Users from #{@group.name} Group
71 84 %small
72   - (#{@group.users.count})
  85 + (#{@group.users_groups.count})
73 86 %ul.well-list
74   - - @group.users.sort_by(&:name).each do |user|
  87 + - @group.users_groups.order('group_access DESC').each do |member|
  88 + - user = member.user
75 89 %li{class: dom_class(user)}
76 90 %strong
77 91 = link_to user.name, admin_user_path(user)
78 92 %span.pull-right.light
79   - = pluralize user.authorized_projects.in_namespace(@group).count, 'project'
  93 + = member.human_access
80 94  
81   - .span6
82   - .ui-box
83   - %h5.title
84   - Projects
85   - %small
86   - (#{@group.projects.count})
87   - %ul.well-list
88   - - @group.projects.sort_by(&:name).each do |project|
89   - %li
90   - %strong
91   - = link_to project.name_with_namespace, [:admin, project]
92   - %span.pull-right.light
93   - %span.monospace= project.path_with_namespace + ".git"
... ...
app/views/groups/_new_group_member.html.haml
1   -= form_for @team_member, as: :team_member, url: team_members_group_path(@group) do |f|
  1 += form_for @users_group, url: group_users_groups_path(@group) do |f|
2 2 %fieldset
3   - %legend= "New Team member(s) for projects in #{@group.name}"
  3 + %legend= "New Group member(s) for #{@group.name}"
4 4  
5   - %h6 1. Choose people you want in the team
  5 + %h6 1. Choose people you want in the group
6 6 .clearfix
7 7 = f.label :user_ids, "People"
8   - .input= users_select_tag(:user_ids, multiple: true)
  8 + .input= users_select_tag(:user_ids, multiple: true, class: 'input-large')
9 9  
10 10 %h6 2. Set access level for them
11 11 .clearfix
12   - = f.label :project_access, "Project Access"
13   - .input= select_tag :project_access, options_for_select(Project.access_options, @team_member.project_access), class: "project-access-select chosen"
  12 + = f.label :group_access, "Group Access"
  13 + .input= select_tag :group_access, options_for_select(UsersGroup.group_access_roles, @users_group.group_access), class: "project-access-select chosen"
14 14  
15 15 .form-actions
16 16 = hidden_field_tag :redirect_to, people_group_path(@group)
17   - = f.submit 'Add', class: "btn btn-save"
  17 + = f.submit 'Add users into group', class: "btn btn-create"
18 18  
... ...
app/views/groups/_new_member.html.haml
... ... @@ -1,18 +0,0 @@
1   -= form_for @team_member, as: :team_member, url: project_team_members_path(@project, @team_member) do |f|
2   - %fieldset
3   - %legend= "New Team member(s) for #{@project.name}"
4   -
5   - %h6 1. Choose people you want in the team
6   - .clearfix
7   - = f.label :user_ids, "People"
8   - .input= users_select_tag(:user_ids, multiple: true)
9   -
10   - %h6 2. Set access level for them
11   - .clearfix
12   - = f.label :project_access, "Project Access"
13   - .input= select_tag :project_access, options_for_select(Project.access_options, @team_member.project_access), class: "project-access-select chosen"
14   -
15   - .form-actions
16   - = hidden_field_tag :redirect_to, people_group_path(@group, project_id: @project.id)
17   - = f.submit 'Add', class: "btn btn-save"
18   -
app/views/groups/_people_filter.html.haml
... ... @@ -1,16 +0,0 @@
1   -= form_tag people_group_path(@group), method: 'get' do
2   - %fieldset
3   - %legend Projects:
4   - %ul.nav.nav-pills.nav-stacked
5   - - @projects.each do |project|
6   - %li{class: ("active" if params[:project_id] == project.id.to_s)}
7   - = link_to people_group_path(@group, project_id: project.id) do
8   - = project.name_with_namespace
9   - %small.pull-right= project.users.count
10   - - if @projects.blank?
11   - %p.nothing_here_message This group has no projects yet
12   -
13   - %fieldset
14   - %hr
15   - = link_to "Reset", people_group_path(@group), class: 'btn pull-right'
16   -
app/views/groups/people.html.haml
  1 +- can_manage_group = current_user.can? :manage_group, @group
1 2 .row
2   - .span3
3   - = render 'people_filter'
4   - .span9
5   - - if can?(current_user, :manage_group, @group)
6   - = render (@project ? "new_member" : "new_group_member")
  3 + .span6
  4 + - if can_manage_group
  5 + = render "new_group_member"
  6 + - else
  7 + .light-well
  8 + %h4.nothing_here_message
  9 + Only group owners can manage group members
  10 + .span6
7 11 .ui-box
8 12 %h5.title
9   - Team
  13 + #{@group.name} Group Members
10 14 %small
11   - (#{@users.size})
  15 + (#{@members.count})
12 16 %ul.well-list
13   - - @users.each do |user|
14   - %li
15   - = image_tag gravatar_icon(user.email, 16), class: "avatar s16"
16   - %strong= user.name
17   - %span.cgray= user.email
18   - - if @group.owner == user
19   - %span.btn.btn-small.disabled.pull-right Group Owner
20   -
  17 + - @members.each do |member|
  18 + = render 'users_groups/users_group', member: member, show_controls: can_manage_group
  19 + %p.light
  20 + Group members get access to all projects in this group
... ...
app/views/issues/_form.html.haml
... ... @@ -21,7 +21,7 @@
21 21 Assign to
22 22 .input
23 23 .pull-left
24   - = f.select(:assignee_id, @project.users.alphabetically.collect {|p| [ p.name, p.id ] }, { include_blank: "Select a user" }, {class: 'chosen'})
  24 + = f.select(:assignee_id, @project.team.members.sort_by(&:name).map {|p| [ p.name, p.id ] }, { include_blank: "Select a user" }, {class: 'chosen'})
25 25 .pull-right
26 26 &nbsp;
27 27 = link_to 'Assign to me', '#', class: 'btn btn-small assign-to-me-link'
... ...
app/views/issues/_issues.html.haml
... ... @@ -7,7 +7,7 @@
7 7 %span.update_issues_text Update selected issues with &nbsp;
8 8 .left
9 9 = select_tag('update[status]', options_for_select(['open', 'closed']), prompt: "Status")
10   - = select_tag('update[assignee_id]', options_from_collection_for_select(@project.users.all, "id", "name", params[:assignee_id]), prompt: "Assignee")
  10 + = select_tag('update[assignee_id]', options_from_collection_for_select(@project.team.members, "id", "name", params[:assignee_id]), prompt: "Assignee")
11 11 = select_tag('update[milestone_id]', options_from_collection_for_select(issues_active_milestones, "id", "title", params[:milestone_id]), prompt: "Milestone")
12 12 = hidden_field_tag 'update[issues_ids]', []
13 13 = hidden_field_tag :status, params[:status]
... ... @@ -50,7 +50,7 @@
50 50 Any
51 51 = link_to project_issues_with_filter_path(@project, assignee_id: 0) do
52 52 Unassigned
53   - - @project.users.sort_by(&:name).each do |user|
  53 + - @project.team.members.sort_by(&:name).each do |user|
54 54 %li
55 55 = link_to project_issues_with_filter_path(@project, assignee_id: user.id) do
56 56 = image_tag gravatar_icon(user.email), class: "avatar s16"
... ...
app/views/projects/_settings_nav.html.haml
... ... @@ -6,7 +6,7 @@
6 6 = nav_link(controller: [:team_members, :teams]) do
7 7 = link_to project_team_index_path(@project), class: "team-tab tab" do
8 8 %i.icon-group
9   - Project Members
  9 + Members
10 10 = nav_link(controller: :deploy_keys) do
11 11 = link_to project_deploy_keys_path(@project) do
12 12 %span
... ...
app/views/team_members/_group_members.html.haml 0 → 100644
... ... @@ -0,0 +1,10 @@
  1 +.ui-box
  2 + %h5.title
  3 + %strong #{@group.name} Group
  4 + members (#{@group.users_groups.count})
  5 + .pull-right
  6 + = link_to people_group_path(@group), class: 'btn btn-small' do
  7 + %i.icon-edit
  8 + %ul.well-list
  9 + - @group.users_groups.order('group_access DESC').each do |member|
  10 + = render 'users_groups/users_group', member: member, show_controls: false
... ...
app/views/team_members/_team.html.haml
1   -- can_admin_project = (can? current_user, :admin_project, @project)
2   -- team.each do |access, members|
3   - - role = Project.access_options.key(access).pluralize
4   - .ui-box{class: role.downcase}
  1 +.team-table
  2 + - can_admin_project = (can? current_user, :admin_project, @project)
  3 + .ui-box
5 4 %h5.title
6   - = role
7   - %span.light (#{members.size})
  5 + %strong #{@project.name} Project
  6 + members (#{members.count})
8 7 %ul.well-list
9   - - members.sort_by(&:user_name).each do |team_member|
  8 + - members.each do |team_member|
10 9 = render 'team_members/team_member', member: team_member, current_user_can_admin_project: can_admin_project
... ...
app/views/team_members/_team_member.html.haml
1 1 - user = member.user
2 2 - allow_admin = current_user_can_admin_project
3 3 %li{id: dom_id(user), class: "team_member_row user_#{user.id}"}
4   - .row
5   - .span4
6   - = link_to user, title: user.name, class: "dark" do
7   - = image_tag gravatar_icon(user.email, 32), class: "avatar s32"
8   - %strong= truncate(user.name, lenght: 40)
9   - %br
10   - %small.cgray= user.username
  4 + .pull-right
  5 + - if allow_admin
  6 + .pull-left
  7 + = form_for(member, as: :team_member, url: project_team_member_path(@project, member.user)) do |f|
  8 + = f.select :project_access, options_for_select(UsersProject.access_roles, member.project_access), {}, class: "medium project-access-select span2 trigger-submit"
  9 + &nbsp;
  10 + = link_to project_team_member_path(@project, user), confirm: remove_from_project_team_message(@project, user), method: :delete, class: "btn-tiny btn btn-remove", title: 'Remove user from team' do
  11 + %i.icon-minus.icon-white
  12 + = image_tag gravatar_icon(user.email, 32), class: "avatar s32"
  13 + %p
  14 + %strong= user.name
  15 + %span.cgray= user.username
11 16  
12   - .span4.pull-right
13   - - if allow_admin
14   - .left
15   - = form_for(member, as: :team_member, url: project_team_member_path(@project, member.user)) do |f|
16   - = f.select :project_access, options_for_select(UsersProject.access_roles, member.project_access), {}, class: "medium project-access-select span2 trigger-submit"
17   - .pull-right
18   - - if current_user == user
19   - %span.label.label-success This is you!
20   - - if @project.namespace_owner == user
21   - %span.label.label-info Owner
22   - - elsif user.blocked?
23   - %span.label.label-error Blocked
24   - - elsif allow_admin
25   - = link_to project_team_member_path(@project, user), confirm: remove_from_project_team_message(@project, user), method: :delete, class: "btn-tiny btn btn-remove", title: 'Remove user from team' do
26   - %i.icon-minus.icon-white
27 17  
... ...
app/views/team_members/index.html.haml
1 1 = render "projects/settings_nav"
2 2 %h3.page_title
3   - Project Members
4   - (#{@project.users.count})
5   - %small
6   - Read more about project permissions
7   - %strong= link_to "here", help_permissions_path, class: "vlink"
  3 + Users with access to this project
8 4  
9 5 - if can? current_user, :admin_team_member, @project
10 6 %span.pull-right
... ... @@ -15,42 +11,25 @@
15 11 = link_to new_project_team_member_path(@project), class: "btn btn-primary small grouped", title: "New Team Member" do
16 12 New Team Member
17 13  
18   -%hr
  14 +%p.light
  15 + Read more about project permissions
  16 + %strong= link_to "here", help_permissions_path, class: "vlink"
19 17  
20 18 .clearfix
21   -.row
22   - .span3
23   - %ul.nav.nav-pills.nav-stacked
24   - %li{class: ("active" if !params[:type])}
25   - = link_to project_team_index_path(type: nil) do
26   - All
27   - %li{class: ("active" if params[:type] == 'masters')}
28   - = link_to project_team_index_path(type: 'masters') do
29   - Masters
30   - %span.pull-right= @project.users_projects.masters.count
31   - %li{class: ("active" if params[:type] == 'developers')}
32   - = link_to project_team_index_path(type: 'developers') do
33   - Developers
34   - %span.pull-right= @project.users_projects.developers.count
35   - %li{class: ("active" if params[:type] == 'reporters')}
36   - = link_to project_team_index_path(type: 'reporters') do
37   - Reporters
38   - %span.pull-right= @project.users_projects.reporters.count
39   - %li{class: ("active" if params[:type] == 'guests')}
40   - = link_to project_team_index_path(type: 'guests') do
41   - Guests
42   - %span.pull-right= @project.users_projects.guests.count
43   -
44   - - if @assigned_teams.present?
45   - %h5
46   - Assigned teams
47   - (#{@project.user_teams.count})
48   - %div
49   - = render "team_members/assigned_teams", assigned_teams: @assigned_teams
50   -
51   - .span9
52   - %div.team-table
53   - = render "team_members/team", team: @team
54   -
55   -
56 19  
  20 +- if @group
  21 + .row
  22 + .span6
  23 + = render "team_members/group_members"
  24 + .span6
  25 + = render "team_members/team", members: @users_projects
  26 +
  27 +- else
  28 + = render "team_members/team", members: @users_projects
  29 +
  30 +- if @assigned_teams.present?
  31 + %h5
  32 + Assigned teams
  33 + (#{@project.user_teams.count})
  34 + %div
  35 + = render "team_members/assigned_teams", assigned_teams: @assigned_teams
... ...
app/views/users_groups/_users_group.html.haml 0 → 100644
... ... @@ -0,0 +1,16 @@
  1 +- user = member.user
  2 +- return unless user
  3 +%li{class: dom_class(member)}
  4 + = image_tag gravatar_icon(user.email, 16), class: "avatar s16"
  5 + %strong= user.name
  6 + %span.cgray= user.username
  7 +
  8 + %span.pull-right
  9 + - if @group.owners.include?(user)
  10 + %span.label.label-info Group Owner
  11 + - else
  12 + = member.human_access
  13 +
  14 + - if show_controls && user != current_user
  15 + = link_to group_users_group_path(@group, member), confirm: remove_user_from_group_message(@group, user), method: :delete, remote: true, class: "btn-tiny btn btn-remove", title: 'Remove user from group' do
  16 + %i.icon-minus.icon-white
... ...
config/routes.rb
... ... @@ -149,10 +149,10 @@ Gitlab::Application.routes.draw do
149 149 member do
150 150 get :issues
151 151 get :merge_requests
152   - get :search
153 152 get :people
154   - post :team_members
155 153 end
  154 +
  155 + resources :users_groups, only: [:create, :update, :destroy]
156 156 end
157 157  
158 158 #
... ...
db/migrate/20130617095603_create_users_groups.rb 0 → 100644
... ... @@ -0,0 +1,11 @@
  1 +class CreateUsersGroups < ActiveRecord::Migration
  2 + def change
  3 + create_table :users_groups do |t|
  4 + t.integer :group_access, null: false
  5 + t.integer :group_id, null: false
  6 + t.integer :user_id, null: false
  7 +
  8 + t.timestamps
  9 + end
  10 + end
  11 +end
... ...
db/schema.rb
... ... @@ -11,7 +11,7 @@
11 11 #
12 12 # It's strongly recommended to check this file into your version control system.
13 13  
14   -ActiveRecord::Schema.define(:version => 20130614132337) do
  14 +ActiveRecord::Schema.define(:version => 20130617095603) do
15 15  
16 16 create_table "deploy_keys_projects", :force => true do |t|
17 17 t.integer "deploy_key_id", :null => false
... ... @@ -53,8 +53,8 @@ ActiveRecord::Schema.define(:version =&gt; 20130614132337) do
53 53 t.integer "assignee_id"
54 54 t.integer "author_id"
55 55 t.integer "project_id"
56   - t.datetime "created_at", :null => false
57   - t.datetime "updated_at", :null => false
  56 + t.datetime "created_at"
  57 + t.datetime "updated_at"
58 58 t.integer "position", :default => 0
59 59 t.string "branch_name"
60 60 t.text "description"
... ... @@ -71,8 +71,8 @@ ActiveRecord::Schema.define(:version =&gt; 20130614132337) do
71 71  
72 72 create_table "keys", :force => true do |t|
73 73 t.integer "user_id"
74   - t.datetime "created_at", :null => false
75   - t.datetime "updated_at", :null => false
  74 + t.datetime "created_at"
  75 + t.datetime "updated_at"
76 76 t.text "key"
77 77 t.string "title"
78 78 t.string "identifier"
... ... @@ -89,8 +89,8 @@ ActiveRecord::Schema.define(:version =&gt; 20130614132337) do
89 89 t.integer "author_id"
90 90 t.integer "assignee_id"
91 91 t.string "title"
92   - t.datetime "created_at", :null => false
93   - t.datetime "updated_at", :null => false
  92 + t.datetime "created_at"
  93 + t.datetime "updated_at"
94 94 t.text "st_commits", :limit => 2147483647
95 95 t.text "st_diffs", :limit => 2147483647
96 96 t.integer "milestone_id"
... ... @@ -139,8 +139,8 @@ ActiveRecord::Schema.define(:version =&gt; 20130614132337) do
139 139 t.text "note"
140 140 t.string "noteable_type"
141 141 t.integer "author_id"
142   - t.datetime "created_at", :null => false
143   - t.datetime "updated_at", :null => false
  142 + t.datetime "created_at"
  143 + t.datetime "updated_at"
144 144 t.integer "project_id"
145 145 t.string "attachment"
146 146 t.string "line_code"
... ... @@ -158,8 +158,8 @@ ActiveRecord::Schema.define(:version =&gt; 20130614132337) do
158 158 t.string "name"
159 159 t.string "path"
160 160 t.text "description"
161   - t.datetime "created_at", :null => false
162   - t.datetime "updated_at", :null => false
  161 + t.datetime "created_at"
  162 + t.datetime "updated_at"
163 163 t.integer "creator_id"
164 164 t.string "default_branch"
165 165 t.boolean "issues_enabled", :default => true, :null => false
... ... @@ -206,8 +206,8 @@ ActiveRecord::Schema.define(:version =&gt; 20130614132337) do
206 206 t.text "content"
207 207 t.integer "author_id", :null => false
208 208 t.integer "project_id"
209   - t.datetime "created_at", :null => false
210   - t.datetime "updated_at", :null => false
  209 + t.datetime "created_at"
  210 + t.datetime "updated_at"
211 211 t.string "file_name"
212 212 t.datetime "expires_at"
213 213 t.boolean "private", :default => true, :null => false
... ... @@ -228,9 +228,6 @@ ActiveRecord::Schema.define(:version =&gt; 20130614132337) do
228 228 t.datetime "created_at"
229 229 end
230 230  
231   - add_index "taggings", ["tag_id"], :name => "index_taggings_on_tag_id"
232   - add_index "taggings", ["taggable_id", "taggable_type", "context"], :name => "index_taggings_on_taggable_id_and_taggable_type_and_context"
233   -
234 231 create_table "tags", :force => true do |t|
235 232 t.string "name"
236 233 end
... ... @@ -262,53 +259,60 @@ ActiveRecord::Schema.define(:version =&gt; 20130614132337) do
262 259 end
263 260  
264 261 create_table "users", :force => true do |t|
265   - t.string "email", :default => "", :null => false
266   - t.string "encrypted_password", :default => "", :null => false
  262 + t.string "email", :default => "", :null => false
  263 + t.string "encrypted_password", :limit => 128, :default => "", :null => false
267 264 t.string "reset_password_token"
268 265 t.datetime "reset_password_sent_at"
269 266 t.datetime "remember_created_at"
270   - t.integer "sign_in_count", :default => 0
  267 + t.integer "sign_in_count", :default => 0
271 268 t.datetime "current_sign_in_at"
272 269 t.datetime "last_sign_in_at"
273 270 t.string "current_sign_in_ip"
274 271 t.string "last_sign_in_ip"
275   - t.datetime "created_at", :null => false
276   - t.datetime "updated_at", :null => false
  272 + t.datetime "created_at"
  273 + t.datetime "updated_at"
277 274 t.string "name"
278   - t.boolean "admin", :default => false, :null => false
279   - t.integer "projects_limit", :default => 10
280   - t.string "skype", :default => "", :null => false
281   - t.string "linkedin", :default => "", :null => false
282   - t.string "twitter", :default => "", :null => false
  275 + t.boolean "admin", :default => false, :null => false
  276 + t.integer "projects_limit", :default => 10
  277 + t.string "skype", :default => "", :null => false
  278 + t.string "linkedin", :default => "", :null => false
  279 + t.string "twitter", :default => "", :null => false
283 280 t.string "authentication_token"
284   - t.integer "theme_id", :default => 1, :null => false
  281 + t.integer "theme_id", :default => 1, :null => false
285 282 t.string "bio"
286   - t.integer "failed_attempts", :default => 0
  283 + t.integer "failed_attempts", :default => 0
287 284 t.datetime "locked_at"
288 285 t.string "extern_uid"
289 286 t.string "provider"
290 287 t.string "username"
291   - t.boolean "can_create_group", :default => true, :null => false
292   - t.boolean "can_create_team", :default => true, :null => false
  288 + t.boolean "can_create_group", :default => true, :null => false
  289 + t.boolean "can_create_team", :default => true, :null => false
293 290 t.string "state"
294   - t.integer "color_scheme_id", :default => 1, :null => false
295   - t.integer "notification_level", :default => 1, :null => false
  291 + t.integer "color_scheme_id", :default => 1, :null => false
  292 + t.integer "notification_level", :default => 1, :null => false
296 293 t.datetime "password_expires_at"
297 294 t.integer "created_by_id"
298 295 end
299 296  
300 297 add_index "users", ["admin"], :name => "index_users_on_admin"
301 298 add_index "users", ["email"], :name => "index_users_on_email", :unique => true
302   - add_index "users", ["extern_uid", "provider"], :name => "index_users_on_extern_uid_and_provider", :unique => true
303 299 add_index "users", ["name"], :name => "index_users_on_name"
304 300 add_index "users", ["reset_password_token"], :name => "index_users_on_reset_password_token", :unique => true
305 301 add_index "users", ["username"], :name => "index_users_on_username"
306 302  
  303 + create_table "users_groups", :force => true do |t|
  304 + t.integer "group_access", :null => false
  305 + t.integer "group_id", :null => false
  306 + t.integer "user_id", :null => false
  307 + t.datetime "created_at", :null => false
  308 + t.datetime "updated_at", :null => false
  309 + end
  310 +
307 311 create_table "users_projects", :force => true do |t|
308 312 t.integer "user_id", :null => false
309 313 t.integer "project_id", :null => false
310   - t.datetime "created_at", :null => false
311   - t.datetime "updated_at", :null => false
  314 + t.datetime "created_at"
  315 + t.datetime "updated_at"
312 316 t.integer "project_access", :default => 0, :null => false
313 317 t.integer "notification_level", :default => 3, :null => false
314 318 end
... ... @@ -320,8 +324,8 @@ ActiveRecord::Schema.define(:version =&gt; 20130614132337) do
320 324 create_table "web_hooks", :force => true do |t|
321 325 t.string "url"
322 326 t.integer "project_id"
323   - t.datetime "created_at", :null => false
324   - t.datetime "updated_at", :null => false
  327 + t.datetime "created_at"
  328 + t.datetime "updated_at"
325 329 t.string "type", :default => "ProjectHook"
326 330 t.integer "service_id"
327 331 end
... ...
features/steps/admin/admin_groups.rb
... ... @@ -45,7 +45,7 @@ class AdminGroups &lt; Spinach::FeatureSteps
45 45 within "#new_team_member" do
46 46 select "Reporter", from: "project_access"
47 47 end
48   - click_button "Add user to projects in group"
  48 + click_button "Add users into group"
49 49 end
50 50  
51 51 Then 'I should see "John" in team list in every project as "Reporter"' do
... ...
features/steps/group/group.rb
... ... @@ -39,11 +39,11 @@ class Groups &lt; Spinach::FeatureSteps
39 39  
40 40 And 'I select user "John" from list with role "Reporter"' do
41 41 user = User.find_by_name("John")
42   - within "#new_team_member" do
  42 + within ".new_users_group" do
43 43 select2(user.id, from: "#user_ids", multiple: true)
44   - select "Reporter", from: "project_access"
  44 + select "Reporter", from: "group_access"
45 45 end
46   - click_button "Add"
  46 + click_button "Add users into group"
47 47 end
48 48  
49 49 Then 'I should see user "John" in team list' do
... ...
features/steps/project/project_active_tab.rb
... ... @@ -45,7 +45,7 @@ class ProjectActiveTab &lt; Spinach::FeatureSteps
45 45 # Sub Tabs: Home
46 46  
47 47 Given 'I click the "Team" tab' do
48   - click_link('Project Members')
  48 + click_link('Members')
49 49 end
50 50  
51 51 Given 'I click the "Attachments" tab' do
... ... @@ -73,7 +73,7 @@ class ProjectActiveTab &lt; Spinach::FeatureSteps
73 73 end
74 74  
75 75 Then 'the active sub tab should be Team' do
76   - ensure_active_sub_tab('Project Members')
  76 + ensure_active_sub_tab('Members')
77 77 end
78 78  
79 79 Then 'the active sub tab should be Attachments' do
... ...
features/steps/project/project_team_management.rb
... ... @@ -30,14 +30,20 @@ class ProjectTeamManagement &lt; Spinach::FeatureSteps
30 30 end
31 31  
32 32 Then 'I should see "Mike" in team list as "Reporter"' do
33   - within '.reporters' do
  33 + user = User.find_by_name("Mike")
  34 +
  35 + within "#user_#{user.id}" do
34 36 page.should have_content('Mike')
  37 + page.find('#team_member_project_access').value.should == access_value(:reporter)
35 38 end
36 39 end
37 40  
38 41 Given 'I should see "Sam" in team list as "Developer"' do
39   - within '.developers' do
  42 + user = User.find_by_name("Sam")
  43 +
  44 + within "#user_#{user.id}" do
40 45 page.should have_content('Sam')
  46 + page.find('#team_member_project_access').value.should == access_value(:developer)
41 47 end
42 48 end
43 49  
... ... @@ -49,8 +55,10 @@ class ProjectTeamManagement &lt; Spinach::FeatureSteps
49 55 end
50 56  
51 57 And 'I should see "Sam" in team list as "Reporter"' do
52   - within '.reporters' do
  58 + user = User.find_by_name("Sam")
  59 + within ".user_#{user.id}" do
53 60 page.should have_content('Sam')
  61 + page.find('#team_member_project_access').value.should == access_value(:reporter)
54 62 end
55 63 end
56 64  
... ... @@ -103,4 +111,10 @@ class ProjectTeamManagement &lt; Spinach::FeatureSteps
103 111 click_link('Remove user from team')
104 112 end
105 113 end
  114 +
  115 + private
  116 +
  117 + def access_value(key)
  118 + UsersProject.roles_hash[key].to_s
  119 + end
106 120 end
... ...
spec/factories/users_groups.rb 0 → 100644
... ... @@ -0,0 +1,7 @@
  1 +FactoryGirl.define do
  2 + factory :users_group do
  3 + group_access { UsersGroup::OWNER }
  4 + group
  5 + user
  6 + end
  7 +end
... ...
spec/models/users_group_spec.rb 0 → 100644
... ... @@ -0,0 +1,27 @@
  1 +require 'spec_helper'
  2 +
  3 +describe UsersGroup do
  4 + describe "Associations" do
  5 + it { should belong_to(:group) }
  6 + it { should belong_to(:user) }
  7 + end
  8 +
  9 + describe "Mass assignment" do
  10 + it { should_not allow_mass_assignment_of(:group_id) }
  11 + end
  12 +
  13 + describe "Validation" do
  14 + let!(:users_group) { create(:users_group) }
  15 +
  16 + it { should validate_presence_of(:user_id) }
  17 + it { should validate_uniqueness_of(:user_id).scoped_to(:group_id).with_message(/already exists/) }
  18 +
  19 + it { should validate_presence_of(:group_id) }
  20 + it { should ensure_inclusion_of(:group_access).in_array(UsersGroup.group_access_roles.values) }
  21 + end
  22 +
  23 + describe "Delegate methods" do
  24 + it { should respond_to(:user_name) }
  25 + it { should respond_to(:user_email) }
  26 + end
  27 +end
... ...