From c932aea2805adeddae82fb343aae715f2fa155a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20P=C3=A9ters?= Date: Wed, 17 Jan 2018 11:16:47 +0100 Subject: [PATCH] manager: redo day view to always have one cell/one hour (#21213) --- chrono/manager/static/css/style.scss | 32 ++++++++-------- .../templates/chrono/manager_agenda_day_view.html | 19 ++++++---- chrono/manager/views.py | 43 +++++++++++++--------- tests/test_manager.py | 23 +++++++----- 4 files changed, 67 insertions(+), 50 deletions(-) diff --git a/chrono/manager/static/css/style.scss b/chrono/manager/static/css/style.scss index f216888..e1bb282 100644 --- a/chrono/manager/static/css/style.scss +++ b/chrono/manager/static/css/style.scss @@ -88,7 +88,6 @@ a.timeperiod-exception-all { } $dayview-column-width: 14vw; -$dayview-row-height: 4.5ex; .dayview thead th { width: $dayview-column-width; @@ -98,9 +97,8 @@ $dayview-row-height: 4.5ex; .dayview tbody th { box-sizing: border-box; text-align: left; - padding: 0 2ex; - line-height: $dayview-row-height; - height: $dayview-row-height; + padding: 1ex 2ex; + vertical-align: top; } .dayview tbody tr:nth-child(2n+1) th, @@ -111,28 +109,32 @@ $dayview-row-height: 4.5ex; } } -.dayview td { - padding: 0.5ex 1ex; +.dayview tbody td { + padding: 0 1ex; + vertical-align: top; + position: relative; } -/* attr(data-rowspan) is not supported by browsers; emulate it by getting - * the attribute value into a CSS variable. */ -@for $i from 2 through 100 { - [data-rowspan="#{$i}"] { --rowspan: #{$i}; } +@for $i from 1 through 60 { + table.hourspan-#{$i} tbody td { + height: calc(#{$i} * 2.5em); + } } -.dayview div[data-rowspan] { - margin-top: -1ex; +.dayview tbody td div { + z-index: 1; box-sizing: border-box; padding: 1ex; background: #eef; position: absolute; width: calc(#{$dayview-column-width} - 2ex); - height: calc(#{$dayview-row-height} * var(--rowspan) - 2ex); - min-height: calc(#{$dayview-row-height} * var(--rowspan) - 2ex); - border: 1px solid #666; + border: 1px solid #aaa; overflow: hidden; &:hover { height: auto; } } + +span.start-time { + font-size: 80%; +} diff --git a/chrono/manager/templates/chrono/manager_agenda_day_view.html b/chrono/manager/templates/chrono/manager_agenda_day_view.html index e9a5456..6768409 100644 --- a/chrono/manager/templates/chrono/manager_agenda_day_view.html +++ b/chrono/manager/templates/chrono/manager_agenda_day_view.html @@ -25,7 +25,7 @@ {% for period, desk_bookings in view.get_timeperiods %} {% if forloop.first %} - +
@@ -39,17 +39,20 @@ - {% for booking in desk_bookings %} + {% for bookings in desk_bookings %} {% endfor %} + + {% endfor %} + + {% endfor %} {% if forloop.last %} diff --git a/chrono/manager/views.py b/chrono/manager/views.py index 6787b7d..319f6fd 100644 --- a/chrono/manager/views.py +++ b/chrono/manager/views.py @@ -175,6 +175,10 @@ class AgendaDayView(DayArchiveView): def get_context_data(self, **kwargs): context = super(AgendaDayView, self).get_context_data(**kwargs) context['agenda'] = self.agenda + try: + context['hour_span'] = max(60 / self.agenda.get_base_meeting_duration(), 1) + except ValueError: # no meeting types defined + context['hour_span'] = 1 context['user_can_manage'] = self.agenda.can_be_managed(self.request.user) return context @@ -205,31 +209,34 @@ class AgendaDayView(DayArchiveView): min_timeperiod = min([x.start_time for x in timeperiods]) max_timeperiod = max([x.end_time for x in timeperiods]) - interval = datetime.timedelta(minutes=self.agenda.get_base_meeting_duration()) - current_date = self.date.replace(hour=min_timeperiod.hour, minute=min_timeperiod.minute) - max_date = self.date.replace(hour=max_timeperiod.hour, minute=max_timeperiod.minute) + interval = datetime.timedelta(minutes=60) + current_date = self.date.replace(hour=min_timeperiod.hour, minute=0) + if max_timeperiod.minute == 0: + max_date = self.date.replace(hour=max_timeperiod.hour, minute=0) + else: + # until the end of the last hour. + max_date = self.date.replace(hour=max_timeperiod.hour + 1, minute=0) desks = self.agenda.desk_set.all() while current_date < max_date: # for each timeslot return the timeslot date and a list of per-desk # bookings - bookings = [] + desks_bookings = [] # list of bookings for each desk for desk in desks: - booking = None - event = [x for x in self.object_list if x.desk_id == desk.id and x.start_datetime == current_date] - if event: - # if an event exist, check it has a non cancelled booking - event = event[0] - event_bookings = [x for x in event.booking_set.all() if not x.cancellation_datetime] - if event_bookings: - booking = event_bookings[0] - if event.meeting_type.duration > (interval.total_seconds() / 60): - booking.rowspan = int(event.meeting_type.duration / (interval.total_seconds() / 60)) - - bookings.append(booking) - - yield current_date, bookings + bookings = [] # bookings for this desk + finish_datetime = current_date + interval + for event in [x for x in self.object_list if x.desk_id == desk.id and + x.start_datetime >= current_date and x.start_datetime < finish_datetime]: + # don't consider cancelled bookings + for booking in [x for x in event.booking_set.all() if not x.cancellation_datetime]: + booking.css_top = int(100 * event.start_datetime.minute / 60) + booking.css_height = int(100 * event.meeting_type.duration / 60) + bookings.append(booking) + + desks_bookings.append(bookings) + + yield current_date, desks_bookings current_date += interval agenda_day_view = AgendaDayView.as_view() diff --git a/tests/test_manager.py b/tests/test_manager.py index 2119f92..3387f82 100644 --- a/tests/test_manager.py +++ b/tests/test_manager.py @@ -1192,13 +1192,19 @@ def test_agenda_day_view(app, admin_user, manager_user, api_user): resp = resp.follow() assert 'No opening hours this day.' in resp.body # no time pediod - TimePeriod(desk=desk, weekday=today.weekday(), + timeperiod = TimePeriod(desk=desk, weekday=today.weekday(), start_time=datetime.time(10, 0), - end_time=datetime.time(18, 0)).save() + end_time=datetime.time(18, 0)) + timeperiod.save() resp = app.get('/manage/agendas/%s/' % agenda.id, status=302).follow() assert not 'No opening hours this day.' in resp.body assert not 'div class="booked' in resp.body - assert resp.body.count('18 (not included) + + timeperiod.end_time = datetime.time(18, 30) # end during an hour + timeperiod.save() + resp = app.get('/manage/agendas/%s/' % agenda.id, status=302).follow() + assert resp.body.count('18 (included) # book some slots app.reset() @@ -1216,17 +1222,17 @@ def test_agenda_day_view(app, admin_user, manager_user, api_user): resp = app.get('/manage/agendas/%s/%d/%d/%d/' % ( agenda.id, date.year, date.month, date.day)) assert resp.body.count('div class="booked') == 2 - assert resp.body.count('data-rowspan') == 0 + assert 'hourspan-2' in resp.body # table CSS class + assert 'height: 50%; top: 0%;' in resp.body # booking cells - # create a shorted meeting type, this will double the number of rows and - # the bookings will have to span multiple rows. + # create a shorter meeting type, this will change the table CSS class + # (and visually this will give more room for events) meetingtype = MeetingType(agenda=agenda, label='Baz', duration=15) meetingtype.save() resp = app.get('/manage/agendas/%s/%d/%d/%d/' % ( agenda.id, date.year, date.month, date.day)) - assert resp.body.count('
{{ period|date:"TIME_FORMAT" }} - {% if booking %} - - {% endif %} -