From 9dcd2c101259adce7ca8de224e4c0c1ee1002be7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Fri, 21 May 2021 10:29:20 +0200
Subject: [PATCH] manager: show when an event is checked (#53707)

---
 chrono/agendas/migrations/0090_checked.py     | 23 +++++++++
 chrono/agendas/models.py                      | 31 ++++++++++++
 chrono/api/views.py                           |  1 +
 chrono/manager/forms.py                       |  7 ++-
 .../chrono/manager_agenda_event_fragment.html |  9 +++-
 .../templates/chrono/manager_event_check.html | 10 ++++
 .../manager_events_agenda_settings.html       |  4 +-
 chrono/manager/urls.py                        |  5 ++
 chrono/manager/views.py                       | 23 ++++++---
 tests/manager/test_all.py                     |  8 +++
 tests/manager/test_event.py                   | 50 +++++++++++++++++++
 tests/test_api.py                             | 11 ++++
 12 files changed, 172 insertions(+), 10 deletions(-)
 create mode 100644 chrono/agendas/migrations/0090_checked.py

diff --git a/chrono/agendas/migrations/0090_checked.py b/chrono/agendas/migrations/0090_checked.py
new file mode 100644
index 0000000..4557b75
--- /dev/null
+++ b/chrono/agendas/migrations/0090_checked.py
@@ -0,0 +1,23 @@
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('agendas', '0089_agenda_event_display_template'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='agenda',
+            name='mark_event_checked_auto',
+            field=models.BooleanField(
+                default=False, verbose_name='Automatically mark events when they are checked'
+            ),
+        ),
+        migrations.AddField(
+            model_name='event',
+            name='checked',
+            field=models.BooleanField(default=False),
+        ),
+    ]
diff --git a/chrono/agendas/models.py b/chrono/agendas/models.py
index 5e1b44c..8a44a53 100644
--- a/chrono/agendas/models.py
+++ b/chrono/agendas/models.py
@@ -205,6 +205,9 @@ class Agenda(models.Model):
         _('Booking form URL'), max_length=200, blank=True, validators=[django_template_validator]
     )
     desk_simple_management = models.BooleanField(default=False)
+    mark_event_checked_auto = models.BooleanField(
+        _('Automatically mark events when they are checked'), default=False
+    )
     booking_check_filters = models.CharField(
         _('Filters'),
         max_length=250,
@@ -1206,6 +1209,7 @@ class Event(models.Model):
     full = models.BooleanField(default=False)
     cancelled = models.BooleanField(default=False)
     cancellation_scheduled = models.BooleanField(default=False)
+    checked = models.BooleanField(default=False)
     meeting_type = models.ForeignKey(MeetingType, null=True, on_delete=models.CASCADE)
     desk = models.ForeignKey('Desk', null=True, on_delete=models.CASCADE)
     resources = models.ManyToManyField('Resource')
@@ -1256,6 +1260,21 @@ class Event(models.Model):
         )
         self.almost_full = bool(self.booked_places >= 0.9 * self.places)
 
+    def set_is_checked(self):
+        if not self.agenda.mark_event_checked_auto:
+            return
+        if self.checked:
+            return
+        booking_qs = self.booking_set.filter(
+            cancellation_datetime__isnull=True,
+            in_waiting_list=False,
+            user_was_present__isnull=True,
+        )
+        if booking_qs.exists():
+            return
+        self.checked = True
+        self.save(update_fields=['checked'])
+
     def in_bookable_period(self):
         if self.publication_date and localtime(now()).date() < self.publication_date:
             return False
@@ -1686,6 +1705,18 @@ class Booking(models.Model):
             self.secondary_booking_set.update(in_waiting_list=True)
             self.save()
 
+    def mark_user_absence(self, reason=None):
+        self.user_absence_reason = reason
+        self.user_was_present = False
+        self.save()
+        self.event.set_is_checked()
+
+    def mark_user_presence(self):
+        self.user_absence_reason = ''
+        self.user_was_present = True
+        self.save()
+        self.event.set_is_checked()
+
     def get_user_block(self):
         template_vars = Context(settings.TEMPLATE_VARS)
         template_vars.update(
diff --git a/chrono/api/views.py b/chrono/api/views.py
index 4cdeaba..061effb 100644
--- a/chrono/api/views.py
+++ b/chrono/api/views.py
@@ -1523,6 +1523,7 @@ class BookingAPI(APIView):
         serializer.save()
         if 'user_was_present' in request.data:
             self.booking.secondary_booking_set.update(user_was_present=self.booking.user_was_present)
+            self.booking.event.set_is_checked()
         if 'user_absence_reason' in request.data:
             self.booking.secondary_booking_set.update(user_absence_reason=self.booking.user_absence_reason)
         if 'extra_data' in request.data:
diff --git a/chrono/manager/forms.py b/chrono/manager/forms.py
index e547924..0e8dd8a 100644
--- a/chrono/manager/forms.py
+++ b/chrono/manager/forms.py
@@ -784,7 +784,12 @@ class EventCancelForm(forms.ModelForm):
 class AgendaBookingCheckSettingsForm(forms.ModelForm):
     class Meta:
         model = Agenda
-        fields = ['absence_reasons_group', 'booking_check_filters', 'booking_user_block_template']
+        fields = [
+            'absence_reasons_group',
+            'booking_check_filters',
+            'booking_user_block_template',
+            'mark_event_checked_auto',
+        ]
         widgets = {'booking_user_block_template': forms.Textarea(attrs={'rows': 3})}
 
     def __init__(self, *args, **kwargs):
diff --git a/chrono/manager/templates/chrono/manager_agenda_event_fragment.html b/chrono/manager/templates/chrono/manager_agenda_event_fragment.html
index 5dc12cf..be0306b 100644
--- a/chrono/manager/templates/chrono/manager_agenda_event_fragment.html
+++ b/chrono/manager/templates/chrono/manager_agenda_event_fragment.html
@@ -11,8 +11,13 @@
     ><a href="{% if view_mode == 'settings_view' %}{% url 'chrono-manager-event-edit' pk=agenda.pk event_pk=event.pk %}?next=settings{% elif event.pk %}{% url 'chrono-manager-event-view' pk=agenda.pk event_pk=event.pk %}{% else %}{% url 'chrono-manager-event-create-recurrence' pk=agenda.pk event_identifier=event.slug %}{% endif %}">
     {% if event.cancellation_status %}
     <span class="tag">{{ event.cancellation_status }}</span>
-    {% elif event.main_list_full %}
-    <span class="full tag">{% trans "Full" %}</span>
+    {% else %}
+      {% if event.main_list_full %}
+      <span class="full tag">{% trans "Full" %}</span>
+      {% endif %}
+      {% if event.checked %}
+      <span class="checked tag">{% trans "Checked" %}</span>
+      {% endif %}
     {% endif %}
     <span class="event-info">
     {% if view_mode == 'settings_view' %}
diff --git a/chrono/manager/templates/chrono/manager_event_check.html b/chrono/manager/templates/chrono/manager_event_check.html
index c5d05ee..b8224dc 100644
--- a/chrono/manager/templates/chrono/manager_event_check.html
+++ b/chrono/manager/templates/chrono/manager_event_check.html
@@ -27,6 +27,16 @@
     </form>
     <table class="main check-bookings">
       <tbody>
+      {% if booked and not event.checked %}
+        <tr class="booking all-bookings">
+          <td class="booking-actions">
+            <form method="post" action="{% url 'chrono-manager-event-checked' pk=agenda.pk event_pk=object.pk %}">
+              {% csrf_token %}
+              <button class="submit-button">{% trans "Mark the event as checked" %}</button>
+            </form>
+          </td>
+        </tr>
+      {% endif %}
       {% if booked_without_status %}
         <tr class="booking all-bookings">
           <td colspan="2"><b>{% trans "Mark all bookings without status:" %}</b></td>
diff --git a/chrono/manager/templates/chrono/manager_events_agenda_settings.html b/chrono/manager/templates/chrono/manager_events_agenda_settings.html
index efbb164..e632f8b 100644
--- a/chrono/manager/templates/chrono/manager_events_agenda_settings.html
+++ b/chrono/manager/templates/chrono/manager_events_agenda_settings.html
@@ -61,8 +61,10 @@
         {% endif %}
     {% endwith %}
 
-    <p>{% trans "User block template" %}:</p>
+    <p>{% trans "User block template:" %}</p>
     <pre>{{ agenda.get_booking_user_block_template }}</pre>
+
+    <p>{% trans "Automatically mark events when they are checked:" %} {{ agenda.mark_event_checked_auto|yesno }}</p>
 </div>
 </div>
 
diff --git a/chrono/manager/urls.py b/chrono/manager/urls.py
index 7c82e0d..1083df1 100644
--- a/chrono/manager/urls.py
+++ b/chrono/manager/urls.py
@@ -221,6 +221,11 @@ urlpatterns = [
         views.event_absence,
         name='chrono-manager-event-absence',
     ),
+    url(
+        r'^agendas/(?P<pk>\d+)/events/(?P<event_pk>\d+)/checked$',
+        views.event_checked,
+        name='chrono-manager-event-checked',
+    ),
     url(
         r'^agendas/(?P<pk>\d+)/event_cancellation_report/(?P<report_pk>\d+)/$',
         views.event_cancellation_report,
diff --git a/chrono/manager/views.py b/chrono/manager/views.py
index f8d2ebe..4a862d8 100644
--- a/chrono/manager/views.py
+++ b/chrono/manager/views.py
@@ -2024,6 +2024,7 @@ class EventPresenceView(EventCheckMixin, ViewableAgendaMixin, View):
     def post(self, request, *args, **kwargs):
         bookings = self.get_bookings()
         bookings.update(user_absence_reason='', user_was_present=True)
+        self.event.set_is_checked()
         return self.response(request)
 
 
@@ -2045,12 +2046,24 @@ class EventAbsenceView(EventCheckMixin, ViewableAgendaMixin, FormView):
             qs_kwargs['user_absence_reason'] = form.cleaned_data['reason']
         bookings = self.get_bookings()
         bookings.update(user_was_present=False, **qs_kwargs)
+        self.event.set_is_checked()
         return self.response(request)
 
 
 event_absence = EventAbsenceView.as_view()
 
 
+class EventCheckedView(EventCheckMixin, ViewableAgendaMixin, View):
+    def post(self, request, *args, **kwargs):
+        if not self.event.checked:
+            self.event.checked = True
+            self.event.save(update_fields=['checked'])
+        return self.response(request)
+
+
+event_checked = EventCheckedView.as_view()
+
+
 class AgendaAddResourceView(ManagedAgendaMixin, FormView):
     template_name = 'chrono/manager_agenda_resource_form.html'
     form_class = AgendaResourceForm
@@ -2750,9 +2763,7 @@ class BookingCheckMixin(object):
 class BookingPresenceView(ViewableAgendaMixin, BookingCheckMixin, View):
     def post(self, request, *args, **kwargs):
         booking = self.get_booking(**kwargs)
-        booking.user_absence_reason = ''
-        booking.user_was_present = True
-        booking.save()
+        booking.mark_user_presence()
         return self.response(request, booking)
 
 
@@ -2770,10 +2781,10 @@ class BookingAbsenceView(ViewableAgendaMixin, BookingCheckMixin, FormView):
     def post(self, request, *args, **kwargs):
         booking = self.get_booking(**kwargs)
         form = self.get_form()
+        reason = None
         if form.is_valid():
-            booking.user_absence_reason = form.cleaned_data['reason']
-        booking.user_was_present = False
-        booking.save()
+            reason = form.cleaned_data['reason']
+        booking.mark_user_absence(reason=reason)
         return self.response(request, booking)
 
 
diff --git a/tests/manager/test_all.py b/tests/manager/test_all.py
index ee25152..64ae4fd 100644
--- a/tests/manager/test_all.py
+++ b/tests/manager/test_all.py
@@ -562,6 +562,14 @@ def test_options_agenda_booking_check_options(app, admin_user):
     assert agenda.booking_check_filters == 'foo,bar,baz'
     assert agenda.get_booking_check_filters() == ['foo', 'bar', 'baz']
 
+    # check auto checked
+    assert agenda.mark_event_checked_auto is False
+    resp = app.get(url)
+    resp.form['mark_event_checked_auto'] = True
+    resp = resp.form.submit()
+    agenda.refresh_from_db()
+    assert agenda.mark_event_checked_auto is True
+
     # check kind
     agenda.kind = 'meetings'
     agenda.save()
diff --git a/tests/manager/test_event.py b/tests/manager/test_event.py
index a1f397a..10cf48c 100644
--- a/tests/manager/test_event.py
+++ b/tests/manager/test_event.py
@@ -1178,6 +1178,40 @@ def test_event_check(app, admin_user):
     event.cancellation_datetime = None
 
 
+def test_event_checked(app, admin_user):
+    agenda = Agenda.objects.create(label='Events', kind='events', booking_check_filters='foo,bar')
+    event = Event.objects.create(
+        label='xyz',
+        start_datetime=now() - datetime.timedelta(days=1),
+        places=10,
+        agenda=agenda,
+    )
+    login(app)
+
+    resp = app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk))
+    assert 'Mark the event as checked' not in resp
+
+    Booking.objects.create(event=event, user_first_name='User')
+    resp = app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk))
+    assert 'Mark the event as checked' in resp
+    assert event.checked is False
+    resp = app.get('/manage/agendas/%s/settings' % agenda.id)
+    assert 'checked tag' not in resp
+
+    token = resp.context['csrf_token']
+    resp = app.post(
+        '/manage/agendas/%s/events/%s/checked' % (agenda.pk, event.pk),
+        params={'csrfmiddlewaretoken': token},
+    )
+    assert resp.location.endswith('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk))
+    event.refresh_from_db()
+    assert event.checked is True
+    resp = resp.follow()
+    assert 'Mark the event as checked' not in resp
+    resp = app.get('/manage/agendas/%s/settings' % agenda.id)
+    assert 'checked tag' in resp
+
+
 def test_event_check_filters(app, admin_user):
     agenda = Agenda.objects.create(label='Events', kind='events', booking_check_filters='foo,bar')
     event = Event.objects.create(
@@ -1274,6 +1308,7 @@ def test_event_check_booking(app, admin_user):
         agenda=agenda,
     )
     booking = Booking.objects.create(event=event, user_first_name='User', user_last_name='42')
+    assert agenda.mark_event_checked_auto is False
 
     login(app)
     resp = app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk))
@@ -1294,6 +1329,11 @@ def test_event_check_booking(app, admin_user):
     booking.refresh_from_db()
     assert booking.user_was_present is True
     assert booking.user_absence_reason == ''
+    event.refresh_from_db()
+    assert event.checked is False
+
+    agenda.mark_event_checked_auto = True
+    agenda.save()
 
     # set as absent without reason
     resp = app.post(
@@ -1306,6 +1346,8 @@ def test_event_check_booking(app, admin_user):
     booking.refresh_from_db()
     assert booking.user_was_present is False
     assert booking.user_absence_reason == ''
+    event.refresh_from_db()
+    assert event.checked is True
 
     agenda.absence_reasons_group = group
     agenda.save()
@@ -1388,6 +1430,7 @@ def test_event_check_all_bookings(app, admin_user):
         agenda=agenda,
     )
     booking1 = Booking.objects.create(event=event, user_first_name='User', user_last_name='42')
+    assert agenda.mark_event_checked_auto is False
 
     login(app)
     resp = app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk))
@@ -1402,6 +1445,11 @@ def test_event_check_all_bookings(app, admin_user):
     booking1.refresh_from_db()
     assert booking1.user_was_present is False
     assert booking1.user_absence_reason == ''
+    event.refresh_from_db()
+    assert event.checked is False
+
+    agenda.mark_event_checked_auto = True
+    agenda.save()
 
     resp = app.get('/manage/agendas/%s/events/%s/check' % (agenda.pk, event.pk))
     assert 'Mark all bookings without status' not in resp
@@ -1423,6 +1471,8 @@ def test_event_check_all_bookings(app, admin_user):
     booking2.refresh_from_db()
     assert booking2.user_was_present is True
     assert booking2.user_absence_reason == ''
+    event.refresh_from_db()
+    assert event.checked is True
 
     booking3 = Booking.objects.create(event=event, user_first_name='User', user_last_name='51')
     app.post(
diff --git a/tests/test_api.py b/tests/test_api.py
index c4345ef..a582908 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -2646,6 +2646,7 @@ def test_booking_patch_api_present(app, user, flag):
     agenda = Agenda.objects.create(kind='events')
     event = Event.objects.create(agenda=agenda, start_datetime=now(), places=10)
     booking = Booking.objects.create(event=event)
+    assert agenda.mark_event_checked_auto is False
 
     app.authorization = ('Basic', ('john.doe', 'password'))
 
@@ -2653,6 +2654,16 @@ def test_booking_patch_api_present(app, user, flag):
     app.patch_json('/api/booking/%s/' % booking.pk, params={'user_was_present': flag})
     booking.refresh_from_db()
     assert booking.user_was_present == flag
+    event.refresh_from_db()
+    assert event.checked is False
+
+    agenda.mark_event_checked_auto = True
+    agenda.save()
+    app.patch_json('/api/booking/%s/' % booking.pk, params={'user_was_present': flag})
+    booking.refresh_from_db()
+    assert booking.user_was_present == flag
+    event.refresh_from_db()
+    assert event.checked == (flag is not None)
 
     # reset
     app.patch_json('/api/booking/%s/' % booking.pk, params={'user_was_present': None})
-- 
2.30.2

