Skip to content

Commit a7981a0

Browse files
authored
Implement form to request an invitation letter (#4279)
1 parent 6b37a3a commit a7981a0

31 files changed

+1923
-27
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: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,3 +146,13 @@ 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"
154+
155+
156+
@strawberry.type
157+
class FormNotAvailable:
158+
message: str = "Form is not available"
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
from api.visa.types import (
2+
InvitationLetterRequestStatus as InvitationLetterRequestStatusAPI,
3+
)
4+
from visa.models import InvitationLetterRequestStatus as InvitationLetterRequestStatusDB
5+
from users.tests.factories import UserFactory
6+
from visa.models import InvitationLetterRequestOnBehalfOf
7+
from visa.tests.factories import InvitationLetterRequestFactory
8+
from conferences.tests.factories import ConferenceFactory
9+
import pytest
10+
11+
pytestmark = pytest.mark.django_db
12+
13+
14+
def _query_invitation_letter_request(client, conference):
15+
return client.query(
16+
"""query($conference: String!) {
17+
me {
18+
invitationLetterRequest(conference: $conference) {
19+
id
20+
status
21+
}
22+
}
23+
}""",
24+
variables={
25+
"conference": conference.code,
26+
},
27+
)
28+
29+
30+
def test_get_user_invitation_letter_request_with_none_present(graphql_client, user):
31+
graphql_client.force_login(user)
32+
33+
conference = ConferenceFactory()
34+
response = _query_invitation_letter_request(graphql_client, conference)
35+
36+
me = response["data"]["me"]
37+
assert me["invitationLetterRequest"] is None
38+
39+
40+
def test_get_user_invitation_letter_request(graphql_client, user):
41+
graphql_client.force_login(user)
42+
43+
conference = ConferenceFactory()
44+
invitation_letter_request = InvitationLetterRequestFactory(
45+
requester=user,
46+
conference=conference,
47+
)
48+
49+
response = _query_invitation_letter_request(graphql_client, conference)
50+
51+
me = response["data"]["me"]
52+
assert me["invitationLetterRequest"]["id"] == str(invitation_letter_request.id)
53+
assert me["invitationLetterRequest"]["status"] == invitation_letter_request.status
54+
55+
56+
@pytest.mark.parametrize(
57+
"actual_status,exposed_status",
58+
[
59+
(
60+
InvitationLetterRequestStatusDB.PENDING,
61+
InvitationLetterRequestStatusAPI.PENDING,
62+
),
63+
(
64+
InvitationLetterRequestStatusDB.PROCESSING,
65+
InvitationLetterRequestStatusAPI.PENDING,
66+
),
67+
(
68+
InvitationLetterRequestStatusDB.FAILED_TO_GENERATE,
69+
InvitationLetterRequestStatusAPI.PENDING,
70+
),
71+
(
72+
InvitationLetterRequestStatusDB.PROCESSED,
73+
InvitationLetterRequestStatusAPI.PENDING,
74+
),
75+
(InvitationLetterRequestStatusDB.SENT, InvitationLetterRequestStatusAPI.SENT),
76+
(
77+
InvitationLetterRequestStatusDB.REJECTED,
78+
InvitationLetterRequestStatusAPI.REJECTED,
79+
),
80+
],
81+
)
82+
def test_user_invitation_letter_request_has_user_friendly_status(
83+
graphql_client, user, actual_status, exposed_status
84+
):
85+
graphql_client.force_login(user)
86+
87+
conference = ConferenceFactory()
88+
invitation_letter_request = InvitationLetterRequestFactory(
89+
requester=user, conference=conference, status=actual_status
90+
)
91+
92+
response = _query_invitation_letter_request(graphql_client, conference)
93+
94+
me = response["data"]["me"]
95+
assert me["invitationLetterRequest"]["id"] == str(invitation_letter_request.id)
96+
assert me["invitationLetterRequest"]["status"] == exposed_status.name
97+
98+
99+
def test_on_behalf_of_others_invitation_letter_request_are_excluded(
100+
graphql_client, user
101+
):
102+
graphql_client.force_login(user)
103+
104+
conference = ConferenceFactory()
105+
106+
InvitationLetterRequestFactory(
107+
requester=user,
108+
conference=conference,
109+
on_behalf_of=InvitationLetterRequestOnBehalfOf.OTHER,
110+
)
111+
112+
InvitationLetterRequestFactory(
113+
requester=user,
114+
conference=conference,
115+
on_behalf_of=InvitationLetterRequestOnBehalfOf.OTHER,
116+
)
117+
118+
response = _query_invitation_letter_request(graphql_client, conference)
119+
120+
me = response["data"]["me"]
121+
assert me["invitationLetterRequest"] is None
122+
123+
124+
def test_other_users_invitation_letter_requests_are_excluded(graphql_client, user):
125+
graphql_client.force_login(user)
126+
127+
conference = ConferenceFactory()
128+
129+
InvitationLetterRequestFactory(
130+
requester=UserFactory(),
131+
conference=conference,
132+
on_behalf_of=InvitationLetterRequestOnBehalfOf.SELF,
133+
)
134+
135+
response = _query_invitation_letter_request(graphql_client, conference)
136+
137+
me = response["data"]["me"]
138+
assert me["invitationLetterRequest"] is None
139+
140+
141+
def test_other_conferences_invitation_letter_request_are_excluded(graphql_client, user):
142+
graphql_client.force_login(user)
143+
144+
conference = ConferenceFactory()
145+
146+
InvitationLetterRequestFactory(
147+
requester=user,
148+
conference=ConferenceFactory(),
149+
on_behalf_of=InvitationLetterRequestOnBehalfOf.SELF,
150+
)
151+
152+
response = _query_invitation_letter_request(graphql_client, conference)
153+
154+
me = response["data"]["me"]
155+
assert me["invitationLetterRequest"] is None

backend/api/users/tests/test_me.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,3 +95,49 @@ def test_is_python_italia_member_with_expired_membership(graphql_client, user, s
9595

9696
me = response["data"]["me"]
9797
assert me["isPythonItaliaMember"] is False
98+
99+
100+
@pytest.mark.parametrize("has_ticket", [True, False])
101+
def test_has_admission_ticket(mock_has_ticket, has_ticket, graphql_client, user):
102+
graphql_client.force_login(user)
103+
104+
conference = ConferenceFactory()
105+
mock_has_ticket(conference, has_ticket=has_ticket, user=user)
106+
107+
response = graphql_client.query(
108+
"""query($conference: String!) {
109+
me {
110+
hasAdmissionTicket(conference: $conference)
111+
}
112+
}""",
113+
variables={
114+
"conference": conference.code,
115+
},
116+
)
117+
118+
me = response["data"]["me"]
119+
assert me["hasAdmissionTicket"] == has_ticket
120+
121+
122+
@pytest.mark.parametrize("has_ticket", [True, False])
123+
def test_has_admission_ticket_with_non_existent_conference(
124+
mock_has_ticket, has_ticket, graphql_client, user
125+
):
126+
graphql_client.force_login(user)
127+
128+
conference = ConferenceFactory()
129+
mock_has_ticket(conference, has_ticket=has_ticket, user=user)
130+
131+
response = graphql_client.query(
132+
"""query($conference: String!) {
133+
me {
134+
hasAdmissionTicket(conference: $conference)
135+
}
136+
}""",
137+
variables={
138+
"conference": "invalid",
139+
},
140+
)
141+
142+
me = response["data"]["me"]
143+
assert me["hasAdmissionTicket"] is False

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.

0 commit comments

Comments
 (0)