Skip to content
4 changes: 4 additions & 0 deletions backend/api/submissions/mutations.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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()
Expand Down
45 changes: 45 additions & 0 deletions backend/api/submissions/tests/test_edit_submission.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(
"""
Expand Down Expand Up @@ -110,6 +112,7 @@ def _update_submission(

speakerLevel
previousTalkVideo
doNotRecord
}

... on SendSubmissionErrors {
Expand Down Expand Up @@ -168,6 +171,7 @@ def _update_submission(
"speakerMastodonHandle": new_speaker_mastodon_handle,
"speakerAvailabilities": new_speaker_availabilities,
"materials": new_materials,
"doNotRecord": new_do_not_record,
}
},
)
Expand Down Expand Up @@ -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
28 changes: 28 additions & 0 deletions backend/api/submissions/tests/test_send_submission.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ def _submit_proposal(client, conference, submission, **kwargs):
tags {
name
}
doNotRecord
}

... on SendSubmissionErrors {
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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
6 changes: 3 additions & 3 deletions backend/api/submissions/tests/test_submissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
8 changes: 5 additions & 3 deletions backend/api/submissions/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ def resolver(self, info: Info):
class SubmissionType:
id: strawberry.ID
name: str
is_recordable: bool


@strawberry.type
Expand Down Expand Up @@ -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(
Expand Down
3 changes: 2 additions & 1 deletion backend/submissions/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@ class SubmissionAdmin(ExportMixin, ConferencePermissionMixin, admin.ModelAdmin):
"conference",
"audience_level",
"languages",
"do_not_record",
)
},
),
Expand Down Expand Up @@ -325,7 +326,7 @@ class Media:

@admin.register(SubmissionType)
class SubmissionTypeAdmin(admin.ModelAdmin):
list_display = ("name",)
list_display = ("name", "is_recordable")


@admin.register(SubmissionTag)
Expand Down
18 changes: 18 additions & 0 deletions backend/submissions/migrations/0029_submission_do_not_record.py
Original file line number Diff line number Diff line change
@@ -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'),
),
]
Original file line number Diff line number Diff line change
@@ -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'),
),
]
11 changes: 11 additions & 0 deletions backend/submissions/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/cfp-form/cfp-form.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ query CfpForm($conference: String!) {
submissionTypes {
id
name
isRecordable
}

languages {
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/components/cfp-form/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export type CfpFormFields = ParticipantFormFields & {
acceptedPrivacyPolicy: boolean;
speakerAvailabilities: { [time: number]: null | string };
materials: any[];
doNotRecord: boolean;
};

export type SubmissionStructure = {
Expand Down Expand Up @@ -100,6 +101,7 @@ export type SubmissionStructure = {
fileUrl: string;
fileMimeType: string;
}[];
doNotRecord: boolean;
};

type Props = {
Expand Down Expand Up @@ -208,6 +210,7 @@ export const CfpForm = ({
acceptedPrivacyPolicy: formState.values.acceptedPrivacyPolicy,
speakerAvailabilities: formState.values.speakerAvailabilities,
materials: formState.values.materials,
doNotRecord: formState.values.doNotRecord,
});
};

Expand Down Expand Up @@ -268,6 +271,7 @@ export const CfpForm = ({
name: material.name,
})),
);
formState.setField("doNotRecord", submission!.doNotRecord);
}

if (participantData.me.participant) {
Expand Down
25 changes: 25 additions & 0 deletions frontend/src/components/cfp-form/proposal-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<MultiplePartsCard>
Expand Down Expand Up @@ -232,6 +236,27 @@ export const ProposalSection = ({
placeholder={inputPlaceholder}
/>
</InputWrapper>

{isRecordable && (
<InputWrapper
required={false}
title={<FormattedMessage id="cfp.doNotRecordLabel" />}
description={<FormattedMessage id="cfp.doNotRecordDescription" />}
>
<label>
<HorizontalStack gap="small" alignItems="center">
<Checkbox
{...checkbox("doNotRecord")}
required={false}
errors={getErrors("validationDoNotRecord")}
/>
<Text size={2} weight="strong">
<FormattedMessage id="cfp.doNotRecordCheckboxLabel" />
</Text>
</HorizontalStack>
</label>
</InputWrapper>
)}
</Grid>
</CardPart>
</MultiplePartsCard>
Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/cfp-send-submission/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export const CfpSendSubmission = () => {
speakerFacebookUrl: input.participantFacebookUrl,
speakerMastodonHandle: input.participantMastodonHandle,
speakerAvailabilities: input.speakerAvailabilities,
doNotRecord: input.doNotRecord,
},
language,
},
Expand Down
12 changes: 12 additions & 0 deletions frontend/src/locale/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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.",
},
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ query GetSubmission($id: ID!, $language: String!) {
abstract(language: $language)
elevatorPitch(language: $language)
shortSocialSummary
doNotRecord
multilingualTitle {
it
en
Expand Down
1 change: 1 addition & 0 deletions frontend/src/pages/submission/[id]/edit/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export const EditSubmissionPage = () => {
url: material.url,
fileId: material.fileId,
})),
doNotRecord: input.doNotRecord,
},
language,
},
Expand Down
Loading