agendas: allow sending reminders to multiple recipients (#61367)

This commit is contained in:
Valentin Deniaud 2022-02-02 16:28:57 +01:00
parent fd59ece695
commit 6358e4bda5
7 changed files with 170 additions and 16 deletions

View File

@ -88,33 +88,36 @@ class Command(BaseCommand):
ctx.update(settings.TEMPLATE_VARS)
if agenda.reminder_settings.send_email:
self.send_email(booking, kind, ctx)
emails = set(booking.extra_emails)
if booking.user_email:
emails.add(booking.user_email)
for email in emails:
self.send_email(email, booking, kind, ctx)
if agenda.reminder_settings.send_sms:
self.send_sms(booking, kind, ctx)
phone_numbers = set(booking.extra_phone_numbers)
if booking.user_phone_number:
phone_numbers.add(booking.user_phone_number)
if phone_numbers:
self.send_sms(list(phone_numbers), booking, kind, ctx)
@staticmethod
def send_email(booking, kind, ctx):
if not booking.user_email:
return
def send_email(email, booking, kind, ctx):
subject = render_to_string('agendas/%s_reminder_subject.txt' % kind, ctx).strip()
body = render_to_string('agendas/%s_reminder_body.txt' % kind, ctx)
html_body = render_to_string('agendas/%s_reminder_body.html' % kind, ctx)
try:
with atomic():
send_mail(
subject, body, settings.DEFAULT_FROM_EMAIL, [booking.user_email], html_message=html_body
)
send_mail(subject, body, settings.DEFAULT_FROM_EMAIL, [email], html_message=html_body)
booking.reminder_datetime = timezone.now()
booking.save()
except SMTPException:
pass
@staticmethod
def send_sms(booking, kind, ctx):
if not booking.user_phone_number:
return
def send_sms(phone_numbers, booking, kind, ctx):
if not settings.SMS_URL:
return
@ -122,7 +125,7 @@ class Command(BaseCommand):
payload = {
'message': message,
'from': settings.SMS_SENDER,
'to': [booking.user_phone_number],
'to': phone_numbers,
}
try:

View File

@ -0,0 +1,28 @@
# Generated by Django 2.2.19 on 2022-02-02 16:30
import django.contrib.postgres.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('agendas', '0105_subscription'),
]
operations = [
migrations.AddField(
model_name='booking',
name='extra_emails',
field=django.contrib.postgres.fields.ArrayField(
base_field=models.EmailField(max_length=254), default=list, size=None
),
),
migrations.AddField(
model_name='booking',
name='extra_phone_numbers',
field=django.contrib.postgres.fields.ArrayField(
base_field=models.CharField(max_length=16), default=list, size=None
),
),
]

View File

@ -1867,6 +1867,9 @@ class Booking(models.Model):
user_was_present = models.NullBooleanField()
user_absence_reason = models.CharField(max_length=250, blank=True)
extra_emails = ArrayField(models.EmailField(), default=list)
extra_phone_numbers = ArrayField(models.CharField(max_length=16), default=list)
form_url = models.URLField(blank=True)
backoffice_url = models.URLField(blank=True)
cancel_callback_url = models.URLField(blank=True)

View File

@ -53,6 +53,12 @@ class SlotSerializer(serializers.Serializer):
cancel_booking_id = serializers.CharField(max_length=250, allow_blank=True, allow_null=True)
force_waiting_list = serializers.BooleanField(default=False)
use_color_for = serializers.CharField(max_length=250, allow_blank=True)
extra_emails = StringOrListField(
required=False, child=serializers.EmailField(max_length=250, allow_blank=False)
)
extra_phone_numbers = StringOrListField(
required=False, child=serializers.CharField(max_length=16, allow_blank=False)
)
class SlotsSerializer(SlotSerializer):

View File

@ -703,6 +703,8 @@ def make_booking(event, payload, extra_data, primary_booking=None, in_waiting_li
backoffice_url=translate_to_publik_url(payload.get('backoffice_url', '')),
cancel_callback_url=translate_to_publik_url(payload.get('cancel_callback_url', '')),
user_display_label=payload.get('user_display_label', ''),
extra_emails=payload.get('extra_emails', []),
extra_phone_numbers=payload.get('extra_phone_numbers', []),
extra_data=extra_data,
color=color,
)

View File

@ -77,6 +77,8 @@ def test_booking_api(app, some_data, user):
'user_email': 'bar@bar.com',
'user_phone_number': '+33123456789',
'form_url': 'http://example.net/',
'extra_emails': ['baz@baz.com', 'hop@hop.com'],
'extra_phone_numbers': ['+33123456789', '+33123456789'],
},
)
booking = Booking.objects.get(id=resp.json['booking_id'])
@ -170,6 +172,77 @@ def test_booking_api(app, some_data, user):
resp = app.post('/api/agenda/0/fillslot/%s/' % event.id, status=404)
def test_booking_api_extra_emails(app, user):
agenda = Agenda.objects.create(label='Foo bar', kind='events')
event = Event.objects.create(
label='Event', start_datetime=now() + datetime.timedelta(days=5), places=10, agenda=agenda
)
app.authorization = ('Basic', ('john.doe', 'password'))
fillslot_url = '/api/agenda/%s/fillslot/%s/' % (agenda.id, event.slug)
resp = app.post_json(
fillslot_url,
params={
'extra_emails': ['foo@foo.com', 'bar@bar.com'],
'extra_phone_numbers': ['+33123456789', '+33123456790'],
},
)
booking = Booking.objects.get(id=resp.json['booking_id'])
assert booking.extra_emails == ['foo@foo.com', 'bar@bar.com']
assert booking.extra_phone_numbers == ['+33123456789', '+33123456790']
resp = app.post_json(
fillslot_url,
params={
'extra_emails': 'foo@foo.com, bar@bar.com',
'extra_phone_numbers': '+33123456789,+33123456790',
},
)
booking = Booking.objects.get(id=resp.json['booking_id'])
assert booking.extra_emails == ['foo@foo.com', 'bar@bar.com']
assert booking.extra_phone_numbers == ['+33123456789', '+33123456790']
resp = app.post_json(
fillslot_url,
params={
'extra_emails': 'foo@foo.com',
'extra_phone_numbers': '+33123456789',
},
)
booking = Booking.objects.get(id=resp.json['booking_id'])
assert booking.extra_emails == ['foo@foo.com']
assert booking.extra_phone_numbers == ['+33123456789']
resp = app.post_json(
fillslot_url,
params={
'extra_emails': ', bar@bar.com',
'extra_phone_numbers': '',
},
)
booking = Booking.objects.get(id=resp.json['booking_id'])
assert booking.extra_emails == ['bar@bar.com']
assert booking.extra_phone_numbers == []
resp = app.post(fillslot_url)
booking = Booking.objects.get(id=resp.json['booking_id'])
assert booking.extra_emails == []
assert booking.extra_phone_numbers == []
resp = app.post_json(
fillslot_url,
params={
'extra_emails': 'bar.com',
'extra_phone_numbers': 'too loooooooooong',
},
status=400,
)
assert resp.json['errors']['extra_emails']['0'] == ['Enter a valid email address.']
assert resp.json['errors']['extra_phone_numbers']['0'] == [
'Ensure this field has no more than 16 characters.'
]
def test_booking_api_check_delays(app, user):
agenda = Agenda.objects.create(
label='Foo bar', kind='events', minimal_booking_delay=0, maximal_booking_delay=7

View File

@ -1659,6 +1659,31 @@ def test_agenda_reminders(mailoutbox, freezer):
assert len(mailoutbox) == 0
@pytest.mark.freeze_time('2020-01-01 14:00')
def test_agenda_reminders_extra_emails(mailoutbox, freezer):
agenda = Agenda.objects.create(label='Events', kind='events')
AgendaReminderSettings.objects.create(agenda=agenda, days=1, send_email=True)
start_datetime = now() + datetime.timedelta(days=2)
event = Event.objects.create(agenda=agenda, start_datetime=start_datetime, places=10, label='Event')
Booking.objects.create(
event=event,
user_email='t@test.org',
extra_emails=['t@test.org', 'u@test.org', 'v@test.org'],
)
Booking.objects.create(event=event, extra_emails=['w@test.org'])
freezer.move_to('2020-01-02 15:00')
with mock.patch('chrono.utils.requests_wrapper.RequestsSession.send') as mock_send:
mock_response = mock.Mock(status_code=200)
mock_send.return_value = mock_response
call_command('send_booking_reminders')
assert len(mailoutbox) == 4
assert all(len(x.to) == 1 for x in mailoutbox)
assert {x.to[0] for x in mailoutbox} == {'t@test.org', 'u@test.org', 'v@test.org', 'w@test.org'}
@override_settings(SMS_URL='https://passerelle.test.org/sms/send/', SMS_SENDER='EO')
def test_agenda_reminders_sms(freezer):
freezer.move_to('2020-01-01 14:00')
@ -1670,6 +1695,12 @@ def test_agenda_reminders_sms(freezer):
for _ in range(5):
Booking.objects.create(event=event, user_phone_number='+336123456789')
Booking.objects.create(event=event)
Booking.objects.create(
event=event,
user_phone_number='+33111111111',
extra_phone_numbers=['+33111111111', '+33222222222', '+33333333333'],
)
Booking.objects.create(event=event, extra_phone_numbers=['+336123456789'])
freezer.move_to('2020-01-02 15:00')
with mock.patch('chrono.utils.requests_wrapper.RequestsSession.send') as mock_send:
@ -1677,11 +1708,19 @@ def test_agenda_reminders_sms(freezer):
mock_send.return_value = mock_response
call_command('send_booking_reminders')
assert mock_send.call_count == 5
body = json.loads(mock_send.call_args[0][0].body.decode())
assert mock_send.call_count == 7
body = json.loads(mock_send.call_args_list[0][0][0].body.decode())
assert body['from'] == 'EO'
assert body['to'] == ['+336123456789']
body = json.loads(mock_send.call_args_list[5][0][0].body.decode())
assert body['from'] == 'EO'
assert set(body['to']) == {'+33111111111', '+33222222222', '+33333333333'}
body = json.loads(mock_send.call_args_list[6][0][0].body.decode())
assert body['from'] == 'EO'
assert set(body['to']) == {'+336123456789'}
@override_settings(SMS_URL='https://passerelle.test.org/sms/send/', SMS_SENDER='EO')
def test_agenda_reminders_retry(freezer):