Skip to content

Commit eb676f2

Browse files
authored
Migrate grant type to Strawberry Django (#4745)
1 parent 3b9a2c0 commit eb676f2

4 files changed

Lines changed: 90 additions & 119 deletions

File tree

backend/api/grants/mutations.py

Lines changed: 43 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from dataclasses import asdict
22
from enum import Enum
3-
from typing import Annotated, Optional, Union
3+
from typing import Annotated
44

55
import strawberry
66
from django.db import transaction
@@ -17,7 +17,7 @@
1717
)
1818
from generic_forms.models import Form, FormAnswer
1919
from generic_forms.services import validate_answers, wrap_answers
20-
from grants.models import Grant as GrantModel
20+
from grants import models as grant_models
2121
from grants.tasks import (
2222
create_and_send_voucher_to_grantee,
2323
get_name,
@@ -196,7 +196,7 @@ class SendGrantInput(BaseGrantInput):
196196
def validate(self, conference: Conference, user: User) -> GrantErrors | None:
197197
errors = super().validate(conference=conference, user=user)
198198

199-
if GrantModel.objects.of_user(user).for_conference(conference).exists():
199+
if grant_models.Grant.objects.of_user(user).for_conference(conference).exists():
200200
errors.add_error("non_field_errors", "Grant already submitted!")
201201

202202
return errors.if_has_errors
@@ -239,11 +239,11 @@ def validate(self, conference: Conference, user: User) -> GrantErrors | None:
239239

240240

241241
SendGrantResult = Annotated[
242-
Union[Grant, GrantErrors], strawberry.union(name="SendGrantResult")
242+
Grant | GrantErrors, strawberry.union(name="SendGrantResult")
243243
]
244244

245245
UpdateGrantResult = Annotated[
246-
Union[Grant, GrantErrors], strawberry.union(name="UpdateGrantResult")
246+
Grant | GrantErrors, strawberry.union(name="UpdateGrantResult")
247247
]
248248

249249

@@ -252,14 +252,14 @@ class StatusOption(Enum):
252252
confirmed = "confirmed"
253253
refused = "refused"
254254

255-
def to_grant_status(self) -> GrantModel.Status:
256-
return GrantModel.Status(self.name)
255+
def to_grant_status(self) -> grant_models.Grant.Status:
256+
return grant_models.Grant.Status(self.name)
257257

258258

259259
@strawberry.input
260260
class SendGrantReplyInput:
261261
instance: strawberry.ID
262-
status: Optional[StatusOption]
262+
status: StatusOption | None
263263

264264

265265
@strawberry.type
@@ -268,7 +268,7 @@ class SendGrantReplyError:
268268

269269

270270
SendGrantReplyResult = Annotated[
271-
Union[Grant, SendGrantReplyError], strawberry.union(name="SendGrantReplyResult")
271+
Grant | SendGrantReplyError, strawberry.union(name="SendGrantReplyResult")
272272
]
273273

274274

@@ -300,33 +300,29 @@ def send_grant(self, info: Info, input: SendGrantInput) -> SendGrantResult:
300300
if errors := input.validate(conference=conference, user=request.user):
301301
return errors
302302

303-
instance = GrantModel.objects.create(
304-
**{
305-
"user_id": request.user.id,
306-
"conference": conference,
307-
"name": input.name,
308-
"full_name": input.full_name,
309-
# soft columns are NOT NULL; on the answers path they are
310-
# omitted from the input and stored empty
311-
"age_group": input.age_group or "",
312-
"gender": input.gender or "",
313-
"occupation": input.occupation or "",
314-
"grant_type": input.grant_type,
315-
"python_usage": input.python_usage or "",
316-
"been_to_other_events": input.been_to_other_events or "",
317-
"community_contribution": input.community_contribution or "",
318-
"needs_funds_for_travel": input.needs_funds_for_travel,
319-
"need_visa": input.need_visa,
320-
"need_accommodation": input.need_accommodation,
321-
"why": input.why or "",
322-
"notes": input.notes or "",
323-
"departure_country": input.departure_country,
324-
"nationality": input.nationality,
325-
"departure_city": input.departure_city,
326-
"form_answer": _persist_form_answer(
327-
input.answers, conference, request.user
328-
),
329-
}
303+
instance = grant_models.Grant.objects.create(
304+
user_id=request.user.id,
305+
conference=conference,
306+
name=input.name,
307+
full_name=input.full_name,
308+
# soft columns are NOT NULL; on the answers path they are
309+
# omitted from the input and stored empty
310+
age_group=input.age_group or "",
311+
gender=input.gender or "",
312+
occupation=input.occupation or "",
313+
grant_type=input.grant_type,
314+
python_usage=input.python_usage or "",
315+
been_to_other_events=input.been_to_other_events or "",
316+
community_contribution=input.community_contribution or "",
317+
needs_funds_for_travel=input.needs_funds_for_travel,
318+
need_visa=input.need_visa,
319+
need_accommodation=input.need_accommodation,
320+
why=input.why or "",
321+
notes=input.notes or "",
322+
departure_country=input.departure_country,
323+
nationality=input.nationality,
324+
departure_city=input.departure_city,
325+
form_answer=_persist_form_answer(input.answers, conference, request.user),
330326
)
331327

332328
record_privacy_policy_acceptance(
@@ -362,16 +358,14 @@ def send_grant(self, info: Info, input: SendGrantInput) -> SendGrantResult:
362358

363359
create_addition_admin_log_entry(request.user, instance, "Grant created.")
364360

365-
# hack because we return django models
366-
instance.__strawberry_definition__ = Grant.__strawberry_definition__
367361
return instance
368362

369363
@strawberry.mutation(permission_classes=[IsAuthenticated])
370364
@transaction.atomic
371365
def update_grant(self, info: Info, input: UpdateGrantInput) -> UpdateGrantResult:
372366
request = info.context.request
373367

374-
instance = GrantModel.objects.get(id=input.instance)
368+
instance = grant_models.Grant.objects.get(id=input.instance)
375369
if not instance.can_edit(request.user):
376370
return GrantErrors.with_error(
377371
"non_field_errors", "You cannot edit this grant"
@@ -415,7 +409,6 @@ def update_grant(self, info: Info, input: UpdateGrantInput) -> UpdateGrantResult
415409
},
416410
)
417411

418-
instance.__strawberry_definition__ = Grant.__strawberry_definition__
419412
return instance
420413

421414
@strawberry.mutation(permission_classes=[IsAuthenticated])
@@ -424,19 +417,25 @@ def send_grant_reply(
424417
) -> SendGrantReplyResult:
425418
request = info.context.request
426419

427-
grant = GrantModel.objects.get(id=input.instance)
420+
grant = grant_models.Grant.objects.get(id=input.instance)
428421
if not grant.can_edit(request.user):
429422
return SendGrantReplyError(message="You cannot reply to this grant")
430423

431424
# Can't modify the status if the grant is still pending or was already rejected
432-
if grant.status in (GrantModel.Status.pending, GrantModel.Status.rejected):
425+
if grant.status in (
426+
grant_models.Grant.Status.pending,
427+
grant_models.Grant.Status.rejected,
428+
):
433429
return SendGrantReplyError(message="You cannot reply to this grant")
434430

435431
old_status = grant.status
436432
grant.status = input.status.to_grant_status()
437433
grant.save()
438434

439-
if old_status != grant.status and grant.status == GrantModel.Status.confirmed:
435+
if (
436+
old_status != grant.status
437+
and grant.status == grant_models.Grant.Status.confirmed
438+
):
440439
transaction.on_commit(
441440
lambda gid=grant.id: create_and_send_voucher_to_grantee.delay(
442441
grant_id=gid
@@ -450,4 +449,4 @@ def send_grant_reply(
450449
admin_url = request.build_absolute_uri(grant.get_admin_url())
451450
notify_new_grant_reply_slack.delay(grant_id=grant.id, admin_url=admin_url)
452451

453-
return Grant.from_model(grant)
452+
return grant

backend/api/grants/tests/test_frontend_queries.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ def test_frontend_my_grant_query(graphql_client, django_assert_num_queries, user
6060
)
6161
grant.save()
6262

63-
with django_assert_num_queries(4):
63+
with django_assert_num_queries(3):
6464
response = graphql_client.query(
6565
MY_GRANT_QUERY,
6666
variables={"conference": grant.conference.code},

backend/api/grants/types.py

Lines changed: 40 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,76 +1,54 @@
1-
from __future__ import annotations
2-
3-
from datetime import datetime
4-
from typing import Optional
5-
61
import strawberry
2+
import strawberry_django
73
from strawberry.scalars import JSON
84

95
from generic_forms.services import unwrap_answers
10-
from grants.models import Grant as GrantModel
6+
from grants import models
117

12-
Status = strawberry.enum(GrantModel.Status)
13-
AgeGroup = strawberry.enum(GrantModel.AgeGroup)
14-
Occupation = strawberry.enum(GrantModel.Occupation)
15-
GrantType = strawberry.enum(GrantModel.GrantType)
8+
Status = strawberry.enum(models.Grant.Status)
9+
AgeGroup = strawberry.enum(models.Grant.AgeGroup)
10+
Occupation = strawberry.enum(models.Grant.Occupation)
11+
GrantType = strawberry.enum(models.Grant.GrantType)
1612

1713

18-
@strawberry.type
14+
@strawberry_django.type(
15+
models.Grant,
16+
only=["status", "pending_status", "country_type"],
17+
)
1918
class Grant:
20-
id: strawberry.ID
19+
id: strawberry.auto
2120
status: Status
22-
name: str
23-
full_name: str
24-
age_group: Optional[AgeGroup]
25-
gender: str
21+
name: strawberry.auto
22+
full_name: strawberry.auto
23+
24+
age_group: AgeGroup | None
25+
26+
@strawberry_django.field(only=["age_group"])
27+
def age_group(self) -> AgeGroup | None:
28+
return AgeGroup(self.age_group) if self.age_group else None
29+
30+
gender: strawberry.auto
2631
occupation: Occupation
2732
grant_type: list[GrantType]
28-
python_usage: str
29-
community_contribution: str
30-
been_to_other_events: str
31-
needs_funds_for_travel: bool
32-
need_visa: bool
33-
need_accommodation: bool
34-
why: str
35-
notes: str
36-
departure_country: Optional[str]
37-
nationality: Optional[str]
38-
departure_city: Optional[str]
39-
applicant_reply_deadline: Optional[datetime]
40-
41-
@strawberry.field
33+
python_usage: strawberry.auto
34+
community_contribution: strawberry.auto
35+
been_to_other_events: strawberry.auto
36+
needs_funds_for_travel: strawberry.auto
37+
need_visa: strawberry.auto
38+
need_accommodation: strawberry.auto
39+
why: strawberry.auto
40+
notes: strawberry.auto
41+
departure_country: strawberry.auto
42+
nationality: strawberry.auto
43+
departure_city: strawberry.auto
44+
applicant_reply_deadline: strawberry.auto
45+
46+
@strawberry_django.field(
47+
only=["form_answer_id", "form_answer__answers"],
48+
select_related=["form_answer"],
49+
)
4250
def form_answers(self) -> JSON | None:
43-
# root is either the Django model (mutations return it directly)
44-
# or a from_model()-built instance, which attaches form_answer below
45-
form_answer = getattr(self, "form_answer", None)
46-
if form_answer is None:
51+
if self.form_answer_id is None:
4752
return None
48-
return unwrap_answers(form_answer.answers)
4953

50-
@classmethod
51-
def from_model(cls, grant: GrantModel) -> Grant:
52-
instance = cls(
53-
id=grant.id,
54-
status=Status(grant.status),
55-
name=grant.name,
56-
full_name=grant.full_name,
57-
age_group=AgeGroup(grant.age_group) if grant.age_group else None,
58-
gender=grant.gender,
59-
occupation=Occupation(grant.occupation),
60-
grant_type=[GrantType(g) for g in grant.grant_type],
61-
python_usage=grant.python_usage,
62-
community_contribution=grant.community_contribution,
63-
been_to_other_events=grant.been_to_other_events,
64-
needs_funds_for_travel=grant.needs_funds_for_travel,
65-
need_visa=grant.need_visa,
66-
need_accommodation=grant.need_accommodation,
67-
why=grant.why,
68-
notes=grant.notes,
69-
departure_country=grant.departure_country,
70-
nationality=grant.nationality,
71-
departure_city=grant.departure_city,
72-
applicant_reply_deadline=grant.applicant_reply_deadline,
73-
)
74-
# not a declared strawberry field; read by the form_answers resolver
75-
instance.form_answer = grant.form_answer
76-
return instance
54+
return unwrap_answers(self.form_answer.answers)

backend/api/users/types.py

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
from datetime import date
2-
from logging import getLogger
32

43
import strawberry
54
import strawberry_django
@@ -22,7 +21,7 @@
2221
from badges.roles import ConferenceRole, get_conference_roles_for_user
2322
from billing.models import BillingAddress as BillingAddressModel
2423
from conferences.models import Conference
25-
from grants.models import Grant as GrantModel
24+
from grants import models as grant_models
2625
from participants import models as participant_models
2726
from pretix import user_has_admission_ticket
2827
from pycon.signing import sign_path
@@ -37,8 +36,6 @@
3736
InvitationLetterRequestOnBehalfOf,
3837
)
3938

40-
logger = getLogger(__name__)
41-
4239
PRETIX_ORDERS_STATUS_ORDER = [
4340
PretixOrderStatus.PAID,
4441
PretixOrderStatus.PENDING,
@@ -134,15 +131,12 @@ def booked_schedule_items(self, info: Info, conference: str) -> list[ScheduleIte
134131
.order_by("slot__day__day", "slot__hour")
135132
)
136133

137-
@strawberry.field
138-
def grant(self, info: Info, conference: str) -> Grant | None:
139-
grant = GrantModel.objects.filter(
140-
user_id=self.id, conference__code=conference
141-
).first()
142-
logger.info(
143-
"Grant: user_id: %s, conference: %s, grant: %s", self.id, conference, grant
134+
@strawberry_django.field
135+
def grant(self, conference: str) -> Grant | None:
136+
return grant_models.Grant.objects.filter(
137+
user_id=self.id,
138+
conference__code=conference,
144139
)
145-
return Grant.from_model(grant) if grant else None
146140

147141
@strawberry_django.field
148142
def participant(self, info: Info, conference: str) -> Participant | None:

0 commit comments

Comments
 (0)