diff options
Diffstat (limited to 'app')
-rw-r--r-- | app/assets/javascripts/dispatcher.js | 1 | ||||
-rw-r--r-- | app/assets/javascripts/member_expiration_date.js | 34 | ||||
-rw-r--r-- | app/assets/javascripts/project_members.js | 3 | ||||
-rw-r--r-- | app/assets/stylesheets/pages/projects.scss | 26 | ||||
-rw-r--r-- | app/controllers/projects/project_members_controller.rb | 9 | ||||
-rw-r--r-- | app/helpers/members_helper.rb | 4 | ||||
-rw-r--r-- | app/models/group.rb | 2 | ||||
-rw-r--r-- | app/models/member.rb | 8 | ||||
-rw-r--r-- | app/models/members/project_member.rb | 10 | ||||
-rw-r--r-- | app/models/project_team.rb | 9 | ||||
-rw-r--r-- | app/services/members/authorized_destroy_service.rb | 17 | ||||
-rw-r--r-- | app/services/members/destroy_service.rb | 7 | ||||
-rw-r--r-- | app/views/projects/project_members/_new_project_member.html.haml | 7 | ||||
-rw-r--r-- | app/views/projects/project_members/index.html.haml | 2 | ||||
-rw-r--r-- | app/views/projects/project_members/update.js.haml | 1 | ||||
-rw-r--r-- | app/views/shared/members/_member.html.haml | 21 | ||||
-rw-r--r-- | app/workers/remove_expired_members_worker.rb | 13 |
17 files changed, 150 insertions, 24 deletions
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 1163edd8547..0bdab796daf 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -130,6 +130,7 @@ new UsersSelect(); break; case 'projects:project_members:index': + new MemberExpirationDate(); new ProjectMembers(); new UsersSelect(); break; diff --git a/app/assets/javascripts/member_expiration_date.js b/app/assets/javascripts/member_expiration_date.js new file mode 100644 index 00000000000..93c34d5ccd7 --- /dev/null +++ b/app/assets/javascripts/member_expiration_date.js @@ -0,0 +1,34 @@ +(function() { + // Add datepickers to all `js-access-expiration-date` elements. If those elements are + // children of an element with the `clearable-input` class, and have a sibling + // `js-clear-input` element, then show that element when there is a value in the + // datepicker, and make clicking on that element clear the field. + // + this.MemberExpirationDate = function() { + $('.js-access-expiration-date').each(function(i, element) { + var expirationDateInput = $(element); + + if (expirationDateInput.hasClass('hasDatepicker')) { return; } + + function toggleClearInput() { + expirationDateInput.parent().toggleClass('has-value', !!expirationDateInput.val()); + } + + expirationDateInput.datepicker({ + dateFormat: 'yy-mm-dd', + minDate: 1, + onSelect: toggleClearInput + }); + + expirationDateInput.on('blur', toggleClearInput); + + toggleClearInput(); + + expirationDateInput.next('.js-clear-input').on('click', function(event) { + event.preventDefault(); + expirationDateInput.datepicker('setDate', null); + toggleClearInput(); + }); + }); + }; +}).call(this); diff --git a/app/assets/javascripts/project_members.js b/app/assets/javascripts/project_members.js index f6a796b325a..78f7b48bc7d 100644 --- a/app/assets/javascripts/project_members.js +++ b/app/assets/javascripts/project_members.js @@ -5,9 +5,6 @@ return $(this).fadeOut(); }); } - return ProjectMembers; - })(); - }).call(this); diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 27dc2b2a1fa..eaf2d3270b3 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -719,3 +719,29 @@ pre.light-well { width: 300px; } } + +.clearable-input { + position: relative; + + .clear-icon { + @extend .fa-times; + display: none; + position: absolute; + right: 7px; + top: 7px; + color: $location-icon-color; + + &:before { + font-family: FontAwesome; + font-weight: normal; + font-style: normal; + } + } + + &.has-value { + .clear-icon { + cursor: pointer; + display: block; + } + } +} diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb index 3435a118964..42a7e5a2c30 100644 --- a/app/controllers/projects/project_members_controller.rb +++ b/app/controllers/projects/project_members_controller.rb @@ -36,7 +36,12 @@ class Projects::ProjectMembersController < Projects::ApplicationController end def create - @project.team.add_users(params[:user_ids].split(','), params[:access_level], current_user) + @project.team.add_users( + params[:user_ids].split(','), + params[:access_level], + expires_at: params[:expires_at], + current_user: current_user + ) redirect_to namespace_project_project_members_path(@project.namespace, @project) end @@ -94,7 +99,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController protected def member_params - params.require(:project_member).permit(:user_id, :access_level) + params.require(:project_member).permit(:user_id, :access_level, :expires_at) end # MembershipActions concern diff --git a/app/helpers/members_helper.rb b/app/helpers/members_helper.rb index 877c77050be..e9f6172f802 100644 --- a/app/helpers/members_helper.rb +++ b/app/helpers/members_helper.rb @@ -36,4 +36,8 @@ module MembersHelper "Are you sure you want to leave the " \ "\"#{member_source.human_name}\" #{member_source.class.to_s.humanize(capitalize: false)}?" end + + def member_expires_soon?(member) + member.expires_at < 7.days.from_now + end end diff --git a/app/models/group.rb b/app/models/group.rb index 37631b99701..11c39bbdfe4 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -97,7 +97,7 @@ class Group < Namespace def add_users(user_ids, access_level, current_user = nil) user_ids.each do |user_id| - Member.add_user(self.group_members, user_id, access_level, current_user) + Member.add_user(self.group_members, user_id, access_level, current_user: current_user) end end diff --git a/app/models/member.rb b/app/models/member.rb index 24ab1276ee9..84bbbffe718 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -31,6 +31,7 @@ class Member < ActiveRecord::Base scope :non_invite, -> { where(invite_token: nil) } scope :request, -> { where.not(requested_at: nil) } scope :has_access, -> { where('access_level > 0') } + scope :expired, -> { where('expires_at <= ?', Time.current) } scope :guests, -> { where(access_level: GUEST) } scope :reporters, -> { where(access_level: REPORTER) } @@ -73,7 +74,7 @@ class Member < ActiveRecord::Base user end - def add_user(members, user_id, access_level, current_user = nil) + def add_user(members, user_id, access_level, current_user: nil, expires_at: nil) user = user_for_id(user_id) # `user` can be either a User object or an email to be invited @@ -87,6 +88,7 @@ class Member < ActiveRecord::Base if can_update_member?(current_user, member) || project_creator?(member, access_level) member.created_by ||= current_user member.access_level = access_level + member.expires_at = expires_at member.save end @@ -123,6 +125,10 @@ class Member < ActiveRecord::Base invite? || request? end + def expires? + expires_at.present? + end + def accept_request return false unless request? diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb index 18e97c969d7..ec2d40eb11c 100644 --- a/app/models/members/project_member.rb +++ b/app/models/members/project_member.rb @@ -34,7 +34,7 @@ class ProjectMember < Member # :master # ) # - def add_users_to_projects(project_ids, user_ids, access, current_user = nil) + def add_users_to_projects(project_ids, user_ids, access, current_user: nil, expires_at: nil) access_level = if roles_hash.has_key?(access) roles_hash[access] elsif roles_hash.values.include?(access.to_i) @@ -50,7 +50,13 @@ class ProjectMember < Member project = Project.find(project_id) users.each do |user| - Member.add_user(project.project_members, user, access_level, current_user) + Member.add_user( + project.project_members, + user, + access_level, + current_user: current_user, + expires_at: expires_at + ) end end end diff --git a/app/models/project_team.rb b/app/models/project_team.rb index d0a714cd6fc..a65d271d262 100644 --- a/app/models/project_team.rb +++ b/app/models/project_team.rb @@ -15,7 +15,7 @@ class ProjectTeam users, access, current_user = *args if users.respond_to?(:each) - add_users(users, access, current_user) + add_users(users, access, current_user: current_user) else add_user(users, access, current_user) end @@ -33,17 +33,18 @@ class ProjectTeam member end - def add_users(users, access, current_user = nil) + def add_users(users, access, current_user: nil, expires_at: nil) ProjectMember.add_users_to_projects( [project.id], users, access, - current_user + current_user: current_user, + expires_at: expires_at ) end def add_user(user, access, current_user = nil) - add_users([user], access, current_user) + add_users([user], access, current_user: current_user) end # Remove all users from project team diff --git a/app/services/members/authorized_destroy_service.rb b/app/services/members/authorized_destroy_service.rb new file mode 100644 index 00000000000..c23f90a6a10 --- /dev/null +++ b/app/services/members/authorized_destroy_service.rb @@ -0,0 +1,17 @@ +module Members + class AuthorizedDestroyService < BaseService + attr_accessor :member, :user + + def initialize(member, user = nil) + @member, @user = member, user + end + + def execute + member.destroy + + if member.request? && member.user != user + notification_service.decline_access_request(member) + end + end + end +end diff --git a/app/services/members/destroy_service.rb b/app/services/members/destroy_service.rb index 9e3f6af628d..9a2bf82ef51 100644 --- a/app/services/members/destroy_service.rb +++ b/app/services/members/destroy_service.rb @@ -11,12 +11,7 @@ module Members unless member && can?(current_user, "destroy_#{member.type.underscore}".to_sym, member) raise Gitlab::Access::AccessDeniedError end - - member.destroy - - if member.request? && member.user != current_user - notification_service.decline_access_request(member) - end + AuthorizedDestroyService.new(member, current_user).execute end end end 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 978c4dfc5ec..96d05c7fd65 100644 --- a/app/views/projects/project_members/_new_project_member.html.haml +++ b/app/views/projects/project_members/_new_project_member.html.haml @@ -14,5 +14,12 @@ Read more about role permissions %strong= link_to "here", help_page_path("user/permissions"), class: "vlink" + .form-group + = f.label :expires_at, 'Access expiration date', class: 'control-label' + .col-sm-10 + .clearable-input + = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Select access expiration date' + %i.clear-icon.js-clear-input + .form-actions = f.submit 'Add users to project', class: "btn btn-create" diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml index 9031f01b496..9d063b3081f 100644 --- a/app/views/projects/project_members/index.html.haml +++ b/app/views/projects/project_members/index.html.haml @@ -1,6 +1,6 @@ - page_title "Members" -.project-members-page.prepend-top-default +.project-members-page.js-project-members-page.prepend-top-default - if can?(current_user, :admin_project_member, @project) .panel.panel-default .panel-heading diff --git a/app/views/projects/project_members/update.js.haml b/app/views/projects/project_members/update.js.haml index 45f8ef89060..833954bc039 100644 --- a/app/views/projects/project_members/update.js.haml +++ b/app/views/projects/project_members/update.js.haml @@ -1,2 +1,3 @@ :plain $("##{dom_id(@project_member)}").replaceWith('#{escape_javascript(render('shared/members/member', member: @project_member))}'); + new MemberExpirationDate(); diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml index fc6e206d082..2adbf9277fa 100644 --- a/app/views/shared/members/_member.html.haml +++ b/app/views/shared/members/_member.html.haml @@ -16,7 +16,7 @@ = button_tag icon('pencil'), type: 'button', class: 'btn inline js-toggle-button', - title: 'Edit access level' + title: 'Edit' - if member.request? = link_to icon('check inverse'), polymorphic_path([:approve_access_request, member]), @@ -59,6 +59,10 @@ = time_ago_with_tooltip(member.requested_at) - else Joined #{time_ago_with_tooltip(member.created_at)} + - if member.expires? + ยท + %span{ class: ('text-warning' if member_expires_soon?(member)) } + Expires in #{distance_of_time_in_words_to_now(member.expires_at)} - else = image_tag avatar_icon(member.invite_email, 40), class: "avatar s40", alt: '' @@ -73,8 +77,17 @@ - if show_roles .edit-member.hide.js-toggle-content %br - = form_for member, remote: true do |f| - .prepend-top-10 - = f.select :access_level, options_for_select(member.class.access_level_roles, member.access_level), {}, class: 'form-control' + = form_for member, remote: true, html: { class: 'form-horizontal' } do |f| + .form-group + = label_tag "member_access_level_#{member.id}", 'Project access', class: 'control-label' + .col-sm-10 + = f.select :access_level, options_for_select(member.class.access_level_roles, member.access_level), {}, class: 'form-control', id: "member_access_level_#{member.id}" + - if member.is_a?(ProjectMember) + .form-group + = label_tag "member_expires_at_#{member.id}", 'Access expiration date', class: 'control-label' + .col-sm-10 + .clearable-input + = f.text_field :expires_at, class: 'form-control js-access-expiration-date', placeholder: 'Select access expiration date', id: "member_expires_at_#{member.id}" + %i.clear-icon.js-clear-input .prepend-top-10 = f.submit 'Save', class: 'btn btn-save btn-sm' diff --git a/app/workers/remove_expired_members_worker.rb b/app/workers/remove_expired_members_worker.rb new file mode 100644 index 00000000000..cf765af97ce --- /dev/null +++ b/app/workers/remove_expired_members_worker.rb @@ -0,0 +1,13 @@ +class RemoveExpiredMembersWorker + include Sidekiq::Worker + + def perform + Member.expired.find_each do |member| + begin + Members::AuthorizedDestroyService.new(member).execute + rescue => ex + logger.error("Expired Member ID=#{member.id} cannot be removed - #{ex}") + end + end + end +end |