Skip to content

Commit 7211cfa

Browse files
authored
Send voucher to the speaker when accepting their invitation (#4248)
1 parent 8a6a592 commit 7211cfa

File tree

8 files changed

+263
-20
lines changed

8 files changed

+263
-20
lines changed

backend/api/schedule/mutations/update_schedule_invitation.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from api.submissions.permissions import IsSubmissionSpeakerOrStaff
1616

1717
from schedule.tasks import (
18+
create_and_send_voucher_to_speaker,
1819
notify_new_schedule_invitation_answer_slack,
1920
send_schedule_invitation_plain_message,
2021
)
@@ -59,11 +60,9 @@ def update_schedule_invitation(
5960

6061
new_status = input.option.to_schedule_item_status()
6162
new_notes = input.notes
63+
status_changed = schedule_item.status != new_status
6264

63-
if (
64-
schedule_item.status == new_status
65-
and schedule_item.speaker_invitation_notes == new_notes
66-
):
65+
if not status_changed and schedule_item.speaker_invitation_notes == new_notes:
6766
# If nothing changed, do nothing
6867
return ScheduleInvitation.from_django_model(schedule_item)
6968

@@ -72,6 +71,9 @@ def update_schedule_invitation(
7271
schedule_item.speaker_invitation_notes = new_notes
7372
schedule_item.save()
7473

74+
if status_changed and new_status == ScheduleItem.STATUS.confirmed:
75+
create_and_send_voucher_to_speaker.delay(schedule_item.id)
76+
7577
request = info.context.request
7678
invitation_admin_url = request.build_absolute_uri(
7779
schedule_item.get_invitation_admin_url()

backend/api/schedule/tests/test_update_schedule_invitation.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ def test_update_invitation_answer(
2929
mock_plain = mocker.patch(
3030
"api.schedule.mutations.update_schedule_invitation.send_schedule_invitation_plain_message"
3131
)
32+
mock_send_voucher = mocker.patch(
33+
"api.schedule.mutations.update_schedule_invitation.create_and_send_voucher_to_speaker"
34+
)
3235

3336
graphql_client.force_login(user)
3437
submission = SubmissionFactory(
@@ -80,6 +83,9 @@ def test_update_invitation_answer(
8083
message="notes",
8184
)
8285

86+
if expected_status == ScheduleItem.STATUS.confirmed:
87+
mock_send_voucher.delay.assert_called_once_with(schedule_item.id)
88+
8389

8490
def test_saving_the_same_answer_does_not_trigger_event(
8591
graphql_client,
@@ -92,6 +98,9 @@ def test_saving_the_same_answer_does_not_trigger_event(
9298
mock_plain = mocker.patch(
9399
"api.schedule.mutations.update_schedule_invitation.send_schedule_invitation_plain_message"
94100
)
101+
mock_send_voucher = mocker.patch(
102+
"api.schedule.mutations.update_schedule_invitation.create_and_send_voucher_to_speaker"
103+
)
95104

96105
graphql_client.force_login(user)
97106
submission = SubmissionFactory(
@@ -141,6 +150,7 @@ def test_saving_the_same_answer_does_not_trigger_event(
141150

142151
mock_event.delay.assert_not_called()
143152
mock_plain.delay.assert_not_called()
153+
mock_send_voucher.delay.assert_not_called()
144154

145155

146156
def test_changing_notes_triggers_a_new_event(
@@ -350,6 +360,9 @@ def test_staff_can_update_invitation_answer(
350360
mock_plain = mocker.patch(
351361
"api.schedule.mutations.update_schedule_invitation.send_schedule_invitation_plain_message"
352362
)
363+
mock_send_voucher = mocker.patch(
364+
"api.schedule.mutations.update_schedule_invitation.create_and_send_voucher_to_speaker"
365+
)
353366

354367
graphql_client.force_login(admin_user)
355368
submission = SubmissionFactory()
@@ -398,3 +411,4 @@ def test_staff_can_update_invitation_answer(
398411
schedule_item_id=schedule_item.id,
399412
message="notes",
400413
)
414+
mock_send_voucher.delay.assert_called_once_with(schedule_item.id)

backend/conferences/querysets.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,5 @@ def for_conference_code(self, conference):
1414

1515

1616
class ConferenceVoucherQuerySet(ConferenceQuerySetMixin, models.QuerySet):
17-
pass
17+
def for_user(self, user):
18+
return self.filter(user=user)

backend/conferences/vouchers.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from conferences.models.conference_voucher import ConferenceVoucher
2+
from conferences.models.conference import Conference
3+
from users.models import User
4+
from pretix import create_voucher
5+
6+
7+
def create_conference_voucher(
8+
*,
9+
conference: Conference,
10+
user: User,
11+
voucher_type: ConferenceVoucher.VoucherType,
12+
) -> ConferenceVoucher:
13+
conference_voucher = ConferenceVoucher(
14+
conference=conference,
15+
user=user,
16+
voucher_type=voucher_type,
17+
voucher_code=ConferenceVoucher.generate_code(),
18+
)
19+
20+
price_mode, value = conference_voucher.get_voucher_configuration()
21+
22+
pretix_voucher = create_voucher(
23+
conference=conference_voucher.conference,
24+
code=conference_voucher.voucher_code,
25+
comment=f"Voucher for user_id={conference_voucher.user_id}",
26+
tag=conference_voucher.voucher_type,
27+
quota_id=conference_voucher.conference.pretix_conference_voucher_quota_id,
28+
price_mode=price_mode,
29+
value=value,
30+
)
31+
32+
pretix_voucher_id = pretix_voucher["id"]
33+
conference_voucher.pretix_voucher_id = pretix_voucher_id
34+
conference_voucher.save()
35+
return conference_voucher

backend/schedule/models.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -309,12 +309,13 @@ def speakers(self):
309309
if self.submission_id:
310310
speakers.append(self.submission.speaker)
311311

312-
speakers.extend([speaker.user for speaker in self.additional_speakers.all()])
313-
314312
if self.keynote_id:
315-
for speaker_keynote in self.keynote.speakers.all():
313+
for speaker_keynote in self.keynote.speakers.order_by("id").all():
316314
speakers.append(speaker_keynote.user)
317315

316+
speakers.extend(
317+
[speaker.user for speaker in self.additional_speakers.order_by("id").all()]
318+
)
318319
return speakers
319320

320321
def clean(self):

backend/schedule/tasks.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
from django.db.models import Q
2+
from conferences.tasks import send_conference_voucher_email
3+
from conferences.vouchers import create_conference_voucher
4+
from conferences.models.conference_voucher import ConferenceVoucher
25
from pycon.celery_utils import OnlyOneAtTimeTask
36
from google_api.exceptions import NoGoogleCloudQuotaLeftError
47
from googleapiclient.errors import HttpError
@@ -373,3 +376,50 @@ def process_schedule_items_videos_to_upload():
373376
)
374377
sent_for_video_upload_state.failed_reason = str(e)
375378
sent_for_video_upload_state.save(update_fields=["status", "failed_reason"])
379+
380+
381+
@app.task
382+
def create_and_send_voucher_to_speaker(schedule_item_id: int):
383+
schedule_item = ScheduleItem.objects.get(id=schedule_item_id)
384+
speakers = schedule_item.speakers
385+
386+
if not speakers:
387+
return
388+
389+
speaker = speakers[0]
390+
co_speaker = speakers[1] if len(speakers) > 1 else None
391+
392+
_send_conference_voucher(
393+
speaker,
394+
schedule_item.conference,
395+
ConferenceVoucher.VoucherType.SPEAKER,
396+
)
397+
398+
if co_speaker:
399+
_send_conference_voucher(
400+
co_speaker,
401+
schedule_item.conference,
402+
ConferenceVoucher.VoucherType.CO_SPEAKER,
403+
)
404+
405+
406+
def _send_conference_voucher(user, conference, voucher_type):
407+
conference_voucher = (
408+
ConferenceVoucher.objects.for_conference(conference).for_user(user).first()
409+
)
410+
411+
if conference_voucher:
412+
logger.info(
413+
"User %s already has a voucher for conference %s, not creating a new one",
414+
user.id,
415+
conference.id,
416+
)
417+
return
418+
419+
conference_voucher = create_conference_voucher(
420+
conference=conference,
421+
user=user,
422+
voucher_type=voucher_type,
423+
)
424+
425+
send_conference_voucher_email.delay(conference_voucher_id=conference_voucher.id)

backend/schedule/tests/test_tasks.py

Lines changed: 141 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from conferences.models.conference_voucher import ConferenceVoucher
12
from notifications.tests.factories import EmailTemplateFactory
23
from google_api.exceptions import NoGoogleCloudQuotaLeftError
34
from googleapiclient.errors import HttpError
@@ -6,13 +7,14 @@
67
from django.core.files.storage import storages
78
from django.core.files.uploadedfile import InMemoryUploadedFile
89
from unittest import mock
9-
from conferences.tests.factories import ConferenceFactory
10+
from conferences.tests.factories import ConferenceFactory, ConferenceVoucherFactory
1011
from i18n.strings import LazyI18nString
1112
from datetime import datetime, timezone
1213
from unittest.mock import ANY, patch
1314
from django.test import override_settings
1415

1516
from schedule.tasks import (
17+
create_and_send_voucher_to_speaker,
1618
notify_new_schedule_invitation_answer_slack,
1719
process_schedule_items_videos_to_upload,
1820
send_schedule_invitation_email,
@@ -22,6 +24,7 @@
2224
upload_schedule_item_video,
2325
)
2426
from schedule.tests.factories import (
27+
ScheduleItemAdditionalSpeakerFactory,
2528
ScheduleItemFactory,
2629
ScheduleItemSentForVideoUploadFactory,
2730
)
@@ -736,3 +739,140 @@ def test_upload_schedule_item_video_with_failing_thumbnail_upload_fails(mocker):
736739
upload_schedule_item_video(
737740
sent_for_video_upload_state_id=sent_for_upload.id,
738741
)
742+
743+
744+
def test_create_and_send_voucher_to_speaker(mocker):
745+
mock_create = mocker.patch(
746+
"conferences.vouchers.create_voucher", return_value={"id": 123}
747+
)
748+
mock_send_email = mocker.patch("schedule.tasks.send_conference_voucher_email")
749+
750+
schedule_item = ScheduleItemFactory(type=ScheduleItem.TYPES.talk)
751+
create_and_send_voucher_to_speaker(schedule_item.id)
752+
753+
assert (
754+
ConferenceVoucher.objects.for_conference(schedule_item.conference)
755+
.filter(
756+
user=schedule_item.submission.speaker,
757+
voucher_type=ConferenceVoucher.VoucherType.SPEAKER,
758+
)
759+
.exists()
760+
)
761+
762+
mock_create.assert_called_once()
763+
mock_send_email.delay.assert_called_once()
764+
765+
766+
def test_create_and_send_voucher_to_speaker_to_speaker_and_co_speakers(mocker):
767+
mock_create = mocker.patch(
768+
"conferences.vouchers.create_voucher", return_value={"id": 123}
769+
)
770+
mock_send_email = mocker.patch("schedule.tasks.send_conference_voucher_email")
771+
772+
schedule_item = ScheduleItemFactory(
773+
type=ScheduleItem.TYPES.talk,
774+
)
775+
additional_speaker_1 = ScheduleItemAdditionalSpeakerFactory(
776+
scheduleitem=schedule_item
777+
).user
778+
additional_speaker_2 = ScheduleItemAdditionalSpeakerFactory(
779+
scheduleitem=schedule_item
780+
).user
781+
782+
create_and_send_voucher_to_speaker(schedule_item.id)
783+
784+
speaker_voucher = (
785+
ConferenceVoucher.objects.for_conference(schedule_item.conference)
786+
.filter(
787+
user=schedule_item.submission.speaker,
788+
voucher_type=ConferenceVoucher.VoucherType.SPEAKER,
789+
)
790+
.get()
791+
)
792+
793+
co_speaker_voucher = (
794+
ConferenceVoucher.objects.for_conference(schedule_item.conference)
795+
.filter(
796+
user=additional_speaker_1,
797+
voucher_type=ConferenceVoucher.VoucherType.CO_SPEAKER,
798+
)
799+
.get()
800+
)
801+
802+
assert not (
803+
ConferenceVoucher.objects.for_conference(schedule_item.conference)
804+
.filter(
805+
user=additional_speaker_2,
806+
voucher_type=ConferenceVoucher.VoucherType.CO_SPEAKER,
807+
)
808+
.exists()
809+
)
810+
811+
assert mock_create.call_count == 2
812+
mock_send_email.delay.assert_has_calls(
813+
[
814+
mock.call(conference_voucher_id=speaker_voucher.id),
815+
mock.call(conference_voucher_id=co_speaker_voucher.id),
816+
],
817+
any_order=True,
818+
)
819+
820+
821+
def test_create_and_send_voucher_to_speaker_works_if_existing_voucher_is_for_different_conf(
822+
mocker,
823+
):
824+
mock_create = mocker.patch(
825+
"conferences.vouchers.create_voucher", return_value={"id": 123}
826+
)
827+
mock_send_email = mocker.patch("schedule.tasks.send_conference_voucher_email")
828+
829+
schedule_item = ScheduleItemFactory(type=ScheduleItem.TYPES.talk)
830+
831+
ConferenceVoucherFactory(
832+
user=schedule_item.submission.speaker,
833+
)
834+
835+
create_and_send_voucher_to_speaker(schedule_item.id)
836+
837+
assert (
838+
ConferenceVoucher.objects.for_conference(schedule_item.conference)
839+
.filter(
840+
user=schedule_item.submission.speaker,
841+
voucher_type=ConferenceVoucher.VoucherType.SPEAKER,
842+
)
843+
.exists()
844+
)
845+
846+
mock_create.assert_called_once()
847+
mock_send_email.delay.assert_called_once()
848+
849+
850+
def test_create_and_send_voucher_to_speaker_does_nothing_if_voucher_exists(mocker):
851+
mock_create = mocker.patch("conferences.vouchers.create_voucher")
852+
mock_send_email = mocker.patch("schedule.tasks.send_conference_voucher_email")
853+
854+
schedule_item = ScheduleItemFactory(type=ScheduleItem.TYPES.talk)
855+
856+
ConferenceVoucherFactory(
857+
conference=schedule_item.conference,
858+
user=schedule_item.submission.speaker,
859+
)
860+
861+
create_and_send_voucher_to_speaker(schedule_item.id)
862+
863+
mock_create.assert_not_called()
864+
mock_send_email.delay.assert_not_called()
865+
866+
867+
def test_create_and_send_voucher_to_speaker_does_nothing_if_schedule_item_does_not_hav_speakers(
868+
mocker,
869+
):
870+
mock_create = mocker.patch("conferences.vouchers.create_voucher")
871+
mock_send_email = mocker.patch("schedule.tasks.send_conference_voucher_email")
872+
873+
schedule_item = ScheduleItemFactory(type=ScheduleItem.TYPES.talk, submission=None)
874+
875+
create_and_send_voucher_to_speaker(schedule_item.id)
876+
877+
mock_create.assert_not_called()
878+
mock_send_email.delay.assert_not_called()

0 commit comments

Comments
 (0)