Skip to content

Commit e40e524

Browse files
authored
Notify on slack when we receive a new invitation letter request (#4315)
1 parent ba7482b commit e40e524

File tree

7 files changed

+153
-19
lines changed

7 files changed

+153
-19
lines changed

backend/api/visa/mutations/request_invitation_letter.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from api.utils import validate_email
66
from api.visa.types import InvitationLetterOnBehalfOf, InvitationLetterRequest
77
from api.extensions import RateLimit
8+
from visa.tasks import notify_new_invitation_letter_request_on_slack
89
from conferences.models.deadline import Deadline
910
from privacy_policy.record import record_privacy_policy_acceptance
1011
from visa.models import (
@@ -168,5 +169,11 @@ def request_invitation_letter(
168169
conference,
169170
"invitation_letter",
170171
)
172+
transaction.on_commit(
173+
lambda: notify_new_invitation_letter_request_on_slack.delay(
174+
invitation_letter_request_id=invitation_letter.id,
175+
admin_absolute_uri=info.context.request.build_absolute_uri("/"),
176+
)
177+
)
171178

172179
return InvitationLetterRequest.from_model(invitation_letter)

backend/api/visa/tests/mutations/test_request_invitation_letter.py

Lines changed: 46 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,12 @@ def _request_invitation_letter(client, **input):
5050
)
5151

5252

53-
def test_request_invitation_letter(graphql_client, user, mock_has_ticket):
53+
def test_request_invitation_letter(
54+
graphql_client, user, mock_has_ticket, mocker, django_capture_on_commit_callbacks
55+
):
56+
mock_notify = mocker.patch(
57+
"api.visa.mutations.request_invitation_letter.notify_new_invitation_letter_request_on_slack"
58+
)
5459
conference = ConferenceFactory()
5560
ActiveDeadlineFactory(
5661
conference=conference, type=Deadline.TYPES.invitation_letter_request
@@ -59,20 +64,21 @@ def test_request_invitation_letter(graphql_client, user, mock_has_ticket):
5964

6065
graphql_client.force_login(user)
6166

62-
response = _request_invitation_letter(
63-
graphql_client,
64-
input={
65-
"conference": conference.code,
66-
"onBehalfOf": "SELF",
67-
"fullName": "Mario Rossi",
68-
"email": "",
69-
"nationality": "Italian",
70-
"address": "via Roma",
71-
"passportNumber": "YA1234567",
72-
"embassyName": "Italian Embassy in France",
73-
"dateOfBirth": "1999-01-01",
74-
},
75-
)
67+
with django_capture_on_commit_callbacks(execute=True):
68+
response = _request_invitation_letter(
69+
graphql_client,
70+
input={
71+
"conference": conference.code,
72+
"onBehalfOf": "SELF",
73+
"fullName": "Mario Rossi",
74+
"email": "",
75+
"nationality": "Italian",
76+
"address": "via Roma",
77+
"passportNumber": "YA1234567",
78+
"embassyName": "Italian Embassy in France",
79+
"dateOfBirth": "1999-01-01",
80+
},
81+
)
7682

7783
assert (
7884
response["data"]["requestInvitationLetter"]["__typename"]
@@ -103,10 +109,19 @@ def test_request_invitation_letter(graphql_client, user, mock_has_ticket):
103109
user=user, conference=conference, privacy_policy="invitation_letter"
104110
).exists()
105111

112+
mock_notify.delay.assert_called_once_with(
113+
invitation_letter_request_id=invitation_letter_request.id,
114+
admin_absolute_uri=mocker.ANY,
115+
)
116+
106117

107118
def test_can_request_invitation_letter_to_multiple_conferences(
108-
graphql_client, user, mock_has_ticket
119+
graphql_client, user, mock_has_ticket, mocker
109120
):
121+
mocker.patch(
122+
"api.visa.mutations.request_invitation_letter.notify_new_invitation_letter_request_on_slack"
123+
)
124+
110125
graphql_client.force_login(user)
111126

112127
conference = ConferenceFactory()
@@ -176,8 +191,12 @@ def test_can_request_invitation_letter_to_multiple_conferences(
176191

177192

178193
def test_request_invitation_letter_email_is_ignored_for_self_requests(
179-
graphql_client, user, mock_has_ticket
194+
graphql_client, user, mock_has_ticket, mocker
180195
):
196+
mocker.patch(
197+
"api.visa.mutations.request_invitation_letter.notify_new_invitation_letter_request_on_slack"
198+
)
199+
181200
conference = ConferenceFactory()
182201
mock_has_ticket(conference)
183202
ActiveDeadlineFactory(
@@ -224,8 +243,12 @@ def test_request_invitation_letter_email_is_ignored_for_self_requests(
224243

225244
@pytest.mark.parametrize("has_ticket", [True, False])
226245
def test_request_invitation_letter_on_behalf_of_other(
227-
graphql_client, user, mock_has_ticket, has_ticket
246+
graphql_client, user, mock_has_ticket, has_ticket, mocker
228247
):
248+
mocker.patch(
249+
"api.visa.mutations.request_invitation_letter.notify_new_invitation_letter_request_on_slack"
250+
)
251+
229252
conference = ConferenceFactory()
230253
mock_has_ticket(conference, has_ticket=has_ticket, user=user)
231254
ActiveDeadlineFactory(
@@ -278,8 +301,12 @@ def test_request_invitation_letter_on_behalf_of_other(
278301

279302

280303
def test_duplicate_requests_for_others_are_ignored(
281-
graphql_client, user, mock_has_ticket
304+
graphql_client, user, mock_has_ticket, mocker
282305
):
306+
mocker.patch(
307+
"api.visa.mutations.request_invitation_letter.notify_new_invitation_letter_request_on_slack"
308+
)
309+
283310
conference = ConferenceFactory()
284311
mock_has_ticket(conference, has_ticket=True, user=user)
285312
ActiveDeadlineFactory(

backend/conferences/admin/conference.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ class ConferenceAdmin(
158158
"slack_new_grant_reply_channel_id",
159159
"slack_speaker_invitation_answer_channel_id",
160160
"slack_new_sponsor_lead_channel_id",
161+
"slack_new_invitation_letter_request_channel_id",
161162
)
162163
},
163164
),
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 5.1.4 on 2025-01-18 20:49
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('conferences', '0052_alter_deadline_type'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='conference',
15+
name='slack_new_invitation_letter_request_channel_id',
16+
field=models.CharField(blank=True, default='', max_length=255, verbose_name='New invitation letter request Slack channel ID for notification'),
17+
),
18+
]

backend/conferences/models/conference.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,12 @@ class Conference(GeoLocalizedModel, TimeFramedModel, TimeStampedModel):
8686
blank=True,
8787
default="",
8888
)
89+
slack_new_invitation_letter_request_channel_id = models.CharField(
90+
_("New invitation letter request Slack channel ID for notification"),
91+
max_length=255,
92+
blank=True,
93+
default="",
94+
)
8995

9096
grants_default_ticket_amount = models.DecimalField(
9197
verbose_name=_("grants default ticket amount"),

backend/visa/tasks.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from django.urls import reverse
2+
from integrations import slack
13
import time
24
from django.template import Template, Context
35

@@ -179,3 +181,53 @@ def download_pretix_ticket(invitation_letter_request):
179181
break
180182

181183
return io.BytesIO(response.content)
184+
185+
186+
@app.task
187+
def notify_new_invitation_letter_request_on_slack(
188+
*, invitation_letter_request_id: int, admin_absolute_uri: str
189+
):
190+
invitation_letter_request = InvitationLetterRequest.objects.get(
191+
id=invitation_letter_request_id
192+
)
193+
conference = invitation_letter_request.conference
194+
name = invitation_letter_request.full_name
195+
196+
admin_path = reverse(
197+
"admin:visa_invitationletterrequest_change", args=[invitation_letter_request.id]
198+
)
199+
200+
slack.send_message(
201+
[
202+
{
203+
"type": "section",
204+
"text": {
205+
"text": f"New invitation letter request from {name}",
206+
"type": "plain_text",
207+
},
208+
}
209+
],
210+
[
211+
{
212+
"blocks": [
213+
{
214+
"type": "actions",
215+
"elements": [
216+
{
217+
"type": "button",
218+
"text": {
219+
"type": "plain_text",
220+
"text": "Open in Admin",
221+
"emoji": True,
222+
},
223+
"action_id": "ignore-action",
224+
"url": f"{admin_absolute_uri}{admin_path[1:]}",
225+
}
226+
],
227+
}
228+
]
229+
}
230+
],
231+
oauth_token=conference.get_slack_oauth_token(),
232+
channel_id=conference.slack_new_invitation_letter_request_channel_id,
233+
)

backend/visa/tests/test_tasks.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import pytest
1010
from django.test import override_settings
1111
from visa.tasks import (
12+
notify_new_invitation_letter_request_on_slack,
1213
process_invitation_letter_request,
1314
process_invitation_letter_request_failed,
1415
)
@@ -352,3 +353,25 @@ def test_process_invitation_letter_request_failed():
352353
request.refresh_from_db()
353354

354355
assert request.status == InvitationLetterRequestStatus.FAILED_TO_GENERATE
356+
357+
358+
def test_notify_new_invitation_letter_request_on_slack(mocker):
359+
mock_slack = mocker.patch("visa.tasks.slack.send_message")
360+
invitation_letter_request = InvitationLetterRequestFactory(
361+
conference__slack_new_invitation_letter_request_channel_id="S123",
362+
conference__organizer__slack_oauth_bot_token="token123",
363+
)
364+
admin_absolute_uri = (
365+
"http://example.com/admin/visa/invitationletterrequest/1/change/"
366+
)
367+
368+
notify_new_invitation_letter_request_on_slack(
369+
invitation_letter_request_id=invitation_letter_request.id,
370+
admin_absolute_uri=admin_absolute_uri,
371+
)
372+
373+
mock_slack.assert_called_once()
374+
375+
kwargs = mock_slack.mock_calls[0][2]
376+
assert kwargs["oauth_token"] == "token123"
377+
assert kwargs["channel_id"] == "S123"

0 commit comments

Comments
 (0)