Skip to content

Commit 3e8e61a

Browse files
committed
Error handling
1 parent 880cfc9 commit 3e8e61a

File tree

8 files changed

+342
-24
lines changed

8 files changed

+342
-24
lines changed

backend/api/submissions/mutations.py

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,13 @@
2929
LINKEDIN_LINK_MATCH = re.compile(r"^http(s)?:\/\/(www\.)?linkedin\.com\/")
3030

3131

32+
@strawberry.type
33+
class ProposalMaterialErrors:
34+
file_id: list[str] = strawberry.field(default_factory=list)
35+
url: list[str] = strawberry.field(default_factory=list)
36+
id: list[str] = strawberry.field(default_factory=list)
37+
38+
3239
@strawberry.type
3340
class SendSubmissionErrors(BaseErrorType):
3441
@strawberry.type
@@ -46,6 +53,7 @@ class _SendSubmissionErrors:
4653
audience_level: list[str] = strawberry.field(default_factory=list)
4754
tags: list[str] = strawberry.field(default_factory=list)
4855
short_social_summary: list[str] = strawberry.field(default_factory=list)
56+
materials: list[ProposalMaterialErrors] = strawberry.field(default_factory=list)
4957

5058
speaker_bio: list[str] = strawberry.field(default_factory=list)
5159
speaker_photo: list[str] = strawberry.field(default_factory=list)
@@ -249,6 +257,16 @@ class UpdateSubmissionInput(BaseSubmissionInput):
249257
tags: list[ID] = strawberry.field(default_factory=list)
250258
materials: list[SubmissionMaterialInput] = strawberry.field(default_factory=list)
251259

260+
def validate(self, conference: Conference, submission: SubmissionModel):
261+
errors = super().validate(conference)
262+
263+
if self.materials:
264+
for index, material in enumerate(self.materials):
265+
with errors.with_prefix("materials", index):
266+
material.validate(errors, submission)
267+
268+
return errors
269+
252270

253271
SendSubmissionOutput = Annotated[
254272
Union[Submission, SendSubmissionErrors],
@@ -277,7 +295,7 @@ def update_submission(
277295

278296
conference = instance.conference
279297

280-
errors = input.validate(conference=conference)
298+
errors = input.validate(conference=conference, submission=instance)
281299

282300
if errors.has_errors:
283301
return errors
@@ -295,6 +313,7 @@ def update_submission(
295313
instance.speaker_level = input.speaker_level
296314
instance.previous_talk_video = input.previous_talk_video
297315
instance.short_social_summary = input.short_social_summary
316+
298317
languages = Language.objects.filter(code__in=input.languages).all()
299318
instance.languages.set(languages)
300319

@@ -304,13 +323,16 @@ def update_submission(
304323

305324
materials_to_create = []
306325
materials_to_update = []
307-
materials_to_delete = []
308326

309-
existing_materials = list(instance.materials.all())
327+
existing_materials = {
328+
existing_material.id: existing_material
329+
for existing_material in instance.materials.all()
330+
}
310331
for material in input.materials:
311-
existing_material = next(
312-
(m for m in existing_materials if m.id == material.id), None
332+
existing_material = (
333+
existing_materials.get(int(material.id)) if material.id else None
313334
)
335+
314336
if existing_material:
315337
existing_material.name = material.name
316338
existing_material.url = material.url
@@ -326,12 +348,12 @@ def update_submission(
326348
)
327349
)
328350

329-
for material in existing_materials:
330-
if material not in materials_to_update:
331-
materials_to_delete.append(material)
332-
333351
ProposalMaterial.objects.filter(
334-
id__in=[m.id for m in materials_to_delete]
352+
id__in=[
353+
m.id
354+
for m in existing_materials.values()
355+
if m not in materials_to_update
356+
]
335357
).delete()
336358
ProposalMaterial.objects.bulk_create(materials_to_create)
337359
ProposalMaterial.objects.bulk_update(

backend/api/submissions/tests/test_edit_submission.py

Lines changed: 247 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,20 @@
1+
from uuid import uuid4
2+
from users.tests.factories import UserFactory
13
from conferences.tests.factories import ConferenceFactory
2-
from submissions.tests.factories import SubmissionFactory, SubmissionTagFactory
3-
from files_upload.tests.factories import FileFactory
4+
from submissions.tests.factories import (
5+
ProposalMaterialFactory,
6+
SubmissionFactory,
7+
SubmissionTagFactory,
8+
)
9+
from files_upload.tests.factories import (
10+
FileFactory,
11+
ParticipantAvatarFileFactory,
12+
ProposalMaterialFileFactory,
13+
)
414
from pytest import mark
515

616
from participants.models import Participant
7-
from submissions.models import Submission
17+
from submissions.models import ProposalMaterial, Submission
818

919
pytestmark = mark.django_db
1020

@@ -13,11 +23,11 @@ def _update_submission(
1323
graphql_client,
1424
*,
1525
submission,
16-
new_topic,
17-
new_audience,
18-
new_type,
19-
new_tag,
20-
new_duration,
26+
new_topic=None,
27+
new_audience=None,
28+
new_type=None,
29+
new_tag=None,
30+
new_duration=None,
2131
new_title=None,
2232
new_elevator_pitch=None,
2333
new_abstract=None,
@@ -34,13 +44,20 @@ def _update_submission(
3444
new_speaker_facebook_url="",
3545
new_speaker_mastodon_handle="",
3646
new_speaker_availabilities=None,
47+
new_materials=None,
3748
):
49+
new_topic = new_topic or submission.topic
50+
new_audience = new_audience or submission.audience_level
51+
new_type = new_type or submission.type
52+
new_tag = new_tag or submission.tags.first()
53+
new_duration = new_duration or submission.duration
3854
new_title = new_title or {"en": "new title to use"}
3955
new_elevator_pitch = new_elevator_pitch or {"en": "This is an elevator pitch"}
4056
new_abstract = new_abstract or {"en": "abstract here"}
4157
short_social_summary = new_short_social_summary or ""
4258
new_speaker_photo = new_speaker_photo or FileFactory().id
4359
new_speaker_availabilities = new_speaker_availabilities or {}
60+
new_materials = new_materials or []
4461

4562
return graphql_client.query(
4663
"""
@@ -114,6 +131,11 @@ def _update_submission(
114131
validationSpeakerInstagramHandle: speakerInstagramHandle
115132
validationSpeakerLinkedinUrl: speakerLinkedinUrl
116133
validationSpeakerFacebookUrl: speakerFacebookUrl
134+
validationMaterials: materials {
135+
fileId
136+
url
137+
id
138+
}
117139
}
118140
}
119141
}
@@ -144,6 +166,7 @@ def _update_submission(
144166
"speakerFacebookUrl": new_speaker_facebook_url,
145167
"speakerMastodonHandle": new_speaker_mastodon_handle,
146168
"speakerAvailabilities": new_speaker_availabilities,
169+
"materials": new_materials,
147170
}
148171
},
149172
)
@@ -204,6 +227,222 @@ def test_update_submission(graphql_client, user):
204227
assert participant.linkedin_url == "http://linkedin.com/company/pythonpizza"
205228

206229

230+
def test_update_submission_with_materials(graphql_client, user):
231+
conference = ConferenceFactory(
232+
topics=("life", "diy"),
233+
languages=("it", "en"),
234+
durations=("10", "20"),
235+
active_cfp=True,
236+
audience_levels=("adult", "senior"),
237+
submission_types=("talk", "workshop"),
238+
)
239+
240+
submission = SubmissionFactory(
241+
speaker_id=user.id,
242+
custom_topic="life",
243+
custom_duration="10m",
244+
custom_audience_level="adult",
245+
custom_submission_type="talk",
246+
languages=["it"],
247+
tags=["python", "ml"],
248+
conference=conference,
249+
speaker_level=Submission.SPEAKER_LEVELS.intermediate,
250+
previous_talk_video="https://www.youtube.com/watch?v=SlPhMPnQ58k",
251+
)
252+
253+
graphql_client.force_login(user)
254+
255+
new_file = FileFactory()
256+
response = _update_submission(
257+
graphql_client,
258+
submission=submission,
259+
new_materials=[
260+
{
261+
"fileId": new_file.id,
262+
"url": "",
263+
"name": "test.pdf",
264+
},
265+
{
266+
"fileId": None,
267+
"url": "https://www.google.com",
268+
"name": "https://www.google.com",
269+
},
270+
],
271+
)
272+
273+
submission.refresh_from_db()
274+
materials = submission.materials.order_by("id").all()
275+
276+
assert len(materials) == 2
277+
assert materials[0].file_id == new_file.id
278+
assert materials[0].url == ""
279+
assert materials[0].name == "test.pdf"
280+
281+
assert materials[1].file_id is None
282+
assert materials[1].url == "https://www.google.com"
283+
assert materials[1].name == "https://www.google.com"
284+
285+
assert response["data"]["updateSubmission"]["__typename"] == "Submission"
286+
287+
288+
def test_update_submission_with_existing_materials(graphql_client, user):
289+
conference = ConferenceFactory(
290+
topics=("life", "diy"),
291+
languages=("it", "en"),
292+
durations=("10", "20"),
293+
active_cfp=True,
294+
audience_levels=("adult", "senior"),
295+
submission_types=("talk", "workshop"),
296+
)
297+
298+
submission = SubmissionFactory(
299+
speaker_id=user.id,
300+
custom_topic="life",
301+
custom_duration="10m",
302+
custom_audience_level="adult",
303+
custom_submission_type="talk",
304+
languages=["it"],
305+
tags=["python", "ml"],
306+
conference=conference,
307+
speaker_level=Submission.SPEAKER_LEVELS.intermediate,
308+
previous_talk_video="https://www.youtube.com/watch?v=SlPhMPnQ58k",
309+
)
310+
existing_material = ProposalMaterialFactory(proposal=submission, file=FileFactory())
311+
to_delete_material = ProposalMaterialFactory(
312+
proposal=submission, file=FileFactory()
313+
)
314+
315+
graphql_client.force_login(user)
316+
317+
new_file = FileFactory()
318+
response = _update_submission(
319+
graphql_client,
320+
submission=submission,
321+
new_materials=[
322+
{
323+
"fileId": new_file.id,
324+
"url": "",
325+
"name": "test.pdf",
326+
},
327+
{
328+
"id": existing_material.id,
329+
"fileId": None,
330+
"url": "https://www.google.com",
331+
"name": "https://www.google.com",
332+
},
333+
],
334+
)
335+
336+
submission.refresh_from_db()
337+
materials = submission.materials.order_by("id").all()
338+
339+
assert len(materials) == 2
340+
341+
existing_material.refresh_from_db()
342+
343+
assert existing_material.file_id is None
344+
assert existing_material.url == "https://www.google.com"
345+
assert existing_material.name == "https://www.google.com"
346+
347+
assert response["data"]["updateSubmission"]["__typename"] == "Submission"
348+
349+
assert not ProposalMaterial.objects.filter(id=to_delete_material.id).exists()
350+
351+
352+
def test_update_submission_with_invalid_materials(graphql_client, user):
353+
conference = ConferenceFactory(
354+
topics=("life", "diy"),
355+
languages=("it", "en"),
356+
durations=("10", "20"),
357+
active_cfp=True,
358+
audience_levels=("adult", "senior"),
359+
submission_types=("talk", "workshop"),
360+
)
361+
362+
submission = SubmissionFactory(
363+
speaker_id=user.id,
364+
custom_topic="life",
365+
custom_duration="10m",
366+
custom_audience_level="adult",
367+
custom_submission_type="talk",
368+
languages=["it"],
369+
tags=["python", "ml"],
370+
conference=conference,
371+
speaker_level=Submission.SPEAKER_LEVELS.intermediate,
372+
previous_talk_video="https://www.youtube.com/watch?v=SlPhMPnQ58k",
373+
)
374+
other_submission_material = ProposalMaterialFactory(
375+
proposal=SubmissionFactory(conference=conference),
376+
file=ProposalMaterialFileFactory(uploaded_by=user),
377+
)
378+
to_delete_material = ProposalMaterialFactory(
379+
proposal=submission, file=ProposalMaterialFileFactory(uploaded_by=user)
380+
)
381+
382+
graphql_client.force_login(user)
383+
384+
response = _update_submission(
385+
graphql_client,
386+
submission=submission,
387+
new_materials=[
388+
{
389+
"fileId": None,
390+
"url": "invalid-url",
391+
"name": "test.pdf",
392+
},
393+
{
394+
"id": other_submission_material.id,
395+
"fileId": None,
396+
"url": "https://www.google.com",
397+
"name": "https://www.google.com",
398+
},
399+
{
400+
"id": "invalid-id",
401+
"fileId": None,
402+
"url": "https://www.google.com",
403+
"name": "https://www.google.com",
404+
},
405+
{
406+
"fileId": uuid4(),
407+
"url": "",
408+
"name": "name",
409+
},
410+
{
411+
"fileId": ProposalMaterialFileFactory(uploaded_by=UserFactory()).id,
412+
"url": "",
413+
"name": "name",
414+
},
415+
{
416+
"fileId": ParticipantAvatarFileFactory(uploaded_by=user).id,
417+
"url": "",
418+
"name": "name",
419+
},
420+
],
421+
)
422+
423+
assert response["data"]["updateSubmission"]["__typename"] == "SendSubmissionErrors"
424+
assert response["data"]["updateSubmission"]["errors"]["validationMaterials"][0][
425+
"url"
426+
] == ["Invalid URL"]
427+
assert response["data"]["updateSubmission"]["errors"]["validationMaterials"][1][
428+
"id"
429+
] == ["Material not found"]
430+
assert response["data"]["updateSubmission"]["errors"]["validationMaterials"][2][
431+
"id"
432+
] == ["Invalid material id"]
433+
assert response["data"]["updateSubmission"]["errors"]["validationMaterials"][3][
434+
"fileId"
435+
] == ["File not found"]
436+
assert response["data"]["updateSubmission"]["errors"]["validationMaterials"][4][
437+
"fileId"
438+
] == ["File not found"]
439+
assert response["data"]["updateSubmission"]["errors"]["validationMaterials"][5][
440+
"fileId"
441+
] == ["File not found"]
442+
443+
assert ProposalMaterial.objects.filter(id=to_delete_material.id).exists()
444+
445+
207446
def test_update_submission_speaker_availabilities(graphql_client, user):
208447
conference = ConferenceFactory(
209448
topics=("life", "diy"),

0 commit comments

Comments
 (0)