summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorRémy Coutable <remy@rymai.me>2016-04-18 18:53:32 +0200
committerRémy Coutable <remy@rymai.me>2016-06-14 13:07:26 +0200
commitd26f81239a33b80694783ee35f0da0e2ed082c9b (patch)
treef092e818d4b81805a16879b13407a335bbda0054 /app
parent17c22156c5fa5663aae65178ed38cbeef9a80b7e (diff)
downloadgitlab-ce-d26f81239a33b80694783ee35f0da0e2ed082c9b.tar.gz
Add request access for groups
Signed-off-by: Rémy Coutable <remy@rymai.me>
Diffstat (limited to 'app')
-rw-r--r--app/assets/stylesheets/pages/projects.scss4
-rw-r--r--app/controllers/groups/group_members_controller.rb34
-rw-r--r--app/controllers/projects/project_members_controller.rb44
-rw-r--r--app/helpers/groups_helper.rb20
-rw-r--r--app/helpers/members_helper.rb117
-rw-r--r--app/helpers/projects_helper.rb26
-rw-r--r--app/mailers/emails/groups.rb51
-rw-r--r--app/mailers/emails/projects.rb75
-rw-r--r--app/models/ability.rb8
-rw-r--r--app/models/concerns/access_requestable.rb27
-rw-r--r--app/models/group.rb1
-rw-r--r--app/models/member.rb50
-rw-r--r--app/models/members/group_member.rb15
-rw-r--r--app/models/members/project_member.rb30
-rw-r--r--app/models/project.rb13
-rw-r--r--app/models/project_team.rb28
-rw-r--r--app/models/user.rb5
-rw-r--r--app/services/notification_service.rb22
-rw-r--r--app/views/admin/groups/show.html.haml2
-rw-r--r--app/views/admin/projects/show.html.haml4
-rw-r--r--app/views/admin/users/groups.html.haml2
-rw-r--r--app/views/admin/users/projects.html.haml3
-rw-r--r--app/views/groups/group_members/_group_member.html.haml57
-rw-r--r--app/views/groups/group_members/index.html.haml12
-rw-r--r--app/views/groups/group_members/update.js.haml2
-rw-r--r--app/views/layouts/nav/_group_settings.html.haml21
-rw-r--r--app/views/layouts/nav/_project.html.haml26
-rw-r--r--app/views/notify/group_access_denied_email.html.haml2
-rw-r--r--app/views/notify/group_access_denied_email.text.erb3
-rw-r--r--app/views/notify/group_access_granted_email.html.haml5
-rw-r--r--app/views/notify/group_access_granted_email.text.erb5
-rw-r--r--app/views/notify/group_access_requested_email.html.haml3
-rw-r--r--app/views/notify/group_access_requested_email.text.erb3
-rw-r--r--app/views/notify/project_access_denied_email.html.haml3
-rw-r--r--app/views/notify/project_access_denied_email.text.erb (renamed from app/views/notify/project_request_access_denied_email.text.erb)2
-rw-r--r--app/views/notify/project_access_granted_email.html.haml6
-rw-r--r--app/views/notify/project_access_granted_email.text.erb5
-rw-r--r--app/views/notify/project_access_requested_email.html.haml3
-rw-r--r--app/views/notify/project_access_requested_email.text.erb3
-rw-r--r--app/views/notify/project_request_access_accepted_email.html.haml4
-rw-r--r--app/views/notify/project_request_access_accepted_email.text.erb3
-rw-r--r--app/views/notify/project_request_access_denied_email.html.haml4
-rw-r--r--app/views/projects/notes/_note.html.haml2
-rw-r--r--app/views/projects/project_members/_group_members.html.haml13
-rw-r--r--app/views/projects/project_members/_new_project_member.html.haml2
-rw-r--r--app/views/projects/project_members/_pending.html.haml21
-rw-r--r--app/views/projects/project_members/_project_member.html.haml66
-rw-r--r--app/views/projects/project_members/_shared_group_members.html.haml6
-rw-r--r--app/views/projects/project_members/_team.html.haml3
-rw-r--r--app/views/projects/project_members/index.html.haml3
-rw-r--r--app/views/shared/_group_or_project_home_dropdown.html.haml30
-rw-r--r--app/views/shared/groups/_group.html.haml2
-rw-r--r--app/views/shared/members/_member.html.haml77
-rw-r--r--app/views/shared/members/_requests.html.haml10
54 files changed, 519 insertions, 469 deletions
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index bb250904255..2505deaf757 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -286,10 +286,6 @@
color: #555;
}
-.project_member_row form {
- margin: 0;
-}
-
.transfer-project .select2-container {
min-width: 200px;
}
diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb
index 48dbf656e84..2ebc506040f 100644
--- a/app/controllers/groups/group_members_controller.rb
+++ b/app/controllers/groups/group_members_controller.rb
@@ -1,11 +1,11 @@
class Groups::GroupMembersController < Groups::ApplicationController
# Authorize
- before_action :authorize_admin_group_member!, except: [:index, :leave]
+ before_action :authorize_admin_group_member!, except: [:index, :leave, :request_access]
def index
@project = @group.projects.find(params[:project_id]) if params[:project_id]
@members = @group.group_members
- @members = @members.non_invite unless can?(current_user, :admin_group, @group)
+ @members = @members.non_pending unless can?(current_user, :admin_group, @group)
if params[:search].present?
users = @group.users.search(params[:search]).to_a
@@ -36,7 +36,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
return render_403 unless can?(current_user, :destroy_group_member, @group_member)
- @group_member.destroy
+ @group_member.request? ? @group_member.decline_request : @group_member.destroy
respond_to do |format|
format.html { redirect_to group_group_members_path(@group), notice: 'User was successfully removed from group.' }
@@ -59,12 +59,20 @@ class Groups::GroupMembersController < Groups::ApplicationController
end
def leave
- @group_member = @group.group_members.find_by(user_id: current_user)
+ @group_member =
+ @group.group_members.find_by(user_id: current_user.id) ||
+ @group.group_members.find_by(created_by_id: current_user.id)
if can?(current_user, :destroy_group_member, @group_member)
+ notice =
+ if @group_member.request?
+ 'You withdrawn your access request to the group.'
+ else
+ "You left #{@group.name} group."
+ end
@group_member.destroy
- redirect_to(dashboard_groups_path, notice: "You left #{group.name} group.")
+ redirect_to dashboard_groups_path, notice: notice
else
if @group.last_owner?(current_user)
redirect_to(dashboard_groups_path, alert: "You can not leave #{group.name} group because you're the last owner. Transfer or delete the group.")
@@ -74,6 +82,22 @@ class Groups::GroupMembersController < Groups::ApplicationController
end
end
+ def request_access
+ @group.request_access(current_user)
+
+ redirect_to group_path(@group), notice: 'Your request for access has been queued for review.'
+ end
+
+ def approve
+ @group_member = @group.group_members.request.find(params[:id])
+
+ return render_403 unless can?(current_user, :update_group_member, @group_member)
+
+ @group_member.accept_request
+
+ redirect_to group_group_members_path(@group)
+ end
+
protected
def member_params
diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb
index ba5ef30be38..c979c5e9fa9 100644
--- a/app/controllers/projects/project_members_controller.rb
+++ b/app/controllers/projects/project_members_controller.rb
@@ -14,9 +14,10 @@ class Projects::ProjectMembersController < Projects::ApplicationController
@project_members = @project_members.order('access_level DESC')
@group = @project.group
+
if @group
@group_members = @group.group_members
- @group_members = @group_members.non_invite unless can?(current_user, :admin_group, @group)
+ @group_members = @group_members.non_pending unless can?(current_user, :admin_group, @group)
if params[:search].present?
users = @group.users.search(params[:search]).to_a
@@ -49,7 +50,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController
return render_403 unless can?(current_user, :destroy_project_member, @project_member)
- @project_member.destroy
+ @project_member.request? ? @project_member.decline_request : @project_member.destroy
respond_to do |format|
format.html do
@@ -74,15 +75,20 @@ class Projects::ProjectMembersController < Projects::ApplicationController
end
def leave
- @project_member = @project.project_members.find_by(user_id: current_user)
+ @project_member =
+ @project.project_members.find_by(user_id: current_user.id) ||
+ @project.project_members.find_by(created_by_id: current_user.id)
if can?(current_user, :destroy_project_member, @project_member)
+ notice =
+ if @project_member.request?
+ 'You withdrawn your access request to the project.'
+ else
+ 'You left the project.'
+ end
@project_member.destroy
- respond_to do |format|
- format.html { redirect_to dashboard_projects_path, notice: "You left the project." }
- format.js { head :ok }
- end
+ redirect_to dashboard_projects_path, notice: notice
else
if current_user == @project.owner
message = 'You can not leave your own project. Transfer or delete the project.'
@@ -94,30 +100,20 @@ class Projects::ProjectMembersController < Projects::ApplicationController
end
def request_access
- redirect_path = namespace_project_path(@project.namespace, @project)
- # current_user
- # @project
- @project_member = ProjectMember.new(source: @project, access_level: ProjectMember::DEVELOPER, user_id: current_user.id, created_by_id: current_user.id, requested: true)
- @project_member.save!
-
+ @project.request_access(current_user)
- redirect_to redirect_path, notice: 'Your request for access has been queued for review.'
+ redirect_to namespace_project_path(@project.namespace, @project),
+ notice: 'Your request for access has been queued for review.'
end
- def approval
- @project_member = @project.project_members.find(params[:id])
+ def approve
+ @project_member = @project.project_members.request.find(params[:id])
return render_403 unless can?(current_user, :update_project_member, @project_member)
- @project_member.requested = nil
- @project_member.save!
+ @project_member.accept_request
- respond_to do |format|
- format.html do
- redirect_to namespace_project_project_members_path(@project.namespace, @project)
- end
- format.js { render nothing: true }
- end
+ redirect_to namespace_project_project_members_path(@project.namespace, @project)
end
def apply_import
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index 4cac69c6795..b9211e88473 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -1,24 +1,4 @@
module GroupsHelper
- def remove_user_from_group_message(group, member)
- if member.user
- "Are you sure you want to remove \"#{member.user.name}\" from \"#{group.name}\"?"
- else
- "Are you sure you want to revoke the invitation for \"#{member.invite_email}\" to join \"#{group.name}\"?"
- end
- end
-
- def leave_group_message(group)
- "Are you sure you want to leave \"#{group}\" group?"
- end
-
- def should_user_see_group_roles?(user, group)
- if user
- user.is_admin? || group.members.exists?(user_id: user.id)
- else
- false
- end
- end
-
def can_change_group_visibility_level?(group)
can?(current_user, :change_visibility_level, group)
end
diff --git a/app/helpers/members_helper.rb b/app/helpers/members_helper.rb
new file mode 100644
index 00000000000..6599c59d1c9
--- /dev/null
+++ b/app/helpers/members_helper.rb
@@ -0,0 +1,117 @@
+module MembersHelper
+ def member_class(member)
+ "#{member.source.class.to_s}Member".constantize
+ end
+
+ def members_association(entity)
+ "#{entity.class.to_s.underscore}_members".to_sym
+ end
+
+ def action_member_permission(action, member)
+ "#{action}_#{member.source.class.to_s.underscore}_member".to_sym
+ end
+
+ def can_see_entity_roles?(user, entity)
+ return false unless user
+
+ user.is_admin? || entity.send(members_association(entity)).exists?(user_id: user.id)
+ end
+
+ def member_path(member)
+ case member.source
+ when Project
+ namespace_project_project_member_path(member.source.namespace, member.source, member)
+ when Group
+ group_group_member_path(member.source, member)
+ else
+ raise ArgumentError.new('Unknown object class')
+ end
+ end
+
+ def resend_invite_member_path(member)
+ case member.source
+ when Project
+ resend_invite_namespace_project_project_member_path(member.source.namespace, member.source, member)
+ when Group
+ resend_invite_group_group_member_path(member.source, member)
+ else
+ raise ArgumentError.new('Unknown object class')
+ end
+ end
+
+ def request_access_path(entity)
+ case entity
+ when Project
+ request_access_namespace_project_project_members_path(entity.namespace, entity)
+ when Group
+ request_access_group_group_members_path(entity)
+ else
+ raise ArgumentError.new('Unknown object class')
+ end
+ end
+
+ def approve_request_member_path(member)
+ case member.source
+ when Project
+ approve_namespace_project_project_member_path(member.source.namespace, member.source, member)
+ when Group
+ approve_group_group_member_path(member.source, member)
+ else
+ raise ArgumentError.new('Unknown object class')
+ end
+ end
+
+ def leave_path(entity)
+ case entity
+ when Project
+ leave_namespace_project_project_members_path(entity.namespace, entity)
+ when Group
+ leave_group_group_members_path(entity)
+ else
+ raise ArgumentError.new('Unknown object class')
+ end
+ end
+
+ def withdraw_request_message(entity)
+ "Are you sure you want to withdraw your access request for the \"#{entity_name(entity)}\" #{entity_type(entity)}?"
+ end
+
+ def remove_member_message(member)
+ entity = member.source
+ entity_type = entity_type(entity)
+ entity_name = entity_name(entity)
+
+ if member.request?
+ "You are going to deny #{member.created_by.name}'s request to join the #{entity_name} #{entity_type}. Are you sure?"
+ elsif member.invite?
+ "You are going to revoke the invitation for #{member.invite_email} to join the #{entity_name} #{entity_type}. Are you sure?"
+ else
+ "You are going to remove #{member.user.name} from the #{entity_name} #{entity_type}. Are you sure?"
+ end
+ end
+
+ def remove_member_title(member)
+ member.request? ? 'Deny access request' : 'Remove user'
+ end
+
+ def leave_confirmation_message(entity)
+ "Are you sure you want to leave \"#{entity_name(entity)}\" #{entity_type(entity)}?"
+ end
+
+ private
+
+ def entity_type(entity)
+ entity.class.to_s.underscore
+ end
+
+ def entity_name(entity)
+ case entity
+ when Project
+ entity.name_with_namespace
+ when Group
+ entity.name
+ else
+ raise ArgumentError.new('Unknown object class')
+ end
+ end
+end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index a015b5e6a02..03941f87b13 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -1,16 +1,6 @@
module ProjectsHelper
- def remove_from_project_team_message(project, member)
- if !member.user
- "You are going to revoke the invitation for #{member.invite_email} to join #{project.name} project team. Are you sure?"
- elsif member.request?
- "You are going to deny #{member.user.name}'s request to join #{project.name} project team. Are you sure?"
- else
- "You are going to remove #{member.user.name} from #{project.name} project team. Are you sure?"
- end
- end
-
- def approve_for_project_team_message(project, member)
- "You are going to approve #{member.user.name}'s request for #{member.human_access} access to the #{project.name} project team. Are you sure?"
+ def max_access_level(project, user)
+ Gitlab::Access.options_with_owner.key(project.team.max_member_access(user.id))
end
def link_to_project(project)
@@ -121,14 +111,6 @@ module ProjectsHelper
end
end
- def user_max_access_in_project(user_id, project)
- level = project.team.max_member_access(user_id)
-
- if level
- Gitlab::Access.options_with_owner.key(level)
- end
- end
-
def license_short_name(project)
return 'LICENSE' if project.repository.license_key.nil?
@@ -292,10 +274,6 @@ module ProjectsHelper
end
end
- def leave_project_message(project)
- "Are you sure you want to leave \"#{project.name}\" project?"
- end
-
def new_readme_path
ref = @repository.root_ref if @repository
ref ||= 'master'
diff --git a/app/mailers/emails/groups.rb b/app/mailers/emails/groups.rb
index 1c43f95dc8c..fe218bfbe05 100644
--- a/app/mailers/emails/groups.rb
+++ b/app/mailers/emails/groups.rb
@@ -1,22 +1,38 @@
module Emails
module Groups
+ def group_access_requested_email(group_member_id)
+ setup_group_member_mail(group_member_id)
+
+ @requester = @group_member.created_by
+
+ group_admins = User.where(id: @group.group_members.admins.pluck(:user_id)).pluck(:notification_email)
+
+ mail(to: group_admins,
+ subject: subject("Request to join #{@group.name} group"))
+ end
+
def group_access_granted_email(group_member_id)
- @group_member = GroupMember.find(group_member_id)
- @group = @group_member.group
+ setup_group_member_mail(group_member_id)
- @target_url = group_url(@group)
@current_user = @group_member.user
- mail(to: @group_member.user.notification_email,
- subject: subject("Access to group was granted"))
+ mail(to: @current_user.notification_email,
+ subject: subject("Access to #{@group.name} group was granted"))
+ end
+
+ def group_access_denied_email(group_id, user_id)
+ @group = Group.find(group_id)
+ @current_user = User.find(user_id)
+ @target_url = group_url(@group)
+
+ mail(to: @current_user.notification_email,
+ subject: subject("Access to #{@group.name} group was denied"))
end
def group_member_invited_email(group_member_id, token)
- @group_member = GroupMember.find group_member_id
- @group = @group_member.group
- @token = token
+ setup_group_member_mail(group_member_id)
- @target_url = group_url(@group)
+ @token = token
@current_user = @group_member.user
mail(to: @group_member.invite_email,
@@ -24,15 +40,12 @@ module Emails
end
def group_invite_accepted_email(group_member_id)
- @group_member = GroupMember.find group_member_id
+ setup_group_member_mail(group_member_id)
return if @group_member.created_by.nil?
- @group = @group_member.group
-
- @target_url = group_url(@group)
@current_user = @group_member.created_by
- mail(to: @group_member.created_by.notification_email,
+ mail(to: @current_user.notification_email,
subject: subject("Invitation accepted"))
end
@@ -43,10 +56,18 @@ module Emails
@current_user = @created_by = User.find(created_by_id)
@access_level = access_level
@invite_email = invite_email
-
+
@target_url = group_url(@group)
mail(to: @created_by.notification_email,
subject: subject("Invitation declined"))
end
+
+ private
+
+ def setup_group_member_mail(group_member_id)
+ @group_member = GroupMember.find(group_member_id)
+ @group = @group_member.group
+ @target_url = group_url(@group)
+ end
end
end
diff --git a/app/mailers/emails/projects.rb b/app/mailers/emails/projects.rb
index 6662c407c2c..43a2a7e80a8 100644
--- a/app/mailers/emails/projects.rb
+++ b/app/mailers/emails/projects.rb
@@ -1,64 +1,38 @@
module Emails
module Projects
- def project_access_granted_email(project_member_id)
- @project_member = ProjectMember.find project_member_id
- @project = @project_member.project
-
- @target_url = namespace_project_url(@project.namespace, @project)
- @current_user = @project_member.user
+ def project_access_requested_email(project_member_id)
+ setup_project_member_mail(project_member_id)
- mail(to: @project_member.user.notification_email,
- subject: subject("Access to project was granted"))
- end
+ @requester = @project_member.created_by
- def project_member_requested_access(project_member_id)
- @project_member = ProjectMember.find project_member_id
- @project = @project_member.project
- @target_url = namespace_project_url(@project.namespace, @project)
+ project_admins = User.where(id: @project.project_members.admins.pluck(:user_id)).pluck(:notification_email)
- project_admins = ProjectMember.in_project(@project)
- .where(access_level: [Gitlab::Access::OWNER, Gitlab::Access::MASTER])
- .pluck(:notification_email)
-
- project_admins.each do |address|
- mail(to: address,
- subject: subject("Request to join project: #{@project.name_with_namespace}"))
- end
+ mail(to: project_admins,
+ subject: subject("Request to join #{@project.name_with_namespace} project"))
end
- def project_request_access_accepted_email(project_member_id)
- @project_member = ProjectMember.find project_member_id
- return if @project_member.created_by.nil?
-
- @project = @project_member.project
+ def project_access_granted_email(project_member_id)
+ setup_project_member_mail(project_member_id)
- @target_url = namespace_project_url(@project.namespace, @project)
- @current_user = @project_member.created_by
+ @current_user = @project_member.user
- mail(to: @project_member.created_by.notification_email,
- subject: subject('Request for access granted'))
+ mail(to: @current_user.notification_email,
+ subject: subject("Access to #{@project.name_with_namespace} project was granted"))
end
- def project_request_access_declined_email(project_member_id)
- @project_member = ProjectMember.find project_member_id
- return if @project_member.created_by.nil?
-
- @project = @project_member.project
-
+ def project_access_denied_email(project_id, user_id)
+ @project = Project.find(project_id)
+ @current_user = User.find(user_id)
@target_url = namespace_project_url(@project.namespace, @project)
- @current_user = @project_member.created_by
- mail(to: @project_member.created_by.notification_email,
- subject: subject('Request for access declined'))
+ mail(to: @current_user.notification_email,
+ subject: subject("Access to #{@project.name_with_namespace} project was denied"))
end
-
def project_member_invited_email(project_member_id, token)
- @project_member = ProjectMember.find project_member_id
- @project = @project_member.project
- @token = token
+ setup_project_member_mail(project_member_id)
- @target_url = namespace_project_url(@project.namespace, @project)
+ @token = token
@current_user = @project_member.user
mail(to: @project_member.invite_email,
@@ -66,12 +40,9 @@ module Emails
end
def project_invite_accepted_email(project_member_id)
- @project_member = ProjectMember.find project_member_id
+ setup_project_member_mail(project_member_id)
return if @project_member.created_by.nil?
- @project = @project_member.project
-
- @target_url = namespace_project_url(@project.namespace, @project)
@current_user = @project_member.created_by
mail(to: @project_member.created_by.notification_email,
@@ -117,5 +88,13 @@ module Emails
reply_to: @message.reply_to,
subject: @message.subject)
end
+
+ private
+
+ def setup_project_member_mail(project_member_id)
+ @project_member = ProjectMember.find(project_member_id)
+ @project = @project_member.project
+ @target_url = namespace_project_url(@project.namespace, @project)
+ end
end
end
diff --git a/app/models/ability.rb b/app/models/ability.rb
index b3db26f989e..90156bf130c 100644
--- a/app/models/ability.rb
+++ b/app/models/ability.rb
@@ -153,7 +153,7 @@ class Ability
RequestStore.store[key] ||= begin
# Push abilities on the users team role
- rules.push(*project_team_rules(project.team, user)) unless project.team.pending?(user)
+ rules.push(*project_team_rules(project.team, user))
if project.owner == user ||
(project.group && project.group.has_owner?(user)) ||
@@ -187,6 +187,8 @@ class Ability
project_report_rules
elsif team.guest?(user)
project_guest_rules
+ else
+ []
end
end
@@ -458,6 +460,8 @@ class Ability
rules << :destroy_group_member
elsif user == target_user
rules << :destroy_group_member
+ elsif subject.request? && user == subject.created_by
+ rules << :destroy_group_member
end
end
@@ -477,6 +481,8 @@ class Ability
rules << :destroy_project_member
elsif user == target_user
rules << :destroy_project_member
+ elsif subject.request? && user == subject.created_by
+ rules << :destroy_project_member
end
end
diff --git a/app/models/concerns/access_requestable.rb b/app/models/concerns/access_requestable.rb
new file mode 100644
index 00000000000..cf37284e31a
--- /dev/null
+++ b/app/models/concerns/access_requestable.rb
@@ -0,0 +1,27 @@
+# == AccessRequestable concern
+#
+# Contains functionality related to objects that can receive request for access.
+#
+# Used by Project, and Group.
+#
+module AccessRequestable
+ extend ActiveSupport::Concern
+
+ def request_access(user)
+ members.create(
+ access_level: Gitlab::Access::DEVELOPER,
+ created_by: user,
+ requested_at: Time.now.utc)
+ end
+
+ def access_requested?(user)
+ members.where(created_by_id: user.id).where.not(requested_at: nil).any?
+ end
+
+ private
+
+ # Returns a `<entities>_members` association, e.g.: project_members, group_members
+ def members
+ @members ||= send("#{self.class.to_s.underscore}_members".to_sym)
+ end
+end
diff --git a/app/models/group.rb b/app/models/group.rb
index aec92e335e6..b6929112cba 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -3,6 +3,7 @@ require 'carrierwave/orm/activerecord'
class Group < Namespace
include Gitlab::ConfigHelper
include Gitlab::VisibilityLevel
+ include AccessRequestable
include Referable
has_many :group_members, dependent: :destroy, as: :source, class_name: 'GroupMember'
diff --git a/app/models/member.rb b/app/models/member.rb
index 2210e7dd66a..5c3a5eab406 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -8,7 +8,7 @@ class Member < ActiveRecord::Base
belongs_to :user
belongs_to :source, polymorphic: true
- validates :user, presence: true, unless: :invite?
+ validates :user, presence: true, unless: :pending?
validates :source, presence: true
validates :user_id, uniqueness: { scope: [:source_type, :source_id],
message: "already exists in source",
@@ -26,29 +26,25 @@ class Member < ActiveRecord::Base
allow_nil: true
}
- scope :invite, -> { where(user_id: nil) }
- scope :non_invite, -> { where('user_id IS NOT NULL') }
- scope :request, -> { where(requested: true) }
- scope :non_request, -> { where(requested: nil) }
- scope :pending, -> { where("user_id IS NULL OR requested") }
- scope :non_pending, -> { self.non_invite.non_request }
+ scope :invite, -> { where.not(invite_token: nil) }
+ scope :request, -> { where.not(requested_at: nil) }
+ scope :non_request, -> { where(requested_at: nil) }
+ scope :non_pending, -> { where.not(user_id: nil) }
scope :guests, -> { where(access_level: GUEST) }
scope :reporters, -> { where(access_level: REPORTER) }
scope :developers, -> { where(access_level: DEVELOPER) }
scope :masters, -> { where(access_level: MASTER) }
scope :owners, -> { where(access_level: OWNER) }
+ scope :admins, -> { where(access_level: [OWNER, MASTER]) }
before_validation :generate_invite_token, on: :create, if: -> (member) { member.invite_email.present? }
after_create :send_invite, if: :invite?
- after_create :send_request_access, if: :request?
-
+ after_create :send_request, if: :request?
after_create :create_notification_setting, unless: :pending?
after_create :post_create_hook, unless: :pending?
-
after_update :post_update_hook, unless: :pending?
-
after_destroy :post_destroy_hook, unless: :pending?
delegate :name, :username, :email, to: :user, prefix: true
@@ -111,31 +107,29 @@ class Member < ActiveRecord::Base
end
def request?
- self.requested
+ user.nil? && created_by.present? && requested_at.present?
end
def invite?
self.invite_token.present?
end
- def accept_request_access!
+ def accept_request
return false unless request?
- self.request = false
- saved = self.save
+ updated = self.update(user: created_by, requested_at: nil)
+ after_accept_request if updated
- after_accept_request_access if saved
-
- saved
+ updated
end
- def decline_request_access!
+ def decline_request
return false unless request?
- destroyed = self.destroy
- after_decline_request_access if destroyed
+ self.destroy
+ after_decline_request if destroyed?
- destroyed
+ destroyed?
end
def accept_invite!(new_user)
@@ -191,11 +185,11 @@ class Member < ActiveRecord::Base
private
- def send_request_access
+ def send_invite
# override in subclass
end
- def send_invite
+ def send_request
# override in subclass
end
@@ -211,19 +205,19 @@ class Member < ActiveRecord::Base
system_hook_service.execute_hooks_for(self, :destroy)
end
- def after_accept_request_access
+ def after_accept_invite
post_create_hook
end
- def after_decline_request_access
+ def after_decline_invite
# override in subclass
end
- def after_accept_invite
+ def after_accept_request
post_create_hook
end
- def after_decline_invite
+ def after_decline_request
# override in subclass
end
diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb
index f63a0debf1a..476b4816b90 100644
--- a/app/models/members/group_member.rb
+++ b/app/models/members/group_member.rb
@@ -8,9 +8,6 @@ class GroupMember < Member
validates_format_of :source_type, with: /\ANamespace\z/
default_scope { where(source_type: SOURCE_TYPE) }
- scope :with_group, ->(group) { where(source_id: group.id) }
- scope :with_user, ->(user) { where(user_id: user.id) }
-
def self.access_level_roles
Gitlab::Access.options_with_owner
end
@@ -31,6 +28,12 @@ class GroupMember < Member
super
end
+ def send_request
+ notification_service.new_group_access_request(self)
+
+ super
+ end
+
def post_create_hook
notification_service.new_group_member(self)
@@ -56,4 +59,10 @@ class GroupMember < Member
super
end
+
+ def after_decline_request
+ notification_service.decline_group_access_request(group, created_by)
+
+ super
+ end
end
diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb
index 9db8db8450d..c6fd1a5c3d1 100644
--- a/app/models/members/project_member.rb
+++ b/app/models/members/project_member.rb
@@ -11,8 +11,6 @@ class ProjectMember < Member
default_scope { where(source_type: SOURCE_TYPE) }
scope :in_project, ->(project) { where(source_id: project.id) }
- scope :in_projects, ->(projects) { where(source_id: projects.pluck(:id)) }
- scope :with_user, ->(user) { where(user_id: user.id) }
before_destroy :delete_member_todos
@@ -84,7 +82,7 @@ class ProjectMember < Member
Gitlab::Access.sym_options
end
- def access_roles
+ def access_level_roles
Gitlab::Access.options
end
end
@@ -107,14 +105,14 @@ class ProjectMember < Member
user.todos.where(project_id: source_id).destroy_all if user
end
- def send_request_access
- notification_service.request_access_project_member(self)
+ def send_invite
+ notification_service.invite_project_member(self, @raw_invite_token)
super
end
- def send_invite
- notification_service.invite_project_member(self, @raw_invite_token)
+ def send_request
+ notification_service.new_project_access_request(self)
super
end
@@ -142,18 +140,6 @@ class ProjectMember < Member
super
end
- def after_accept_request_access
- notification_service.accept_project_request_access(self)
-
- super
- end
-
- def after_decline_request_access
- notification_service.decline_project_request_access(self)
-
- super
- end
-
def after_accept_invite
notification_service.accept_project_invite(self)
@@ -166,6 +152,12 @@ class ProjectMember < Member
super
end
+ def after_decline_request
+ notification_service.decline_project_access_request(project, created_by)
+
+ super
+ end
+
def event_service
EventCreateService.new
end
diff --git a/app/models/project.rb b/app/models/project.rb
index dfa99fe0df2..ef665373495 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -5,6 +5,7 @@ class Project < ActiveRecord::Base
include Gitlab::ShellAdapter
include Gitlab::VisibilityLevel
include Gitlab::CurrentSettings
+ include AccessRequestable
include Referable
include Sortable
include AfterCommitQueue
@@ -102,7 +103,7 @@ class Project < ActiveRecord::Base
has_many :snippets, dependent: :destroy, class_name: 'ProjectSnippet'
has_many :hooks, dependent: :destroy, class_name: 'ProjectHook'
has_many :protected_branches, dependent: :destroy
- has_many :project_members, dependent: :destroy, as: :source, class_name: 'ProjectMember'
+ has_many :project_members, dependent: :destroy, as: :source, class_name: 'ProjectMember'
has_many :users, through: :project_members
has_many :deploy_keys_projects, dependent: :destroy
has_many :deploy_keys, through: :deploy_keys_projects
@@ -680,16 +681,6 @@ class Project < ActiveRecord::Base
end
end
- def project_member_by_name_or_email(name = nil, email = nil)
- user = users.find_by('name like ? or email like ?', name, email)
- project_members.where(user: user) if user
- end
-
- # Get Team Member record by user id
- def project_member_by_id(user_id)
- project_members.find_by(user_id: user_id)
- end
-
def name_with_namespace
@name_with_namespace ||= begin
if namespace
diff --git a/app/models/project_team.rb b/app/models/project_team.rb
index 769b73666ce..7fb17df0e96 100644
--- a/app/models/project_team.rb
+++ b/app/models/project_team.rb
@@ -21,16 +21,6 @@ class ProjectTeam
end
end
- def find(user_id)
- user = project.users.find_by(id: user_id)
-
- if group
- user ||= group.users.find_by(id: user_id)
- end
-
- user
- end
-
def find_member(user_id)
member = project.project_members.find_by(user_id: user_id)
@@ -61,13 +51,10 @@ class ProjectTeam
ProjectMember.truncate_team(project)
end
- def users
- members
- end
-
def members
@members ||= fetch_members
end
+ alias_method :users, :members
def guests
@guests ||= fetch_members(:guests)
@@ -115,12 +102,6 @@ class ProjectTeam
false
end
- def pending?(user)
- project.project_members.each do |member|
- return member.pending? if member.user_id == user.id
- end
- end
-
def guest?(user)
max_member_access(user.id) == Gitlab::Access::GUEST
end
@@ -147,10 +128,6 @@ class ProjectTeam
end
end
- def human_max_access(user_id)
- Gitlab::Access.options_with_owner.key(max_member_access(user_id))
- end
-
# This method assumes project and group members are eager loaded for optimal
# performance.
def max_member_access(user_id)
@@ -179,6 +156,7 @@ class ProjectTeam
access.compact.max
end
+ private
def max_invited_level(user_id)
project.project_group_links.map do |group_link|
@@ -195,8 +173,6 @@ class ProjectTeam
end.compact.max
end
- private
-
def fetch_members(level = nil)
project_members = project.project_members
group_members = group ? group.group_members : []
diff --git a/app/models/user.rb b/app/models/user.rb
index a5b3c8afe51..8d0427da5ab 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -56,8 +56,7 @@ class User < ActiveRecord::Base
# Groups
has_many :members, dependent: :destroy
- has_many :project_members, source: 'ProjectMember'
- has_many :group_members, source: 'GroupMember'
+ has_many :group_members, dependent: :destroy, source: 'GroupMember'
has_many :groups, through: :group_members
has_many :owned_groups, -> { where members: { access_level: Gitlab::Access::OWNER } }, through: :group_members, source: :group
has_many :masters_groups, -> { where members: { access_level: Gitlab::Access::MASTER } }, through: :group_members, source: :group
@@ -65,13 +64,13 @@ class User < ActiveRecord::Base
# Projects
has_many :groups_projects, through: :groups, source: :projects
has_many :personal_projects, through: :namespace, source: :projects
+ has_many :project_members, dependent: :destroy, class_name: 'ProjectMember'
has_many :projects, through: :project_members
has_many :created_projects, foreign_key: :creator_id, class_name: 'Project'
has_many :users_star_projects, dependent: :destroy
has_many :starred_projects, through: :users_star_projects, source: :project
has_many :snippets, dependent: :destroy, foreign_key: :author_id, class_name: "Snippet"
- has_many :project_members, dependent: :destroy, class_name: 'ProjectMember'
has_many :issues, dependent: :destroy, foreign_key: :author_id
has_many :notes, dependent: :destroy, foreign_key: :author_id
has_many :merge_requests, dependent: :destroy, foreign_key: :author_id
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index e7676861e9b..cd11feb9d7a 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -173,16 +173,13 @@ class NotificationService
end
end
- def request_access_project_member(project_member)
- mailer.project_member_requested_access(project_member.id).deliver_later
+ # Project access request
+ def new_project_access_request(project_member)
+ mailer.project_access_requested_email(project_member.id).deliver_later
end
- def accept_project_request_access(project_member)
- mailer.project_request_access_accepted_email(project_member.id).deliver_later
- end
-
- def decline_project_request_access(project_member)
- mailer.project_request_access_declined_email(project_member.id).deliver_later
+ def decline_project_access_request(project, user)
+ mailer.project_access_denied_email(project.id, user.id).deliver_later
end
def invite_project_member(project_member, token)
@@ -210,6 +207,15 @@ class NotificationService
mailer.project_access_granted_email(project_member.id).deliver_later
end
+ # Group access request
+ def new_group_access_request(group_member)
+ mailer.group_access_requested_email(group_member.id).deliver_later
+ end
+
+ def decline_group_access_request(group, user)
+ mailer.group_access_denied_email(group.id, user.id).deliver_later
+ end
+
def invite_group_member(group_member, token)
mailer.group_member_invited_email(group_member.id, token).deliver_later
end
diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml
index f309e80a39a..5b8a0262ea0 100644
--- a/app/views/admin/groups/show.html.haml
+++ b/app/views/admin/groups/show.html.haml
@@ -109,7 +109,7 @@
%span.pull-right.light
= member.human_access
- if can?(current_user, :destroy_group_member, member)
- = link_to group_group_member_path(@group, member), data: { confirm: remove_user_from_group_message(@group, member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from group' do
+ = link_to group_group_member_path(@group, member), data: { confirm: remove_member_message(member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from group' do
%i.fa.fa-minus.fa-inverse
.panel-footer
= paginate @members, param_name: 'members_page', theme: 'gitlab'
diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml
index 73986d21bcf..9e55a562e18 100644
--- a/app/views/admin/projects/show.html.haml
+++ b/app/views/admin/projects/show.html.haml
@@ -142,7 +142,7 @@
%i.fa.fa-pencil-square-o
%ul.well-list
- @group_members.each do |member|
- = render 'groups/group_members/group_member', member: member, show_controls: false
+ = render 'shared/members/member', member: member, show_controls: false
.panel-footer
= paginate @group_members, param_name: 'group_members_page', theme: 'gitlab'
@@ -172,7 +172,7 @@
%span.light Owner
- else
%span.light= project_member.human_access
- = link_to namespace_project_project_member_path(@project.namespace, @project, project_member), data: { confirm: remove_from_project_team_message(@project, project_member)}, method: :delete, remote: true, class: "btn btn-sm btn-remove" do
+ = link_to namespace_project_project_member_path(@project.namespace, @project, project_member), data: { confirm: remove_member_message(project_member)}, method: :delete, remote: true, class: "btn btn-sm btn-remove" do
%i.fa.fa-times
.panel-footer
= paginate @project_members, param_name: 'project_members_page', theme: 'gitlab'
diff --git a/app/views/admin/users/groups.html.haml b/app/views/admin/users/groups.html.haml
index dbecb7bbfd6..b0a709a568a 100644
--- a/app/views/admin/users/groups.html.haml
+++ b/app/views/admin/users/groups.html.haml
@@ -13,7 +13,7 @@
.pull-right
%span.light= group_member.human_access
- unless group_member.owner?
- = link_to group_group_member_path(group, group_member), data: { confirm: remove_user_from_group_message(group, group_member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from group' do
+ = link_to group_group_member_path(group, group_member), data: { confirm: remove_member_message(group_member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from group' do
%i.fa.fa-times.fa-inverse
- else
.nothing-here-block This user has no groups.
diff --git a/app/views/admin/users/projects.html.haml b/app/views/admin/users/projects.html.haml
index b655b2a15f5..84b9ceb23b3 100644
--- a/app/views/admin/users/projects.html.haml
+++ b/app/views/admin/users/projects.html.haml
@@ -38,6 +38,5 @@
%span.light= member.human_access
- if member.respond_to? :project
- = link_to namespace_project_project_member_path(project.namespace, project, member), data: { confirm: remove_from_project_team_message(project, member) }, remote: true, method: :delete, class: "btn-xs btn btn-remove", title: 'Remove user from project' do
+ = link_to namespace_project_project_member_path(project.namespace, project, member), data: { confirm: remove_member_message(member) }, remote: true, method: :delete, class: "btn-xs btn btn-remove", title: 'Remove user from project' do
%i.fa.fa-times
-
diff --git a/app/views/groups/group_members/_group_member.html.haml b/app/views/groups/group_members/_group_member.html.haml
deleted file mode 100644
index 6bb542e658d..00000000000
--- a/app/views/groups/group_members/_group_member.html.haml
+++ /dev/null
@@ -1,57 +0,0 @@
-- user = member.user
-- return unless user || member.invite?
-- show_roles = local_assigns.fetch(:show_roles, true)
-
-%li{class: "#{dom_class(member)} js-toggle-container", id: dom_id(member)}
- %span{class: ("list-item-name" if show_controls)}
- - if member.user
- = image_tag avatar_icon(user, 24), class: "avatar s24", alt: ''
- %strong
- = link_to user.name, user_path(user)
- %span.cgray= user.username
- - if user == current_user
- %span.label.label-success It's you
- - if user.blocked?
- %label.label.label-danger
- %strong Blocked
- - else
- = image_tag avatar_icon(member.invite_email, 24), class: "avatar s24", alt: ''
- %strong
- = member.invite_email
- %span.cgray
- invited
- - if member.created_by
- by
- = link_to member.created_by.name, user_path(member.created_by)
- = time_ago_with_tooltip(member.created_at)
-
- - if show_controls && can?(current_user, :admin_group_member, @group)
- = link_to resend_invite_group_group_member_path(@group, member), method: :post, class: "btn-xs btn", title: 'Resend invite' do
- Resend invite
-
- - if show_roles && should_user_see_group_roles?(current_user, @group)
- %span.pull-right
- %strong.member-access-level= member.human_access
- - if show_controls
- - if can?(current_user, :update_group_member, member)
- = button_tag class: "btn-xs btn btn-grouped inline js-toggle-button",
- title: 'Edit access level', type: 'button' do
- = icon('pencil')
-
- - if can?(current_user, :destroy_group_member, member)
- &nbsp;
- - if current_user == user
- = link_to leave_group_group_members_path(@group), data: { confirm: leave_group_message(@group.name)}, method: :delete, class: "btn-xs btn btn-remove", title: 'Remove user from group' do
- = icon("sign-out")
- Leave
- - else
- = link_to group_group_member_path(@group, member), data: { confirm: remove_user_from_group_message(@group, member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from group' do
- = icon('trash')
-
- .edit-member.hide.js-toggle-content
- %br
- = form_for [@group, member], remote: true do |f|
- .prepend-top-10
- = f.select :access_level, options_for_select(GroupMember.access_level_roles, member.access_level), {}, class: 'form-control'
- .prepend-top-10
- = f.submit 'Save', class: 'btn btn-save btn-sm'
diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml
index 0eb6bbd4420..a39d5d3d0f0 100644
--- a/app/views/groups/group_members/index.html.haml
+++ b/app/views/groups/group_members/index.html.haml
@@ -6,12 +6,13 @@
.panel-heading
Add new user to group
.panel-body
- - if should_user_see_group_roles?(current_user, @group)
- %p.light
- Members of group have access to all group projects.
+ %p.light
+ Members of group have access to all group projects.
.new-group-member-holder
= render "new_group_member"
+ = render "shared/members/requests", entity: @group, members: @members
+
.panel.panel-default
.panel-heading
%strong #{@group.name}
@@ -25,9 +26,8 @@
= button_tag class: 'btn', title: 'Search' do
= icon("search")
%ul.content-list
- - @members.each do |member|
- = render 'groups/group_members/group_member', member: member, show_controls: true
- = paginate @members, theme: 'gitlab'
+ = render partial: 'shared/members/member', collection: @members.non_request, as: :member
+ = paginate @members.non_request, theme: 'gitlab'
:javascript
$('form.member-search-form').on('submit', function(event) {
diff --git a/app/views/groups/group_members/update.js.haml b/app/views/groups/group_members/update.js.haml
index df726e2b2b9..b0b3a51ce58 100644
--- a/app/views/groups/group_members/update.js.haml
+++ b/app/views/groups/group_members/update.js.haml
@@ -1,2 +1,2 @@
:plain
- $("##{dom_id(@group_member)}").replaceWith('#{escape_javascript(render(@group_member, member: @group_member, show_controls: true))}');
+ $("##{dom_id(@group_member)}").replaceWith('#{escape_javascript(render(@group_member, member: @group_member))}');
diff --git a/app/views/layouts/nav/_group_settings.html.haml b/app/views/layouts/nav/_group_settings.html.haml
index 0b2673f1a82..b461772b87e 100644
--- a/app/views/layouts/nav/_group_settings.html.haml
+++ b/app/views/layouts/nav/_group_settings.html.haml
@@ -1,20 +1,3 @@
- if current_user
- - if access = @group.users.find_by(id: current_user.id)
- .controls
- .dropdown.group-settings-dropdown
- %a.dropdown-new.btn.btn-default#group-settings-button{href: '#', 'data-toggle' => 'dropdown'}
- = icon('cog')
- = icon('caret-down')
- %ul.dropdown-menu.dropdown-menu-align-right
- - if can?(current_user, :admin_group, @group)
- = nav_link(path: 'groups#projects') do
- = link_to projects_group_path(@group), title: 'Projects' do
- Projects
- %li.divider
- %li
- = link_to edit_group_path(@group) do
- Edit Group
- %li
- = link_to leave_group_group_members_path(@group),
- data: { confirm: leave_group_message(@group.name) }, method: :delete, title: 'Leave group' do
- Leave Group
+ .controls
+ = render 'shared/group_or_project_home_dropdown', entity: @group
diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml
index 1336191bc5e..3398794302f 100644
--- a/app/views/layouts/nav/_project.html.haml
+++ b/app/views/layouts/nav/_project.html.haml
@@ -8,19 +8,6 @@
= icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right
= render 'layouts/nav/project_settings'
-
- - if access
- %li
- = link_to leave_namespace_project_project_members_path(@project.namespace, @project),
- data: { confirm: leave_project_message(@project) }, method: :delete, title: 'Leave project' do
- Leave Project
- - else
- = link_to request_access_namespace_project_project_members_path(@project.namespace, @project),
- class: 'btn btn-gray', style: 'margin-left: 10px', method: :post, title: 'Request access' do
- Request Access
-
-
-
%li.divider
- if can_edit
%li
@@ -28,13 +15,18 @@
Edit Project
- if access
%li
- = link_to leave_namespace_project_project_members_path(@project.namespace, @project),
- data: { confirm: leave_project_message(@project) }, method: :delete, title: 'Leave project' do
+ = link_to leave_path(@project),
+ data: { confirm: leave_confirmation_message(@project) }, method: :delete do
Leave Project
+ - elsif @project.access_requested?(current_user)
+ %li
+ = link_to leave_path(@project),
+ data: { confirm: withdraw_request_message(@project) }, method: :delete do
+ Withdraw Request
- else
%li
- = link_to request_access_namespace_project_project_members_path(@project.namespace, @project),
- class: 'btn btn-gray', style: 'margin-left: 10px', method: :post, title: 'Request access' do
+ = link_to request_access_path(@project),
+ class: 'btn btn-gray', style: 'margin-left: 10px', method: :post do
Request Access
%div{ class: nav_control_class }
diff --git a/app/views/notify/group_access_denied_email.html.haml b/app/views/notify/group_access_denied_email.html.haml
new file mode 100644
index 00000000000..4edfd4e4793
--- /dev/null
+++ b/app/views/notify/group_access_denied_email.html.haml
@@ -0,0 +1,2 @@
+%p
+ Your request to join group #{link_to @group.name, @target_url} has been denied.
diff --git a/app/views/notify/group_access_denied_email.text.erb b/app/views/notify/group_access_denied_email.text.erb
new file mode 100644
index 00000000000..cb32177e826
--- /dev/null
+++ b/app/views/notify/group_access_denied_email.text.erb
@@ -0,0 +1,3 @@
+Your request to join group <%= @group.name %> has been denied.
+
+<%= @target_url %>
diff --git a/app/views/notify/group_access_granted_email.html.haml b/app/views/notify/group_access_granted_email.html.haml
index f1916d624b6..1283758c576 100644
--- a/app/views/notify/group_access_granted_email.html.haml
+++ b/app/views/notify/group_access_granted_email.html.haml
@@ -1,4 +1,3 @@
%p
- = "You have been granted #{@group_member.human_access} access to group"
- = link_to group_url(@group) do
- = @group.name
+ You have been granted #{@group_member.human_access} access to group
+ #{link_to @group.name, @target_url}.
diff --git a/app/views/notify/group_access_granted_email.text.erb b/app/views/notify/group_access_granted_email.text.erb
index ef9617bfc16..c7568350075 100644
--- a/app/views/notify/group_access_granted_email.text.erb
+++ b/app/views/notify/group_access_granted_email.text.erb
@@ -1,4 +1,3 @@
+You have been granted <%= @group_member.human_access %> access to group <%= @group.name %>.
-You have been granted <%= @group_member.human_access %> access to group <%= @group.name %>
-
-<%= url_for(group_url(@group)) %>
+<%= @target_url %>
diff --git a/app/views/notify/group_access_requested_email.html.haml b/app/views/notify/group_access_requested_email.html.haml
new file mode 100644
index 00000000000..4fbcedabae0
--- /dev/null
+++ b/app/views/notify/group_access_requested_email.html.haml
@@ -0,0 +1,3 @@
+%p
+ #{link_to @requester.name, @requester} requested #{@group_member.human_access}
+ access to group #{link_to @group.name, @target_url}.
diff --git a/app/views/notify/group_access_requested_email.text.erb b/app/views/notify/group_access_requested_email.text.erb
new file mode 100644
index 00000000000..2f9d293a79e
--- /dev/null
+++ b/app/views/notify/group_access_requested_email.text.erb
@@ -0,0 +1,3 @@
+<%= @requester.name %> (<%= user_url(@requester) %>) requested <%= @group_member.human_access %> access to group <%= @group.name %>
+
+<%= @target_url %>
diff --git a/app/views/notify/project_access_denied_email.html.haml b/app/views/notify/project_access_denied_email.html.haml
new file mode 100644
index 00000000000..cecdaf24f39
--- /dev/null
+++ b/app/views/notify/project_access_denied_email.html.haml
@@ -0,0 +1,3 @@
+%p
+ Your request to join project #{link_to @project.name_with_namespace, @target_url}
+ has been denied.
diff --git a/app/views/notify/project_request_access_denied_email.text.erb b/app/views/notify/project_access_denied_email.text.erb
index a9c57e4cab4..24357e059d2 100644
--- a/app/views/notify/project_request_access_denied_email.text.erb
+++ b/app/views/notify/project_access_denied_email.text.erb
@@ -1,3 +1,3 @@
Your request to join project <%= @project.name_with_namespace %> has been denied.
-<%= namespace_project_url(@project.namespace, @project) %>
+<%= @target_url %>
diff --git a/app/views/notify/project_access_granted_email.html.haml b/app/views/notify/project_access_granted_email.html.haml
index dfc30a2d360..88873e7fe52 100644
--- a/app/views/notify/project_access_granted_email.html.haml
+++ b/app/views/notify/project_access_granted_email.html.haml
@@ -1,5 +1,3 @@
%p
- = "You have been granted #{@project_member.human_access} access to project"
-%p
- = link_to namespace_project_url(@project.namespace, @project) do
- = @project.name_with_namespace
+ You have been granted #{@project_member.human_access} access to project
+ #{link_to @project.name_with_namespace, @target_url}.
diff --git a/app/views/notify/project_access_granted_email.text.erb b/app/views/notify/project_access_granted_email.text.erb
index 68eb1611ba7..f5e4b313858 100644
--- a/app/views/notify/project_access_granted_email.text.erb
+++ b/app/views/notify/project_access_granted_email.text.erb
@@ -1,4 +1,3 @@
+You have been granted <%= @project_member.human_access %> access to project <%= @project.name_with_namespace %>.
-You have been granted <%= @project_member.human_access %> access to project <%= @project.name_with_namespace %>
-
-<%= url_for(namespace_project_url(@project.namespace, @project)) %>
+<%= @target_url %>
diff --git a/app/views/notify/project_access_requested_email.html.haml b/app/views/notify/project_access_requested_email.html.haml
new file mode 100644
index 00000000000..2a705ad3b0a
--- /dev/null
+++ b/app/views/notify/project_access_requested_email.html.haml
@@ -0,0 +1,3 @@
+%p
+ #{link_to @requester.name, @requester} requested #{@project_member.human_access}
+ access to project #{link_to @project.name_with_namespace, @target_url}.
diff --git a/app/views/notify/project_access_requested_email.text.erb b/app/views/notify/project_access_requested_email.text.erb
new file mode 100644
index 00000000000..2437fa4ee86
--- /dev/null
+++ b/app/views/notify/project_access_requested_email.text.erb
@@ -0,0 +1,3 @@
+<%= @requester.name %> (<%= user_url(@requester) %>) requested <%= @project_member.human_access %> access to project <%= @project.name_with_namespace %>.
+
+<%= @target_url %>
diff --git a/app/views/notify/project_request_access_accepted_email.html.haml b/app/views/notify/project_request_access_accepted_email.html.haml
deleted file mode 100644
index dfdf82e70a5..00000000000
--- a/app/views/notify/project_request_access_accepted_email.html.haml
+++ /dev/null
@@ -1,4 +0,0 @@
-%p
- Your request to join project
- #{link_to @project.name_with_namespace, namespace_project_url(@project.namespace, @project)}
- has been granted with #{@project_member.human_access} access.
diff --git a/app/views/notify/project_request_access_accepted_email.text.erb b/app/views/notify/project_request_access_accepted_email.text.erb
deleted file mode 100644
index 9fb68874494..00000000000
--- a/app/views/notify/project_request_access_accepted_email.text.erb
+++ /dev/null
@@ -1,3 +0,0 @@
-Your request to join project <%= @project.name_with_namespace %> has been granted with <%= @project_member.human_access %> access.
-
-<%= namespace_project_url(@project.namespace, @project) %>
diff --git a/app/views/notify/project_request_access_denied_email.html.haml b/app/views/notify/project_request_access_denied_email.html.haml
deleted file mode 100644
index 8ad75b96cf9..00000000000
--- a/app/views/notify/project_request_access_denied_email.html.haml
+++ /dev/null
@@ -1,4 +0,0 @@
-%p
- Your request to join project
- #{link_to @project.name_with_namespace, namespace_project_url(@project.namespace, @project)}
- has been denied.
diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml
index bcdbff08011..112a532f9d3 100644
--- a/app/views/projects/notes/_note.html.haml
+++ b/app/views/projects/notes/_note.html.haml
@@ -17,7 +17,7 @@
%a{ href: "##{dom_id(note)}" }
= time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago')
.note-actions
- - access = note.project.team.human_max_access(note.author.id)
+ - access = max_access_level(note.project, note.author)
- if access
%span.note-role.hidden-xs= access
- if current_user
diff --git a/app/views/projects/project_members/_group_members.html.haml b/app/views/projects/project_members/_group_members.html.haml
index 6671ee2c6d6..78c12d52a78 100644
--- a/app/views/projects/project_members/_group_members.html.haml
+++ b/app/views/projects/project_members/_group_members.html.haml
@@ -9,8 +9,13 @@
= link_to group_group_members_path(@group), class: 'btn' do
Manage group members
%ul.content-list
- - members.limit(20).each do |member|
- = render 'groups/group_members/group_member', member: member, show_controls: false
- - if members.count > 20
+ = render partial: 'shared/members/member',
+ collection: members.limit(20),
+ as: :member,
+ locals: { show_controls: false }
+ - if members.size > 20
%li
- and #{members.count - 20} more. For full list visit #{link_to 'group members page', group_group_members_path(@group)}
+ and
+ = members.size - 20
+ more. For full list visit
+ = link_to 'group members page', group_group_members_path(@group)
diff --git a/app/views/projects/project_members/_new_project_member.html.haml b/app/views/projects/project_members/_new_project_member.html.haml
index f0f3bb3c177..82892a33358 100644
--- a/app/views/projects/project_members/_new_project_member.html.haml
+++ b/app/views/projects/project_members/_new_project_member.html.haml
@@ -9,7 +9,7 @@
.form-group
= f.label :access_level, "Project Access", class: 'control-label'
.col-sm-10
- = select_tag :access_level, options_for_select(ProjectMember.access_roles, @project_member.access_level), class: "project-access-select select2"
+ = select_tag :access_level, options_for_select(ProjectMember.access_level_roles, @project_member.access_level), class: "project-access-select select2"
.help-block
Read more about role permissions
%strong= link_to "here", help_page_path("permissions", "permissions"), class: "vlink"
diff --git a/app/views/projects/project_members/_pending.html.haml b/app/views/projects/project_members/_pending.html.haml
deleted file mode 100644
index 88ac36937ac..00000000000
--- a/app/views/projects/project_members/_pending.html.haml
+++ /dev/null
@@ -1,21 +0,0 @@
-.panel.panel-default
- .panel-heading
- %strong #{@project.name}
- candidates
- %small
- (#{members.count})
- .controls
- = form_tag namespace_project_project_members_path(@project.namespace, @project), method: :get, class: 'form-inline member-search-form' do
- .form-group
- = search_field_tag :search, params[:search], { placeholder: 'Find existing member by name', class: 'form-control', spellcheck: false }
- = button_tag class: 'btn', title: 'Search' do
- = icon("search")
- %ul.content-list
- - members.each do |project_member|
- = render 'project_member', member: project_member
-
-:javascript
- $('form.member-search-form').on('submit', function (event) {
- event.preventDefault();
- Turbolinks.visit(this.action + '?' + $(this).serialize());
- });
diff --git a/app/views/projects/project_members/_project_member.html.haml b/app/views/projects/project_members/_project_member.html.haml
deleted file mode 100644
index 3faf5dba8a2..00000000000
--- a/app/views/projects/project_members/_project_member.html.haml
+++ /dev/null
@@ -1,66 +0,0 @@
-- user = member.user
-- return unless user || member.invite?
-
-%li{class: "#{dom_class(member)} js-toggle-container project_member_row access-#{member.human_access.downcase}", id: dom_id(member)}
- %span.list-item-name
- - if member.user
- = image_tag avatar_icon(user, 24), class: "avatar s24", alt: ''
- %strong
- = link_to user.name, user_path(user)
- %span.cgray= user.username
- - if user == current_user
- %span.label.label-success It's you
- - if user.blocked?
- %label.label.label-danger
- %strong Blocked
- - if member.request?
- %span.label.label-info
- Pending Approval
- - else
- = image_tag avatar_icon(member.invite_email, 24), class: "avatar s24", alt: ''
- %strong
- = member.invite_email
- %span.cgray
- invited
- - if member.created_by
- by
- = link_to member.created_by.name, user_path(member.created_by)
- = time_ago_with_tooltip(member.created_at)
-
- - if can?(current_user, :admin_project_member, @project)
- = link_to resend_invite_namespace_project_project_member_path(@project.namespace, @project, member), method: :post, class: "btn-xs btn", title: 'Resend invite' do
- Resend invite
- - if can?(current_user, :admin_project_member, @project)
- .pull-right
- %strong= member.human_access
- - if can?(current_user, :update_project_member, member)
- = button_tag class: "btn-xs btn-grouped inline btn js-toggle-button",
- title: 'Edit access level', type: 'button' do
- = icon('pencil')
- - if member.request?
- &nbsp;
- = link_to approval_namespace_project_project_member_path(@project.namespace, @project, member),
- class: "btn-xs btn btn-success",
- title: 'Grant access', type: 'button' do
- %i.fa.fa-check.fa-inverse
-
- - if can?(current_user, :destroy_project_member, member)
- &nbsp;
- - if member.request?
- = link_to namespace_project_project_member_path(@project.namespace, @project, member), data: { confirm: remove_from_project_team_message(@project, member) }, method: :delete, class: "btn-xs btn btn-remove", title: 'Deny access' do
- %i.fa.fa-times.fa-inverse
- - elsif current_user == user
- = link_to leave_namespace_project_project_members_path(@project.namespace, @project), data: { confirm: leave_project_message(@project) }, method: :delete, class: "btn-xs btn btn-remove", title: 'Leave project' do
- = icon("sign-out")
- Leave
- - else
- = link_to namespace_project_project_member_path(@project.namespace, @project, member), data: { confirm: remove_from_project_team_message(@project, member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from team' do
- = icon('trash')
-
- .edit-member.hide.js-toggle-content
- %br
- = form_for member, as: :project_member, url: namespace_project_project_member_path(@project.namespace, @project, member), remote: true do |f|
- .prepend-top-10
- = f.select :access_level, options_for_select(ProjectMember.access_roles, member.access_level), {}, class: 'form-control'
- .prepend-top-10
- = f.submit 'Save', class: 'btn btn-save'
diff --git a/app/views/projects/project_members/_shared_group_members.html.haml b/app/views/projects/project_members/_shared_group_members.html.haml
index ae13f8428f0..952844acefc 100644
--- a/app/views/projects/project_members/_shared_group_members.html.haml
+++ b/app/views/projects/project_members/_shared_group_members.html.haml
@@ -14,8 +14,10 @@
%i.fa.fa-pencil-square-o
Edit group members
%ul.content-list
- - shared_group.group_members.order('access_level DESC').limit(20).each do |member|
- = render 'groups/group_members/group_member', member: member, show_controls: false, show_roles: false
+ = render partial: 'shared/members/member',
+ collection: shared_group.group_members.order(access_level: :desc).limit(20),
+ as: :member,
+ locals: { show_controls: false, show_roles: false }
- if shared_group_users_count > 20
%li
and #{shared_group_users_count - 20} more. For full list visit #{link_to 'group members page', group_group_members_path(shared_group)}
diff --git a/app/views/projects/project_members/_team.html.haml b/app/views/projects/project_members/_team.html.haml
index e8dce30425f..03207614258 100644
--- a/app/views/projects/project_members/_team.html.haml
+++ b/app/views/projects/project_members/_team.html.haml
@@ -11,8 +11,7 @@
= button_tag class: 'btn', title: 'Search' do
= icon("search")
%ul.content-list
- - members.each do |project_member|
- = render 'project_member', member: project_member
+ = render partial: 'shared/members/member', collection: members, as: :member
:javascript
$('form.member-search-form').on('submit', function (event) {
diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml
index d5a19799c49..61a82724d69 100644
--- a/app/views/projects/project_members/index.html.haml
+++ b/app/views/projects/project_members/index.html.haml
@@ -12,7 +12,8 @@
%p.light
Users with access to this project are listed below.
= render "new_project_member"
- = render "pending", members: @project_members.request
+
+ = render "shared/members/requests", entity: @project, members: @project_members
= render "team", members: @project_members.non_request
diff --git a/app/views/shared/_group_or_project_home_dropdown.html.haml b/app/views/shared/_group_or_project_home_dropdown.html.haml
new file mode 100644
index 00000000000..fb9e63f2bd4
--- /dev/null
+++ b/app/views/shared/_group_or_project_home_dropdown.html.haml
@@ -0,0 +1,30 @@
+- member = entity.send(members_association(entity)).find_by(user_id: current_user.id)
+- can_edit = can?(current_user, "admin_#{entity.class.to_s.underscore}".to_sym, entity)
+
+- if member || can_edit
+ .dropdown.project-settings-dropdown
+ %a.dropdown-new.btn.btn-gray{ href: '#', id: "#{entity.class.to_s.underscore}-settings-button", data: { toggle: 'dropdown' } }
+ = icon('cog')
+ = icon('caret-down')
+ %ul.dropdown-menu.dropdown-menu-align-right
+ - if can_edit
+ %li
+ = link_to "Edit #{entity.class.to_s}", [:edit, entity]
+
+ - if member
+ %li
+ = link_to "Leave #{entity.class.to_s}",
+ leave_path(entity),
+ method: :delete,
+ data: { confirm: leave_confirmation_message(entity) }
+- elsif entity.access_requested?(current_user)
+ = link_to 'Withdraw Request',
+ leave_path(entity),
+ data: { confirm: withdraw_request_message(entity) },
+ method: :delete,
+ class: 'btn btn-grouped btn-gray'
+- else
+ = link_to 'Request Access',
+ request_access_path(entity),
+ method: :post,
+ class: 'btn btn-grouped btn-gray'
diff --git a/app/views/shared/groups/_group.html.haml b/app/views/shared/groups/_group.html.haml
index a25365a94f2..1ad95351005 100644
--- a/app/views/shared/groups/_group.html.haml
+++ b/app/views/shared/groups/_group.html.haml
@@ -9,7 +9,7 @@
= link_to edit_group_path(group), class: "btn" do
= icon('cogs')
- = link_to leave_group_group_members_path(group), data: { confirm: leave_group_message(group.name) }, method: :delete, class: "btn", title: 'Leave this group' do
+ = link_to leave_group_group_members_path(group), data: { confirm: leave_confirmation_message(group) }, method: :delete, class: "btn", title: 'Leave this group' do
= icon('sign-out')
.stats
diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml
new file mode 100644
index 00000000000..7e119155a6c
--- /dev/null
+++ b/app/views/shared/members/_member.html.haml
@@ -0,0 +1,77 @@
+- show_roles = local_assigns.fetch(:show_roles, true)
+- show_controls = local_assigns.fetch(:show_controls, true)
+- user = member.request? ? member.created_by : member.user
+
+%li.js-toggle-container{ class: dom_class(member), id: dom_id(member) }
+ %span{ class: ("list-item-name" if show_controls) }
+ - if user
+ = image_tag avatar_icon(user, 24), class: "avatar s24", alt: ''
+ %strong
+ = link_to user.name, user_path(user)
+ %span.cgray= user.username
+
+ - if user == current_user
+ %span.label.label-success It's you
+
+ - if user.blocked?
+ %label.label.label-danger
+ %strong Blocked
+
+ - if member.request?
+ %small
+ – Requested
+ = time_ago_with_tooltip(member.requested_at)
+ - else
+ = image_tag avatar_icon(member.invite_email, 24), class: "avatar s24", alt: ''
+ %strong= member.invite_email
+ %span.cgray
+ invited
+ - if member.created_by
+ by
+ = link_to member.created_by.name, user_path(member.created_by)
+ = time_ago_with_tooltip(member.created_at)
+
+ - if show_controls && can?(current_user, action_member_permission(:admin, member), member.source)
+ = link_to 'Resend invite', resend_invite_member_path(member),
+ method: :post,
+ class: 'btn-xs btn'
+
+ - if show_roles && can_see_entity_roles?(current_user, member.source)
+ %span.pull-right
+ %strong= member.human_access
+ - if show_controls
+ - if can?(current_user, action_member_permission(:update, member), member)
+ = button_tag icon('pencil'),
+ type: 'button',
+ class: 'btn-xs btn btn-grouped inline js-toggle-button',
+ title: 'Edit access level'
+
+ - if member.request?
+ &nbsp;
+ = link_to icon('check inverse'), approve_request_member_path(member),
+ method: :post,
+ type: 'button',
+ class: 'btn-xs btn btn-success',
+ title: 'Grant access'
+
+ - if can?(current_user, action_member_permission(:destroy, member), member)
+ &nbsp;
+ - if current_user == user
+ = link_to leave_path(member.source), data: { confirm: leave_confirmation_message(member.source)}, method: :delete, class: "btn-xs btn btn-remove", title: 'Remove user from group' do
+ = icon("sign-out")
+ Leave
+ - else
+ = link_to icon('trash'), member_path(member),
+ method: :delete,
+ remote: true,
+ data: { confirm: remove_member_message(member) },
+ class: 'btn-xs btn btn-remove',
+ title: remove_member_title(member)
+
+ .edit-member.hide.js-toggle-content
+ %br
+ = form_for member_path(member), as: "#{member.source.class.to_s.underscore}_member".to_sym, remote: true do |f|
+ .prepend-top-10
+ = f.select :access_level, options_for_select(member_class(member).access_level_roles, member.access_level), {}, class: 'form-control'
+ .prepend-top-10
+ = f.submit 'Save', class: 'btn btn-save btn-sm'
diff --git a/app/views/shared/members/_requests.html.haml b/app/views/shared/members/_requests.html.haml
new file mode 100644
index 00000000000..ffbb380f794
--- /dev/null
+++ b/app/views/shared/members/_requests.html.haml
@@ -0,0 +1,10 @@
+- requesters = members.request
+
+- if requesters.any?
+ .panel.panel-default
+ .panel-heading
+ %strong= entity.name
+ access requests
+ %small= "(#{requesters.size})"
+ %ul.content-list
+ = render partial: 'shared/members/member', collection: requesters, as: :member