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()