Repository navigation
Extract organiser grant/revoke diffing into WorkshopOrganiserAssigner service (#2941) #3049
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| class WorkshopOrganiserAssigner | ||
| def initialize(workshop, organiser_ids, default_to_chapter: false) | ||
| @workshop = workshop | ||
| @organiser_ids = organiser_ids | ||
| @default_to_chapter = default_to_chapter | ||
| end | ||
|
|
||
| def call | ||
| if @default_to_chapter && @organiser_ids.blank? | ||
| grant(@workshop.chapter.organisers.pluck(:id)) | ||
| else | ||
| raise ArgumentError, 'organiser_ids is required' if @organiser_ids.nil? | ||
|
|
||
| desired_ids = Array(@organiser_ids).reject(&:empty?).map(&:to_s) | ||
| grant(desired_ids) | ||
| revoke_other_organisers(desired_ids) | ||
| end | ||
| end | ||
|
|
||
| private | ||
|
|
||
| def grant(organiser_ids) | ||
| organiser_ids.each { |id| Member.find(id).add_role(:organiser, @workshop) } | ||
| end | ||
|
|
||
| def revoke_other_organisers(desired_ids) | ||
| current_ids = @workshop.organisers.pluck(:id).map(&:to_s) | ||
| (current_ids - desired_ids).each { |id| Member.find(id).revoke(:organiser, @workshop) } | ||
| end | ||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -282,6 +282,64 @@ def create_workshop | |
| end | ||
| end | ||
|
|
||
| describe 'POST #create organiser assignment' do | ||
| let(:organiser) { Fabricate(:member) } | ||
|
|
||
| it 'does not raise when organiser_ids are provided' do | ||
| expect do | ||
| post :create, params: { | ||
| workshop: { | ||
| chapter_id: workshop.chapter.id, | ||
| local_date: (Time.zone.now + 1.week).strftime('%d/%m/%Y'), | ||
| local_time: '15:00', local_end_time: '17:00', | ||
| virtual: '1', coach_spaces: 5, student_spaces: 15, | ||
| host: nil, organisers: [organiser.id.to_s] | ||
| } | ||
| } | ||
| end.not_to raise_error | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
workshop.reload
expect(workshop.organisers).to include(organiser)The
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this should now be resolved |
||
| created = Workshop.unscoped.order(:id).last | ||
| expect(created.chapter).to eq(workshop.chapter) | ||
| end | ||
|
|
||
| it 'does not raise when organiser_ids are blank' do | ||
| expect do | ||
| post :create, params: { | ||
| workshop: { | ||
| chapter_id: workshop.chapter.id, | ||
| local_date: (Time.zone.now + 1.week).strftime('%d/%m/%Y'), | ||
| local_time: '15:00', local_end_time: '17:00', | ||
| virtual: '1', coach_spaces: 5, student_spaces: 15, | ||
| host: nil | ||
| } | ||
| } | ||
| end.not_to raise_error | ||
| created = Workshop.unscoped.order(:id).last | ||
| expect(created.chapter).to eq(workshop.chapter) | ||
| end | ||
| end | ||
|
|
||
| describe 'PATCH #update organiser revocation' do | ||
| let(:existing_organiser) { Fabricate(:member) } | ||
| let(:new_organiser) { Fabricate(:member) } | ||
|
|
||
| it 'revokes removed organisers after update' do | ||
| existing_organiser.add_role(:organiser, workshop) | ||
| patch :update, params: { | ||
| id: workshop.id, | ||
| workshop: { | ||
| local_date: workshop.date_and_time.strftime('%d/%m/%Y'), | ||
| local_time: workshop.date_and_time.strftime('%H:%M'), | ||
| local_end_time: (workshop.date_and_time + 2.hours).strftime('%H:%M'), | ||
| host: workshop.host&.id || '', | ||
| organisers: [new_organiser.id.to_s] | ||
| } | ||
| } | ||
| workshop.reload | ||
| expect(workshop.organisers).to include(new_organiser) | ||
| expect(workshop.organisers).not_to include(existing_organiser) | ||
| end | ||
| end | ||
|
|
||
| describe 'DELETE #destroy' do | ||
| context 'when workshop invitations have been sent' do | ||
| before do | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| require 'rails_helper' | ||
|
|
||
| RSpec.describe WorkshopOrganiserAssigner, type: :service do | ||
| let(:workshop) { Fabricate(:workshop) } | ||
| let(:chapter) { workshop.chapter } | ||
|
|
||
| describe '#call' do | ||
| context 'when organiser_ids are provided' do | ||
| let(:organiser) { Fabricate(:member) } | ||
|
|
||
| it 'grants organiser role to members in the list' do | ||
| expect do | ||
| described_class.new(workshop, [organiser.id.to_s], default_to_chapter: false).call | ||
| workshop.reload | ||
| end.to change { workshop.organisers.include?(organiser) }.from(false).to(true) | ||
| end | ||
|
|
||
| it 'revokes organiser role from members not in the list' do | ||
| existing_organiser = Fabricate(:member) | ||
| existing_organiser.add_role(:organiser, workshop) | ||
| workshop.reload | ||
| described_class.new(workshop, [organiser.id.to_s], default_to_chapter: false).call | ||
| workshop.reload | ||
| expect(workshop.organisers).not_to include(existing_organiser) | ||
| end | ||
| end | ||
|
|
||
| context 'when organiser_ids are blank with default_to_chapter' do | ||
| it 'defaults to the chapter organisers' do | ||
| chapter_organiser = Fabricate(:member) | ||
| chapter_organiser.add_role(:organiser, chapter) | ||
| described_class.new(workshop, [], default_to_chapter: true).call | ||
| workshop.reload | ||
| expect(workshop.organisers).to include(chapter_organiser) | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This pins the chapter-fallback grant but not that the fallback is grant-only. If the branch ever picked up revocation (or the condition on line 9 drifted), pre-existing workshop organisers would lose their roles with this spec still green. Could it also assert a workshop-only organiser keeps the role? existing = Fabricate(:member)
existing.add_role(:organiser, workshop)
described_class.new(workshop, [], default_to_chapter: true).call
workshop.reload
expect(workshop.organisers).to include(existing)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this should now be resolved |
||
| end | ||
|
|
||
| it 'preserves existing workshop organisers when falling back to chapter' do | ||
| chapter_organiser = Fabricate(:member) | ||
| chapter_organiser.add_role(:organiser, chapter) | ||
| existing_organiser = Fabricate(:member) | ||
| existing_organiser.add_role(:organiser, workshop) | ||
| described_class.new(workshop, [], default_to_chapter: true).call | ||
| workshop.reload | ||
| expect(workshop.organisers).to include(existing_organiser) | ||
| expect(workshop.organisers).to include(chapter_organiser) | ||
| end | ||
| end | ||
|
|
||
| context 'when organiser_ids contain only blank values (update path)' do | ||
| it 'revokes all and does not fall back to chapter' do | ||
| chapter_organiser = Fabricate(:member) | ||
| chapter_organiser.add_role(:organiser, chapter) | ||
| existing_organiser = Fabricate(:member) | ||
| existing_organiser.add_role(:organiser, workshop) | ||
| described_class.new(workshop, [''], default_to_chapter: false).call | ||
| workshop.reload | ||
| expect(workshop.organisers).not_to include(existing_organiser) | ||
| expect(workshop.organisers).to eq([]) | ||
| end | ||
| end | ||
|
|
||
| context 'when organiser_ids is nil' do | ||
| it 'raises ArgumentError' do | ||
| expect do | ||
| described_class.new(workshop, nil, default_to_chapter: false).call | ||
| end.to raise_error(ArgumentError, 'organiser_ids is required') | ||
| end | ||
| end | ||
| end | ||
| end | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the
organisersparam is absent,@organiser_idsis nil andArray(nil)gives[], sorevoke_other_organisers([])strips every current organiser and the update redirects with a success notice. Before this change the same request raisedNoMethodErroronnil.reject!and left the roles untouched. The edit form's multi-select omits the key entirely when nothing is selected, so deselecting all organisers and saving hits exactly this path.Nil also means opposite things on the two routes now: chapter fallback on create (
default_to_chapter: true), full strip on update.If the old 500 was simply broken and a silent clear is the intended behaviour for #2941, could you note that in the PR description and add a spec for the absent-param case? If not, guarding the else branch reads like:
or returning without revoking, so only a submitted list can clear the roster.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've left the error as it was previously implemented