From 2958305221a63a73c4081f125e6314a03253b5a9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Tue, 29 Jun 2021 16:43:26 +0200
Subject: [PATCH] agenda: option to disable check update when event is checked
 (#53933)

---
 .../0089_agenda_event_display_template.py     | 11 +++-
 chrono/agendas/migrations/0096_checked.py     | 18 +++++++
 chrono/agendas/models.py                      |  3 ++
 chrono/api/views.py                           | 15 ++++++
 chrono/manager/forms.py                       |  1 +
 .../templates/chrono/manager_event_check.html |  2 +
 .../manager_event_check_booking_fragment.html |  2 +
 .../manager_events_agenda_settings.html       | 41 ++++++++------
 chrono/manager/views.py                       |  2 +
 tests/api/test_booking.py                     | 32 +++++++++++
 tests/manager/test_all.py                     |  8 +++
 tests/manager/test_event.py                   | 54 +++++++++++++++++++
 12 files changed, 171 insertions(+), 18 deletions(-)
 create mode 100644 chrono/agendas/migrations/0096_checked.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..36df410 100644
--- a/chrono/agendas/migrations/0089_agenda_event_display_template.py
+++ b/chrono/agendas/migrations/0089_agenda_event_display_template.py
@@ -2,6 +2,8 @@
 
 from django.db import migrations, models
 
+import chrono.agendas.models
+
 
 class Migration(migrations.Migration):
 
@@ -15,8 +17,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_checked.py b/chrono/agendas/migrations/0096_checked.py
new file mode 100644
index 0000000..5b3655a
--- /dev/null
+++ b/chrono/agendas/migrations/0096_checked.py
@@ -0,0 +1,18 @@
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('agendas', '0095_checked'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='agenda',
+            name='disable_check_update',
+            field=models.BooleanField(
+                default=False, verbose_name='Prevent the check of bookings when event was marked as checked'
+            ),
+        ),
+    ]
diff --git a/chrono/agendas/models.py b/chrono/agendas/models.py
index f4286bc..ab33b47 100644
--- a/chrono/agendas/models.py
+++ b/chrono/agendas/models.py
@@ -222,6 +222,9 @@ class Agenda(models.Model):
     mark_event_checked_auto = models.BooleanField(
         _('Automatically mark event as checked when all bookings have been checked'), default=False
     )
+    disable_check_update = models.BooleanField(
+        _('Prevent the check of bookings when event was marked as checked'), default=False
+    )
     booking_check_filters = models.CharField(
         _('Filters'),
         max_length=250,
diff --git a/chrono/api/views.py b/chrono/api/views.py
index 3c268b3..7b001d8 100644
--- a/chrono/api/views.py
+++ b/chrono/api/views.py
@@ -1610,6 +1610,21 @@ class BookingAPI(APIView):
                 status=status.HTTP_400_BAD_REQUEST,
             )
 
+        if (
+            self.booking.event.checked
+            and self.booking.event.agenda.disable_check_update
+            and ('user_was_present' in request.data or 'user_absence_reason' in request.data)
+        ):
+            return Response(
+                {
+                    'err': 5,
+                    'err_class': 'event is marked as checked',
+                    'err_desc': _('event is marked as checked'),
+                    'errors': serializer.errors,
+                },
+                status=status.HTTP_400_BAD_REQUEST,
+            )
+
         if 'extra_data' in serializer.validated_data:
             extra_data = self.booking.extra_data or {}
             extra_data.update(serializer.validated_data['extra_data'] or {})
diff --git a/chrono/manager/forms.py b/chrono/manager/forms.py
index 40b1f40..8138928 100644
--- a/chrono/manager/forms.py
+++ b/chrono/manager/forms.py
@@ -837,6 +837,7 @@ class AgendaBookingCheckSettingsForm(forms.ModelForm):
             'booking_check_filters',
             'booking_user_block_template',
             'mark_event_checked_auto',
+            'disable_check_update',
         ]
         widgets = {'booking_user_block_template': forms.Textarea(attrs={'rows': 3})}
 
diff --git a/chrono/manager/templates/chrono/manager_event_check.html b/chrono/manager/templates/chrono/manager_event_check.html
index b8224dc..d5b5c16 100644
--- a/chrono/manager/templates/chrono/manager_event_check.html
+++ b/chrono/manager/templates/chrono/manager_event_check.html
@@ -38,6 +38,7 @@
         </tr>
       {% endif %}
       {% if booked_without_status %}
+        {% if not event.checked or not agenda.disable_check_update %}
         <tr class="booking all-bookings">
           <td colspan="2"><b>{% trans "Mark all bookings without status:" %}</b></td>
           <td class="booking-actions">
@@ -60,6 +61,7 @@
             </form>
           </td>
         </tr>
+        {% endif %}
       {% endif %}
       {% for booking in booked %}
         <tr class="booking">
diff --git a/chrono/manager/templates/chrono/manager_event_check_booking_fragment.html b/chrono/manager/templates/chrono/manager_event_check_booking_fragment.html
index 6980029..08af918 100644
--- a/chrono/manager/templates/chrono/manager_event_check_booking_fragment.html
+++ b/chrono/manager/templates/chrono/manager_event_check_booking_fragment.html
@@ -7,6 +7,7 @@
     ({{ booking.user_absence_reason }})
   {% endif %}
 </td>
+{% if not event.checked or not agenda.disable_check_update %}
 <td class="booking-actions" data-booking-id="{{ booking.id }}">
   <form method="post" action="{% url 'chrono-manager-booking-presence' pk=agenda.pk booking_pk=booking.pk %}" class="with-ajax">
     {% csrf_token %}
@@ -30,3 +31,4 @@
     </script>
   </form>
 </td>
+{% endif %}
diff --git a/chrono/manager/templates/chrono/manager_events_agenda_settings.html b/chrono/manager/templates/chrono/manager_events_agenda_settings.html
index e632f8b..1f8c83b 100644
--- a/chrono/manager/templates/chrono/manager_events_agenda_settings.html
+++ b/chrono/manager/templates/chrono/manager_events_agenda_settings.html
@@ -35,36 +35,43 @@
     <a rel="popup" class="button" href="{% url 'chrono-manager-agenda-booking-check-settings' pk=object.pk %}">{% trans 'Configure' %}</a>
 </h3>
 <div>
+    <ul>
     {% if has_absence_reasons %}
         {% if agenda.absence_reasons_group %}
-        <p>{% trans "Absence reasons group:" %} {{ agenda.absence_reasons_group }}</p>
-        <ul>
-          {% for reason in agenda.absence_reasons_group.absence_reasons.all %}
-          <li>{{ reason }}</li>
-          {% endfor %}
-        </ul>
+        <li>{% trans "Absence reasons group:" %} {{ agenda.absence_reasons_group }}
+            <ul>
+              {% for reason in agenda.absence_reasons_group.absence_reasons.all %}
+              <li>{{ reason }}</li>
+              {% endfor %}
+            </ul>
+        </li>
         {% else %}
-        <p>{% trans "No absence reasons configured for this agenda." %}</p>
+        <li>{% trans "No absence reasons configured for this agenda." %}</li>
         {% endif %}
     {% endif %}
 
     {% with agenda.get_booking_check_filters as check_filters %}
         {% if check_filters %}
-        <p>{% trans "Filters:" %}</p>
-        <ul>
-            {% for key in check_filters %}
-            <li>{{ key }}</li>
-            {% endfor %}
-        </ul>
+        <li>{% trans "Filters:" %}
+            <ul>
+                {% for key in check_filters %}
+                <li>{{ key }}</li>
+                {% endfor %}
+            </ul>
+        </li>
         {% else %}
-        <p>{% trans "No filters configured for this agenda." %}</p>
+        <li>{% trans "No filters configured for this agenda." %}</li>
         {% endif %}
     {% endwith %}
 
-    <p>{% trans "User block template:" %}</p>
-    <pre>{{ agenda.get_booking_user_block_template }}</pre>
+        <li>
+            {% trans "User block template:" %}
+            <pre>{{ agenda.get_booking_user_block_template }}</pre>
+        </li>
 
-    <p>{% trans "Automatically mark events when they are checked:" %} {{ agenda.mark_event_checked_auto|yesno }}</p>
+        <li>{% trans "Automatically mark event as checked when all bookings have been checked:" %} {{ agenda.mark_event_checked_auto|yesno }}</li>
+        <li>{% trans "Prevent the check of bookings when event was marked as checked:" %} {{ agenda.disable_check_update|yesno }}</li>
+    </ul>
 </div>
 </div>
 
diff --git a/chrono/manager/views.py b/chrono/manager/views.py
index 3a323ae..062491b 100644
--- a/chrono/manager/views.py
+++ b/chrono/manager/views.py
@@ -1995,6 +1995,7 @@ class EventCheckMixin(object):
         )
         self.event = get_object_or_404(
             Event,
+            Q(checked=False) | Q(agenda__disable_check_update=False),
             pk=kwargs.get('event_pk'),
             agenda=self.agenda,
             start_datetime__date__lte=now().date(),
@@ -2734,6 +2735,7 @@ class BookingCheckMixin(object):
     def get_booking(self, **kwargs):
         return get_object_or_404(
             Booking,
+            Q(event__checked=False) | Q(event__agenda__disable_check_update=False),
             pk=kwargs['booking_pk'],
             event__agenda=self.agenda,
             event__start_datetime__date__lte=now().date(),
diff --git a/tests/api/test_booking.py b/tests/api/test_booking.py
index 8ef642e..895cc6c 100644
--- a/tests/api/test_booking.py
+++ b/tests/api/test_booking.py
@@ -448,6 +448,21 @@ def test_booking_patch_api_present(app, user, flag):
     other_booking.refresh_from_db()
     assert other_booking.user_was_present is None  # not changed
 
+    # mark the event as checked
+    event.checked = True
+    event.save()
+    resp = app.patch_json('/api/booking/%s/' % booking.pk, params={'user_was_present': flag})
+    assert resp.json['err'] == 0
+
+    # now disable check update
+    agenda.disable_check_update = True
+    agenda.save()
+    resp = app.patch_json('/api/booking/%s/' % booking.pk, params={'user_was_present': flag}, status=400)
+    assert resp.json['err'] == 5
+    assert resp.json['err_desc'] == 'event is marked as checked'
+    resp = app.patch_json('/api/booking/%s/' % booking.pk)
+    assert resp.json['err'] == 0
+
 
 def test_booking_patch_api_absence_reason(app, user):
     agenda = Agenda.objects.create(kind='events')
@@ -514,6 +529,23 @@ def test_booking_patch_api_absence_reason(app, user):
     other_booking.refresh_from_db()
     assert other_booking.user_absence_reason == ''  # not changed
 
+    # mark the event as checked
+    event.checked = True
+    event.save()
+    resp = app.patch_json('/api/booking/%s/' % booking.pk, params={'user_absence_reason': reason.slug})
+    assert resp.json['err'] == 0
+
+    # now disable check update
+    agenda.disable_check_update = True
+    agenda.save()
+    resp = app.patch_json(
+        '/api/booking/%s/' % booking.pk, params={'user_absence_reason': reason.slug}, status=400
+    )
+    assert resp.json['err'] == 5
+    assert resp.json['err_desc'] == 'event is marked as checked'
+    resp = app.patch_json('/api/booking/%s/' % booking.pk)
+    assert resp.json['err'] == 0
+
 
 def test_booking_patch_api_extra_data(app, user):
     agenda = Agenda.objects.create(kind='events')
diff --git a/tests/manager/test_all.py b/tests/manager/test_all.py
index b2cc118..99606df 100644
--- a/tests/manager/test_all.py
+++ b/tests/manager/test_all.py
@@ -570,6 +570,14 @@ def test_options_agenda_booking_check_options(app, admin_user):
     agenda.refresh_from_db()
     assert agenda.mark_event_checked_auto is True
 
+    # check disable check
+    assert agenda.disable_check_update is False
+    resp = app.get(url)
+    resp.form['disable_check_update'] = True
+    resp = resp.form.submit()
+    agenda.refresh_from_db()
+    assert agenda.disable_check_update is True
+
     # check kind
     agenda.kind = 'meetings'
     agenda.save()
diff --git a/tests/manager/test_event.py b/tests/manager/test_event.py
index 783cb33..5201434 100644
--- a/tests/manager/test_event.py
+++ b/tests/manager/test_event.py
@@ -1385,6 +1385,40 @@ def test_event_check_booking(app, admin_user):
     assert booking.user_was_present is True
     assert booking.user_absence_reason == ''
 
+    # mark the event as checked
+    event.checked = True
+    event.save()
+    resp = app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk))
+    assert '/manage/agendas/%s/bookings/%s/presence' % (agenda.pk, booking.pk) in resp
+    assert '/manage/agendas/%s/bookings/%s/absence' % (agenda.pk, booking.pk) in resp
+    resp = app.post(
+        '/manage/agendas/%s/bookings/%s/absence' % (agenda.pk, booking.pk),
+        params={'csrfmiddlewaretoken': token},
+        status=302,
+    )
+    app.post(
+        '/manage/agendas/%s/bookings/%s/presence' % (agenda.pk, booking.pk),
+        params={'csrfmiddlewaretoken': token},
+        status=302,
+    )
+
+    # now disable check update
+    agenda.disable_check_update = True
+    agenda.save()
+    resp = app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk))
+    assert '/manage/agendas/%s/bookings/%s/presence' % (agenda.pk, booking.pk) not in resp
+    assert '/manage/agendas/%s/bookings/%s/absence' % (agenda.pk, booking.pk) not in resp
+    resp = app.post(
+        '/manage/agendas/%s/bookings/%s/absence' % (agenda.pk, booking.pk),
+        params={'csrfmiddlewaretoken': token},
+        status=404,
+    )
+    app.post(
+        '/manage/agendas/%s/bookings/%s/presence' % (agenda.pk, booking.pk),
+        params={'csrfmiddlewaretoken': token},
+        status=404,
+    )
+
 
 def test_event_check_booking_ajax(app, admin_user):
     group = AbsenceReasonGroup.objects.create(label='Foo bar')
@@ -1482,7 +1516,10 @@ def test_event_check_all_bookings(app, admin_user):
     event.refresh_from_db()
     assert event.checked is True
 
+    # event is checked
     booking3 = Booking.objects.create(event=event, user_first_name='User', user_last_name='51')
+    resp = app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk))
+    assert 'Mark all bookings without status' in resp
     app.post(
         '/manage/agendas/%s/events/%s/absence' % (agenda.pk, event.pk),
         params={'csrfmiddlewaretoken': token, 'reason': 'Foo reason'},
@@ -1496,3 +1533,20 @@ def test_event_check_all_bookings(app, admin_user):
     booking3.refresh_from_db()
     assert booking3.user_was_present is False
     assert booking3.user_absence_reason == 'Foo reason'
+
+    # now disable check update
+    agenda.disable_check_update = True
+    agenda.save()
+    Booking.objects.create(event=event, user_first_name='User', user_last_name='52')
+    resp = app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk))
+    assert 'Mark all bookings without status' not in resp
+    app.post(
+        '/manage/agendas/%s/events/%s/absence' % (agenda.pk, event.pk),
+        params={'csrfmiddlewaretoken': token, 'reason': 'Foo reason'},
+        status=404,
+    )
+    resp = app.post(
+        '/manage/agendas/%s/events/%s/absence' % (agenda.pk, event.pk),
+        params={'csrfmiddlewaretoken': token},
+        status=404,
+    )
-- 
2.30.2

