Skip to content

Commit ba1882e

Browse files
committed
Implement form to request an invitation letter
1 parent 6b37a3a commit ba1882e

File tree

21 files changed

+915
-21
lines changed

21 files changed

+915
-21
lines changed

backend/api/schema.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@
2626
from .association_membership.mutation import AssociationMembershipMutation
2727
from .cms.schema import CMSQuery
2828
from .sponsors.schema import SponsorsMutation
29-
from .visa.queries import VisaQuery
30-
from .visa.mutations import VisaMutation
29+
from .visa.query import VisaQuery
30+
from .visa.mutation import VisaMutation
3131

3232

3333
@strawberry.type

backend/api/types.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,3 +146,8 @@ def paginate_list(
146146
@strawberry.type
147147
class NotFound:
148148
message: str = "Not found"
149+
150+
151+
@strawberry.type
152+
class NoAdmissionTicket:
153+
message: str = "User does not have admission ticket"

backend/api/users/types.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@
55

66
from django.urls import reverse
77
from api.billing.types import BillingAddress
8+
from api.visa.types import InvitationLetterRequest
9+
from visa.models import (
10+
InvitationLetterRequest as InvitationLetterRequestModel,
11+
InvitationLetterRequestOnBehalfOf,
12+
)
13+
from pretix import user_has_admission_ticket
814
from pycon.signing import sign_path
915
import strawberry
1016
from strawberry.types import Info
@@ -139,6 +145,19 @@ def tickets(
139145
attendee_tickets = get_user_tickets(conference, self.email, language)
140146
return [ticket for ticket in attendee_tickets]
141147

148+
@strawberry.field
149+
def has_admission_ticket(self, conference: str) -> bool:
150+
conference = Conference.objects.filter(code=conference).first()
151+
152+
if not conference:
153+
return False
154+
155+
return user_has_admission_ticket(
156+
email=self.email,
157+
event_organizer=conference.pretix_organizer_id,
158+
event_slug=conference.pretix_event_id,
159+
)
160+
142161
@strawberry.field
143162
def submissions(self, info: Info, conference: str) -> list[Submission]:
144163
return SubmissionModel.objects.filter(
@@ -162,6 +181,22 @@ def billing_addresses(self, conference: str) -> list[BillingAddress]:
162181
.all()
163182
]
164183

184+
@strawberry.field
185+
def invitation_letter_request(
186+
self, conference: str
187+
) -> InvitationLetterRequest | None:
188+
invitation_letter_request = (
189+
InvitationLetterRequestModel.objects.for_conference_code(conference)
190+
.of_user(self.id)
191+
.filter(on_behalf_of=InvitationLetterRequestOnBehalfOf.SELF)
192+
.first()
193+
)
194+
return (
195+
InvitationLetterRequest.from_model(invitation_letter_request)
196+
if invitation_letter_request
197+
else None
198+
)
199+
165200
@classmethod
166201
def from_django_model(cls, user):
167202
return cls(

backend/api/visa/mutation.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from api.visa.mutations.update_invitation_letter_document import (
2+
update_invitation_letter_document,
3+
)
4+
from api.visa.mutations.request_invitation_letter import request_invitation_letter
5+
from strawberry.tools import create_type
6+
7+
VisaMutation = create_type(
8+
"VisaMutation",
9+
(
10+
update_invitation_letter_document,
11+
request_invitation_letter,
12+
),
13+
)

backend/api/visa/mutations/__init__.py

Whitespace-only changes.
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
from django.db import transaction
2+
from datetime import date
3+
from typing import Annotated
4+
from api.types import BaseErrorType, NoAdmissionTicket
5+
from api.utils import validate_email
6+
from api.visa.types import InvitationLetterOnBehalfOf, InvitationLetterRequest
7+
from api.extensions import RateLimit
8+
from privacy_policy.record import record_privacy_policy_acceptance
9+
from visa.models import (
10+
InvitationLetterRequest as InvitationLetterRequestModel,
11+
InvitationLetterRequestOnBehalfOf,
12+
)
13+
from api.permissions import IsAuthenticated
14+
from api.context import Context
15+
from api.conferences.types import Conference
16+
from conferences.models import Conference as ConferenceModel
17+
from pretix import user_has_admission_ticket
18+
import strawberry
19+
20+
21+
@strawberry.type
22+
class InvitationLetterAlreadyRequested:
23+
message: str = "Invitation letter has already been requested for this conference"
24+
25+
26+
@strawberry.type
27+
class RequestInvitationLetterErrors(BaseErrorType):
28+
@strawberry.type
29+
class _RequestInvitationLetterErrors:
30+
conference: list[str] = strawberry.field(default_factory=list)
31+
on_behalf_of: list[str] = strawberry.field(default_factory=list)
32+
email: list[str] = strawberry.field(default_factory=list)
33+
full_name: list[str] = strawberry.field(default_factory=list)
34+
date_of_birth: list[str] = strawberry.field(default_factory=list)
35+
nationality: list[str] = strawberry.field(default_factory=list)
36+
address: list[str] = strawberry.field(default_factory=list)
37+
passport_number: list[str] = strawberry.field(default_factory=list)
38+
embassy_name: list[str] = strawberry.field(default_factory=list)
39+
40+
errors: _RequestInvitationLetterErrors = None
41+
42+
43+
@strawberry.input
44+
class RequestInvitationLetterInput:
45+
conference: str
46+
on_behalf_of: InvitationLetterOnBehalfOf
47+
email: str
48+
full_name: str
49+
nationality: str
50+
address: str
51+
passport_number: str
52+
embassy_name: str
53+
date_of_birth: date
54+
55+
def validate(self, conference: Conference) -> RequestInvitationLetterErrors | None:
56+
errors = RequestInvitationLetterErrors()
57+
58+
required_fields = [
59+
"conference",
60+
"on_behalf_of",
61+
"full_name",
62+
"nationality",
63+
"address",
64+
"passport_number",
65+
"embassy_name",
66+
"date_of_birth",
67+
]
68+
69+
if self.on_behalf_of == InvitationLetterOnBehalfOf.OTHER:
70+
required_fields.append("email")
71+
72+
for field_name in required_fields:
73+
if not getattr(self, field_name):
74+
errors.add_error(field_name, "This field is required")
75+
76+
if self.email and not validate_email(self.email):
77+
errors.add_error("email", "Invalid email address")
78+
79+
if not conference:
80+
errors.add_error("conference", "Conference not found")
81+
82+
return errors.if_has_errors
83+
84+
85+
RequestInvitationLetterResult = Annotated[
86+
InvitationLetterRequest
87+
| RequestInvitationLetterErrors
88+
| NoAdmissionTicket
89+
| InvitationLetterAlreadyRequested,
90+
strawberry.union(name="RequestInvitationLetterResult"),
91+
]
92+
93+
94+
@strawberry.mutation(
95+
permission_classes=[IsAuthenticated],
96+
extensions=[RateLimit("5/m")],
97+
)
98+
def request_invitation_letter(
99+
info: strawberry.Info[Context], input: RequestInvitationLetterInput
100+
) -> RequestInvitationLetterResult:
101+
conference = ConferenceModel.objects.filter(code=input.conference).first()
102+
103+
if errors := input.validate(conference):
104+
return errors
105+
106+
user = info.context.request.user
107+
108+
if input.on_behalf_of == InvitationLetterOnBehalfOf.SELF:
109+
input.email = ""
110+
111+
if not user_has_admission_ticket(
112+
email=info.context.request.user.email,
113+
event_organizer=conference.pretix_organizer_id,
114+
event_slug=conference.pretix_event_id,
115+
):
116+
return NoAdmissionTicket()
117+
118+
if (
119+
InvitationLetterRequestModel.objects.for_conference(conference)
120+
.of_user(user)
121+
.filter(
122+
on_behalf_of=InvitationLetterRequestOnBehalfOf.SELF,
123+
)
124+
.exists()
125+
):
126+
return InvitationLetterAlreadyRequested()
127+
128+
with transaction.atomic():
129+
invitation_letter, _ = InvitationLetterRequestModel.objects.get_or_create(
130+
conference=conference,
131+
requester=user,
132+
on_behalf_of=InvitationLetterRequestOnBehalfOf(input.on_behalf_of.name),
133+
full_name=input.full_name,
134+
email_address=input.email,
135+
nationality=input.nationality,
136+
address=input.address,
137+
date_of_birth=input.date_of_birth,
138+
passport_number=input.passport_number,
139+
embassy_name=input.embassy_name,
140+
)
141+
142+
record_privacy_policy_acceptance(
143+
info.context.request,
144+
conference,
145+
"invitation_letter",
146+
)
147+
148+
return InvitationLetterRequest.from_model(invitation_letter)

backend/api/visa/mutations.py renamed to backend/api/visa/mutations/update_invitation_letter_document.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
from custom_admin.audit import create_change_admin_log_entry
55
from visa.models import InvitationLetterDocument as InvitationLetterDocumentModel
66
from api.visa.permissions import CanEditInvitationLetterDocument
7-
from strawberry.tools import create_type
87
from api.visa.types import InvitationLetterDocument
98
import strawberry
109

@@ -65,9 +64,3 @@ def update_invitation_letter_document(
6564
change_message="Invitation letter document updated",
6665
)
6766
return InvitationLetterDocument.from_model(invitation_letter_document)
68-
69-
70-
VisaMutation = create_type(
71-
"VisaMutation",
72-
(update_invitation_letter_document,),
73-
)

backend/api/visa/queries/__init__.py

Whitespace-only changes.

backend/api/visa/queries.py renamed to backend/api/visa/queries/invitation_letter_document.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
from visa.models import InvitationLetterDocument as InvitationLetterDocumentModel
33
from api.visa.types import InvitationLetterDocument
44
import strawberry
5-
from strawberry.tools import create_type
65

76

87
@strawberry.field(permission_classes=[CanViewInvitationLetterDocument])
@@ -13,9 +12,3 @@ def invitation_letter_document(id: strawberry.ID) -> InvitationLetterDocument |
1312
return InvitationLetterDocument.from_model(invitation_letter_document)
1413

1514
return None
16-
17-
18-
VisaQuery = create_type(
19-
"VisaQuery",
20-
(invitation_letter_document,),
21-
)

backend/api/visa/query.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from api.visa.queries.invitation_letter_document import invitation_letter_document
2+
from strawberry.tools import create_type
3+
4+
5+
VisaQuery = create_type(
6+
"VisaQuery",
7+
(invitation_letter_document,),
8+
)

0 commit comments

Comments
 (0)