From cc1982ff3a620b19b401aa970f2bf1ca3b30c630 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Mon, 5 Jul 2021 16:04:30 +0200
Subject: [PATCH] agenda: option to have min delay in working days (#55049)

---
 .../0089_agenda_event_display_template.py     | 13 ++++++--
 .../0096_min_booking_delay_working_days.py    | 16 ++++++++++
 chrono/agendas/models.py                      | 18 +++++++++--
 chrono/manager/forms.py                       |  3 ++
 .../chrono/manager_agenda_settings.html       | 10 ++++--
 chrono/settings.py                            |  4 ++-
 tests/manager/test_all.py                     | 31 ++++++++++++++++---
 tests/test_agendas.py                         | 21 +++++++++++++
 8 files changed, 103 insertions(+), 13 deletions(-)
 create mode 100644 chrono/agendas/migrations/0096_min_booking_delay_working_days.py

diff --git a/chrono/agendas/migrations/0089_agenda_event_display_template.py b/chrono/agendas/migrations/0089_agenda_event_display_template.py
index 3a6653f..c5a9ff3 100644
--- a/chrono/agendas/migrations/0089_agenda_event_display_template.py
+++ b/chrono/agendas/migrations/0089_agenda_event_display_template.py
@@ -1,7 +1,7 @@
-# Generated by Django 2.2.19 on 2021-05-19 12:23
-
 from django.db import migrations, models
 
+import chrono.agendas.models
+
 
 class Migration(migrations.Migration):
 
@@ -15,8 +15,15 @@ class Migration(migrations.Migration):
             name='event_display_template',
             field=models.CharField(
                 blank=True,
-                help_text='By default event labels will be displayed to users. This allows for a custom template to include additional informations. For example, "{{ event.label }} - {{ event.start_datetime }}" will show event datetime after label. Available variables: event.label (label), event.start_datetime (start date/time), event.places (places), event.remaining_places (remaining places), event.duration (duration), event.pricing (pricing).',
+                help_text=(
+                    'By default event labels will be displayed to users. '
+                    'This allows for a custom template to include additional informations. '
+                    'For example, "{{ event.label }} - {{ event.start_datetime }}" will show event datetime after label. '
+                    'Available variables: event.label (label), event.start_datetime (start date/time), event.places (places), '
+                    'event.remaining_places (remaining places), event.duration (duration), event.pricing (pricing).'
+                ),
                 max_length=256,
+                validators=[chrono.agendas.models.event_template_validator],
                 verbose_name='Event display template',
             ),
         ),
diff --git a/chrono/agendas/migrations/0096_min_booking_delay_working_days.py b/chrono/agendas/migrations/0096_min_booking_delay_working_days.py
new file mode 100644
index 0000000..cac1004
--- /dev/null
+++ b/chrono/agendas/migrations/0096_min_booking_delay_working_days.py
@@ -0,0 +1,16 @@
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('agendas', '0095_checked'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='agenda',
+            name='minimal_booking_delay_in_working_days',
+            field=models.BooleanField(default=False, verbose_name='Minimal booking delay in working days'),
+        ),
+    ]
diff --git a/chrono/agendas/models.py b/chrono/agendas/models.py
index 41a7642..16e1f48 100644
--- a/chrono/agendas/models.py
+++ b/chrono/agendas/models.py
@@ -164,6 +164,10 @@ class Agenda(models.Model):
         blank=True,
         validators=[MaxValueValidator(10000)],
     )
+    minimal_booking_delay_in_working_days = models.BooleanField(
+        _('Minimal booking delay in working days'),
+        default=False,
+    )
     maximal_booking_delay = models.PositiveIntegerField(
         _('Maximal booking delay (in days)'),
         default=None,
@@ -610,9 +614,17 @@ class Agenda(models.Model):
         # compute middle of today with localtime
         # 28 Mar 2021 12:00 +01:00
         t = localtime(now()).replace(hour=12, minute=0)
-        # advance of self.maximal_booking_delay - 1 days
-        # 28 Mar 2021 12:00 +01:00 == 28 Mars 2021 13:00 +02:00 as DST happend on 28 Mar 2021.
-        t += datetime.timedelta(days=self.minimal_booking_delay)
+
+        if settings.WORKING_DAY_CALENDAR is not None and self.minimal_booking_delay_in_working_days:
+            source_class = import_string(settings.WORKING_DAY_CALENDAR)
+            calendar = source_class()
+            t = calendar.add_working_days(t, self.minimal_booking_delay)
+            t = make_aware(datetime.datetime.combine(t, datetime.time(12, 0)))
+        else:
+            # advance of self.maximal_booking_delay - 1 days
+            # 28 Mar 2021 12:00 +01:00 == 28 Mars 2021 13:00 +02:00 as DST happend on 28 Mar 2021.
+            t += datetime.timedelta(days=self.minimal_booking_delay)
+
         # move to midnight of the day before, DST happen between 2h/3h so it
         # always exists because +/- timedelta does not move the timezone, only
         # localtime() does it.
diff --git a/chrono/manager/forms.py b/chrono/manager/forms.py
index 40b1f40..fdae13e 100644
--- a/chrono/manager/forms.py
+++ b/chrono/manager/forms.py
@@ -121,6 +121,7 @@ class AgendaBookingDelaysForm(forms.ModelForm):
         model = Agenda
         fields = [
             'minimal_booking_delay',
+            'minimal_booking_delay_in_working_days',
             'maximal_booking_delay',
         ]
 
@@ -129,6 +130,8 @@ class AgendaBookingDelaysForm(forms.ModelForm):
         if kwargs['instance'].kind != 'virtual':
             self.fields['minimal_booking_delay'].required = True
             self.fields['maximal_booking_delay'].required = True
+        if kwargs['instance'].kind != 'events' or settings.WORKING_DAY_CALENDAR is None:
+            del self.fields['minimal_booking_delay_in_working_days']
 
 
 class AgendaRolesForm(AgendaAddForm):
diff --git a/chrono/manager/templates/chrono/manager_agenda_settings.html b/chrono/manager/templates/chrono/manager_agenda_settings.html
index 5a04f4e..5726818 100644
--- a/chrono/manager/templates/chrono/manager_agenda_settings.html
+++ b/chrono/manager/templates/chrono/manager_agenda_settings.html
@@ -85,10 +85,16 @@
 <div>
 <ul>
   <li>{% trans "Minimal booking delay:" %}
-      {% if agenda.minimal_booking_delay is not None %}{{ agenda.minimal_booking_delay }} {% trans "days" %}
+      {% if agenda.minimal_booking_delay is not None %}
+        {% if agenda.minimal_booking_delay_in_working_days %}
+        {% blocktrans count count=agenda.minimal_booking_delay %}{{ count }} working day{% plural %}{{ count }} working days{% endblocktrans %}
+        {% else %}
+        {% blocktrans count count=agenda.minimal_booking_delay %}{{ count }} day{% plural %}{{ count }} days{% endblocktrans %}
+        {% endif %}
       {% else %}<i>{% trans "undefined" %}</i>{% endif %}</li>
   <li>{% trans "Maximal booking delay:" %}
-      {% if agenda.maximal_booking_delay is not None %}{{ agenda.maximal_booking_delay }} {% trans "days" %}
+      {% if agenda.maximal_booking_delay is not None %}
+        {% blocktrans count count=agenda.maximal_booking_delay %}{{ count }} day{% plural %}{{ count }} days{% endblocktrans %}
       {% else %}<i>{% trans "undefined" %}</i>{% endif %}</li>
 </ul>
 </div>
diff --git a/chrono/settings.py b/chrono/settings.py
index 1312e15..530d6d1 100644
--- a/chrono/settings.py
+++ b/chrono/settings.py
@@ -176,10 +176,12 @@ REQUESTS_TIMEOUT = 28
 try:
     import workalendar
 
+    WORKING_DAY_CALENDAR = 'workalendar.europe.France'
     EXCEPTIONS_SOURCES = {
-        'holidays': {'class': 'workalendar.europe.France', 'label': _('Holidays')},
+        'holidays': {'class': WORKING_DAY_CALENDAR, 'label': _('Holidays')},
     }
 except ImportError:
+    WORKING_DAY_CALENDAR = None
     EXCEPTIONS_SOURCES = {}
 
 TEMPLATE_VARS = {}
diff --git a/tests/manager/test_all.py b/tests/manager/test_all.py
index 55aa441..43e0ff3 100644
--- a/tests/manager/test_all.py
+++ b/tests/manager/test_all.py
@@ -499,27 +499,50 @@ def test_options_agenda(app, admin_user):
     assert 'open_events' not in [k for k, v in resp.context['form'].fields['default_view'].choices]
 
 
-def test_options_agenda_cant_unset_delays(app, admin_user):
+def test_options_events_agenda_delays(settings, app, admin_user):
+    settings.WORKING_DAY_CALENDAR = None
     agenda = Agenda.objects.create(label=u'Foo bar')
     assert agenda.minimal_booking_delay == 1
     app = login(app)
     url = '/manage/agendas/%s/booking-delays' % agenda.pk
     resp = app.get(url)
+    assert 'minimal_booking_delay_in_working_days' not in resp.context['form'].fields
     resp.form['minimal_booking_delay'] = None
     resp = resp.form.submit()
-    agenda = Agenda.objects.get(label=u'Foo bar')
+    agenda.refresh_from_db()
     assert agenda.minimal_booking_delay == 1
 
+    settings.WORKING_DAY_CALENDAR = 'workalendar.europe.France'
+    resp = app.get(url)
+    resp.form['minimal_booking_delay_in_working_days'] = True
+    resp = resp.form.submit()
+    agenda.refresh_from_db()
+    assert agenda.minimal_booking_delay_in_working_days is True
+
+
+def test_options_meetings_agenda_delays(app, admin_user):
+    agenda = Agenda.objects.create(label=u'Foo bar', kind='meetings', maximal_booking_delay=2)
+    assert agenda.maximal_booking_delay == 2
+    app = login(app)
+    url = '/manage/agendas/%s/booking-delays' % agenda.pk
+    resp = app.get(url)
+    assert 'minimal_booking_delay_in_working_days' not in resp.context['form'].fields
+    resp.form['maximal_booking_delay'] = None
+    resp = resp.form.submit()
+    agenda.refresh_from_db()
+    assert agenda.maximal_booking_delay == 2
+
 
-def test_options_virtual_agenda_can_unset_delays(app, admin_user):
+def test_options_virtual_agenda_delays(app, admin_user):
     agenda = Agenda.objects.create(label=u'Foo bar', kind='virtual', maximal_booking_delay=2)
     assert agenda.maximal_booking_delay == 2
     app = login(app)
     url = '/manage/agendas/%s/booking-delays' % agenda.pk
     resp = app.get(url)
+    assert 'minimal_booking_delay_in_working_days' not in resp.context['form'].fields
     resp.form['maximal_booking_delay'] = None
     resp = resp.form.submit()
-    agenda = Agenda.objects.get(label=u'Foo bar')
+    agenda.refresh_from_db()
     assert agenda.maximal_booking_delay is None
 
 
diff --git a/tests/test_agendas.py b/tests/test_agendas.py
index dcbd443..9c8f63e 100644
--- a/tests/test_agendas.py
+++ b/tests/test_agendas.py
@@ -196,6 +196,27 @@ def test_absence_reason_group_duplicate_slugs():
     assert group.slug == 'foo-baz-2'
 
 
+@pytest.mark.freeze_time('2021-07-09')
+def test_agenda_minimal_booking_delay_in_working_days(settings):
+    agenda = Agenda.objects.create(label='Agenda', minimal_booking_delay=1)
+
+    settings.WORKING_DAY_CALENDAR = None
+    agenda.minimal_booking_delay_in_working_days = True
+    agenda.save()
+    assert agenda.min_booking_datetime.date() == datetime.date(2021, 7, 10)
+    agenda.minimal_booking_delay_in_working_days = False
+    agenda.save()
+    assert agenda.min_booking_datetime.date() == datetime.date(2021, 7, 10)
+
+    settings.WORKING_DAY_CALENDAR = 'workalendar.europe.France'
+    agenda.minimal_booking_delay_in_working_days = True
+    agenda.save()
+    assert agenda.min_booking_datetime.date() == datetime.date(2021, 7, 12)
+    agenda.minimal_booking_delay_in_working_days = False
+    agenda.save()
+    assert agenda.min_booking_datetime.date() == datetime.date(2021, 7, 10)
+
+
 @pytest.mark.parametrize('with_prefetch', [True, False])
 def test_agenda_is_available_for_simple_management(settings, with_prefetch):
     settings.EXCEPTIONS_SOURCES = {
-- 
2.30.2

