From 703d976efed898664acf7c9b7c7aba69b0e7d31f Mon Sep 17 00:00:00 2001
From: Valentin Deniaud <vdeniaud@entrouvert.com>
Date: Tue, 15 Feb 2022 10:52:24 +0100
Subject: [PATCH 3/3] manager: test booking reminders sending (#61234)

---
 chrono/agendas/management/commands/utils.py   |  17 +--
 chrono/agendas/models.py                      |  14 +++
 chrono/manager/forms.py                       |  52 ++++++++-
 .../chrono/manager_agenda_settings.html       |   5 +
 .../chrono/manager_send_reminder_form.html    |  23 ++++
 chrono/manager/urls.py                        |   5 +
 chrono/manager/views.py                       |  30 ++++++
 tests/manager/test_all.py                     | 100 ++++++++++++++++++
 8 files changed, 232 insertions(+), 14 deletions(-)
 create mode 100644 chrono/manager/templates/chrono/manager_send_reminder_form.html

diff --git a/chrono/agendas/management/commands/utils.py b/chrono/agendas/management/commands/utils.py
index 4f16143c..51b0be11 100644
--- a/chrono/agendas/management/commands/utils.py
+++ b/chrono/agendas/management/commands/utils.py
@@ -32,20 +32,11 @@ def send_reminder(booking, msg_type):
         except (VariableDoesNotExist, TemplateSyntaxError):
             pass
 
-    if msg_type == 'email':
-        emails = set(booking.extra_emails)
-        if booking.user_email:
-            emails.add(booking.user_email)
-
-        for email in emails:
+    if msg_type == 'email' and booking.emails:
+        for email in booking.emails:
             send_email_reminder(email, booking, kind, ctx)
-    elif msg_type == 'sms':
-        phone_numbers = set(booking.extra_phone_numbers)
-        if booking.user_phone_number:
-            phone_numbers.add(booking.user_phone_number)
-
-        if phone_numbers:
-            send_sms_reminder(list(phone_numbers), booking, kind, ctx)
+    elif msg_type == 'sms' and booking.phone_numbers:
+        send_sms_reminder(booking.phone_numbers, booking, kind, ctx)
 
 
 def send_email_reminder(email, booking, kind, ctx):
diff --git a/chrono/agendas/models.py b/chrono/agendas/models.py
index 250df393..cff93277 100644
--- a/chrono/agendas/models.py
+++ b/chrono/agendas/models.py
@@ -1925,6 +1925,20 @@ class Booking(models.Model):
     def user_name(self):
         return ('%s %s' % (self.user_first_name, self.user_last_name)).strip()
 
+    @cached_property
+    def emails(self):
+        emails = set(self.extra_emails)
+        if self.user_email:
+            emails.add(self.user_email)
+        return list(emails)
+
+    @cached_property
+    def phone_numbers(self):
+        phone_numbers = set(self.extra_phone_numbers)
+        if self.user_phone_number:
+            phone_numbers.add(self.user_phone_number)
+        return list(phone_numbers)
+
     def cancel(self, trigger_callback=False):
         timestamp = now()
         with transaction.atomic():
diff --git a/chrono/manager/forms.py b/chrono/manager/forms.py
index 916d3b4e..569d7152 100644
--- a/chrono/manager/forms.py
+++ b/chrono/manager/forms.py
@@ -26,8 +26,9 @@ from django.core.exceptions import FieldDoesNotExist
 from django.db import transaction
 from django.forms import ValidationError
 from django.utils.encoding import force_text
+from django.utils.formats import date_format
 from django.utils.six import StringIO
-from django.utils.timezone import make_aware, now
+from django.utils.timezone import localtime, make_aware, now
 from django.utils.translation import ugettext_lazy as _
 
 from chrono.agendas.models import (
@@ -926,6 +927,55 @@ class AgendaReminderForm(forms.ModelForm):
             del self.fields['sms_extra_info']
 
 
+class BookingChoiceField(forms.ModelChoiceField):
+    def label_from_instance(self, obj):
+        name = obj.user_name or obj.label or _('Anonymous')
+        date = date_format(localtime(obj.creation_datetime), 'SHORT_DATETIME_FORMAT')
+        emails = ', '.join(sorted(obj.emails)) or _('no email')
+        phone_numbers = ', '.join(sorted(obj.phone_numbers)) or _('no phone number')
+
+        if settings.SMS_URL:
+            return '%s, %s, %s (%s)' % (name, emails, phone_numbers, date)
+        else:
+            return '%s, %s (%s)' % (name, emails, date)
+
+
+class AgendaReminderTestForm(forms.Form):
+    booking = BookingChoiceField(
+        label=_('Booking'),
+        queryset=Booking.objects.none(),
+        help_text=_('Only the last ten bookings are displayed.'),
+    )
+    msg_type = forms.MultipleChoiceField(
+        label=_('Send via'),
+        choices=(('email', _('Email')), ('sms', _('SMS'))),
+        widget=forms.CheckboxSelectMultiple(),
+    )
+    email = forms.EmailField(
+        label=_('Email'),
+        help_text=_('This will override email specified on booking creation, if any.'),
+        required=False,
+    )
+    phone_number = forms.CharField(
+        label=_('Phone number'),
+        max_length=16,
+        help_text=_('This will override phone number specified on booking creation, if any.'),
+        required=False,
+    )
+
+    def __init__(self, *args, **kwargs):
+        agenda = kwargs.pop('agenda')
+        super().__init__(*args, **kwargs)
+        self.fields['booking'].queryset = Booking.objects.filter(
+            pk__in=Booking.objects.filter(event__agenda=agenda).order_by('-creation_datetime')[:10]
+        ).order_by('-creation_datetime')
+
+        if not settings.SMS_URL:
+            self.fields['msg_type'].initial = ['email']
+            self.fields['msg_type'].widget = forms.MultipleHiddenInput()
+            del self.fields['phone_number']
+
+
 class AgendasExportForm(forms.Form):
     agendas = forms.BooleanField(label=_('Agendas'), required=False, initial=True)
     unavailability_calendars = forms.BooleanField(
diff --git a/chrono/manager/templates/chrono/manager_agenda_settings.html b/chrono/manager/templates/chrono/manager_agenda_settings.html
index 7bd33350..18c4363e 100644
--- a/chrono/manager/templates/chrono/manager_agenda_settings.html
+++ b/chrono/manager/templates/chrono/manager_agenda_settings.html
@@ -57,6 +57,11 @@
 <a rel="popup" data-selector="#message-preview"  href="{% url 'chrono-manager-agenda-reminder-preview' pk=object.id type='sms' %}">{% trans "Preview SMS" %}</a>
 {% endif %}
 </p>
+{% if agenda.reminder_settings.days_before_email or agenda.reminder_settings.days_before_sms %}
+<p>
+<a rel="popup" href="{% url 'chrono-manager-agenda-reminder-test' pk=object.pk %}">{% trans "Test reminder sending" %}</a>
+</p>
+{% endif %}
 </div>
 </div>
 {% endblock %}
diff --git a/chrono/manager/templates/chrono/manager_send_reminder_form.html b/chrono/manager/templates/chrono/manager_send_reminder_form.html
new file mode 100644
index 00000000..e0612f2c
--- /dev/null
+++ b/chrono/manager/templates/chrono/manager_send_reminder_form.html
@@ -0,0 +1,23 @@
+{% extends "chrono/manager_agenda_view.html" %}
+{% load i18n gadjo %}
+
+{% block breadcrumb %}
+{{ block.super }}
+<a href="{% url 'chrono-manager-agenda-settings' agenda.pk %}">{% trans 'Settings' %}</a>
+<a href="{% url 'chrono-manager-agenda-edit' agenda.pk %}">{{ title }}</a>
+{% endblock %}
+
+{% block appbar %}
+<h2>{% trans "Test reminder sending" %}</h2>
+{% endblock %}
+
+{% block content %}
+<form method="post" enctype="multipart/form-data">
+  {% csrf_token %}
+  {{ form|with_template }}
+  <div class="buttons">
+    <button class="submit-button">{% trans "Send" %}</button>
+    <a class="cancel" href="{% url 'chrono-manager-agenda-settings' pk=agenda.id %}">{% trans 'Cancel' %}</a>
+  </div>
+</form>
+{% endblock %}
diff --git a/chrono/manager/urls.py b/chrono/manager/urls.py
index 83cbb7df..e0e79979 100644
--- a/chrono/manager/urls.py
+++ b/chrono/manager/urls.py
@@ -186,6 +186,11 @@ urlpatterns = [
         views.agenda_reminder_settings,
         name='chrono-manager-agenda-reminder-settings',
     ),
+    url(
+        r'^agendas/(?P<pk>\d+)/reminder/test/$',
+        views.agenda_reminder_test,
+        name='chrono-manager-agenda-reminder-test',
+    ),
     url(
         r'^agendas/(?P<pk>\d+)/reminder/preview/(?P<type>(email|sms))/$',
         views.agenda_reminder_preview,
diff --git a/chrono/manager/views.py b/chrono/manager/views.py
index 07bf0c2a..9bae0167 100644
--- a/chrono/manager/views.py
+++ b/chrono/manager/views.py
@@ -55,6 +55,7 @@ from django.views.generic import (
     View,
 )
 
+from chrono.agendas.management.commands.utils import send_reminder
 from chrono.agendas.models import (
     AbsenceReason,
     AbsenceReasonGroup,
@@ -87,6 +88,7 @@ from .forms import (
     AgendaEditForm,
     AgendaNotificationsForm,
     AgendaReminderForm,
+    AgendaReminderTestForm,
     AgendaResourceForm,
     AgendaRolesForm,
     AgendasExportForm,
@@ -1901,6 +1903,34 @@ class AgendaReminderSettingsView(AgendaEditView):
 agenda_reminder_settings = AgendaReminderSettingsView.as_view()
 
 
+class AgendaReminderTestView(ManagedAgendaMixin, FormView):
+    template_name = 'chrono/manager_send_reminder_form.html'
+    form_class = AgendaReminderTestForm
+
+    def get_form_kwargs(self):
+        kwargs = super(FormView, self).get_form_kwargs()
+        kwargs['agenda'] = self.agenda
+        return kwargs
+
+    def form_valid(self, form):
+        booking = form.cleaned_data['booking']
+
+        if form.cleaned_data.get('phone_number'):
+            booking.user_phone_number = form.cleaned_data['phone_number']
+            booking.extra_phone_numbers.clear()
+
+        if form.cleaned_data.get('email'):
+            booking.user_email = form.cleaned_data['email']
+            booking.extra_emails.clear()
+
+        for msg_type in form.cleaned_data['msg_type']:
+            send_reminder(booking, msg_type)
+        return super().form_valid(form)
+
+
+agenda_reminder_test = AgendaReminderTestView.as_view()
+
+
 class AgendaReminderPreviewView(ManagedAgendaMixin, TemplateView):
     template_name = 'chrono/manager_agenda_reminder_preview.html'
 
diff --git a/tests/manager/test_all.py b/tests/manager/test_all.py
index ba732597..32425117 100644
--- a/tests/manager/test_all.py
+++ b/tests/manager/test_all.py
@@ -1,4 +1,5 @@
 import datetime
+import json
 from unittest import mock
 
 import freezegun
@@ -2977,6 +2978,105 @@ def test_manager_reminders_preview(app, admin_user):
     assert '{{ booking.extra_data.xxx }}' in resp.text
 
 
+def test_manager_reminders_test_sending(app, admin_user, freezer, mailoutbox, settings):
+    agenda = Agenda.objects.create(label='Events', kind='events')
+    Desk.objects.create(agenda=agenda, slug='_exceptions_holder')
+    AgendaReminderSettings.objects.create(
+        agenda=agenda,
+        days_before_email=1,
+        email_extra_info='Take your {{ booking.extra_data.document_type }}.',
+        days_before_sms=1,
+        sms_extra_info='Take {{ booking.extra_data.document_type }}.',
+    )
+
+    login(app)
+    resp = app.get('/manage/agendas/%s/settings' % agenda.id)
+    resp = resp.click('Test reminder sending')
+
+    assert 'phone_number' not in resp.form.fields
+    assert resp.form['msg_type'].attrs['type'] == 'hidden'
+    assert resp.form['booking'].options == [('', True, '---------')]
+
+    # add bookings
+    event = Event.objects.create(agenda=agenda, start_datetime=now(), places=10)
+    freezer.move_to('2020-01-01 14:00')
+    Booking.objects.create(user_first_name='oldest', user_email='t@test.org', event=event)
+    freezer.move_to('2020-01-02 14:00')
+    for _ in range(10):
+        Booking.objects.create(
+            event=event,
+            user_first_name='Jon',
+            user_last_name='Doe',
+            user_email='t@test.org',
+            user_phone_number='+336123456780',
+        )
+    freezer.move_to('2020-01-03 14:00')
+    last_booking = Booking.objects.create(
+        event=event,
+        user_first_name='Jane',
+        user_last_name='Doe',
+        user_email='t@test.org',
+        extra_emails=['u@test.org'],
+        user_phone_number='+33122334455',
+        extra_phone_numbers=['+33122334456'],
+        extra_data={'document_type': 'receipt'},
+    )
+
+    resp = app.get('/manage/agendas/%s/reminder/test/' % agenda.id)
+    assert [x[2] for x in resp.form['booking'].options[:2]] == [
+        '---------',
+        'Jane Doe, t@test.org, u@test.org (01/03/2020 3 p.m.)',
+    ]
+    assert [x[2] for x in resp.form['booking'].options[2:]] == ['Jon Doe, t@test.org (01/02/2020 3 p.m.)'] * 9
+
+    resp.form['booking'] = last_booking.pk
+    resp = resp.form.submit().follow()
+
+    assert len(mailoutbox) == 2
+    assert {x.to[0] for x in mailoutbox} == {'t@test.org', 'u@test.org'}
+    assert all('Take your receipt' in mail.body for mail in mailoutbox)
+    mailoutbox.clear()
+
+    settings.SMS_URL = 'https://passerelle.test.org/sms/send/'
+    settings.SMS_SENDER = 'EO'
+
+    resp = app.get('/manage/agendas/%s/reminder/test/' % agenda.id)
+    assert [x[2] for x in resp.form['booking'].options[:2]] == [
+        '---------',
+        'Jane Doe, t@test.org, u@test.org, +33122334455, +33122334456 (01/03/2020 3 p.m.)',
+    ]
+    assert [x[2] for x in resp.form['booking'].options[2:]] == [
+        'Jon Doe, t@test.org, +336123456780 (01/02/2020 3 p.m.)'
+    ] * 9
+
+    resp.form['booking'] = last_booking.pk
+    resp.form['msg_type'] = ['email', 'sms']
+    with mock.patch('chrono.utils.requests_wrapper.RequestsSession.send') as mock_send:
+        mock_send.return_value = mock.Mock(status_code=200)
+        resp = resp.form.submit().follow()
+
+    body = json.loads(mock_send.call_args[0][0].body.decode())
+    assert 'Take receipt' in body['message']
+    assert set(body['to']) == {'+33122334455', '+33122334456'}
+
+    assert len(mailoutbox) == 2
+    mailoutbox.clear()
+
+    resp = app.get('/manage/agendas/%s/reminder/test/' % agenda.id)
+    resp.form['booking'] = last_booking.pk
+    resp.form['msg_type'] = ['sms', 'email']
+    resp.form['email'] = 'v@test.org'
+    resp.form['phone_number'] = '+33333333333'
+    with mock.patch('chrono.utils.requests_wrapper.RequestsSession.send') as mock_send:
+        mock_send.return_value = mock.Mock(status_code=200)
+        resp = resp.form.submit().follow()
+
+    body = json.loads(mock_send.call_args[0][0].body.decode())
+    assert body['to'] == ['+33333333333']
+    assert len(mailoutbox) == 1
+    assert mailoutbox[0].to == ['v@test.org']
+
+
 def test_manager_agenda_roles(app, admin_user, manager_user):
     agenda = Agenda.objects.create(label='Events', kind='events')
     Desk.objects.create(agenda=agenda, slug='_exceptions_holder')
-- 
2.30.2

