Skip to content

Commit 880cfc9

Browse files
committed
Finish files upload
1 parent f10aa5c commit 880cfc9

File tree

12 files changed

+361
-106
lines changed

12 files changed

+361
-106
lines changed

backend/api/files_upload/permissions.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,11 @@ def _check_proposal_material(self, user, input: "UploadFileInput") -> bool:
3737
conference_code = input.data.conference_code
3838

3939
try:
40-
proposal = Submission.objects.for_conference_code(
41-
conference_code
42-
).get_by_hashid(proposal_id)
40+
proposal = (
41+
Submission.objects.for_conference_code(conference_code)
42+
.filter(status=Submission.STATUS.accepted)
43+
.get_by_hashid(proposal_id)
44+
)
4345
except (Submission.DoesNotExist, IndexError):
4446
return False
4547

backend/api/files_upload/tests/mutations/test_upload_file.py

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import pytest
22
from files_upload.models import File
3-
from submissions.tests.factories import SubmissionFactory
3+
from submissions.tests.factories import AcceptedSubmissionFactory, SubmissionFactory
44
from conferences.tests.factories import ConferenceFactory
55
from django.test import override_settings
66

@@ -64,7 +64,7 @@ def test_upload_participant_avatar_to_invalid_conf_fails(graphql_client, user):
6464

6565

6666
def test_upload_proposal_material_file(graphql_client, user):
67-
proposal = SubmissionFactory(speaker=user)
67+
proposal = AcceptedSubmissionFactory(speaker=user)
6868
graphql_client.force_login(user)
6969

7070
response = _upload_file(
@@ -88,7 +88,26 @@ def test_upload_proposal_material_file(graphql_client, user):
8888

8989

9090
def test_cannot_upload_proposal_material_file_if_not_speaker(graphql_client, user):
91-
proposal = SubmissionFactory()
91+
proposal = AcceptedSubmissionFactory()
92+
graphql_client.force_login(user)
93+
94+
response = _upload_file(
95+
graphql_client,
96+
{
97+
"proposalMaterial": {
98+
"filename": "test.txt",
99+
"proposalId": proposal.hashid,
100+
"conferenceCode": proposal.conference.code,
101+
}
102+
},
103+
)
104+
105+
assert not response["data"]
106+
assert response["errors"][0]["message"] == "You cannot upload files of this type"
107+
108+
109+
def test_cannot_upload_proposal_material_file_if_not_accepted(graphql_client, user):
110+
proposal = SubmissionFactory(status="proposed")
92111
graphql_client.force_login(user)
93112

94113
response = _upload_file(
@@ -129,7 +148,7 @@ def test_cannot_upload_proposal_material_file_with_invalid_proposal_id(
129148
def test_cannot_upload_proposal_material_file_with_invalid_proposal_id_for_conference(
130149
graphql_client, user
131150
):
132-
proposal = SubmissionFactory()
151+
proposal = AcceptedSubmissionFactory()
133152
graphql_client.force_login(user)
134153

135154
response = _upload_file(
@@ -151,7 +170,7 @@ def test_cannot_upload_proposal_material_file_with_invalid_proposal_id_for_confe
151170
"file_type", [File.Type.PARTICIPANT_AVATAR, File.Type.PROPOSAL_MATERIAL]
152171
)
153172
def test_superusers_can_upload_anything(graphql_client, admin_superuser, file_type):
154-
proposal = SubmissionFactory()
173+
proposal = AcceptedSubmissionFactory()
155174
graphql_client.force_login(admin_superuser)
156175

157176
req_input = {}

backend/api/submissions/mutations.py

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,10 @@
2020
from i18n.strings import LazyI18nString
2121
from languages.models import Language
2222
from participants.models import Participant
23-
from submissions.models import Submission as SubmissionModel
23+
from submissions.models import ProposalMaterial, Submission as SubmissionModel
2424
from submissions.tasks import notify_new_cfp_submission
2525

26-
from .types import Submission
26+
from .types import Submission, SubmissionMaterialInput
2727

2828
FACEBOOK_LINK_MATCH = re.compile(r"^http(s)?:\/\/(www\.)?facebook\.com\/")
2929
LINKEDIN_LINK_MATCH = re.compile(r"^http(s)?:\/\/(www\.)?linkedin\.com\/")
@@ -247,6 +247,7 @@ class UpdateSubmissionInput(BaseSubmissionInput):
247247

248248
topic: Optional[ID] = strawberry.field(default=None)
249249
tags: list[ID] = strawberry.field(default_factory=list)
250+
materials: list[SubmissionMaterialInput] = strawberry.field(default_factory=list)
250251

251252

252253
SendSubmissionOutput = Annotated[
@@ -301,6 +302,42 @@ def update_submission(
301302

302303
instance.save()
303304

305+
materials_to_create = []
306+
materials_to_update = []
307+
materials_to_delete = []
308+
309+
existing_materials = list(instance.materials.all())
310+
for material in input.materials:
311+
existing_material = next(
312+
(m for m in existing_materials if m.id == material.id), None
313+
)
314+
if existing_material:
315+
existing_material.name = material.name
316+
existing_material.url = material.url
317+
existing_material.file_id = material.file_id
318+
materials_to_update.append(existing_material)
319+
else:
320+
materials_to_create.append(
321+
ProposalMaterial(
322+
proposal=instance,
323+
name=material.name,
324+
url=material.url,
325+
file_id=material.file_id,
326+
)
327+
)
328+
329+
for material in existing_materials:
330+
if material not in materials_to_update:
331+
materials_to_delete.append(material)
332+
333+
ProposalMaterial.objects.filter(
334+
id__in=[m.id for m in materials_to_delete]
335+
).delete()
336+
ProposalMaterial.objects.bulk_create(materials_to_create)
337+
ProposalMaterial.objects.bulk_update(
338+
materials_to_update, fields=["name", "url", "file_id"]
339+
)
340+
304341
Participant.objects.update_or_create(
305342
user_id=request.user.id,
306343
conference=conference,

backend/api/submissions/types.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ class ProposalMaterial:
8181
id: strawberry.ID
8282
name: str
8383
url: str | None
84+
file_id: str | None
8485
file_url: str | None
8586
file_mime_type: str | None
8687

@@ -90,6 +91,7 @@ def from_django(cls, material):
9091
id=material.id,
9192
name=material.name,
9293
url=material.url,
94+
file_id=material.file_id,
9395
file_url=material.file.url if material.file_id else None,
9496
file_mime_type=material.file.mime_type if material.file_id else None,
9597
)
@@ -199,3 +201,11 @@ def materials(self, info) -> list[ProposalMaterial]:
199201
class SubmissionsPagination:
200202
submissions: list[Submission]
201203
total_pages: int
204+
205+
206+
@strawberry.input
207+
class SubmissionMaterialInput:
208+
name: str
209+
id: strawberry.ID | None = None
210+
url: str | None = None
211+
file_id: str | None = None

backend/submissions/tests/factories.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,10 @@ def _after_postgeneration(cls, obj, create, results=None):
127127
obj.save()
128128

129129

130+
class AcceptedSubmissionFactory(SubmissionFactory):
131+
status = "accepted"
132+
133+
130134
class SubmissionCommentFactory(DjangoModelFactory):
131135
class Meta:
132136
model = SubmissionComment

frontend/src/components/cfp-form/index.tsx

Lines changed: 58 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,30 @@ import { AvailabilitySection } from "./availability-section";
3232
import { MaterialsSection } from "./materials-section";
3333
import { ProposalSection } from "./proposal-section";
3434

35+
export type GetErrorsKey =
36+
| "validationTitle"
37+
| "validationAbstract"
38+
| "validationLanguages"
39+
| "validationType"
40+
| "validationDuration"
41+
| "validationElevatorPitch"
42+
| "validationNotes"
43+
| "validationAudienceLevel"
44+
| "validationTags"
45+
| "validationSpeakerLevel"
46+
| "validationPreviousTalkVideo"
47+
| "validationShortSocialSummary"
48+
| "validationSpeakerBio"
49+
| "validationSpeakerWebsite"
50+
| "validationSpeakerPhoto"
51+
| "validationSpeakerTwitterHandle"
52+
| "validationSpeakerInstagramHandle"
53+
| "validationSpeakerLinkedinUrl"
54+
| "validationSpeakerFacebookUrl"
55+
| "validationSpeakerMastodonHandle"
56+
| "validationMaterials"
57+
| "nonFieldErrors";
58+
3559
export type CfpFormFields = ParticipantFormFields & {
3660
type: string;
3761
title: { it?: string; en?: string };
@@ -51,7 +75,9 @@ export type CfpFormFields = ParticipantFormFields & {
5175
};
5276

5377
export type SubmissionStructure = {
78+
id: string;
5479
type: { id: string };
80+
status: string;
5581
title: string;
5682
elevatorPitch: string;
5783
abstract: string;
@@ -66,6 +92,14 @@ export type SubmissionStructure = {
6692
speakerLevel: string;
6793
tags: { id: string }[];
6894
shortSocialSummary: string;
95+
materials: {
96+
id: string;
97+
name: string;
98+
url: string;
99+
fileId: string;
100+
fileUrl: string;
101+
fileMimeType: string;
102+
}[];
69103
};
70104

71105
type Props = {
@@ -173,6 +207,7 @@ export const CfpForm = ({
173207
participantData.me.participant?.photoId,
174208
acceptedPrivacyPolicy: formState.values.acceptedPrivacyPolicy,
175209
speakerAvailabilities: formState.values.speakerAvailabilities,
210+
materials: formState.values.materials,
176211
});
177212
};
178213

@@ -223,6 +258,16 @@ export const CfpForm = ({
223258
);
224259
formState.setField("shortSocialSummary", submission!.shortSocialSummary);
225260
formState.setField("acceptedPrivacyPolicy", true);
261+
formState.setField(
262+
"materials",
263+
submission!.materials.map((material) => ({
264+
type: material.fileId ? "file" : "link",
265+
id: material.id,
266+
fileId: material.fileId,
267+
url: material.url,
268+
name: material.name,
269+
})),
270+
);
226271
}
227272

228273
if (participantData.me.participant) {
@@ -274,30 +319,7 @@ export const CfpForm = ({
274319
submissionData?.mutationOp.__typename === "SendSubmissionErrors";
275320

276321
/* todo refactor to avoid multiple __typename? */
277-
const getErrors = (
278-
key:
279-
| "validationTitle"
280-
| "validationAbstract"
281-
| "validationLanguages"
282-
| "validationType"
283-
| "validationDuration"
284-
| "validationElevatorPitch"
285-
| "validationNotes"
286-
| "validationAudienceLevel"
287-
| "validationTags"
288-
| "validationSpeakerLevel"
289-
| "validationPreviousTalkVideo"
290-
| "validationShortSocialSummary"
291-
| "validationSpeakerBio"
292-
| "validationSpeakerWebsite"
293-
| "validationSpeakerPhoto"
294-
| "validationSpeakerTwitterHandle"
295-
| "validationSpeakerInstagramHandle"
296-
| "validationSpeakerLinkedinUrl"
297-
| "validationSpeakerFacebookUrl"
298-
| "validationSpeakerMastodonHandle"
299-
| "nonFieldErrors",
300-
): string[] =>
322+
const getErrors = (key: GetErrorsKey): string[] =>
301323
(submissionData?.mutationOp.__typename === "SendSubmissionErrors" &&
302324
submissionData!.mutationOp.errors[key]) ||
303325
[];
@@ -331,14 +353,19 @@ export const CfpForm = ({
331353
conferenceData={conferenceData}
332354
/>
333355

334-
<Spacer size="medium" />
356+
{submission?.status === "accepted" && (
357+
<>
358+
<Spacer size="medium" />
335359

336-
<MaterialsSection
337-
formState={formState}
338-
getErrors={getErrors}
339-
formOptions={formOptions}
340-
conferenceData={conferenceData}
341-
/>
360+
<MaterialsSection
361+
formState={formState}
362+
getErrors={getErrors}
363+
formOptions={formOptions}
364+
conferenceData={conferenceData}
365+
submission={submission}
366+
/>
367+
</>
368+
)}
342369

343370
<Spacer size="medium" />
344371

0 commit comments

Comments
 (0)