diff --git a/back/app/models/phase.rb b/back/app/models/phase.rb index e2d87f538194..219362d864dd 100644 --- a/back/app/models/phase.rb +++ b/back/app/models/phase.rb @@ -129,7 +129,6 @@ class Phase < ApplicationRecord validates :participation_method, inclusion: { in: PARTICIPATION_METHODS } validates :placement_type, inclusion: { in: PLACEMENT_TYPES } validates :prescreening_mode, inclusion: { in: PRESCREENING_MODES }, allow_nil: true - validate :validate_prescreening_mode, if: :prescreening_mode_changed? with_options if: ->(phase) { phase.pmethod.supports_public_visibility? } do validates :presentation_mode, inclusion: { in: PRESENTATION_MODES } @@ -342,30 +341,26 @@ def feed_enabled? available_views&.include?('feed') end - def prescreening_enabled? = prescreening_mode.present? - def prescreening_flagged_only? = prescreening_mode == 'flagged_only' - def prescreening_all? = prescreening_mode == 'all' + def prescreening_enabled? = effective_prescreening_mode.present? + def prescreening_flagged_only? = effective_prescreening_mode == 'flagged_only' + def prescreening_all? = effective_prescreening_mode == 'all' - private - - def validate_prescreening_mode + # The configured `prescreening_mode`, reduced to what this platform's features permit. + # The column is configuration data and is stored verbatim: tenant templates and project + # copies carry it between platforms whose features differ, so it can be set on a + # platform where prescreening is not available. It only takes effect where it is. + def effective_prescreening_mode return if prescreening_mode.nil? prescreening_flag = participation_method == 'proposals' ? 'prescreening' : 'prescreening_ideation' + return unless AppConfiguration.instance.feature_activated?(prescreening_flag) + return if prescreening_mode == 'flagged_only' && !AppConfiguration.instance.feature_activated?('flag_inappropriate_content') - # Any prescreening mode requires the prescreening feature to be enabled. - unless AppConfiguration.instance.feature_activated?(prescreening_flag) - errors.add(:prescreening_mode, "requires the #{prescreening_flag} feature to be enabled") - return - end - - # 'flagged_only' mode requires the flag_inappropriate_content feature to be enabled. - return unless prescreening_mode == 'flagged_only' - return if AppConfiguration.instance.feature_activated?('flag_inappropriate_content') - - errors.add(:prescreening_mode, 'requires the flag_inappropriate_content feature to be enabled') + prescreening_mode end + private + def sanitize_description_multiloc self.description_multiloc = sanitize_html_multiloc(description_multiloc) end diff --git a/back/app/serializers/web_api/v1/phase_serializer.rb b/back/app/serializers/web_api/v1/phase_serializer.rb index a5b82b434a82..9308ad33bcf0 100644 --- a/back/app/serializers/web_api/v1/phase_serializer.rb +++ b/back/app/serializers/web_api/v1/phase_serializer.rb @@ -10,7 +10,7 @@ class WebApi::V1::PhaseSerializer < WebApi::V1::BaseSerializer :reacting_enabled, :reacting_like_method, :reacting_like_limited_max, :reacting_dislike_enabled, :reacting_dislike_method, :reacting_dislike_limited_max, :presentation_mode, :available_views, :ideas_order, :input_term, :vote_term, - :prescreening_mode, :manual_voters_amount, :manual_votes_count, + :prescreening_mode, :effective_prescreening_mode, :manual_voters_amount, :manual_votes_count, :similarity_enabled, :similarity_threshold_title, :similarity_threshold_body, :survey_popup_frequency diff --git a/back/engines/commercial/flag_inappropriate_content/spec/jobs/toxicity_detection_job_spec.rb b/back/engines/commercial/flag_inappropriate_content/spec/jobs/toxicity_detection_job_spec.rb index 5b544e3710f0..530cf83090c8 100644 --- a/back/engines/commercial/flag_inappropriate_content/spec/jobs/toxicity_detection_job_spec.rb +++ b/back/engines/commercial/flag_inappropriate_content/spec/jobs/toxicity_detection_job_spec.rb @@ -152,6 +152,26 @@ def stub_toxicity_detection!(service) end end + # A phase can carry a prescreening_mode on a platform without the prescreening feature: + # tenant templates and project copies bring the value across from platforms that have it. + describe 'prescreening_mode: flagged_only, but the prescreening feature is disabled' do + before { SettingsService.new.deactivate_feature!('prescreening_ideation') } + + it 'creates flag but does not move the idea to prescreening' do + phase = create(:phase, prescreening_mode: 'flagged_only') + idea = create( + :idea, phases: [phase], title_multiloc: { 'en' => 'offensive idea' }, + idea_status: idea_proposed_status, publication_status: 'published' + ) + + expect { described_class.perform_now(idea, attributes: [:title_multiloc]) } + .to not_change(idea, :idea_status) + .and not_change(idea, :publication_status) + + expect(idea.reload.inappropriate_content_flag).to be_present + end + end + describe 'prescreening_mode: nil (disabled)' do it 'creates flag but does not change status' do phase = create(:proposals_phase, prescreening_mode: nil) diff --git a/back/engines/commercial/multi_tenancy/spec/services/multi_tenancy/templates/tenant_deserializer_spec.rb b/back/engines/commercial/multi_tenancy/spec/services/multi_tenancy/templates/tenant_deserializer_spec.rb index e145053c1101..4c5267c6185f 100644 --- a/back/engines/commercial/multi_tenancy/spec/services/multi_tenancy/templates/tenant_deserializer_spec.rb +++ b/back/engines/commercial/multi_tenancy/spec/services/multi_tenancy/templates/tenant_deserializer_spec.rb @@ -126,5 +126,54 @@ expect(idea.body_multiloc.keys).to contain_exactly('nl-BE') end end + + # Templates carry model data but no feature flags (AppConfiguration is not serialized), + # so a template derived from a platform with screening enabled records a + # `prescreening_mode` that the target platform may have no feature for. Applying it + # must succeed regardless; the mode simply has no effect where the feature is off. + describe 'a phase with a prescreening_mode' do + let(:template) do + YAML.load(<<~YAML, permitted_classes: [Time], aliases: true) + models: + project: + - &project + title_multiloc: + en: Project + admin_publication: + - publication_ref: *project + publication_status: published + phase: + - project_ref: *project + title_multiloc: + en: Ideation phase + participation_method: ideation + start_at: '2026-01-01' + end_at: '2026-12-31' + prescreening_mode: all + YAML + end + + context 'when the target platform does not have the prescreening feature' do + it 'applies the template, storing the mode but leaving it without effect' do + expect { service.deserialize(template) }.not_to raise_error + + phase = Phase.first + expect(phase.prescreening_mode).to eq 'all' + expect(phase.prescreening_all?).to be false + end + end + + context 'when the target platform has the prescreening feature' do + before { SettingsService.new.activate_feature!('prescreening_ideation') } + + it 'applies the template, and the mode takes effect' do + service.deserialize(template) + + phase = Phase.first + expect(phase.prescreening_mode).to eq 'all' + expect(phase.prescreening_all?).to be true + end + end + end end end diff --git a/back/spec/acceptance/ideas/ideas_update_spec.rb b/back/spec/acceptance/ideas/ideas_update_spec.rb index 70dc13b64925..002431f71fb0 100644 --- a/back/spec/acceptance/ideas/ideas_update_spec.rb +++ b/back/spec/acceptance/ideas/ideas_update_spec.rb @@ -190,6 +190,24 @@ def public_input_params(spec) end end + # A phase can carry a prescreening_mode on a platform without the feature: tenant + # templates and project copies bring the value across from platforms that have it. + # The mode must not restrict the author where screening never applied. + context 'when prescreening_mode is all but the prescreening_ideation feature is disabled' do + let!(:proposed_status) { create(:idea_status_proposed) } + + let(:phase) { create(:phase, :ongoing, prescreening_mode: 'all') } + let(:input) { create(:idea, phases: [phase], idea_status: proposed_status) } + let(:title_multiloc) { { 'en' => 'Changed title' } } + + example 'Author can edit a published idea', document: false do + do_request + + assert_status 200 + expect(response_data.dig(:attributes, :title_multiloc, :en)).to eq('Changed title') + end + end + context 'when prescreening_mode is flagged_only' do before_all do SettingsService.new.activate_feature!('prescreening_ideation') diff --git a/back/spec/lib/participation_method/ideation_spec.rb b/back/spec/lib/participation_method/ideation_spec.rb index bed6567b21ea..8ecf15d7fdc3 100644 --- a/back/spec/lib/participation_method/ideation_spec.rb +++ b/back/spec/lib/participation_method/ideation_spec.rb @@ -91,6 +91,20 @@ end end end + + # A phase can carry a prescreening_mode on a platform without the feature: tenant + # templates and project copies bring the value across from platforms that have it. + context 'with prescreening_mode but the prescreening_ideation feature disabled' do + let!(:proposed_status) { create(:idea_status_proposed) } + let(:phase) { create(:phase, prescreening_mode: 'all', with_permissions: true) } + + it 'ignores the mode: sets idea_status to proposed and publication_status to published' do + input = build(:idea, idea_status: nil, publication_status: nil) + participation_method.assign_defaults input + expect(input.idea_status).to eq proposed_status + expect(input.publication_status).to eq 'published' + end + end end describe '#assign_defaults_for_phase' do diff --git a/back/spec/lib/participation_method/proposals_spec.rb b/back/spec/lib/participation_method/proposals_spec.rb index 295de83049a7..2684e8284c8f 100644 --- a/back/spec/lib/participation_method/proposals_spec.rb +++ b/back/spec/lib/participation_method/proposals_spec.rb @@ -73,6 +73,21 @@ expect(proposal.idea_status).to eq custom_status end end + + # A phase can carry a prescreening_mode on a platform without the feature: tenant + # templates and project copies bring the value across from platforms that have it. + context 'when prescreening_mode is all but the prescreening feature is disabled' do + before { SettingsService.new.deactivate_feature!('prescreening') } + + let(:phase) { create(:proposals_phase, prescreening_mode: 'all') } + + it 'ignores the mode: assigns the default "proposed" status' do + proposal = build(:proposal, idea_status: nil, creation_phase: phase, project: phase.project) + participation_method.assign_defaults proposal + expect(proposal.idea_status).to eq proposed_status + expect(proposal.publication_status).to eq 'published' + end + end end context 'when the proposed status is not available' do diff --git a/back/spec/models/phase_spec.rb b/back/spec/models/phase_spec.rb index f76a0a1b9930..ecf3406384b0 100644 --- a/back/spec/models/phase_spec.rb +++ b/back/spec/models/phase_spec.rb @@ -667,77 +667,127 @@ describe 'prescreening_mode' do using RSpec::Parameterized::TableSyntax - describe 'validation' do - def set_feature_flags(prescreening:, flag_inappropriate_content:) - AppConfiguration.instance.settings.tap do |settings| - settings['prescreening'] = { 'allowed' => true, 'enabled' => prescreening } - settings['prescreening_ideation'] = { 'allowed' => true, 'enabled' => prescreening } - settings['flag_inappropriate_content'] = { 'allowed' => true, 'enabled' => flag_inappropriate_content } - end - - AppConfiguration.instance.save! - end - - describe 'on create' do - where(:factory, :prescreening_mode, :prescreening, :flag_inappropriate_content, :valid) do - :phase | nil | false | false | true - :phase | 'all' | false | false | false - :phase | 'all' | true | false | true - :phase | 'flagged_only' | true | false | false - :phase | 'flagged_only' | true | true | true - :phase | 'invalid' | true | true | false - :proposals_phase | nil | false | false | true - :proposals_phase | 'all' | false | false | false - :proposals_phase | 'all' | true | false | true - :proposals_phase | 'flagged_only' | true | false | false - :proposals_phase | 'flagged_only' | true | true | true - :proposals_phase | 'invalid' | true | true | false - end - - with_them do - before { set_feature_flags(prescreening:, flag_inappropriate_content:) } - - it { expect(build(factory, prescreening_mode: prescreening_mode).valid?).to eq valid } - end + def set_feature_flags(prescreening:, flag_inappropriate_content:) + AppConfiguration.instance.settings.tap do |settings| + settings['prescreening'] = { 'allowed' => true, 'enabled' => prescreening } + settings['prescreening_ideation'] = { 'allowed' => true, 'enabled' => prescreening } + settings['flag_inappropriate_content'] = { 'allowed' => true, 'enabled' => flag_inappropriate_content } end - describe 'on update' do - it 'remains valid when feature is disabled but prescreening_mode unchanged' do - set_feature_flags(prescreening: true, flag_inappropriate_content: false) - phase = create(:phase, prescreening_mode: 'all') - set_feature_flags(prescreening: false, flag_inappropriate_content: false) + AppConfiguration.instance.save! + end - expect(phase).to be_valid - phase.title_multiloc = { 'en' => 'Updated title' } - expect(phase).to be_valid - end + # `prescreening_mode` is configuration data, not a feature-gated invariant: tenant + # templates and project copies carry it onto platforms whose features differ, so any + # valid mode is storable regardless of the flags. It is the *effect* that is gated - + # see 'effective mode' below. + describe 'validation' do + where(:factory, :prescreening_mode, :prescreening, :flag_inappropriate_content, :valid) do + :phase | nil | false | false | true + :phase | 'all' | false | false | true + :phase | 'all' | true | false | true + :phase | 'flagged_only' | false | false | true + :phase | 'flagged_only' | true | true | true + :phase | 'invalid' | true | true | false + :proposals_phase | nil | false | false | true + :proposals_phase | 'all' | false | false | true + :proposals_phase | 'flagged_only' | false | false | true + :proposals_phase | 'invalid' | true | true | false + end - it 'becomes invalid when changing prescreening_mode to non-nil with feature disabled' do - set_feature_flags(prescreening: true, flag_inappropriate_content: true) - phase = create(:phase, prescreening_mode: 'flagged_only') - set_feature_flags(prescreening: false, flag_inappropriate_content: false) + with_them do + before { set_feature_flags(prescreening:, flag_inappropriate_content:) } - expect(phase).to be_valid - phase.prescreening_mode = 'all' - expect(phase).not_to be_valid - end + it { expect(build(factory, prescreening_mode: prescreening_mode).valid?).to eq valid } end end - describe 'helper methods' do - where(:mode, :enabled, :flagged_only, :all) do - nil | false | false | false - 'flagged_only' | true | true | false - 'all' | true | false | true + # The mode only takes effect where the platform has the corresponding feature. Note + # ideation phases are gated on `prescreening_ideation` and proposals phases on + # `prescreening`; `set_feature_flags` sets both together. + describe 'effective mode' do + where(:factory, :mode, :prescreening, :flag_inappropriate_content, :enabled, :flagged_only, :all) do + # No mode configured: inert regardless of the flags. + :phase | nil | true | true | false | false | false + :phase | nil | false | false | false | false | false + + # Mode configured, prescreening feature off: inert. + :phase | 'all' | false | false | false | false | false + :phase | 'flagged_only' | false | true | false | false | false + :proposals_phase | 'all' | false | false | false | false | false + + # Mode configured, prescreening feature on: in effect. + :phase | 'all' | true | false | true | false | true + :proposals_phase | 'all' | true | false | true | false | true + + # 'flagged_only' additionally requires flag_inappropriate_content. + :phase | 'flagged_only' | true | false | false | false | false + :phase | 'flagged_only' | true | true | true | true | false + :proposals_phase | 'flagged_only' | true | true | true | true | false end with_them do - subject(:phase) { build(:phase, prescreening_mode: mode) } + subject(:phase) { build(factory, prescreening_mode: mode) } + + before { set_feature_flags(prescreening:, flag_inappropriate_content:) } it { expect(phase.prescreening_enabled?).to eq enabled } it { expect(phase.prescreening_flagged_only?).to eq flagged_only } it { expect(phase.prescreening_all?).to eq all } end + + it 'preserves the configured mode even where the feature is off' do + set_feature_flags(prescreening: false, flag_inappropriate_content: false) + phase = create(:phase, prescreening_mode: 'all') + + expect(phase.reload.prescreening_mode).to eq 'all' + expect(phase.prescreening_all?).to be false + end + + it 'takes effect once the feature is enabled, without rewriting the phase' do + set_feature_flags(prescreening: false, flag_inappropriate_content: false) + phase = create(:phase, prescreening_mode: 'all') + expect(phase.prescreening_all?).to be false + + set_feature_flags(prescreening: true, flag_inappropriate_content: false) + + expect(phase.prescreening_all?).to be true + end + + # The two participation methods are gated on different features, so converting a + # phase moves it from one gate to the other. This matters on a default-configured + # platform, where `prescreening` (proposals) is enabled and `prescreening_ideation` + # is not. + describe 'when the participation_method is converted' do + before do + AppConfiguration.instance.settings.tap do |settings| + settings['prescreening'] = { 'allowed' => true, 'enabled' => true } + settings['prescreening_ideation'] = { 'allowed' => false, 'enabled' => false } + end + + AppConfiguration.instance.save! + end + + it 'stops taking effect when proposals (feature on) becomes ideation (feature off)' do + phase = create(:proposals_phase, prescreening_mode: 'all') + expect(phase.prescreening_all?).to be true + + phase.update!(participation_method: 'ideation') + + expect(phase.reload.prescreening_mode).to eq 'all' + expect(phase.prescreening_all?).to be false + end + + it 'starts taking effect when ideation (feature off) becomes proposals (feature on)' do + phase = create(:phase, prescreening_mode: 'all') + expect(phase.prescreening_all?).to be false + + phase.update!(participation_method: 'proposals') + + expect(phase.reload.prescreening_mode).to eq 'all' + expect(phase.prescreening_all?).to be true + end + end end end end diff --git a/back/spec/serializers/web_api/v1/phase_serializer_spec.rb b/back/spec/serializers/web_api/v1/phase_serializer_spec.rb index 418aa2b78d4a..468ef6e6dd1b 100644 --- a/back/spec/serializers/web_api/v1/phase_serializer_spec.rb +++ b/back/spec/serializers/web_api/v1/phase_serializer_spec.rb @@ -84,6 +84,56 @@ end end + # The raw `prescreening_mode` is configuration data and is always serialized verbatim; + # `effective_prescreening_mode` is what the platform's feature flags actually permit, + # so the front end can rely on it without duplicating the flag logic. + describe 'effective_prescreening_mode' do + let(:user) { create(:user) } + + def set_feature_flags(prescreening:, flag_inappropriate_content:) + AppConfiguration.instance.settings.tap do |settings| + settings['prescreening'] = { 'allowed' => true, 'enabled' => prescreening } + settings['prescreening_ideation'] = { 'allowed' => true, 'enabled' => prescreening } + settings['flag_inappropriate_content'] = { 'allowed' => true, 'enabled' => flag_inappropriate_content } + end + + AppConfiguration.instance.save! + end + + context 'when the prescreening feature is enabled' do + before { set_feature_flags(prescreening: true, flag_inappropriate_content: false) } + + let(:phase) { create(:phase, prescreening_mode: 'all') } + + it 'serializes the configured mode as effective' do + expect(result.dig(:data, :attributes, :prescreening_mode)).to eq 'all' + expect(result.dig(:data, :attributes, :effective_prescreening_mode)).to eq 'all' + end + end + + context 'when the prescreening feature is disabled' do + before { set_feature_flags(prescreening: false, flag_inappropriate_content: false) } + + let(:phase) { create(:phase, prescreening_mode: 'all') } + + it 'serializes the configured mode but a nil effective mode' do + expect(result.dig(:data, :attributes, :prescreening_mode)).to eq 'all' + expect(result.dig(:data, :attributes, :effective_prescreening_mode)).to be_nil + end + end + + context 'when the mode is flagged_only and flag_inappropriate_content is disabled' do + before { set_feature_flags(prescreening: true, flag_inappropriate_content: false) } + + let(:phase) { create(:phase, prescreening_mode: 'flagged_only') } + + it 'serializes the configured mode but a nil effective mode' do + expect(result.dig(:data, :attributes, :prescreening_mode)).to eq 'flagged_only' + expect(result.dig(:data, :attributes, :effective_prescreening_mode)).to be_nil + end + end + end + context 'for a community monitor phase' do let(:user) { create(:user) } let(:phase) { create(:community_monitor_survey_phase, with_permissions: true) } diff --git a/back/spec/services/project_copy_service_spec.rb b/back/spec/services/project_copy_service_spec.rb index 0faed915cddc..50fc4bdd27d2 100644 --- a/back/spec/services/project_copy_service_spec.rb +++ b/back/spec/services/project_copy_service_spec.rb @@ -489,6 +489,39 @@ expect(copied_project.space_id).to be_nil end end + + # A project can be copied onto a platform whose features differ from the source's. + # The prescreening_mode travels with the phase, but only takes effect where the + # target platform has the corresponding feature. + describe 'a phase with a prescreening_mode' do + let(:project) do + SettingsService.new.activate_feature!('prescreening_ideation') + create(:project).tap do |project| + create(:phase, project: project, prescreening_mode: 'all') + end + end + + it 'copies the mode, without it taking effect where the target lacks the feature' do + template = service.export project, local_copy: false + SettingsService.new.deactivate_feature!('prescreening_ideation') + + copied_project = service.import template, local_copy: false + + copied_phase = copied_project.phases.first + expect(copied_phase.prescreening_mode).to eq 'all' + expect(copied_phase.prescreening_all?).to be false + end + + it 'copies the mode, and it takes effect where the target has the feature' do + template = service.export project, local_copy: false + + copied_project = service.import template, local_copy: false + + copied_phase = copied_project.phases.first + expect(copied_phase.prescreening_mode).to eq 'all' + expect(copied_phase.prescreening_all?).to be true + end + end end private diff --git a/front/app/api/phases/types.ts b/front/app/api/phases/types.ts index 6ca1e8ff1501..615362ff6391 100644 --- a/front/app/api/phases/types.ts +++ b/front/app/api/phases/types.ts @@ -79,6 +79,12 @@ export interface IPhaseAttributes { native_survey_title_multiloc?: Multiloc; native_survey_button_multiloc?: Multiloc; prescreening_mode?: PrescreeningMode; + /** The prescreening_mode reduced to what the platform's feature flags permit. A phase + * can carry a prescreening_mode on a platform without the screening feature (tenant + * templates and project copies bring the value across), in which case the back end + * ignores it and this is null. Use this, not prescreening_mode, to decide whether + * screening is in effect. */ + effective_prescreening_mode?: PrescreeningMode | null; manual_voters_amount?: number; similarity_enabled?: boolean; similarity_threshold_title?: number | null; diff --git a/front/app/components/IdeaCards/IdeasWithoutFiltersSidebar/index.tsx b/front/app/components/IdeaCards/IdeasWithoutFiltersSidebar/index.tsx index 1fbb1528f6e4..4b4879795272 100644 --- a/front/app/components/IdeaCards/IdeasWithoutFiltersSidebar/index.tsx +++ b/front/app/components/IdeaCards/IdeasWithoutFiltersSidebar/index.tsx @@ -255,7 +255,7 @@ const IdeasWithoutFiltersSidebar = ({ alignment={smallerThanTablet ? 'right' : 'left'} participationMethod={participationMethod} isScreeningEnabled={ - !!phase?.data.attributes.prescreening_mode + !!phase?.data.attributes.effective_prescreening_mode } /> )} diff --git a/front/app/components/IdeaCards/shared/Filters/StatusFilterBox.test.tsx b/front/app/components/IdeaCards/shared/Filters/StatusFilterBox.test.tsx new file mode 100644 index 000000000000..1430bc10f00c --- /dev/null +++ b/front/app/components/IdeaCards/shared/Filters/StatusFilterBox.test.tsx @@ -0,0 +1,83 @@ +import React from 'react'; + +import useIdeaStatuses from 'api/idea_statuses/useIdeaStatuses'; +import { phasesData } from 'api/phases/__mocks__/_mockServer'; +import { IPhase, IPhaseData } from 'api/phases/types'; + +import { render } from 'utils/testUtils/rtl'; + +import StatusFilterBox from './StatusFilterBox'; + +jest.mock('api/idea_statuses/useIdeaStatuses'); +jest.mock('api/ideas_filter_counts/useIdeasFilterCounts', () => + jest.fn(() => ({ + data: { + data: { + type: 'filter_counts', + attributes: { + idea_status_id: {}, + area_id: {}, + input_topic_id: {}, + total: 0, + }, + }, + }, + })) +); +jest.mock('hooks/useFeatureFlag', () => jest.fn(() => true)); + +const mockPhase = (attributes: Partial): IPhase => ({ + data: { + ...phasesData[0], + attributes: { ...phasesData[0].attributes, ...attributes }, + }, +}); + +const renderStatusFilterBox = (phase: IPhase) => + render( + + ); + +describe('StatusFilterBox', () => { + beforeEach(() => { + jest.mocked(useIdeaStatuses).mockClear(); + }); + + /* + A phase can carry a prescreening_mode on a platform where screening is not in effect + (tenant templates and project copies bring the value across from platforms whose + feature flags differ). The back end serializes the flag-aware effective mode, and + that — not the raw prescreening_mode, nor the feature flag (mocked on here) — must + decide whether the public list offers a screening status that can never match. + */ + it('excludes the screening status when the configured mode is not in effect', () => { + renderStatusFilterBox( + mockPhase({ prescreening_mode: 'all', effective_prescreening_mode: null }) + ); + + expect(jest.mocked(useIdeaStatuses)).toHaveBeenCalledWith({ + queryParams: { + participation_method: 'ideation', + exclude_codes: ['prescreening'], + }, + }); + }); + + it('includes the screening status when the mode is in effect', () => { + renderStatusFilterBox( + mockPhase({ + prescreening_mode: 'all', + effective_prescreening_mode: 'all', + }) + ); + + expect(jest.mocked(useIdeaStatuses)).toHaveBeenCalledWith({ + queryParams: { participation_method: 'ideation' }, + }); + }); +}); diff --git a/front/app/components/IdeaCards/shared/Filters/StatusFilterBox.tsx b/front/app/components/IdeaCards/shared/Filters/StatusFilterBox.tsx index e2bfd99ec24d..2df9a7ec67c9 100644 --- a/front/app/components/IdeaCards/shared/Filters/StatusFilterBox.tsx +++ b/front/app/components/IdeaCards/shared/Filters/StatusFilterBox.tsx @@ -33,9 +33,7 @@ const StatusFilterBox = ({ name: 'prescreening_ideation', }); const showScreeningStatus = phase - ? // This prescreening_mode setting is the same for ideation and proposal phases. - // So no need to differentiate between prescreening and prescreening_ideation. - !!phase.data.attributes.prescreening_mode + ? !!phase.data.attributes.effective_prescreening_mode : /* On the All inputs page, with no phase, we show all statuses if the prescreening_ideation feature is enabled (similar to platform input manager behavior). diff --git a/front/app/components/admin/PostManager/components/FilterSidebar/statuses/ScreeningStatusFilter.tsx b/front/app/components/admin/PostManager/components/FilterSidebar/statuses/ScreeningStatusFilter.tsx index 340b69ad683f..6defa4654631 100644 --- a/front/app/components/admin/PostManager/components/FilterSidebar/statuses/ScreeningStatusFilter.tsx +++ b/front/app/components/admin/PostManager/components/FilterSidebar/statuses/ScreeningStatusFilter.tsx @@ -51,7 +51,8 @@ const ScreeningStatusFilter = ({ status, active, onClick, type }: Props) => { }); // Both ideation and proposal phases use the same setting. - const phaseSettingEnabled = !!phase?.data.attributes.prescreening_mode; + const phaseSettingEnabled = + !!phase?.data.attributes.effective_prescreening_mode; const statusFilterIsEnabled = // We only show ideation inputs in the general input manager, so we don't need to check // for the proposals screening feature being allowed here.