agendas: allow sending reminders to multiple recipients (#61367)
This commit is contained in:
parent
fd59ece695
commit
6358e4bda5
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
),
|
||||
),
|
||||
]
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
Loading…
Reference in New Issue