diff --git a/backend/api/grants/types.py b/backend/api/grants/types.py
index 87ba072fcf..aa0d2b9162 100644
--- a/backend/api/grants/types.py
+++ b/backend/api/grants/types.py
@@ -4,8 +4,6 @@
from typing import Optional
import strawberry
-from api.participants.types import Participant
-from participants.models import Participant as ParticipantModel
from grants.models import Grant as GrantModel
@@ -36,15 +34,8 @@ class Grant:
travelling_from: Optional[str]
applicant_reply_deadline: Optional[datetime]
- participant: Participant
-
@classmethod
def from_model(cls, grant: GrantModel) -> Grant:
- participant = ParticipantModel.objects.filter(
- user_id=grant.user_id,
- conference=grant.conference,
- ).first()
-
return cls(
id=grant.id,
status=Status(grant.status),
@@ -64,5 +55,4 @@ def from_model(cls, grant: GrantModel) -> Grant:
notes=grant.notes,
travelling_from=grant.travelling_from,
applicant_reply_deadline=grant.applicant_reply_deadline,
- participant=Participant.from_model(participant),
)
diff --git a/backend/api/participants/types.py b/backend/api/participants/types.py
index df704c2620..c1228e6797 100644
--- a/backend/api/participants/types.py
+++ b/backend/api/participants/types.py
@@ -1,5 +1,6 @@
from typing import Optional
+from strawberry.scalars import JSON
import strawberry
from strawberry import ID
@@ -22,6 +23,7 @@ class Participant:
mastodon_handle: str
speaker_id: strawberry.Private[int]
fullname: str
+ speaker_availabilities: JSON
_speaker_level: strawberry.Private[str]
_previous_talk_video: strawberry.Private[str]
@@ -59,4 +61,5 @@ def from_model(cls, instance):
linkedin_url=instance.linkedin_url,
facebook_url=instance.facebook_url,
mastodon_handle=instance.mastodon_handle,
+ speaker_availabilities=instance.speaker_availabilities or {},
)
diff --git a/backend/api/submissions/mutations.py b/backend/api/submissions/mutations.py
index 91a7f368be..0e610b59ac 100644
--- a/backend/api/submissions/mutations.py
+++ b/backend/api/submissions/mutations.py
@@ -1,3 +1,5 @@
+from strawberry.scalars import JSON
+
from django.db import transaction
import math
import re
@@ -208,6 +210,7 @@ class SendSubmissionInput(BaseSubmissionInput):
speaker_linkedin_url: str
speaker_facebook_url: str
speaker_mastodon_handle: str
+ speaker_availabilities: JSON
topic: Optional[ID] = strawberry.field(default=None)
tags: list[ID] = strawberry.field(default_factory=list)
@@ -236,6 +239,7 @@ class UpdateSubmissionInput(BaseSubmissionInput):
speaker_linkedin_url: str
speaker_facebook_url: str
speaker_mastodon_handle: str
+ speaker_availabilities: JSON
topic: Optional[ID] = strawberry.field(default=None)
tags: list[ID] = strawberry.field(default_factory=list)
@@ -307,6 +311,7 @@ def update_submission(
"linkedin_url": input.speaker_linkedin_url,
"facebook_url": input.speaker_facebook_url,
"mastodon_handle": input.speaker_mastodon_handle,
+ "speaker_availabilities": input.speaker_availabilities,
},
)
@@ -368,6 +373,7 @@ def send_submission(
"linkedin_url": input.speaker_linkedin_url,
"facebook_url": input.speaker_facebook_url,
"mastodon_handle": input.speaker_mastodon_handle,
+ "speaker_availabilities": input.speaker_availabilities,
},
)
diff --git a/backend/api/submissions/tests/test_edit_submission.py b/backend/api/submissions/tests/test_edit_submission.py
index f8ebac9882..6c484a3339 100644
--- a/backend/api/submissions/tests/test_edit_submission.py
+++ b/backend/api/submissions/tests/test_edit_submission.py
@@ -33,12 +33,14 @@ def _update_submission(
new_speaker_linkedin_url="",
new_speaker_facebook_url="",
new_speaker_mastodon_handle="",
+ new_speaker_availabilities=None,
):
new_title = new_title or {"en": "new title to use"}
new_elevator_pitch = new_elevator_pitch or {"en": "This is an elevator pitch"}
new_abstract = new_abstract or {"en": "abstract here"}
short_social_summary = new_short_social_summary or ""
new_speaker_photo = new_speaker_photo or FileFactory().id
+ new_speaker_availabilities = new_speaker_availabilities or {}
return graphql_client.query(
"""
@@ -141,6 +143,7 @@ def _update_submission(
"speakerLinkedinUrl": new_speaker_linkedin_url,
"speakerFacebookUrl": new_speaker_facebook_url,
"speakerMastodonHandle": new_speaker_mastodon_handle,
+ "speakerAvailabilities": new_speaker_availabilities,
}
},
)
@@ -201,6 +204,67 @@ def test_update_submission(graphql_client, user):
assert participant.linkedin_url == "http://linkedin.com/company/pythonpizza"
+def test_update_submission_speaker_availabilities(graphql_client, 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",
+ )
+
+ graphql_client.force_login(user)
+
+ new_topic = conference.topics.filter(name="diy").first()
+ new_audience = conference.audience_levels.filter(name="senior").first()
+ new_tag = SubmissionTagFactory(name="yello")
+ new_duration = conference.durations.filter(name="20m").first()
+ new_type = conference.submission_types.filter(name="workshop").first()
+
+ response = _update_submission(
+ graphql_client,
+ submission=submission,
+ new_topic=new_topic,
+ new_audience=new_audience,
+ new_tag=new_tag,
+ new_duration=new_duration,
+ new_type=new_type,
+ new_speaker_level=Submission.SPEAKER_LEVELS.experienced,
+ new_speaker_availabilities={
+ "2023-12-10@am": "unavailable",
+ "2023-12-11@pm": "unavailable",
+ "2023-12-12@am": "preferred",
+ "2023-12-13@am": None,
+ },
+ )
+
+ submission.refresh_from_db()
+
+ assert response["data"]["updateSubmission"]["__typename"] == "Submission"
+
+ participant = Participant.objects.first()
+ assert participant.speaker_availabilities == {
+ "2023-12-10@am": "unavailable",
+ "2023-12-11@pm": "unavailable",
+ "2023-12-12@am": "preferred",
+ "2023-12-13@am": None,
+ }
+
+
def test_update_submission_with_invalid_facebook_social_url(graphql_client, user):
conference = ConferenceFactory(
topics=("life", "diy"),
diff --git a/backend/api/submissions/tests/test_send_submission.py b/backend/api/submissions/tests/test_send_submission.py
index ba1980481e..0a319aa8b9 100644
--- a/backend/api/submissions/tests/test_send_submission.py
+++ b/backend/api/submissions/tests/test_send_submission.py
@@ -67,6 +67,7 @@ def _submit_proposal(client, conference, submission, **kwargs):
"speakerFacebookUrl": "https://facebook.com/fake-link",
"speakerMastodonHandle": "fake@mastodon.social",
"tags": [tag.id],
+ "speakerAvailabilities": {},
}
override_conference = kwargs.pop("override_conference", None)
@@ -163,6 +164,11 @@ def test_submit_talk(graphql_client, user, django_capture_on_commit_callbacks, m
shortSocialSummary="summary",
speakerBio="my bio",
speakerPhoto=speaker_photo,
+ speakerAvailabilities={
+ "2023-10-10@am": "preferred",
+ "2023-10-11@pm": "unavailable",
+ "2023-10-12@am": "available",
+ },
)
assert resp["data"]["sendSubmission"]["__typename"] == "Submission"
@@ -190,6 +196,11 @@ def test_submit_talk(graphql_client, user, django_capture_on_commit_callbacks, m
participant = Participant.objects.get(conference=conference, user_id=user.id)
assert participant.bio == "my bio"
assert participant.photo_file_id == speaker_photo
+ assert participant.speaker_availabilities == {
+ "2023-10-10@am": "preferred",
+ "2023-10-11@pm": "unavailable",
+ "2023-10-12@am": "available",
+ }
assert PrivacyPolicyAcceptanceRecord.objects.filter(
user=user, conference=conference, privacy_policy="cfp"
diff --git a/backend/participants/admin.py b/backend/participants/admin.py
index 36448d4a65..beaafaea67 100644
--- a/backend/participants/admin.py
+++ b/backend/participants/admin.py
@@ -8,20 +8,7 @@
class ParticipantForm(forms.ModelForm):
class Meta:
model = Participant
- fields = [
- "conference",
- "user",
- "photo",
- "bio",
- "website",
- "twitter_handle",
- "instagram_handle",
- "linkedin_url",
- "facebook_url",
- "mastodon_handle",
- "speaker_level",
- "previous_talk_video",
- ]
+ fields = "__all__"
@admin.register(Participant)
@@ -54,6 +41,7 @@ class ParticipantAdmin(admin.ModelAdmin):
"mastodon_handle",
"speaker_level",
"previous_talk_video",
+ "speaker_availabilities",
),
},
),
diff --git a/backend/participants/migrations/0012_participant_speaker_availabilities.py b/backend/participants/migrations/0012_participant_speaker_availabilities.py
new file mode 100644
index 0000000000..211fb791ed
--- /dev/null
+++ b/backend/participants/migrations/0012_participant_speaker_availabilities.py
@@ -0,0 +1,18 @@
+# Generated by Django 5.1.1 on 2024-11-28 22:08
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('participants', '0011_alter_participant_photo'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='participant',
+ name='speaker_availabilities',
+ field=models.JSONField(blank=True, null=True, verbose_name='speaker availabilities'),
+ ),
+ ]
diff --git a/backend/participants/models.py b/backend/participants/models.py
index 31a68a03f6..29e6cc0cb3 100644
--- a/backend/participants/models.py
+++ b/backend/participants/models.py
@@ -54,6 +54,9 @@ class SpeakerLevels(models.TextChoices):
previous_talk_video = models.URLField(
_("previous talk video"), blank=True, max_length=2049
)
+ speaker_availabilities = models.JSONField(
+ _("speaker availabilities"), null=True, blank=True
+ )
objects = ParticipantQuerySet().as_manager()
diff --git a/frontend/src/components/cfp-form/about-you-section.tsx b/frontend/src/components/cfp-form/about-you-section.tsx
new file mode 100644
index 0000000000..6a7405b271
--- /dev/null
+++ b/frontend/src/components/cfp-form/about-you-section.tsx
@@ -0,0 +1,93 @@
+import {
+ CardPart,
+ Grid,
+ Heading,
+ Input,
+ InputWrapper,
+ MultiplePartsCard,
+ Select,
+ Text,
+} from "@python-italia/pycon-styleguide";
+import { FormattedMessage } from "react-intl";
+import { useTranslatedMessage } from "~/helpers/use-translated-message";
+
+const SPEAKER_LEVEL_OPTIONS = [
+ {
+ value: "",
+ disabled: true,
+ messageId: "cfp.selectSpeakerLevel",
+ },
+ {
+ disabled: false,
+ value: "new",
+ messageId: "cfp.speakerLevel.new",
+ },
+ {
+ disabled: false,
+ value: "intermediate",
+ messageId: "cfp.speakerLevel.intermediate",
+ },
+ {
+ disabled: false,
+ value: "experienced",
+ messageId: "cfp.speakerLevel.experienced",
+ },
+];
+
+export const AboutYouSection = ({ formOptions, getErrors }) => {
+ const inputPlaceholder = useTranslatedMessage("input.placeholder");
+ const { select, url } = formOptions;
+
+ return (
+