Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 2 additions & 18 deletions app/controllers/admin/workshops_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -219,9 +219,7 @@ def assign_host(host_id)
end

def assign_organisers(organiser_ids)
organiser_ids.reject!(&:empty?)
grant_organiser_access(organiser_ids)
revoke_organiser_access(organiser_ids)
WorkshopOrganiserAssigner.new(@workshop, organiser_ids).call
end

def finish_creation
Expand All @@ -233,11 +231,7 @@ def finish_creation
end

def assign_organisers_or_default
if organiser_ids.present?
assign_organisers(organiser_ids)
else
grant_organiser_access(@workshop.chapter.organisers.pluck(:id))
end
WorkshopOrganiserAssigner.new(@workshop, organiser_ids, default_to_chapter: true).call
end

def host_id
Expand All @@ -248,16 +242,6 @@ def organiser_ids
params[:workshop][:organisers]
end

def grant_organiser_access(organiser_ids)
organiser_ids.each { |id| Member.find(id).add_role(:organiser, @workshop) }
end

def revoke_organiser_access(organiser_ids)
(@workshop.organisers.pluck(:id).map(&:to_s) - organiser_ids).each do |id|
Member.find(id).revoke(:organiser, @workshop)
end
end

def physical_workshop_and_no_host?(workshop)
!workshop.virtual && host_id.empty?
end
Expand Down
30 changes: 30 additions & 0 deletions app/services/workshop_organiser_assigner.rb
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)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the organisers param is absent, @organiser_ids is nil and Array(nil) gives [], so revoke_other_organisers([]) strips every current organiser and the update redirects with a success notice. Before this change the same request raised NoMethodError on nil.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:

raise ArgumentError, 'organiser_ids is required on the update path' if @organiser_ids.nil?

or returning without revoking, so only a submitted list can clear the roster.

Copy link
Copy Markdown
Contributor Author

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

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
58 changes: 58 additions & 0 deletions spec/controllers/admin/workshops_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.not_to raise_error is the only assertion in both new create specs, so they stay green even if the grant never happens: a mistyped service call, or a wrong params key falling back to the chapter default, passes just the same. Could the provided-ids spec also assert the effect?

workshop.reload
expect(workshop.organisers).to include(organiser)

The PATCH #update spec below has real value assertions, so this is only about the two create cases.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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
Expand Down
70 changes: 70 additions & 0 deletions spec/services/workshop_organiser_assigner_spec.rb
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)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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
Loading