From 2b45d66d194ef9b43cac17f42c94ded8fbf7911e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Fri, 25 Sep 2020 11:00:31 +0200
Subject: [PATCH] manager: display link to booking form on events (#45417)

---
 .../migrations/0062_auto_20200915_1401.py     |  4 +-
 .../migrations/0063_booking_form_url.py       | 25 ++++++
 chrono/agendas/models.py                      | 22 ++++++
 chrono/manager/forms.py                       |  2 +
 .../chrono/manager_agenda_event_fragment.html |  4 +-
 .../chrono/manager_event_detail.html          |  3 +
 tests/test_import_export.py                   | 23 ++++++
 tests/test_manager.py                         | 77 ++++++++++++++-----
 8 files changed, 138 insertions(+), 22 deletions(-)
 create mode 100644 chrono/agendas/migrations/0063_booking_form_url.py

diff --git a/chrono/agendas/migrations/0062_auto_20200915_1401.py b/chrono/agendas/migrations/0062_auto_20200915_1401.py
index 32f00ae..9b88bb0 100644
--- a/chrono/agendas/migrations/0062_auto_20200915_1401.py
+++ b/chrono/agendas/migrations/0062_auto_20200915_1401.py
@@ -39,7 +39,7 @@ class Migration(migrations.Migration):
                     'email_extra_info',
                     models.TextField(
                         blank=True,
-                        help_text='Basic information such as event name, time and date are already included',
+                        help_text='Basic information such as event name, time and date are already included.',
                         verbose_name='Additional text to incude in emails',
                     ),
                 ),
@@ -48,7 +48,7 @@ class Migration(migrations.Migration):
                     'sms_extra_info',
                     models.TextField(
                         blank=True,
-                        help_text='Basic information such as event name, time and date are already included',
+                        help_text='Basic information such as event name, time and date are already included.',
                         verbose_name='Additional text to incude in SMS',
                     ),
                 ),
diff --git a/chrono/agendas/migrations/0063_booking_form_url.py b/chrono/agendas/migrations/0063_booking_form_url.py
new file mode 100644
index 0000000..dac3225
--- /dev/null
+++ b/chrono/agendas/migrations/0063_booking_form_url.py
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+import chrono.agendas.models
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('agendas', '0062_auto_20200915_1401'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='agenda',
+            name='booking_form_url',
+            field=models.CharField(
+                blank=True,
+                max_length=200,
+                validators=[chrono.agendas.models.django_template_validator],
+                verbose_name='Booking form URL',
+            ),
+        ),
+    ]
diff --git a/chrono/agendas/models.py b/chrono/agendas/models.py
index a36d212..f2a3b2b 100644
--- a/chrono/agendas/models.py
+++ b/chrono/agendas/models.py
@@ -35,6 +35,7 @@ from django.core.exceptions import ValidationError
 from django.core.validators import MaxValueValidator
 from django.db import models, transaction
 from django.db.models import Count, Q, Case, When
+from django.template import engines, Context, Template, TemplateSyntaxError, VariableDoesNotExist
 from django.urls import reverse
 from django.utils import functional
 from django.utils.dates import WEEKDAYS
@@ -115,6 +116,13 @@ def validate_not_digit(value):
         raise ValidationError(_('This value cannot be a number.'))
 
 
+def django_template_validator(value):
+    try:
+        engines['django'].from_string(value)
+    except TemplateSyntaxError as e:
+        raise ValidationError(_('syntax error: %s') % e)
+
+
 class ICSError(Exception):
     pass
 
@@ -171,6 +179,9 @@ class Agenda(models.Model):
         'Category', verbose_name=_('Category'), blank=True, null=True, on_delete=models.SET_NULL
     )
     default_view = models.CharField(_('Default view'), max_length=20, choices=AGENDA_VIEWS, default='month')
+    booking_form_url = models.CharField(
+        _('Booking form URL'), max_length=200, blank=True, validators=[django_template_validator]
+    )
 
     class Meta:
         ordering = ['label']
@@ -289,6 +300,8 @@ class Agenda(models.Model):
         if hasattr(self, 'reminder_settings'):
             agenda['reminder_settings'] = self.reminder_settings.export_json()
         if self.kind == 'events':
+            agenda['default_view'] = self.default_view
+            agenda['booking_form_url'] = self.booking_form_url
             agenda['events'] = [x.export_json() for x in self.event_set.all()]
             if hasattr(self, 'notifications_settings'):
                 agenda['notifications_settings'] = self.notifications_settings.export_json()
@@ -487,6 +500,15 @@ class Agenda(models.Model):
             )
         return entries
 
+    def get_booking_form_url(self):
+        if not self.booking_form_url:
+            return
+        template_vars = Context(settings.TEMPLATE_VARS)
+        try:
+            return Template(self.booking_form_url).render(template_vars)
+        except (VariableDoesNotExist, TemplateSyntaxError):
+            return
+
 
 class VirtualMember(models.Model):
     '''Trough model to link virtual agendas to their real agendas.
diff --git a/chrono/manager/forms.py b/chrono/manager/forms.py
index 90bf36a..9d32086 100644
--- a/chrono/manager/forms.py
+++ b/chrono/manager/forms.py
@@ -75,6 +75,7 @@ class AgendaEditForm(AgendaAddForm):
             'minimal_booking_delay',
             'maximal_booking_delay',
             'default_view',
+            'booking_form_url',
         ]
 
     def __init__(self, *args, **kwargs):
@@ -84,6 +85,7 @@ class AgendaEditForm(AgendaAddForm):
             self.fields['maximal_booking_delay'].required = True
         if kwargs['instance'].kind != 'events':
             del self.fields['default_view']
+            del self.fields['booking_form_url']
 
 
 class ResourceAddForm(forms.ModelForm):
diff --git a/chrono/manager/templates/chrono/manager_agenda_event_fragment.html b/chrono/manager/templates/chrono/manager_agenda_event_fragment.html
index 6a920b0..5403f23 100644
--- a/chrono/manager/templates/chrono/manager_agenda_event_fragment.html
+++ b/chrono/manager/templates/chrono/manager_agenda_event_fragment.html
@@ -44,8 +44,8 @@
         </a>
     {% if settings_view %}
     <a rel="popup" class="delete" href="{% url 'chrono-manager-event-delete' pk=agenda.pk event_pk=event.pk %}?next=settings">{% trans "remove" %}</a>
-    {% elif not event.cancellation_status %}
-    <a rel="popup" class="link-action-text cancel" href="{% url 'chrono-manager-event-cancel' pk=agenda.pk event_pk=event.pk %}?next={{ request.path }}">{% trans "Cancel" %}</a>
+    {% elif agenda.booking_form_url %}
+    <a class="link-action-text" href="{{ agenda.get_booking_form_url }}?agenda={{ agenda.slug }}&event={{ event.slug }}">{% trans "Booking form" %}</a>
     {% endif %}
     <span class="occupation-bar"></span>
 </li>
diff --git a/chrono/manager/templates/chrono/manager_event_detail.html b/chrono/manager/templates/chrono/manager_event_detail.html
index c96e9a1..51cc378 100644
--- a/chrono/manager/templates/chrono/manager_event_detail.html
+++ b/chrono/manager/templates/chrono/manager_event_detail.html
@@ -33,6 +33,9 @@
 <a rel="popup" class="link-action-text cancel" href="{% url 'chrono-manager-event-cancel' pk=agenda.pk event_pk=event.pk %}?next={{ request.path }}">{% trans "Cancel" %}</a>
 <a href="{% url 'chrono-manager-event-edit' pk=agenda.id event_pk=object.id %}">{% trans "Options" %}</a>
 {% endif %}
+{% if object.agenda.booking_form_url %}
+<a href="{{ object.agenda.get_booking_form_url }}?agenda={{ object.agenda.slug }}&event={{ event.slug }}">{% trans "Booking form" %}</a>
+{% endif %}
 </span>
 {% endblock %}
 
diff --git a/tests/test_import_export.py b/tests/test_import_export.py
index 1487680..2cf3e25 100644
--- a/tests/test_import_export.py
+++ b/tests/test_import_export.py
@@ -134,6 +134,29 @@ def test_import_export(app):
     shutil.rmtree(tempdir)
 
 
+def test_import_export_events_agenda_options(app):
+    agenda = Agenda.objects.create(
+        label='Foo Bar',
+        kind='events',
+        default_view='open_events',
+        booking_form_url='{{ eservices_url }}backoffice/submission/inscription-aux-activites/',
+    )
+
+    output = get_output_of_command('export_site')
+    assert len(json.loads(output)['agendas']) == 1
+    import_site(data={}, clean=True)
+
+    with tempfile.NamedTemporaryFile() as f:
+        f.write(force_bytes(output))
+        f.flush()
+        call_command('import_site', f.name)
+
+    assert Agenda.objects.count() == 1
+    agenda = Agenda.objects.first()
+    assert agenda.default_view == 'open_events'
+    assert agenda.booking_form_url == '{{ eservices_url }}backoffice/submission/inscription-aux-activites/'
+
+
 def test_import_export_event_details(app):
     agenda = Agenda.objects.create(label='Foo Bar', kind='events')
     Event.objects.create(
diff --git a/tests/test_manager.py b/tests/test_manager.py
index 4def61e..54b2938 100644
--- a/tests/test_manager.py
+++ b/tests/test_manager.py
@@ -17,7 +17,6 @@ from django.urls import reverse
 from django.utils.encoding import force_text
 from django.utils.timezone import make_aware, now, localtime
 
-import datetime
 import freezegun
 import pytest
 import requests
@@ -877,6 +876,7 @@ def test_options_agenda(app, admin_user):
     assert resp.form['label'].value == 'Foo bar'
     resp.form['label'] = 'Foo baz'
     assert 'default_view' in resp.context['form'].fields
+    assert 'booking_form_url' in resp.context['form'].fields
     resp = resp.form.submit()
     assert resp.location.endswith('/manage/agendas/%s/settings' % agenda_events.pk)
     resp = resp.follow()
@@ -886,8 +886,10 @@ def test_options_agenda(app, admin_user):
 
     resp = app.get('/manage/agendas/%s/edit' % agenda_meetings.pk)
     assert 'default_view' not in resp.context['form'].fields
+    assert 'booking_form_url' not in resp.context['form'].fields
     resp = app.get('/manage/agendas/%s/edit' % agenda_virtual.pk)
     assert 'default_view' not in resp.context['form'].fields
+    assert 'booking_form_url' not in resp.context['form'].fields
 
 
 def test_options_agenda_cant_unset_delays(app, admin_user):
@@ -3805,27 +3807,26 @@ def test_event_cancellation(app, admin_user):
     event = Event.objects.create(
         label='xyz', start_datetime=now() + datetime.timedelta(days=1), places=10, agenda=agenda
     )
-    day = event.start_datetime
 
     login(app)
-    resp = app.get('/manage/agendas/%s/%d/%d/' % (agenda.id, day.year, day.month))
-    assert '0/10 bookings' in resp.text
+    resp = app.get('/manage/agendas/%d/events/%d/' % (agenda.pk, event.pk))
+    assert 'Bookings (0/10)' in resp.text
 
     resp = resp.click('Cancel', href='/cancel')
-    assert not 'related bookings' in resp.text
+    assert 'related bookings' not in resp.text
 
-    booking = Booking.objects.create(event=event)
-    booking2 = Booking.objects.create(event=event)
+    Booking.objects.create(event=event)
+    Booking.objects.create(event=event)
 
-    resp = app.get('/manage/agendas/%s/%d/%d/' % (agenda.id, day.year, day.month))
-    assert '2/10 bookings' in resp.text
+    resp = app.get('/manage/agendas/%d/events/%d/' % (agenda.pk, event.pk))
+    assert 'Bookings (2/10)' in resp.text
 
-    resp = resp.click('Cancel', href='/cancel')
+    resp = resp.click('Cancel', href='manage/agendas/%d/events/%d/cancel' % (agenda.pk, event.pk))
     assert '2 related bookings will also be cancelled.' in resp.text
 
     resp = resp.form.submit().follow()
     assert 'Cancelled' in resp.text
-    assert '0/10 bookings' in resp.text
+    assert 'Bookings (0/10)' in resp.text
     assert Booking.objects.filter(event=event, cancellation_datetime__isnull=False).count() == 2
 
 
@@ -3847,8 +3848,7 @@ def test_event_cancellation_error_report(app, admin_user):
     resp = resp.click('Cancellation error reports')
     assert 'No error report' in resp.text
 
-    resp = app.get('/manage/agendas/%s/%d/%d/' % (agenda.id, day.year, day.month))
-    resp = resp.click('Cancel', href='/cancel')
+    resp = app.get('/manage/agendas/%d/events/%d/cancel' % (agenda.pk, event.pk))
     resp = resp.form.submit().follow()
     assert 'Cancellation in progress' in resp.text
 
@@ -3891,17 +3891,58 @@ def test_event_cancellation_forbidden(app, admin_user):
     event = Event.objects.create(
         label='xyz', start_datetime=now() + datetime.timedelta(days=1), places=10, agenda=agenda
     )
-    booking = Booking.objects.create(event=event)
-    booking2 = Booking.objects.create(event=event, backoffice_url='http://example.org/backoffice/xx/')
-    day = event.start_datetime
+    Booking.objects.create(event=event)
+    Booking.objects.create(event=event, backoffice_url='http://example.org/backoffice/xx/')
 
     login(app)
-    resp = app.get('/manage/agendas/%s/%d/%d/' % (agenda.id, day.year, day.month))
-    resp = resp.click('Cancel', href='/cancel')
+    resp = app.get('/manage/agendas/%d/events/%d/cancel' % (agenda.pk, event.pk))
     assert 'event has bookings with no callback url configured' in resp.text
     assert 'Proceed with cancellation' not in resp.text
 
 
+def test_event_booking_form_url(settings, app, admin_user):
+    settings.TEMPLATE_VARS = {'eservices_url': 'http://demarches/'}
+    agenda = Agenda.objects.create(label='Events', kind='events')
+    event = Event.objects.create(
+        label='xyz', start_datetime=now() + datetime.timedelta(days=1), places=10, agenda=agenda
+    )
+    day = event.start_datetime
+
+    login(app)
+
+    assert agenda.get_booking_form_url() is None
+    resp = app.get('/manage/agendas/%d/%d/%d/' % (agenda.pk, day.year, day.month))
+    assert 'Booking form' not in resp.text
+    resp = app.get('/manage/agendas/%d/events/open/' % agenda.pk)
+    assert 'Booking form' not in resp.text
+    resp = app.get('/manage/agendas/%d/events/%d/' % (agenda.pk, event.pk))
+    assert 'Booking form' not in resp.text
+
+    agenda.booking_form_url = '{{ eservices_url }}backoffice/submission/inscription-aux-activites/'
+    agenda.save()
+    assert (
+        agenda.get_booking_form_url() == 'http://demarches/backoffice/submission/inscription-aux-activites/'
+    )
+    resp = app.get('/manage/agendas/%d/%d/%d/' % (agenda.pk, day.year, day.month))
+    assert (
+        '<a class="link-action-text" href="http://demarches/backoffice/submission/inscription-aux-activites/?agenda=%s&event=%s">Booking form</a>'
+        % (agenda.slug, event.slug)
+        in resp.text
+    )
+    resp = app.get('/manage/agendas/%d/events/open/' % agenda.pk)
+    assert (
+        '<a class="link-action-text" href="http://demarches/backoffice/submission/inscription-aux-activites/?agenda=%s&event=%s">Booking form</a>'
+        % (agenda.slug, event.slug)
+        in resp.text
+    )
+    resp = app.get('/manage/agendas/%d/events/%d/' % (agenda.pk, event.pk))
+    assert (
+        '<a href="http://demarches/backoffice/submission/inscription-aux-activites/?agenda=%s&event=%s">Booking form</a>'
+        % (agenda.slug, event.slug)
+        in resp.text
+    )
+
+
 def test_agenda_notifications(app, admin_user, managers_group):
     agenda = Agenda.objects.create(label='Events', kind='events')
 
-- 
2.28.0

