Use contracted member services and show only manageable projects in user administration (#9076)

* Reuse contracted services in memberships controller

* Show only manageable projects

* Show only manageable projects in select box

* Reject empty values in role_ids

* Add spec for when user has no manage_members permission

* Also handle delete through service

* Extend spec for when user has no manage_members permission

* Extend Members SetAttributesSpec

* Make manageable_Members an instance method and add spec

* hide buttons if user is not allowed to use them

* Rename manageable_projects to assignable_projects

* Build a membership for the contract

* Fix url for global_roles to work with update and create methods

Co-authored-by: ulferts <jens.ulferts@googlemail.com>
pull/9080/head
Oliver Günther 4 years ago committed by GitHub
parent 4a54e9b41a
commit e9f1781d2b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 6
      app/contracts/members/base_contract.rb
  2. 8
      app/contracts/members/delete_contract.rb
  3. 12
      app/services/members/set_attributes_service.rb
  4. 7
      app/views/individual_principals/_memberships.html.erb
  5. 11
      app/views/users/_available_global_roles.html.erb
  6. 51
      lib/individual_principals/membership_controller_methods.rb
  7. 24
      spec/contracts/members/create_contract_spec.rb
  8. 59
      spec/controllers/placeholder_users/memberships_controller_spec.rb
  9. 19
      spec/features/principals/shared_memberships_examples.rb
  10. 12
      spec/services/members/set_attributes_service_spec.rb

@ -40,6 +40,12 @@ module Members
validate :project_set validate :project_set
validate :project_manageable validate :project_manageable
def assignable_projects
Project
.active
.where(id: Project.allowed_to(user, :manage_members))
end
private private
def user_allowed_to_manage def user_allowed_to_manage

@ -29,5 +29,13 @@
module Members module Members
class DeleteContract < ::DeleteContract class DeleteContract < ::DeleteContract
delete_permission :manage_members delete_permission :manage_members
validate :member_is_deletable
private
def member_is_deletable
errors.add(:base, :not_deletable) unless model.deletable?
end
end end
end end

@ -33,9 +33,19 @@ module Members
private private
def set_attributes(params) def set_attributes(params)
model.assign_roles(params.delete(:role_ids)) if params[:role_ids] assign_roles(params)
super super
end end
def assign_roles(params)
return unless params[:role_ids]
role_ids = params
.delete(:role_ids)
.select(&:present?)
model.assign_roles(role_ids)
end
end end
end end

@ -27,7 +27,10 @@ See docs/COPYRIGHT.rdoc for more details.
++#%> ++#%>
<% roles = Role.givable %> <% roles = Role.givable %>
<% projects = Project.visible.active.order(Arel.sql('lft')) %> <% projects = ::Members::CreateContract
.new(@individual_principal.memberships.build, current_user)
.assignable_projects
.order(Arel.sql('lft')) %>
<% memberships = @individual_principal.memberships.visible(current_user) %> <% memberships = @individual_principal.memberships.visible(current_user) %>
<div class="grid-block"> <div class="grid-block">
@ -111,6 +114,7 @@ See docs/COPYRIGHT.rdoc for more details.
roles: roles, roles: roles,
projects: projects) %> projects: projects) %>
<td class="buttons"> <td class="buttons">
<% if User.current.allowed_to?(:manage_members, membership.project) %>
<%= link_to_function icon_wrapper('icon icon-edit', t(:button_edit)), <%= link_to_function icon_wrapper('icon icon-edit', t(:button_edit)),
"jQuery('.member-#{membership.id}--edit-toggle-item').toggle();", "jQuery('.member-#{membership.id}--edit-toggle-item').toggle();",
class: "member-#{membership.id}--edit-toggle-item memberships--edit-button", class: "member-#{membership.id}--edit-toggle-item memberships--edit-button",
@ -119,6 +123,7 @@ See docs/COPYRIGHT.rdoc for more details.
polymorphic_path([@individual_principal, :membership], id: membership), polymorphic_path([@individual_principal, :membership], id: membership),
method: :delete, method: :delete,
title: t(:button_remove)) if membership.deletable? %> title: t(:button_remove)) if membership.deletable? %>
<% end %>
</td> </td>
</tr> </tr>
<% end %> <% end %>

@ -38,9 +38,16 @@ See docs/COPYRIGHT.rdoc for more details.
</span> </span>
<% else %> <% else %>
<span id="additional_principal_roles"> <span id="additional_principal_roles">
<%= form_for(:principal_roles, :url => user_memberships_path(user_id: user), :method => :post) do %> <% args =
if global_member
{ url: user_membership_path(id: global_member.id, user_id: user.id), method: :patch }
else
{ url: user_memberships_path(user_id: user.id), method: :post }
end
%>
<%= form_for(:principal_roles, **args) do %>
<% if global_member %> <% if global_member %>
<%= hidden_field_tag(:id, global_member.id ) %> <%= hidden_field_tag('membership[id]', global_member.id ) %>
<% global_member.roles.each do |role| %> <% global_member.roles.each do |role| %>
<%= hidden_field_tag('membership[role_ids][]', role.id ) %> <%= hidden_field_tag('membership[role_ids][]', role.id ) %>

@ -1,43 +1,52 @@
module IndividualPrincipals module IndividualPrincipals
module MembershipControllerMethods module MembershipControllerMethods
def update extend ActiveSupport::Concern
update_or_create(request.patch?, :notice_successful_update)
included do
before_action :find_membership, only: %i[update destroy]
end end
def create def create
update_or_create(request.post?, :notice_successful_create) membership_params = permitted_params.membership.merge(principal: @individual_principal)
end call = ::Members::CreateService
.new(user: current_user)
.call(membership_params)
def destroy respond_with_service_call call, message: :notice_successful_create
@membership = @individual_principal.memberships.find(params[:id]) end
tab = redirected_to_tab(@membership)
if @membership.deletable? && request.delete? def update
@membership.destroy call = ::Members::UpdateService
@membership = nil .new(model: @membership, user: current_user)
.call(permitted_params.membership)
flash[:notice] = I18n.t(:notice_successful_delete) respond_with_service_call call, message: :notice_successful_update
end end
redirect_to edit_polymorphic_path(@individual_principal, tab: tab) def destroy
call = ::Members::DeleteService
.new(model: @membership, user: current_user)
.call
respond_with_service_call call, message: :notice_successful_delete
end end
private private
def update_or_create(save_record, message) def find_membership
@membership = params[:id].present? ? Member.find(params[:id]) : Member.new(principal: @individual_principal, project: nil) @membership = Member.visible(current_user).find(params[:id])
rescue ActiveRecord::RecordNotFound
result = ::Members::EditMembershipService render_404
.new(@membership, save: save_record, current_user: current_user) end
.call(attributes: permitted_params.membership)
if result.success? def respond_with_service_call(call, message:)
if call.success?
flash[:notice] = I18n.t(message) flash[:notice] = I18n.t(message)
else else
flash[:error] = result.errors.full_messages.join("\n") flash[:error] = call.errors.full_messages.join("\n")
end end
redirect_to edit_polymorphic_path(@individual_principal, tab: redirected_to_tab(@membership)) redirect_to edit_polymorphic_path(@individual_principal, tab: redirected_to_tab(call.result))
end end
end end
end end

@ -61,5 +61,29 @@ describe Members::CreateContract do
it_behaves_like 'contract is invalid', principal: :unassignable it_behaves_like 'contract is invalid', principal: :unassignable
end end
end end
describe '#assignable_projects' do
context 'as a user without permission' do
let(:current_user) { FactoryBot.build_stubbed :user }
it 'is empty' do
expect(contract.assignable_projects).to be_empty
end
end
context 'as a user with permission in one project' do
let!(:project1) { FactoryBot.create :project }
let!(:project2) { FactoryBot.create :project }
let(:current_user) do
FactoryBot.create :user,
member_in_project: project1,
member_with_permissions: %i[manage_members]
end
it 'returns the one project' do
expect(contract.assignable_projects.to_a).to eq [project1]
end
end
end
end end
end end

@ -81,7 +81,7 @@ describe PlaceholderUsers::MembershipsController, type: :controller do
id: 1234 id: 1234
} }
expect(response.status).to eq 403 expect(response.status).to eq 404
end end
end end
@ -92,7 +92,7 @@ describe PlaceholderUsers::MembershipsController, type: :controller do
id: 1234 id: 1234
} }
expect(response.status).to eq 403 expect(response.status).to eq 404
end end
end end
end end
@ -103,12 +103,63 @@ describe PlaceholderUsers::MembershipsController, type: :controller do
it_behaves_like 'update memberships flow' it_behaves_like 'update memberships flow'
end end
context 'as user with global permission' do context 'as user with global permission and manage_members' do
current_user { FactoryBot.create :user, global_permission: %i[manage_placeholder_user] } current_user do
FactoryBot.create :user,
member_in_project: project,
member_with_permissions: %i[manage_members],
global_permission: %i[manage_placeholder_user]
end
it_behaves_like 'update memberships flow' it_behaves_like 'update memberships flow'
end end
context 'as user with global permission but not project permission' do
current_user { FactoryBot.create :user, global_permission: %i[manage_placeholder_user] }
describe 'POST create' do
it 'redirects but fails to create' do
post :create, params: {
placeholder_user_id: placeholder_user.id,
membership: {
project_id: project.id,
role_ids: [role.id]
}
}
expect(response.status).to eq 302
expect(placeholder_user.reload.memberships).to be_empty
end
end
context 'with a membership in another project that is invisible' do
shared_let(:project2) { FactoryBot.create :project }
shared_let(:membership) { FactoryBot.create :member, principal: placeholder_user, project: project2, roles: [role] }
describe 'PUT update' do
it 'returns an error' do
put :update, params: {
placeholder_user_id: placeholder_user.id,
id: membership.id
}
expect(response.status).to eq 404
end
end
describe 'DELETE destroy' do
it 'returns an error' do
delete :destroy, params: {
placeholder_user_id: placeholder_user.id,
id: membership.id
}
expect(response.status).to eq 404
end
end
end
end
context 'as user without global permission' do context 'as user without global permission' do
current_user { FactoryBot.create :user } current_user { FactoryBot.create :user }

@ -81,6 +81,25 @@ shared_examples 'global user principal membership management flows' do |permissi
end end
end end
context 'as user with global and project permissions, but not manage_members' do
current_user do
FactoryBot.create :user,
global_permission: permission,
member_in_project: project,
member_with_permissions: %i[view_work_packages]
end
it 'does not allow to select that project' do
principal_page.visit!
principal_page.open_projects_tab!
expect(page).to have_no_selector('tr.member')
expect(page).to have_text 'There is currently nothing to display.'
expect(page).to have_no_text project.name
expect(page).to have_no_text project2.name
end
end
context 'as user without global permission' do context 'as user without global permission' do
current_user { FactoryBot.create :user } current_user { FactoryBot.create :user }

@ -130,6 +130,18 @@ describe Members::SetAttributesService, type: :model do
it 'adds the new role' do it 'adds the new role' do
expect(subject.result.roles = [second_role, third_role]) expect(subject.result.roles = [second_role, third_role])
end end
context 'with role_ids not all being present' do
let(:call_attributes) do
{
role_ids: [nil, '', second_role.id, third_role.id]
}
end
it 'ignores the empty values' do
expect(subject.result.roles = [second_role, third_role])
end
end
end end
end end
end end

Loading…
Cancel
Save