From 051eef22973a567d817611b24c7eca02e274768c Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Wed, 7 Jan 2026 01:12:43 +0000 Subject: [PATCH 1/4] Make proposal submission limit configurable per conference - Add max_proposals field to Conference model (default: None for no limit) - Update API validation to use conference-specific limit instead of hardcoded 3 - Update existing test to set max_proposals=3 - Add new test to verify unlimited submissions when max_proposals is None Fixes #4523 Co-authored-by: Marco Acierno --- backend/api/submissions/mutations.py | 20 ++++++----- .../submissions/tests/test_send_submission.py | 34 +++++++++++++++++++ .../0056_conference_max_proposals.py | 18 ++++++++++ backend/conferences/models/conference.py | 7 ++++ 4 files changed, 70 insertions(+), 9 deletions(-) create mode 100644 backend/conferences/migrations/0056_conference_max_proposals.py diff --git a/backend/api/submissions/mutations.py b/backend/api/submissions/mutations.py index 02c9c14f12..8bc203d18d 100644 --- a/backend/api/submissions/mutations.py +++ b/backend/api/submissions/mutations.py @@ -412,16 +412,18 @@ def send_submission( if not conference.is_cfp_open: errors.add_error("non_field_errors", "The call for paper is not open!") - if ( - SubmissionModel.objects.of_user(request.user) - .for_conference(conference) - .non_cancelled() - .count() - >= 3 - ): - errors.add_error( - "non_field_errors", "You can only submit up to 3 proposals" + if conference.max_proposals is not None: + user_submissions_count = ( + SubmissionModel.objects.of_user(request.user) + .for_conference(conference) + .non_cancelled() + .count() ) + if user_submissions_count >= conference.max_proposals: + errors.add_error( + "non_field_errors", + f"You can only submit up to {conference.max_proposals} proposals", + ) if errors.has_errors: return errors diff --git a/backend/api/submissions/tests/test_send_submission.py b/backend/api/submissions/tests/test_send_submission.py index 0a8156cb29..3c0aa46b27 100644 --- a/backend/api/submissions/tests/test_send_submission.py +++ b/backend/api/submissions/tests/test_send_submission.py @@ -1249,6 +1249,7 @@ def test_cannot_submit_more_than_3_proposals(graphql_client, user): active_cfp=True, durations=("50",), audience_levels=("Beginner",), + max_proposals=3, ) SubmissionFactory( @@ -1275,6 +1276,39 @@ def test_cannot_submit_more_than_3_proposals(graphql_client, user): ] +def test_can_submit_unlimited_proposals_when_max_proposals_is_none(graphql_client, user): + graphql_client.force_login(user) + + conference = ConferenceFactory( + topics=("my-topic",), + languages=("en", "it"), + submission_types=("talk",), + active_cfp=True, + durations=("50",), + audience_levels=("Beginner",), + max_proposals=None, # No limit + ) + + EmailTemplateFactory( + conference=conference, + identifier=EmailTemplateIdentifier.proposal_received_confirmation, + ) + + # Create 3 existing submissions + for _ in range(3): + SubmissionFactory( + speaker_id=user.id, + conference=conference, + status=Submission.STATUS.proposed, + ) + + # Should be able to submit a 4th proposal + resp, _ = _submit_talk(graphql_client, conference, title={"en": "My fourth talk"}) + + assert resp["data"]["sendSubmission"]["__typename"] == "Submission" + assert resp["data"]["sendSubmission"]["title"]["en"] == "My fourth talk" + + def test_submit_talk_with_do_not_record_true(graphql_client, user): graphql_client.force_login(user) diff --git a/backend/conferences/migrations/0056_conference_max_proposals.py b/backend/conferences/migrations/0056_conference_max_proposals.py new file mode 100644 index 0000000000..54e672c951 --- /dev/null +++ b/backend/conferences/migrations/0056_conference_max_proposals.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.4 on 2026-01-07 01:11 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('conferences', '0055_remove_conference_grants_default_accommodation_amount_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='conference', + name='max_proposals', + field=models.PositiveIntegerField(blank=True, help_text='Maximum number of proposals a user can submit. Leave empty for no limit.', null=True, verbose_name='max proposals per user'), + ), + ] diff --git a/backend/conferences/models/conference.py b/backend/conferences/models/conference.py index 1388becd07..001c63ad1f 100644 --- a/backend/conferences/models/conference.py +++ b/backend/conferences/models/conference.py @@ -117,6 +117,13 @@ class Conference(GeoLocalizedModel, TimeFramedModel, TimeStampedModel): max_length=32224, ) + max_proposals = models.PositiveIntegerField( + _("max proposals per user"), + null=True, + blank=True, + help_text=_("Maximum number of proposals a user can submit. Leave empty for no limit."), + ) + def get_slack_oauth_token(self): return self.organizer.slack_oauth_bot_token From 76288df8f4f37f98b91c09ba3d0dbb411291aa20 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Wed, 7 Jan 2026 02:40:03 +0000 Subject: [PATCH 2/4] Rename max_proposals to max_proposals_per_user and expose in admin - Renamed field from max_proposals to max_proposals_per_user for clarity - Updated migration to use new field name - Updated API mutations to reference new field name - Updated tests to use new field name - Exposed field in Conference admin under Conference fieldset Co-authored-by: Marco Acierno --- backend/api/submissions/mutations.py | 6 +++--- backend/api/submissions/tests/test_send_submission.py | 4 ++-- backend/conferences/admin/conference.py | 1 + .../conferences/migrations/0056_conference_max_proposals.py | 2 +- backend/conferences/models/conference.py | 2 +- 5 files changed, 8 insertions(+), 7 deletions(-) diff --git a/backend/api/submissions/mutations.py b/backend/api/submissions/mutations.py index 8bc203d18d..94bc82c055 100644 --- a/backend/api/submissions/mutations.py +++ b/backend/api/submissions/mutations.py @@ -412,17 +412,17 @@ def send_submission( if not conference.is_cfp_open: errors.add_error("non_field_errors", "The call for paper is not open!") - if conference.max_proposals is not None: + if conference.max_proposals_per_user is not None: user_submissions_count = ( SubmissionModel.objects.of_user(request.user) .for_conference(conference) .non_cancelled() .count() ) - if user_submissions_count >= conference.max_proposals: + if user_submissions_count >= conference.max_proposals_per_user: errors.add_error( "non_field_errors", - f"You can only submit up to {conference.max_proposals} proposals", + f"You can only submit up to {conference.max_proposals_per_user} proposals", ) if errors.has_errors: diff --git a/backend/api/submissions/tests/test_send_submission.py b/backend/api/submissions/tests/test_send_submission.py index 3c0aa46b27..79343da8d5 100644 --- a/backend/api/submissions/tests/test_send_submission.py +++ b/backend/api/submissions/tests/test_send_submission.py @@ -1249,7 +1249,7 @@ def test_cannot_submit_more_than_3_proposals(graphql_client, user): active_cfp=True, durations=("50",), audience_levels=("Beginner",), - max_proposals=3, + max_proposals_per_user=3, ) SubmissionFactory( @@ -1286,7 +1286,7 @@ def test_can_submit_unlimited_proposals_when_max_proposals_is_none(graphql_clien active_cfp=True, durations=("50",), audience_levels=("Beginner",), - max_proposals=None, # No limit + max_proposals_per_user=None, # No limit ) EmailTemplateFactory( diff --git a/backend/conferences/admin/conference.py b/backend/conferences/admin/conference.py index a833b8af9a..4c637bc86b 100644 --- a/backend/conferences/admin/conference.py +++ b/backend/conferences/admin/conference.py @@ -181,6 +181,7 @@ class ConferenceAdmin( "audience_levels", "languages", "proposal_tags", + "max_proposals_per_user", ) }, ), diff --git a/backend/conferences/migrations/0056_conference_max_proposals.py b/backend/conferences/migrations/0056_conference_max_proposals.py index 54e672c951..c0d9fa7a51 100644 --- a/backend/conferences/migrations/0056_conference_max_proposals.py +++ b/backend/conferences/migrations/0056_conference_max_proposals.py @@ -12,7 +12,7 @@ class Migration(migrations.Migration): operations = [ migrations.AddField( model_name='conference', - name='max_proposals', + name='max_proposals_per_user', field=models.PositiveIntegerField(blank=True, help_text='Maximum number of proposals a user can submit. Leave empty for no limit.', null=True, verbose_name='max proposals per user'), ), ] diff --git a/backend/conferences/models/conference.py b/backend/conferences/models/conference.py index 001c63ad1f..397724f190 100644 --- a/backend/conferences/models/conference.py +++ b/backend/conferences/models/conference.py @@ -117,7 +117,7 @@ class Conference(GeoLocalizedModel, TimeFramedModel, TimeStampedModel): max_length=32224, ) - max_proposals = models.PositiveIntegerField( + max_proposals_per_user = models.PositiveIntegerField( _("max proposals per user"), null=True, blank=True, From f8d487e35a3fd644301cd4762eecb095bb147b0f Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Thu, 8 Jan 2026 01:43:11 +0000 Subject: [PATCH 3/4] Fix test_can_submit_unlimited_proposals_when_max_proposals_is_none - Fix incorrect assertion: title is a string, not an object - Don't explicitly pass max_proposals_per_user=None, let it default Co-authored-by: Marco Acierno --- backend/api/submissions/tests/test_send_submission.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/api/submissions/tests/test_send_submission.py b/backend/api/submissions/tests/test_send_submission.py index 79343da8d5..0b7f45881d 100644 --- a/backend/api/submissions/tests/test_send_submission.py +++ b/backend/api/submissions/tests/test_send_submission.py @@ -1286,7 +1286,7 @@ def test_can_submit_unlimited_proposals_when_max_proposals_is_none(graphql_clien active_cfp=True, durations=("50",), audience_levels=("Beginner",), - max_proposals_per_user=None, # No limit + # max_proposals_per_user defaults to None (no limit) ) EmailTemplateFactory( @@ -1306,7 +1306,7 @@ def test_can_submit_unlimited_proposals_when_max_proposals_is_none(graphql_clien resp, _ = _submit_talk(graphql_client, conference, title={"en": "My fourth talk"}) assert resp["data"]["sendSubmission"]["__typename"] == "Submission" - assert resp["data"]["sendSubmission"]["title"]["en"] == "My fourth talk" + assert resp["data"]["sendSubmission"]["title"] == "My fourth talk" def test_submit_talk_with_do_not_record_true(graphql_client, user): From 0442a49eac9398dc69adb21edca8d4566fc1333e Mon Sep 17 00:00:00 2001 From: Marco Acierno Date: Sat, 10 Jan 2026 08:03:40 +0100 Subject: [PATCH 4/4] Fix test --- .../api/submissions/tests/test_send_submission.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/backend/api/submissions/tests/test_send_submission.py b/backend/api/submissions/tests/test_send_submission.py index 0b7f45881d..0f3bfeef16 100644 --- a/backend/api/submissions/tests/test_send_submission.py +++ b/backend/api/submissions/tests/test_send_submission.py @@ -1268,7 +1268,9 @@ def test_cannot_submit_more_than_3_proposals(graphql_client, user): status=Submission.STATUS.proposed, ) - resp, _ = _submit_talk(graphql_client, conference, title={"en": "My first talk"}) + resp, _ = _submit_talk( + graphql_client, conference, title={"en": "My first talk"}, languages=["en"] + ) assert resp["data"]["sendSubmission"]["__typename"] == "SendSubmissionErrors" assert resp["data"]["sendSubmission"]["errors"]["nonFieldErrors"] == [ @@ -1276,7 +1278,9 @@ def test_cannot_submit_more_than_3_proposals(graphql_client, user): ] -def test_can_submit_unlimited_proposals_when_max_proposals_is_none(graphql_client, user): +def test_can_submit_unlimited_proposals_when_max_proposals_is_none( + graphql_client, user +): graphql_client.force_login(user) conference = ConferenceFactory( @@ -1303,7 +1307,9 @@ def test_can_submit_unlimited_proposals_when_max_proposals_is_none(graphql_clien ) # Should be able to submit a 4th proposal - resp, _ = _submit_talk(graphql_client, conference, title={"en": "My fourth talk"}) + resp, _ = _submit_talk( + graphql_client, conference, title={"en": "My fourth talk"}, languages=["en"] + ) assert resp["data"]["sendSubmission"]["__typename"] == "Submission" assert resp["data"]["sendSubmission"]["title"] == "My fourth talk"