diff options
author | Rémy Coutable <remy@rymai.me> | 2016-04-18 19:53:32 +0300 |
---|---|---|
committer | Rémy Coutable <remy@rymai.me> | 2016-06-14 14:07:26 +0300 |
commit | d26f81239a33b80694783ee35f0da0e2ed082c9b (patch) | |
tree | f092e818d4b81805a16879b13407a335bbda0054 /app | |
parent | 17c22156c5fa5663aae65178ed38cbeef9a80b7e (diff) |
Add request access for groups
Signed-off-by: Rémy Coutable <remy@rymai.me>
Diffstat (limited to 'app')
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) - - - 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? - - = 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) - - - 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? + + = 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) + + - 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 |