diff --git a/backend/api/submissions/mutations.py b/backend/api/submissions/mutations.py index 0f1c9802be..02c9c14f12 100644 --- a/backend/api/submissions/mutations.py +++ b/backend/api/submissions/mutations.py @@ -227,6 +227,7 @@ class SendSubmissionInput(BaseSubmissionInput): topic: Optional[ID] = strawberry.field(default=None) tags: list[ID] = strawberry.field(default_factory=list) + do_not_record: bool = strawberry.field(default=False) @strawberry.input @@ -257,6 +258,7 @@ class UpdateSubmissionInput(BaseSubmissionInput): topic: Optional[ID] = strawberry.field(default=None) tags: list[ID] = strawberry.field(default_factory=list) materials: list[SubmissionMaterialInput] = strawberry.field(default_factory=list) + do_not_record: bool = strawberry.field(default=False) def validate(self, conference: Conference, submission: SubmissionModel): errors = super().validate(conference) @@ -319,6 +321,7 @@ def update_submission( instance.speaker_level = input.speaker_level instance.previous_talk_video = input.previous_talk_video instance.short_social_summary = input.short_social_summary + instance.do_not_record = input.do_not_record languages = Language.objects.filter(code__in=input.languages).all() instance.languages.set(languages) @@ -437,6 +440,7 @@ def send_submission( notes=input.notes, audience_level_id=input.audience_level, short_social_summary=input.short_social_summary, + do_not_record=input.do_not_record, ) languages = Language.objects.filter(code__in=input.languages).all() diff --git a/backend/api/submissions/tests/test_edit_submission.py b/backend/api/submissions/tests/test_edit_submission.py index 7a671420fb..65c20a06f8 100644 --- a/backend/api/submissions/tests/test_edit_submission.py +++ b/backend/api/submissions/tests/test_edit_submission.py @@ -46,6 +46,7 @@ def _update_submission( new_speaker_mastodon_handle="", new_speaker_availabilities=None, new_materials=None, + new_do_not_record=None, ): new_topic = new_topic or submission.topic new_audience = new_audience or submission.audience_level @@ -59,6 +60,7 @@ def _update_submission( new_speaker_photo = new_speaker_photo or FileFactory().id new_speaker_availabilities = new_speaker_availabilities or {} new_materials = new_materials or [] + new_do_not_record = new_do_not_record or submission.do_not_record return graphql_client.query( """ @@ -110,6 +112,7 @@ def _update_submission( speakerLevel previousTalkVideo + doNotRecord } ... on SendSubmissionErrors { @@ -168,6 +171,7 @@ def _update_submission( "speakerMastodonHandle": new_speaker_mastodon_handle, "speakerAvailabilities": new_speaker_availabilities, "materials": new_materials, + "doNotRecord": new_do_not_record, } }, ) @@ -1256,3 +1260,44 @@ def test_edit_submission_multi_lingual_fields_required(graphql_client, user): ] assert submission.languages.count() == 1 + + +def test_update_submission_with_do_not_record_true(graphql_client, user): + graphql_client.force_login(user) + + conference = ConferenceFactory( + topics=("life", "diy"), + languages=("it", "en"), + durations=("10", "20"), + active_cfp=True, + audience_levels=("adult", "senior"), + submission_types=("talk", "workshop"), + ) + + submission = SubmissionFactory( + speaker_id=user.id, + custom_topic="life", + custom_duration="10m", + custom_audience_level="adult", + custom_submission_type="talk", + languages=["it"], + tags=["python", "ml"], + conference=conference, + speaker_level=Submission.SPEAKER_LEVELS.intermediate, + previous_talk_video="https://www.youtube.com/watch?v=SlPhMPnQ58k", + do_not_record=False, + ) + + graphql_client.force_login(user) + + response = _update_submission( + graphql_client, + submission=submission, + new_do_not_record=True, + ) + + assert response["data"]["updateSubmission"]["__typename"] == "Submission" + assert response["data"]["updateSubmission"]["doNotRecord"] is True + + submission.refresh_from_db() + assert submission.do_not_record is True diff --git a/backend/api/submissions/tests/test_send_submission.py b/backend/api/submissions/tests/test_send_submission.py index 0b37060f5b..0a8156cb29 100644 --- a/backend/api/submissions/tests/test_send_submission.py +++ b/backend/api/submissions/tests/test_send_submission.py @@ -104,6 +104,7 @@ def _submit_proposal(client, conference, submission, **kwargs): tags { name } + doNotRecord } ... on SendSubmissionErrors { @@ -207,6 +208,7 @@ def test_submit_talk( assert talk.speaker_id == user.id assert talk.audience_level.name == "Beginner" assert talk.short_social_summary == "summary" + assert talk.do_not_record is False participant = Participant.objects.get(conference=conference, user_id=user.id) assert participant.bio == "my bio" @@ -1271,3 +1273,29 @@ def test_cannot_submit_more_than_3_proposals(graphql_client, user): assert resp["data"]["sendSubmission"]["errors"]["nonFieldErrors"] == [ "You can only submit up to 3 proposals" ] + + +def test_submit_talk_with_do_not_record_true(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",), + ) + + EmailTemplateFactory( + conference=conference, + identifier=EmailTemplateIdentifier.proposal_received_confirmation, + ) + + resp, _ = _submit_talk(graphql_client, conference, doNotRecord=True) + + assert resp["data"]["sendSubmission"]["__typename"] == "Submission" + assert resp["data"]["sendSubmission"]["doNotRecord"] is True + + talk = Submission.objects.get_by_hashid(resp["data"]["sendSubmission"]["id"]) + assert talk.do_not_record is True diff --git a/backend/api/submissions/tests/test_submissions.py b/backend/api/submissions/tests/test_submissions.py index 1379fc4bdf..55333a5e8d 100644 --- a/backend/api/submissions/tests/test_submissions.py +++ b/backend/api/submissions/tests/test_submissions.py @@ -10,9 +10,9 @@ def test_submissions_are_random_by_user(graphql_client, mock_has_ticket): - user_1 = UserFactory(id=100) - user_2 = UserFactory(id=103) - user_3 = UserFactory(id=104) + user_1 = UserFactory() + user_2 = UserFactory() + user_3 = UserFactory() graphql_client.force_login(user_1) diff --git a/backend/api/submissions/types.py b/backend/api/submissions/types.py index 128bcb18b3..5679b5b147 100644 --- a/backend/api/submissions/types.py +++ b/backend/api/submissions/types.py @@ -39,6 +39,7 @@ def resolver(self, info: Info): class SubmissionType: id: strawberry.ID name: str + is_recordable: bool @strawberry.type @@ -114,10 +115,11 @@ class Submission: topic: Annotated["Topic", strawberry.lazy("api.conferences.types")] | None type: SubmissionType | None duration: Annotated["Duration", strawberry.lazy("api.conferences.types")] | None - audience_level: Annotated[ - "AudienceLevel", strawberry.lazy("api.conferences.types") - ] | None + audience_level: ( + Annotated["AudienceLevel", strawberry.lazy("api.conferences.types")] | None + ) notes: str | None = private_field() + do_not_record: bool | None = private_field() @strawberry.field def schedule_items( diff --git a/backend/submissions/admin.py b/backend/submissions/admin.py index 7201ad726e..5e6773820c 100644 --- a/backend/submissions/admin.py +++ b/backend/submissions/admin.py @@ -249,6 +249,7 @@ class SubmissionAdmin(ExportMixin, ConferencePermissionMixin, admin.ModelAdmin): "conference", "audience_level", "languages", + "do_not_record", ) }, ), @@ -325,7 +326,7 @@ class Media: @admin.register(SubmissionType) class SubmissionTypeAdmin(admin.ModelAdmin): - list_display = ("name",) + list_display = ("name", "is_recordable") @admin.register(SubmissionTag) diff --git a/backend/submissions/migrations/0029_submission_do_not_record.py b/backend/submissions/migrations/0029_submission_do_not_record.py new file mode 100644 index 0000000000..aa834743ab --- /dev/null +++ b/backend/submissions/migrations/0029_submission_do_not_record.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.8 on 2025-12-14 13:25 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('submissions', '0028_alter_submission_pending_status'), + ] + + operations = [ + migrations.AddField( + model_name='submission', + name='do_not_record', + field=models.BooleanField(default=False, help_text='If true, the submission will not be recorded.', verbose_name='do not record'), + ), + ] diff --git a/backend/submissions/migrations/0030_submissiontype_is_recordable.py b/backend/submissions/migrations/0030_submissiontype_is_recordable.py new file mode 100644 index 0000000000..01c4c2bced --- /dev/null +++ b/backend/submissions/migrations/0030_submissiontype_is_recordable.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.8 on 2025-12-14 14:29 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('submissions', '0029_submission_do_not_record'), + ] + + operations = [ + migrations.AddField( + model_name='submissiontype', + name='is_recordable', + field=models.BooleanField(default=True, help_text='If true, the proposals of this type can be recorded.', verbose_name='is recordable'), + ), + ] diff --git a/backend/submissions/models.py b/backend/submissions/models.py index 66066c306b..c045f0c4c1 100644 --- a/backend/submissions/models.py +++ b/backend/submissions/models.py @@ -107,6 +107,12 @@ class Submission(TimeStampedModel): _("pending status"), choices=STATUS, max_length=20, null=True, blank=True ) + do_not_record = models.BooleanField( + _("do not record"), + default=False, + help_text=_("If true, the submission will not be recorded."), + ) + objects = SubmissionQuerySet().as_manager() @property @@ -230,6 +236,11 @@ def __str__(self): class SubmissionType(models.Model): name = models.CharField(max_length=100, unique=True) + is_recordable = models.BooleanField( + _("is recordable"), + default=True, + help_text=_("If true, the proposals of this type can be recorded."), + ) def __str__(self): return self.name diff --git a/frontend/src/components/cfp-form/cfp-form.graphql b/frontend/src/components/cfp-form/cfp-form.graphql index 0f1d2b3116..4d7f8e7eab 100644 --- a/frontend/src/components/cfp-form/cfp-form.graphql +++ b/frontend/src/components/cfp-form/cfp-form.graphql @@ -22,6 +22,7 @@ query CfpForm($conference: String!) { submissionTypes { id name + isRecordable } languages { diff --git a/frontend/src/components/cfp-form/index.tsx b/frontend/src/components/cfp-form/index.tsx index 5bde742c24..6e01baa20f 100644 --- a/frontend/src/components/cfp-form/index.tsx +++ b/frontend/src/components/cfp-form/index.tsx @@ -72,6 +72,7 @@ export type CfpFormFields = ParticipantFormFields & { acceptedPrivacyPolicy: boolean; speakerAvailabilities: { [time: number]: null | string }; materials: any[]; + doNotRecord: boolean; }; export type SubmissionStructure = { @@ -100,6 +101,7 @@ export type SubmissionStructure = { fileUrl: string; fileMimeType: string; }[]; + doNotRecord: boolean; }; type Props = { @@ -208,6 +210,7 @@ export const CfpForm = ({ acceptedPrivacyPolicy: formState.values.acceptedPrivacyPolicy, speakerAvailabilities: formState.values.speakerAvailabilities, materials: formState.values.materials, + doNotRecord: formState.values.doNotRecord, }); }; @@ -268,6 +271,7 @@ export const CfpForm = ({ name: material.name, })), ); + formState.setField("doNotRecord", submission!.doNotRecord); } if (participantData.me.participant) { diff --git a/frontend/src/components/cfp-form/proposal-section.tsx b/frontend/src/components/cfp-form/proposal-section.tsx index 7e215d7397..4d4dd896ee 100644 --- a/frontend/src/components/cfp-form/proposal-section.tsx +++ b/frontend/src/components/cfp-form/proposal-section.tsx @@ -26,6 +26,10 @@ export const ProposalSection = ({ }) => { const inputPlaceholder = useTranslatedMessage("input.placeholder"); const { radio, raw, select, textarea, checkbox } = formOptions; + const selectedType = conferenceData!.conference.submissionTypes.find( + (type) => type.id === formState.values.type, + ); + const isRecordable = selectedType?.isRecordable; return ( @@ -232,6 +236,27 @@ export const ProposalSection = ({ placeholder={inputPlaceholder} /> + + {isRecordable && ( + } + description={} + > + + + )} diff --git a/frontend/src/components/cfp-send-submission/index.tsx b/frontend/src/components/cfp-send-submission/index.tsx index 1c2715373d..c568c0a528 100644 --- a/frontend/src/components/cfp-send-submission/index.tsx +++ b/frontend/src/components/cfp-send-submission/index.tsx @@ -76,6 +76,7 @@ export const CfpSendSubmission = () => { speakerFacebookUrl: input.participantFacebookUrl, speakerMastodonHandle: input.participantMastodonHandle, speakerAvailabilities: input.speakerAvailabilities, + doNotRecord: input.doNotRecord, }, language, }, diff --git a/frontend/src/locale/index.ts b/frontend/src/locale/index.ts index c00dbd99d2..3d15e2bb54 100644 --- a/frontend/src/locale/index.ts +++ b/frontend/src/locale/index.ts @@ -577,6 +577,12 @@ Click the box to change. If left empty, we will assume you are available.`, "tickets.productsList.tshirtTitle": "T-shirt", + "cfp.doNotRecordLabel": "Do not record", + "cfp.doNotRecordDescription": + "By default we record all talks and later upload them to our YouTube channel. If you don't want your talk to be recorded, please check this box. Note: Your talk will still be live streamed.", + "cfp.doNotRecordCheckboxLabel": + "I confirm that I do not want my talk to be recorded. I understand that it will not be uploaded to Python Italia’s YouTube channel and that it will not be possible to recover it in the future.", + "tickets.checkout.answerCardAdmissionTitle": "{attendeeName}'s ticket", "tickets.checkout.openAnswerCard": "Attendee Info", "tickets.checkout.billing": "Billing", @@ -2314,6 +2320,12 @@ Clicca sulla casella per cambiare. Se lasciato vuoto, presumeremo che tu sia dis "cfp.materials.add": "Aggiungi", "cfp.materials.remove": "X", "fileInput.currentFile": "File attuale: {name}", + + "cfp.doNotRecordLabel": "Non registrare", + "cfp.doNotRecordDescription": + "Di norma registriamo tutti i talk e li pubblichiamo successivamente sul nostro canale YouTube. Se non desideri che il tuo intervento venga registrato, seleziona questa opzione. Nota: il talk sarà comunque trasmesso in streaming.", + "cfp.doNotRecordCheckboxLabel": + "Confermo di non voler che il mio talk venga registrato. Comprendo che non verrà caricato sul canale YouTube di Python Italia e che non sarà possibile recuperarlo in futuro.", }, }; diff --git a/frontend/src/pages/submission/[id]/edit/get-submission.graphql b/frontend/src/pages/submission/[id]/edit/get-submission.graphql index e4cb4d0f31..fd4a99afd5 100644 --- a/frontend/src/pages/submission/[id]/edit/get-submission.graphql +++ b/frontend/src/pages/submission/[id]/edit/get-submission.graphql @@ -6,6 +6,7 @@ query GetSubmission($id: ID!, $language: String!) { abstract(language: $language) elevatorPitch(language: $language) shortSocialSummary + doNotRecord multilingualTitle { it en diff --git a/frontend/src/pages/submission/[id]/edit/index.tsx b/frontend/src/pages/submission/[id]/edit/index.tsx index 8f26ee0fe7..c8eb4df999 100644 --- a/frontend/src/pages/submission/[id]/edit/index.tsx +++ b/frontend/src/pages/submission/[id]/edit/index.tsx @@ -76,6 +76,7 @@ export const EditSubmissionPage = () => { url: material.url, fileId: material.fileId, })), + doNotRecord: input.doNotRecord, }, language, },