From 38d5fce24df9a822c15242e959a2948fdeb95de1 Mon Sep 17 00:00:00 2001 From: Serghei Mihai Date: Wed, 20 Jun 2018 22:16:14 +0200 Subject: [PATCH] manager: add a monthly view for meeting agendas (#21326) --- chrono/manager/static/css/style.scss | 41 ++++++- .../chrono/manager_agenda_day_view.html | 3 +- .../chrono/manager_agenda_month_view.html | 80 ++++++++++++++ chrono/manager/urls.py | 2 + chrono/manager/views.py | 104 +++++++++++++++++- tests/test_manager.py | 89 ++++++++++++++- tox.ini | 1 + 7 files changed, 312 insertions(+), 8 deletions(-) create mode 100644 chrono/manager/templates/chrono/manager_agenda_month_view.html diff --git a/chrono/manager/static/css/style.scss b/chrono/manager/static/css/style.scss index 9b42892..04a5705 100644 --- a/chrono/manager/static/css/style.scss +++ b/chrono/manager/static/css/style.scss @@ -101,21 +101,33 @@ table.agenda-table { } @for $i from 1 through 7 { - .agenda-table.desks-#{$i} { + .agenda-table { width: 100%; - thead th { width: (100%/$i)-1%; } + .desks-#{$i} { + thead th { width: (100%/$i)-1%; } + } } } -.agenda-table tbody th { +.agenda-table tbody tr th { box-sizing: border-box; - text-align: left; padding: 1ex 2ex; vertical-align: top; width: 8ex; + &.hour { + width: 5%; + text-align: left; + } + a { + color: #000; + border: 0; + } + &.weekday { + width: 12.5%; + } } -.agenda-table tbody tr:nth-child(2n+1) th, +.agenda-table tbody tr:nth-child(2n+1) th.hour, .agenda-table tbody tr:nth-child(2n+1) td { background: #f0f0f0; @media print { @@ -129,6 +141,10 @@ table.agenda-table { position: relative; } +.agenda-table tbody tr td.other-month { + background: transparent; +} + @for $i from 1 through 60 { table.hourspan-#{$i} tbody td { height: calc(#{$i} * 2.5em); @@ -160,6 +176,21 @@ table.agenda-table { } } +.monthview tbody td div.booking { + padding: 0; + transition: width 100ms ease-in, left 100ms ease-in, color 200ms ease-in; + text-indent: -9999px; + &:hover { + text-indent: 0; + color: inherit; + left: 0% !important; + width: 100% !important + } + span.desk { + display: block; + } +} + 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 af5a1cf..8599a84 100644 --- a/chrono/manager/templates/chrono/manager_agenda_day_view.html +++ b/chrono/manager/templates/chrono/manager_agenda_day_view.html @@ -27,6 +27,7 @@ {% trans 'Settings' %} {% endif %} {% trans 'Print' %} +{% trans 'Month view' %} {% endblock %} @@ -48,7 +49,7 @@ {% endif %} - {{ period|date:"TIME_FORMAT" }} + {{ period|date:"TIME_FORMAT" }} {% for desk_info in desk_infos %} diff --git a/chrono/manager/templates/chrono/manager_agenda_month_view.html b/chrono/manager/templates/chrono/manager_agenda_month_view.html new file mode 100644 index 0000000..b5453b4 --- /dev/null +++ b/chrono/manager/templates/chrono/manager_agenda_month_view.html @@ -0,0 +1,80 @@ +{% extends "chrono/manager_agenda_view.html" %} +{% load i18n %} + +{% block bodyargs %}class="monthview"{% endblock %} + +{% block breadcrumb %} +{{ block.super }} +{{ view.date|date:"F Y" }} +{% endblock %} + +{% block appbar %} +

+ + {{ view.date|date:"F Y" }} + {% with selected_month=view.date|date:"n" selected_year=view.date|date:"Y" %} + + {% endwith %} + +

+ +{% if user_can_manage %} + {% trans 'Settings' %} +{% endif %} +{% trans 'Print' %} +{% trans 'Day view' %} +{% endblock %} + + +{% block content %} +{% for week_days in view.get_timetable_infos %} +{% if forloop.first %} + + +{% endif %} + + + {% for day in week_days.days %} + + {% endfor %} + + {% for hour in week_days.periods %} + + + {% for day in week_days.days %} + + {% if forloop.parentloop.first %} + {% for slot in day.infos.opening_hours %} +
+ {% endfor %} + {% for slot in day.infos.booked_slots %} + + {% endfor %} + {% endif %} + + {% endfor %} +
+ {% endfor %} +{% if forloop.last %} + +
{% if not day.other_month %}{{ day.date|date:"l d" }}{% endif %}
{{ hour|date:"TIME_FORMAT" }}
+{% endif %} + +{% empty %} +
+

{% trans "No opening hours this month." %}

+
+{% endfor %} + +{% endblock %} diff --git a/chrono/manager/urls.py b/chrono/manager/urls.py index d975315..ba89398 100644 --- a/chrono/manager/urls.py +++ b/chrono/manager/urls.py @@ -24,6 +24,8 @@ urlpatterns = [ name='chrono-manager-agenda-add'), url(r'^agendas/(?P\w+)/$', views.agenda_view, name='chrono-manager-agenda-view'), + url(r'^agendas/(?P\w+)/(?P[0-9]{4})/(?P[0-9]+)/$', views.agenda_monthly_view, + name='chrono-manager-agenda-month-view'), url(r'^agendas/(?P\w+)/(?P[0-9]{4})/(?P[0-9]+)/(?P[0-9]+)/$', views.agenda_day_view, name='chrono-manager-agenda-day-view'), url(r'^agendas/(?P\w+)/settings$', views.agenda_settings, diff --git a/chrono/manager/views.py b/chrono/manager/views.py index 7a7720a..0a66f3e 100644 --- a/chrono/manager/views.py +++ b/chrono/manager/views.py @@ -29,7 +29,8 @@ from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ungettext from django.utils.encoding import force_text from django.views.generic import (DetailView, CreateView, UpdateView, - ListView, DeleteView, FormView, TemplateView, DayArchiveView) + ListView, DeleteView, FormView, TemplateView, DayArchiveView, + MonthArchiveView) from chrono.agendas.models import (Agenda, Event, MeetingType, TimePeriod, Booking, Desk, TimePeriodException, ICSError) @@ -268,6 +269,107 @@ class AgendaDayView(AgendaDateView, DayArchiveView): agenda_day_view = AgendaDayView.as_view() +class AgendaMonthView(AgendaDateView, MonthArchiveView): + template_name = 'chrono/manager_agenda_month_view.html' + + def get_previous_month_url(self): + previous_month = self.get_previous_month(self.date.date()) + return reverse('chrono-manager-agenda-month-view', + kwargs={'pk': self.agenda.id, + 'year': previous_month.year, + 'month': previous_month.month}) + + def get_next_month_url(self): + next_month = self.get_next_month(self.date.date()) + return reverse('chrono-manager-agenda-month-view', + kwargs={'pk': self.agenda.id, + 'year': next_month.year, + 'month': next_month.month}) + + def get_day(self): + return '1' + + def get_timetable_infos(self): + timeperiods = TimePeriod.objects.filter(desk__agenda=self.agenda) + if not timeperiods: + return + + first_week_number = self.date.isocalendar()[1] + last_month_day = self.get_next_month(self.date.date()) - datetime.timedelta(days=1) + last_week_number = last_month_day.isocalendar()[1] + + for week_number in range(first_week_number, last_week_number + 1): + yield self.get_week_timetable_infos(week_number-first_week_number, timeperiods) + + def get_week_timetable_infos(self, week_index, timeperiods): + + date = self.date + datetime.timedelta(week_index*7) + year, week_number, dow = date.isocalendar() + start_date = date - datetime.timedelta(dow) + + self.min_timeperiod = min([x.start_time for x in timeperiods]) + self.max_timeperiod = max([x.end_time for x in timeperiods]) + interval = datetime.timedelta(minutes=60) + + period = self.date.replace(hour=self.min_timeperiod.hour, minute=0) + max_date = self.date.replace(hour=self.max_timeperiod.hour, minute=0) + + periods = [] + while period < max_date: + periods.append(period) + period = period + interval + + return {'days': [self.get_day_timetable_infos(start_date + datetime.timedelta(i), interval) for i in range(1, 8)], + 'periods': periods} + + def get_day_timetable_infos(self, day, interval): + period = current_date = day.replace(hour=self.min_timeperiod.hour, minute=0) + timetable = {'date': current_date, + 'other_month': day.month != self.date.month, + 'infos': {'opening_hours': [], 'booked_slots': []}} + + desks = self.agenda.desk_set.all() + desks_len = len(desks) + max_date = day.replace(hour=self.max_timeperiod.hour, minute=0) + + # compute booking and opening hours only for current month + if self.date.month != day.month: + return timetable + + while period <= max_date: + period_end = period + interval + for desk_index, desk in enumerate(desks): + for event in [x for x in self.object_list if x.desk_id == desk.id and + x.start_datetime >= period and x.start_datetime < period_end]: + # don't consider cancelled bookings + bookings = [x for x in event.booking_set.all() if not x.cancellation_datetime] + if not bookings: + continue + booking = {'css_top': 100 * (event.start_datetime - current_date).seconds // 3600, + 'css_height': 100 * event.meeting_type.duration // 60, + 'css_width': 100.0 / desks_len, + 'css_left': 100.0 * desk_index / desks_len, + 'desk': desk, + 'booking': bookings[0] + } + timetable['infos']['booked_slots'].append(booking) + + # get desks opening hours on last period iteration + if period == max_date: + for hour in desk.get_opening_hours(current_date): + timetable['infos']['opening_hours'].append({ + 'css_top': 100 * (hour.begin - current_date).seconds // 3600, + 'css_height': 100 * (hour.end - hour.begin).seconds // 3600, + 'css_width': 100.0 / desks_len, + 'css_left': 100.0 * desk_index / desks_len + }) + period += interval + + return timetable + +agenda_monthly_view = AgendaMonthView.as_view() + + class ManagedAgendaMixin(object): agenda = None diff --git a/tests/test_manager.py b/tests/test_manager.py index 7f3c863..6513f59 100644 --- a/tests/test_manager.py +++ b/tests/test_manager.py @@ -1359,4 +1359,91 @@ def test_agenda_day_view_late_meeting(app, admin_user, manager_user, api_user): login(app) resp = app.get('/manage/agendas/%s/' % agenda.id, status=302).follow() assert resp.text.count('11 p.m.' in resp.text + assert '11 p.m.' in resp.text + +def test_agenda_month_view(app, admin_user, manager_user, api_user): + agenda = Agenda.objects.create(label='Passeports', kind='meetings') + desk = Desk.objects.create(agenda=agenda, label='Desk A') + + meetingtype = MeetingType(agenda=agenda, label='passeport', duration=20) + meetingtype.save() + + login(app) + resp = app.get('/manage/agendas/%s/' % agenda.id, status=302) + resp = resp.follow() + assert 'Month view' in resp.text + resp = resp.click('Month view') + today = datetime.date.today() + assert resp.request.url.endswith('%s/%s/' % (today.year, today.month)) + + assert 'Day view' in resp.text # date view link should be present + assert 'No opening hours this month.' in resp.text + + timeperiod_weekday = today.weekday() + timeperiod = TimePeriod(desk=desk, weekday=timeperiod_weekday, + start_time=datetime.time(10, 0), + end_time=datetime.time(18, 0)) + timeperiod.save() + resp = app.get('/manage/agendas/%s/%s/%s/' % (agenda.id, today.year, today.month)) + assert not 'No opening hours this month.' in resp.text + assert not '
=1.2.35 pytest-freezegun -- 2.19.0.rc2