From f6bb6282ac5a817ec8c8a883f3cf1ccd5bbe58e2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20P=C3=A9ters?= <fpeters@entrouvert.com>
Date: Sun, 16 Feb 2020 17:05:52 +0100
Subject: [PATCH] manager: display exceptions in day/month views (#39906)

---
 chrono/manager/static/css/style.scss          |  6 +++-
 .../chrono/manager_agenda_day_view.html       |  5 +++
 .../manager_meetings_agenda_month_view.html   |  5 +++
 chrono/manager/views.py                       | 33 +++++++++++++++++--
 tests/test_manager.py                         | 32 ++++++++++++++++++
 5 files changed, 77 insertions(+), 4 deletions(-)

diff --git a/chrono/manager/static/css/style.scss b/chrono/manager/static/css/style.scss
index 348e232..0f90197 100644
--- a/chrono/manager/static/css/style.scss
+++ b/chrono/manager/static/css/style.scss
@@ -172,13 +172,17 @@ a.timeperiod-exception-all {
 	padding: 1ex;
 	position: absolute;
 	overflow: hidden;
-	&.opening-hours {
+	&.opening-hours, &.exception-hours {
 		z-index: 1;
 		background: #b1ea4d linear-gradient(135deg, #b1ea4d 0%, #459522 100%);
 		opacity: 0.6;
 		left: 0.5ex;
 		width: calc(100% - 1ex);
 	}
+	&.exception-hours {
+		background: #fee linear-gradient(135deg, #fee 0%, #fdd 100%);
+		text-align: center;
+	}
 	&.booking {
 		background: #eef linear-gradient(135deg, #eef 0%, #ddf 100%);
 		box-shadow: 0 0 1px 0 #2d2dad;
diff --git a/chrono/manager/templates/chrono/manager_agenda_day_view.html b/chrono/manager/templates/chrono/manager_agenda_day_view.html
index 766d8bb..492d670 100644
--- a/chrono/manager/templates/chrono/manager_agenda_day_view.html
+++ b/chrono/manager/templates/chrono/manager_agenda_day_view.html
@@ -60,6 +60,11 @@
                style="height: {{ slot.css_height }}%; top: {{ slot.css_top }}%;"
                   >{{slot.begin}} {{slot.end}}</div>
           {% endfor %}
+          {% for slot in desk_info.exceptions %}
+          <div class="exception-hours"
+               style="height: {{ slot.css_height }}%; top: {{ slot.css_top }}%;"
+               >{% if slot.label %}<span class="exception-label">{{slot.label}}</span>{% endif %}</div>
+          {% endfor %}
         {% endif %}
 
         {% for booking in desk_info.bookings %}
diff --git a/chrono/manager/templates/chrono/manager_meetings_agenda_month_view.html b/chrono/manager/templates/chrono/manager_meetings_agenda_month_view.html
index 7f0b963..2e917e7 100644
--- a/chrono/manager/templates/chrono/manager_meetings_agenda_month_view.html
+++ b/chrono/manager/templates/chrono/manager_meetings_agenda_month_view.html
@@ -26,6 +26,11 @@
       {% for slot in day.infos.opening_hours %}
       <div class="opening-hours" style="height:{{ slot.css_height|stringformat:".1f" }}%;top:{{ slot.css_top|stringformat:".1f" }}%;width:{{ slot.css_width|stringformat:".1f" }}%;left:{{ slot.css_left|stringformat:".1f" }}%;"></div>
       {% endfor %}
+
+      {% for slot in day.infos.exceptions %}
+      <div class="exception-hours" style="height:{{ slot.css_height|stringformat:".1f" }}%;top:{{ slot.css_top|stringformat:".1f" }}%;width:{{ slot.css_width|stringformat:".1f" }}%;left:{{ slot.css_left|stringformat:".1f" }}%;" {% if slot.label %}title="{{ slot.label }}"{% endif %}></div>
+      {% endfor %}
+
       {% for slot in day.infos.booked_slots %}
       <div class="booking" style="left:{{ slot.css_left|stringformat:".1f" }}%;height:{{ slot.css_height|stringformat:".1f" }}%;top:{{ slot.css_top|stringformat:".1f" }}%;width:{{ slot.css_width|stringformat:".1f" }}%">
         <span class="start-time">{{slot.booking.event.start_datetime|date:"TIME_FORMAT"}}</span>
diff --git a/chrono/manager/views.py b/chrono/manager/views.py
index 634e2a4..c949c81 100644
--- a/chrono/manager/views.py
+++ b/chrono/manager/views.py
@@ -380,7 +380,7 @@ class AgendaDayView(AgendaDateView, DayArchiveView):
             # until the end of the last hour.
             max_date += datetime.timedelta(hours=1)
 
-        desks = self.agenda.desk_set.all()
+        desks = self.agenda.desk_set.all().prefetch_related('timeperiodexception_set')
 
         first = True
         while current_date < max_date:
@@ -399,6 +399,20 @@ class AgendaDayView(AgendaDateView, DayArchiveView):
                                 'css_height': 100 * (opening_hour.end - opening_hour.begin).seconds // 3600,
                             }
                         )
+
+                    # and exceptions for this desk
+                    info['exceptions'] = []
+                    for exception in desk.timeperiodexception_set.all():
+                        if exception.end_datetime < start_date:
+                            continue
+                        if exception.start_datetime > max_date:
+                            continue
+                        start_max = max(start_date, exception.start_datetime)
+                        end_min = min(max_date, exception.end_datetime)
+                        exception.css_top = int(100 * (start_max - start_date).seconds // 3600)
+                        exception.css_height = int(100 * (end_min - start_max).seconds // 3600)
+                        info['exceptions'].append(exception)
+
                 infos.append(info)
                 info['bookings'] = bookings = []  # bookings for this desk
                 finish_datetime = current_date + interval
@@ -511,10 +525,10 @@ class AgendaMonthView(AgendaDateView, MonthArchiveView):
             'date': current_date,
             'today': day.date() == datetime.date.today(),
             'other_month': day.month != self.date.month,
-            'infos': {'opening_hours': [], 'booked_slots': []},
+            'infos': {'opening_hours': [], 'exceptions': [], 'booked_slots': []},
         }
 
-        desks = self.agenda.desk_set.all()
+        desks = self.agenda.desk_set.all().prefetch_related('timeperiodexception_set')
         desks_len = len(desks)
         max_date = day.replace(hour=self.max_timeperiod.hour, minute=0)
 
@@ -557,6 +571,19 @@ class AgendaMonthView(AgendaDateView, MonthArchiveView):
                                 'css_left': left,
                             }
                         )
+                    for exception in desk.timeperiodexception_set.all():
+                        if exception.end_datetime < current_date:
+                            continue
+                        if exception.start_datetime > max_date:
+                            continue
+                        start_max = max(current_date, exception.start_datetime)
+                        end_min = min(max_date, exception.end_datetime)
+                        exception.css_top = int(100 * (start_max - current_date).seconds // 3600)
+                        exception.css_height = int(100 * (end_min - start_max).seconds // 3600)
+                        exception.css_width = width
+                        exception.css_left = left
+                        timetable['infos']['exceptions'].append(exception)
+
                 left += width + 1
             period += interval
 
diff --git a/tests/test_manager.py b/tests/test_manager.py
index 6f9e4c1..f8f858d 100644
--- a/tests/test_manager.py
+++ b/tests/test_manager.py
@@ -4,6 +4,7 @@ from __future__ import unicode_literals
 import copy
 import json
 import os
+import re
 
 from django.contrib.auth.models import User, Group
 from django.utils.encoding import force_text
@@ -1724,6 +1725,23 @@ def test_agenda_day_view(app, admin_user, manager_user, api_user):
     agenda.save()
     resp = app.get('/manage/agendas/%s/%d/%d/%d/' % (agenda.id, date.year, date.month, date.day), status=200)
 
+    # display exception
+    TimePeriodException.objects.create(
+        label='Exception for the afternoon',
+        desk=desk,
+        start_datetime=make_aware(datetime.datetime(date.year, date.month, date.day, 13, 0)),
+        end_datetime=make_aware(datetime.datetime(date.year, date.month, date.day, 23, 0)),
+    )
+    resp = app.get('/manage/agendas/%s/%d/%d/%d/' % (agenda.id, date.year, date.month, date.day), status=200)
+    # day is displaying rows from 10am to 6pm,
+    # opening hours, 10am to 1pm gives top: 300%
+    # rest of the day, 1pm to 6(+1)pm gives 600%
+    assert resp.pyquery.find('.exception-hours')[0].attrib == {
+        'class': 'exception-hours',
+        'style': 'height: 600%; top: 300%;',
+    }
+    assert resp.pyquery.find('.exception-hours span')[0].text == 'Exception for the afternoon'
+
 
 def test_agenda_day_view_late_meeting(app, admin_user, manager_user, api_user):
     agenda = Agenda.objects.create(label='New Example', kind='meetings')
@@ -1888,6 +1906,20 @@ def test_agenda_month_view(app, admin_user, manager_user, api_user):
     resp = app.get('/manage/agendas/%s/%s/%s/' % (agenda.id, today.year, today.month))
     assert not 'No opening hours this month.' in resp.text
 
+    # display exception
+    TimePeriodException.objects.create(
+        label='Exception for a December day',
+        desk=desk,
+        start_datetime=make_aware(datetime.datetime(2018, 12, 15, 5, 0)),
+        end_datetime=make_aware(datetime.datetime(2018, 12, 15, 23, 0)),
+    )
+    resp = app.get('/manage/agendas/%s/%s/%s/' % (agenda.id, today.year, today.month))
+    assert resp.pyquery.find('.exception-hours')[0].attrib == {
+        'class': 'exception-hours',
+        'style': 'height:800.0%;top:0.0%;width:48.0%;left:50.0%;',
+        'title': 'Exception for a December day',
+    }
+
 
 def test_agenda_month_view_weekend(app, admin_user, manager_user, api_user):
     agenda = Agenda.objects.create(label='Passeports', kind='meetings')
-- 
2.25.1

