Skip to content
Merged
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
11 changes: 9 additions & 2 deletions app/controllers/entries_controller.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
class EntriesController < ApplicationController
include AvailableContestsConcern
before_action :set_entry, only: %i[ show edit update destroy soft_delete toggle_disqualified modal_details ]
before_action :set_entry, only: %i[ show edit update destroy soft_delete modal_details ]
before_action :set_entry_for_toggle_disqualified, only: %i[ toggle_disqualified ]
before_action :set_entry_for_profile, only: %i[ applicant_profile ]
before_action :authorize_entry, only: %i[show edit update destroy]
before_action :authorize_index, only: [ :index ]
Expand Down Expand Up @@ -111,7 +112,7 @@ def soft_delete
end

def toggle_disqualified
authorize @entry
authorize @entry, :toggle_disqualified?
@entry.toggle!(:disqualified)
redirect_to request.referer || root_path, notice: 'Entry disqualification status has been updated.'
end
Expand Down Expand Up @@ -143,6 +144,12 @@ def set_entry
@entry = policy_scope(Entry).find(params[:id])
end

# For toggle_disqualified, find the entry directly and let authorization handle access control
# This ensures container admins can toggle entries even if the scope doesn't include them
def set_entry_for_toggle_disqualified
@entry = Entry.find(params[:id])
end

# For applicant_profile, we want to find the entry first, then authorize it
def set_entry_for_profile
@entry = policy_scope(Entry).find(params[:id])
Expand Down
3 changes: 3 additions & 0 deletions config/environments/staging.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@
config.action_mailer.delivery_method = :letter_opener_web
config.action_mailer.perform_deliveries = true

# Enable mailer previews (protected by authorization in config/initializers/mailer_previews.rb)
config.action_mailer.show_previews = true

# I18n
config.i18n.fallbacks = true

Expand Down
169 changes: 169 additions & 0 deletions spec/controllers/entries_controller_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,173 @@
end
end
end

describe "PATCH #toggle_disqualified" do
let(:container) { create(:container) }
let(:contest_description) { create(:contest_description, :active, container: container) }
let(:contest_instance) { create(:contest_instance, contest_description: contest_description) }
let(:profile) { create(:profile) }
let(:entry) { create(:entry, profile: profile, contest_instance: contest_instance, disqualified: false) }

context "when user is a Container Administrator for the container" do
let(:admin_user) { create(:user) }
let(:admin_role) { create(:role, kind: 'Collection Administrator') }

before do
create(:assignment, user: admin_user, container: container, role: admin_role)
sign_in admin_user
end

it "toggles the disqualification status from false to true" do
expect {
patch :toggle_disqualified, params: { id: entry.id }
}.to change { entry.reload.disqualified }.from(false).to(true)
end

it "toggles the disqualification status from true to false" do
entry.update(disqualified: true)
expect {
patch :toggle_disqualified, params: { id: entry.id }
}.to change { entry.reload.disqualified }.from(true).to(false)
end

it "redirects to the referer" do
request.env['HTTP_REFERER'] = '/some/path'
patch :toggle_disqualified, params: { id: entry.id }
expect(response).to redirect_to('/some/path')
end

it "redirects to root path when no referer" do
patch :toggle_disqualified, params: { id: entry.id }
expect(response).to redirect_to(root_path)
end

it "sets a success notice message" do
patch :toggle_disqualified, params: { id: entry.id }
expect(flash[:notice]).to eq('Entry disqualification status has been updated.')
end
end

context "when user is a Collection Manager for the container" do
let(:manager_user) { create(:user) }
let(:manager_role) { create(:role, kind: 'Collection Manager') }

before do
create(:assignment, user: manager_user, container: container, role: manager_role)
sign_in manager_user
end

it "toggles the disqualification status" do
expect {
patch :toggle_disqualified, params: { id: entry.id }
}.to change { entry.reload.disqualified }.from(false).to(true)
end

it "sets a success notice message" do
patch :toggle_disqualified, params: { id: entry.id }
expect(flash[:notice]).to eq('Entry disqualification status has been updated.')
end
end

context "when user is Axis Mundi" do
let(:axis_mundi_user) { create(:user, :axis_mundi) }

before do
sign_in axis_mundi_user
end

it "toggles the disqualification status" do
expect {
patch :toggle_disqualified, params: { id: entry.id }
}.to change { entry.reload.disqualified }.from(false).to(true)
end

it "sets a success notice message" do
patch :toggle_disqualified, params: { id: entry.id }
expect(flash[:notice]).to eq('Entry disqualification status has been updated.')
end
end

context "when user is a Container Administrator for a different container" do
let(:other_container) { create(:container) }
let(:other_admin_user) { create(:user) }
let(:admin_role) { create(:role, kind: 'Collection Administrator') }

before do
create(:assignment, user: other_admin_user, container: other_container, role: admin_role)
sign_in other_admin_user
end

it "does not toggle the disqualification status" do
expect {
patch :toggle_disqualified, params: { id: entry.id }
}.not_to change { entry.reload.disqualified }
end

it "redirects with unauthorized message" do
patch :toggle_disqualified, params: { id: entry.id }
expect(response).to redirect_to(root_path)
expect(flash[:alert]).to eq("!!! Not authorized !!!")
end
end

context "when user is the entry owner" do
before do
sign_in profile.user
end

it "does not toggle the disqualification status" do
expect {
patch :toggle_disqualified, params: { id: entry.id }
}.not_to change { entry.reload.disqualified }
end

it "redirects with unauthorized message" do
patch :toggle_disqualified, params: { id: entry.id }
expect(response).to redirect_to(root_path)
expect(flash[:alert]).to eq("!!! Not authorized !!!")
end
end

context "when user is a judge assigned to the contest instance" do
let(:judge_user) { create(:user, :with_judge_role) }

before do
create(:judging_assignment, user: judge_user, contest_instance: contest_instance)
sign_in judge_user
end

it "does not toggle the disqualification status" do
expect {
patch :toggle_disqualified, params: { id: entry.id }
}.not_to change { entry.reload.disqualified }
end

it "redirects with unauthorized message" do
patch :toggle_disqualified, params: { id: entry.id }
expect(response).to redirect_to(root_path)
expect(flash[:alert]).to eq("!!! Not authorized !!!")
end
end

context "when user has no special role" do
let(:regular_user) { create(:user) }

before do
sign_in regular_user
end

it "does not toggle the disqualification status" do
expect {
patch :toggle_disqualified, params: { id: entry.id }
}.not_to change { entry.reload.disqualified }
end

it "redirects with unauthorized message" do
patch :toggle_disqualified, params: { id: entry.id }
expect(response).to redirect_to(root_path)
expect(flash[:alert]).to eq("!!! Not authorized !!!")
end
end
end
end
80 changes: 80 additions & 0 deletions spec/policies/entry_policy_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,84 @@
let(:user) { create(:user, :axis_mundi) }
it { is_expected.to permit_action(:show) }
end

describe "#toggle_disqualified?" do
let(:container) { contest_instance.contest_description.container }

context "for a visitor" do
let(:user) { nil }
it { is_expected.to forbid_action(:toggle_disqualified) }
end

context "for a Container Administrator in the same container" do
let(:user) { create(:user) }
let(:admin_role) { create(:role, kind: 'Collection Administrator') }

before do
create(:assignment, user: user, container: container, role: admin_role)
end

it { is_expected.to permit_action(:toggle_disqualified) }
end

context "for a Collection Manager in the same container" do
let(:user) { create(:user) }
let(:manager_role) { create(:role, kind: 'Collection Manager') }

before do
create(:assignment, user: user, container: container, role: manager_role)
end

it { is_expected.to permit_action(:toggle_disqualified) }
end

context "for Axis Mundi" do
let(:user) { create(:user, :axis_mundi) }
it { is_expected.to permit_action(:toggle_disqualified) }
end

context "for a Container Administrator in a different container" do
let(:other_container) { create(:container) }
let(:user) { create(:user) }
let(:admin_role) { create(:role, kind: 'Collection Administrator') }

before do
create(:assignment, user: user, container: other_container, role: admin_role)
end

it { is_expected.to forbid_action(:toggle_disqualified) }
end

context "for a Collection Manager in a different container" do
let(:other_container) { create(:container) }
let(:user) { create(:user) }
let(:manager_role) { create(:role, kind: 'Collection Manager') }

before do
create(:assignment, user: user, container: other_container, role: manager_role)
end

it { is_expected.to forbid_action(:toggle_disqualified) }
end

context "for the entry owner" do
let(:user) { profile.user }
it { is_expected.to forbid_action(:toggle_disqualified) }
end

context "for a judge assigned to the contest instance" do
let(:user) { create(:user, :with_judge_role) }

before do
create(:judging_assignment, user: user, contest_instance: contest_instance)
end

it { is_expected.to forbid_action(:toggle_disqualified) }
end

context "for a regular user with no special role" do
let(:user) { create(:user) }
it { is_expected.to forbid_action(:toggle_disqualified) }
end
end
end
6 changes: 3 additions & 3 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3891,9 +3891,9 @@ undici-types@~6.20.0:
integrity sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==

undici@^6.16.1:
version "6.21.3"
resolved "https://registry.yarnpkg.com/undici/-/undici-6.21.3.tgz#185752ad92c3d0efe7a7d1f6854a50f83b552d7a"
integrity sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==
version "6.23.0"
resolved "https://registry.yarnpkg.com/undici/-/undici-6.23.0.tgz#7953087744d9095a96f115de3140ca3828aff3a4"
integrity sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==

unicode-canonical-property-names-ecmascript@^2.0.0:
version "2.0.1"
Expand Down