From b059b0a25c180d3db7f589d1d37a330f1faddd30 Mon Sep 17 00:00:00 2001 From: Carlo Beltrame Date: Fri, 14 Apr 2023 16:51:50 +0200 Subject: [PATCH] Allow managers to create and manage event participations of their manageds Most of the changes here can use the new for_self_or_manageds mechanism. But when a condition combines her_own_or_something_else, then we must manually add the manager option, to make sure that the manager only inherits the her_own part, not the something_else part. Refs hitobito/hitobito#1967 --- .../youth/event/application_ability.rb | 19 ++++ .../youth/event/invitation_ability.rb | 19 ++++ .../youth/event/participation_ability.rb | 29 ++++- .../participation_contact_data_ability.rb | 19 ++++ app/abilities/youth/event_ability.rb | 9 +- lib/hitobito_youth/wagon.rb | 3 + .../event/application_ability_spec.rb | 43 ++++++++ .../event/invitation_ability_spec.rb | 40 +++++++ .../event/participation_ability_spec.rb | 102 ++++++++++++++++++ ...participation_contact_data_ability_spec.rb | 49 +++++++++ 10 files changed, 330 insertions(+), 2 deletions(-) create mode 100644 app/abilities/youth/event/application_ability.rb create mode 100644 app/abilities/youth/event/invitation_ability.rb create mode 100644 app/abilities/youth/event/participation_contact_data_ability.rb create mode 100644 spec/abilities/event/application_ability_spec.rb create mode 100644 spec/abilities/event/invitation_ability_spec.rb create mode 100644 spec/abilities/event/participation_contact_data_ability_spec.rb diff --git a/app/abilities/youth/event/application_ability.rb b/app/abilities/youth/event/application_ability.rb new file mode 100644 index 00000000..6c98c882 --- /dev/null +++ b/app/abilities/youth/event/application_ability.rb @@ -0,0 +1,19 @@ +# encoding: utf-8 + +# Copyright (c) 2023, Pfadibewegung Schweiz. This file is part of +# hitobito_youth and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito_youth. + +module Youth::Event::ApplicationAbility + extend ActiveSupport::Concern + + included do + on(Event::Application) do + for_self_or_manageds do + # abilities which managers inherit from their managed children + permission(:any).may(:show_priorities, :show_approval).her_own + end + end + end +end diff --git a/app/abilities/youth/event/invitation_ability.rb b/app/abilities/youth/event/invitation_ability.rb new file mode 100644 index 00000000..3adc5ad1 --- /dev/null +++ b/app/abilities/youth/event/invitation_ability.rb @@ -0,0 +1,19 @@ +# encoding: utf-8 + +# Copyright (c) 2023, Pfadibewegung Schweiz. This file is part of +# hitobito_youth and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito_youth. + +module Youth::Event::InvitationAbility + extend ActiveSupport::Concern + + included do + on(Event::Invitation) do + for_self_or_manageds do + # abilities which managers inherit from their managed children + permission(:any).may(:decline).own_invitation + end + end + end +end diff --git a/app/abilities/youth/event/participation_ability.rb b/app/abilities/youth/event/participation_ability.rb index 8486577d..bab8d988 100644 --- a/app/abilities/youth/event/participation_ability.rb +++ b/app/abilities/youth/event/participation_ability.rb @@ -1,6 +1,6 @@ # encoding: utf-8 -# Copyright (c) 2012-2014, Pfadibewegung Schweiz. This file is part of +# Copyright (c) 2012-2023, Pfadibewegung Schweiz. This file is part of # hitobito_youth and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/hitobito/hitobito_youth. @@ -10,6 +10,16 @@ module Youth::Event::ParticipationAbility included do on(Event::Participation) do + # abilities which managers inherit from their managed children + permission(:any).may(:show).her_own_or_manager_or_for_participations_read_events + permission(:any).may(:show_details, :print). + her_own_or_manager_or_for_participations_full_events + for_self_or_manageds do + permission(:any).may(:create).her_own_if_application_possible + permission(:any).may(:destroy).her_own_if_application_cancelable + general(:create).at_least_one_group_not_deleted + end + permission(:any).may(:cancel, :absent, :assign, :attend).for_participations_full_events permission(:group_full).may(:cancel, :reject, :absent, :assign, :attend).in_same_group permission(:group_and_below_full). @@ -26,6 +36,14 @@ module Youth::Event::ParticipationAbility end end + def her_own_or_manager_or_for_participations_read_events + her_own_or_for_participations_read_events || manager + end + + def her_own_or_manager_or_for_participations_full_events + her_own_or_for_participations_full_events || manager + end + def person_in_same_layer person.nil? || permission_in_layers?(person.layer_group_ids) end @@ -45,8 +63,17 @@ def if_application event.supports_applications && participation.application_id? end + def participant_can_show_event? + participation.person && + (AbilityWithoutManagerAbilities.new(participation.person).can? :show, participation.event) + end + private + def manager + contains_any?([user.id], person.managers.pluck(:id)) + end + def event participation.event end diff --git a/app/abilities/youth/event/participation_contact_data_ability.rb b/app/abilities/youth/event/participation_contact_data_ability.rb new file mode 100644 index 00000000..7415646a --- /dev/null +++ b/app/abilities/youth/event/participation_contact_data_ability.rb @@ -0,0 +1,19 @@ +# encoding: utf-8 + +# Copyright (c) 2023, Pfadibewegung Schweiz. This file is part of +# hitobito_youth and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito_youth. + +module Youth::Event::ParticipationContactDataAbility + extend ActiveSupport::Concern + + included do + on(Event::ParticipationContactData) do + for_self_or_manageds do + # abilities which managers inherit from their managed children + permission(:any).may(:show, :update).her_own + end + end + end +end diff --git a/app/abilities/youth/event_ability.rb b/app/abilities/youth/event_ability.rb index ee7380e7..c96fc37b 100644 --- a/app/abilities/youth/event_ability.rb +++ b/app/abilities/youth/event_ability.rb @@ -1,6 +1,6 @@ # encoding: utf-8 -# Copyright (c) 2012-2014, Pfadibewegung Schweiz. This file is part of +# Copyright (c) 2012-2023, Pfadibewegung Schweiz. This file is part of # hitobito_youth and licensed under the Affero General Public License version 3 # or later. See the COPYING file at the top-level directory or at # https://github.com/hitobito/hitobito_youth. @@ -10,6 +10,13 @@ module Youth::EventAbility included do on(Event::Course) do + + for_self_or_manageds do + # abilities which managers inherit from their managed children + class_side(:list_available).if_any_role + permission(:any).may(:show).in_same_layer_or_globally_visible_or_participating + end + permission(:any). may(:index_revoked_participations, :list_tentatives). for_participations_full_events diff --git a/lib/hitobito_youth/wagon.rb b/lib/hitobito_youth/wagon.rb index 38cf3854..046dcd46 100644 --- a/lib/hitobito_youth/wagon.rb +++ b/lib/hitobito_youth/wagon.rb @@ -53,7 +53,10 @@ class Wagon < Rails::Engine GroupAbility.include Youth::GroupAbility EventAbility.include Youth::EventAbility PersonAbility.include Youth::PersonAbility + Event::ApplicationAbility.include Youth::Event::ApplicationAbility + Event::InvitationAbility.include Youth::Event::InvitationAbility Event::ParticipationAbility.include Youth::Event::ParticipationAbility + Event::ParticipationContactDataAbility.include Youth::Event::ParticipationContactDataAbility PersonReadables.prepend(Youth::PersonReadables) PersonLayerWritables.prepend(Youth::PersonLayerWritables) diff --git a/spec/abilities/event/application_ability_spec.rb b/spec/abilities/event/application_ability_spec.rb new file mode 100644 index 00000000..4c064cd9 --- /dev/null +++ b/spec/abilities/event/application_ability_spec.rb @@ -0,0 +1,43 @@ +# encoding: utf-8 + +# Copyright (c) 2023, Pfadibewegung Schweiz. This file is part of +# hitobito_youth and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito_youth. + +require 'spec_helper' + +describe Event::ApplicationAbility do + + let(:applicant) { Fabricate(:person) } + let(:manager) { Fabricate(:person) } + let!(:manager_relation) { PeopleManager.create(manager: manager, managed: applicant) } + + let(:course) { Fabricate(:course) } + let(:application) { Fabricate(:event_application, priority_1: course, priority_2: nil) } + let!(:participation) { Fabricate(:event_participation, event: course, application: application, person: applicant) } + + subject { Ability.new(user) } + + [:show_priorities, :show_approval].each do |ability| + context ability.to_s do + + context 'for self' do + let(:user) { applicant } + it { is_expected.to be_able_to ability, application } + end + + context 'for manager' do + let(:user) { manager } + it { is_expected.to be_able_to ability, application } + end + + context 'for unrelated user' do + let(:user) { Fabricate(:person) } + it { is_expected.not_to be_able_to ability, application } + end + + end + end + +end diff --git a/spec/abilities/event/invitation_ability_spec.rb b/spec/abilities/event/invitation_ability_spec.rb new file mode 100644 index 00000000..9178f427 --- /dev/null +++ b/spec/abilities/event/invitation_ability_spec.rb @@ -0,0 +1,40 @@ +# encoding: utf-8 + +# Copyright (c) 2023, Pfadibewegung Schweiz. This file is part of +# hitobito_youth and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito_youth. + +require 'spec_helper' + +describe Event::InvitationAbility do + + let(:invitee) { Fabricate(:person) } + let(:manager) { Fabricate(:person) } + let!(:manager_relation) { PeopleManager.create(manager: manager, managed: invitee) } + + let(:course) { Fabricate(:course) } + let(:invitation) { Fabricate(:event_invitation, event: course, person: invitee) } + + subject { Ability.new(user) } + + context 'decline' do + + context 'for self' do + let(:user) { invitee } + it { is_expected.to be_able_to :decline, invitation } + end + + context 'for manager' do + let(:user) { manager } + it { is_expected.to be_able_to :decline, invitation } + end + + context 'for unrelated user' do + let(:user) { Fabricate(:person) } + it { is_expected.not_to be_able_to :decline, invitation } + end + + end + +end diff --git a/spec/abilities/event/participation_ability_spec.rb b/spec/abilities/event/participation_ability_spec.rb index d85d643b..ef70f09e 100644 --- a/spec/abilities/event/participation_ability_spec.rb +++ b/spec/abilities/event/participation_ability_spec.rb @@ -130,4 +130,106 @@ def build(attrs) end end end + + context 'manager inherited permissions' do + + let(:participant) { Fabricate(:person) } + let(:manager) { Fabricate(:person) } + let!(:manager_relation) { PeopleManager.create(manager: manager, managed: participant) } + + let(:course) { Fabricate(:course) } + let(:participation) { Fabricate(:event_participation, event: course, person: participant) } + + subject { Ability.new(user) } + + context 'for self' do + let(:user) { participant } + + context 'if event applications are not possible / cancelable' do + it { is_expected.to be_able_to :show, participation } + it { is_expected.to be_able_to :show_details, participation } + it { is_expected.not_to be_able_to :create, participation } + it { is_expected.not_to be_able_to :destroy, participation } + end + + context 'if event application is possible / cancelable' do + before { course.update(state: :application_open, applications_cancelable: true) } + it { is_expected.to be_able_to :show, participation } + it { is_expected.to be_able_to :show_details, participation } + it { is_expected.to be_able_to :create, participation } + it { is_expected.to be_able_to :destroy, participation } + + context 'but event is in archived group' do + before { course.groups.each(&:archive!) } + it { is_expected.not_to be_able_to :create, participation } + end + end + + context 'as event leader' do + let!(:leader_role) { Fabricate(Event::Role::Leader.name, participation: participation) } + let(:other_participation) { Fabricate(:event_participation, event: course) } + it { is_expected.to be_able_to :show, other_participation } + it { is_expected.to be_able_to :show_details, other_participation } + end + end + + context 'for manager' do + let(:user) { manager } + + context 'if event applications are not possible / cancelable' do + it { is_expected.to be_able_to :show, participation } + it { is_expected.to be_able_to :show_details, participation } + it { is_expected.not_to be_able_to :create, participation } + it { is_expected.not_to be_able_to :destroy, participation } + end + + context 'if event application is possible / cancelable' do + before { course.update(state: :application_open, applications_cancelable: true) } + it { is_expected.to be_able_to :show, participation } + it { is_expected.to be_able_to :show_details, participation } + it { is_expected.to be_able_to :create, participation } + it { is_expected.to be_able_to :destroy, participation } + + context 'but event is in archived group' do + before { course.groups.each(&:archive!) } + it { is_expected.not_to be_able_to :create, participation } + end + + context 'of event leader' do + let!(:leader_role) { Fabricate(Event::Role::Leader.name, participation: participation) } + let(:other_participation) { Fabricate(:event_participation, event: course) } + it 'does not inherit show permissions on participants' do + is_expected.not_to be_able_to :show, other_participation + end + it 'does not inherit show_details permissions on participants' do + is_expected.not_to be_able_to :show_details, other_participation + end + end + end + end + + context 'for unrelated user' do + let(:user) { Fabricate(:person) } + context 'if event applications are not possible / cancelable' do + it { is_expected.not_to be_able_to :show, participation } + it { is_expected.not_to be_able_to :show_details, participation } + it { is_expected.not_to be_able_to :create, participation } + it { is_expected.not_to be_able_to :destroy, participation } + end + + context 'if event application is possible / cancelable' do + before { course.update(state: :application_open, applications_cancelable: true) } + it { is_expected.not_to be_able_to :show, participation } + it { is_expected.not_to be_able_to :show_details, participation } + it { is_expected.not_to be_able_to :create, participation } + it { is_expected.not_to be_able_to :destroy, participation } + + context 'but event is in archived group' do + before { course.groups.each(&:archive!) } + it { is_expected.not_to be_able_to :create, participation } + end + end + end + + end end diff --git a/spec/abilities/event/participation_contact_data_ability_spec.rb b/spec/abilities/event/participation_contact_data_ability_spec.rb new file mode 100644 index 00000000..b9be9850 --- /dev/null +++ b/spec/abilities/event/participation_contact_data_ability_spec.rb @@ -0,0 +1,49 @@ +# encoding: utf-8 + +# Copyright (c) 2023, Pfadibewegung Schweiz. This file is part of +# hitobito_youth and licensed under the Affero General Public License version 3 +# or later. See the COPYING file at the top-level directory or at +# https://github.com/hitobito/hitobito_youth. + +require 'spec_helper' + +describe Event::ParticipationContactDataAbility do + + let(:participant) { Fabricate(:person) } + let(:manager) { Fabricate(:person) } + let!(:manager_relation) { PeopleManager.create(manager: manager, managed: participant) } + + let(:course) { Fabricate(:course) } + let(:participation) { Fabricate(:event_participation, event: course, person: participant) } + let(:attributes) do + h = ActiveSupport::HashWithIndifferentAccess.new + h.merge({ first_name: 'John', last_name: 'Gonzales', + email: 'somebody@example.com', + nickname: '' }) + end + let(:participation_contact_data) { Event::ParticipationContactData.new(course, participant, attributes) } + + subject { Ability.new(user) } + + [:show, :update].each do |ability| + context ability.to_s do + + context 'for self' do + let(:user) { participant } + it { is_expected.to be_able_to ability, participation_contact_data } + end + + context 'for manager' do + let(:user) { manager } + it { is_expected.to be_able_to ability, participation_contact_data } + end + + context 'for unrelated user' do + let(:user) { Fabricate(:person) } + it { is_expected.not_to be_able_to ability, participation_contact_data } + end + + end + end + +end