From 4ac4fd25de62b5f2737263ff4ce617fc675443b5 Mon Sep 17 00:00:00 2001 From: Serghei Mihai Date: Wed, 20 Jun 2018 22:16:14 +0200 Subject: [PATCH 3/3] manager: add agenda's month and booking view (#21326) --- chrono/manager/static/css/style.scss | 62 ++++++++++- .../chrono/manager_agenda_day_view.html | 1 + .../chrono/manager_agenda_month_view.html | 78 +++++++++++++ chrono/manager/urls.py | 2 + chrono/manager/views.py | 103 +++++++++++++++++- tests/test_manager.py | 68 ++++++++++++ tox.ini | 1 + 7 files changed, 310 insertions(+), 5 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 3aebd4a..857ce27 100644 --- a/chrono/manager/static/css/style.scss +++ b/chrono/manager/static/css/style.scss @@ -91,10 +91,39 @@ a.timeperiod-exception-all { min-width: 24ex; } -.dayview table { +.dayview table, .monthview table { border-collapse: collapse; } +.monthview table { + width: 100%; + th.hour { + width: 5%; + font-weight: normal; + } + th.weekday { + width: 12.5%; + font-weight: normal; + a { + color: #000; + text-decoration: underline; + } + &.current-day { + font-weight: bold; + a { + color: #07C; + } + } + } + td { + position: relative; + } + span.desk { + display: block; + padding: 2px; + } +} + .dayview thead th { width: 14vw; padding-bottom: 1ex; @@ -116,26 +145,38 @@ a.timeperiod-exception-all { } .dayview tbody tr:nth-child(2n+1) th, -.dayview tbody tr:nth-child(2n+1) td { +.monthview tbody tr:nth-child(2n+1) th, +.dayview tbody tr:nth-child(2n+1) td, +.monthview tbody tr:nth-child(2n+1) td { background: #f0f0f0; + background-clip: padding-box; @media print { border-top: 1px solid #aaa; } } -.dayview tbody td { +.dayview tbody td, .monthview tbody td { padding: 0 1ex; vertical-align: top; position: relative; } +.monthview tbody tr td { + border-left: 1px solid #d0d0d0; + border-right: 1px solid #d0d0d0; + &.other-month { + background: #fafafa; + background-clip: padding-box; + } +} + @for $i from 1 through 60 { table.hourspan-#{$i} tbody td { height: calc(#{$i} * 2.5em); } } -.dayview tbody td div { +.dayview tbody td div, .monthview tbody td div { box-sizing: border-box; padding: 1ex; position: absolute; @@ -146,6 +187,7 @@ a.timeperiod-exception-all { opacity: 0.3; left: 0.5ex; width: calc(100% - 1ex); + position: absolute; } &.booking { background: #eef linear-gradient(135deg, #eef 0%, #ddf 100%); @@ -160,6 +202,18 @@ a.timeperiod-exception-all { } } +.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.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 a03c61a..85e9c65 100644 --- a/chrono/manager/templates/chrono/manager_agenda_day_view.html +++ b/chrono/manager/templates/chrono/manager_agenda_day_view.html @@ -26,6 +26,7 @@ {% trans 'Settings' %} {% endif %} {% trans 'Print' %} +{% trans 'Month view' %} {% endblock %} {% block content %} 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..08018f8 --- /dev/null +++ b/chrono/manager/templates/chrono/manager_agenda_month_view.html @@ -0,0 +1,78 @@ +{% 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 day.infos.opening_hours %}{{ 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 fc9f9ca..a31d409 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,106 @@ 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 datetime.date.today().day + + def get_day_timetable_infos(self, day): + period = current_date = day.replace(hour=self.min_timeperiod.hour, minute=0) + timetable = {'date': current_date, + '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 + self.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 += self.interval + + return timetable + + def get_week_timetable_infos(self, i, timeperiods): + date = self.date.replace(day=1) + datetime.timedelta(i*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]) + self.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 + self.interval + + return {'days': [self.get_day_timetable_infos(start_date + datetime.timedelta(i)) for i in range(1, 8)], + 'periods': periods} + + def get_timetable_infos(self): + timeperiods = TimePeriod.objects.filter(desk__agenda=self.agenda) + if not timeperiods: + return + + first_month_day = self.date.replace(day=1) + first_week_number = first_month_day.isocalendar()[1] + last_month_day = self.get_next_month(first_month_day.date()) - datetime.timedelta(days=1) + last_week_number = last_month_day.isocalendar()[1] + + for i, week_number in enumerate(range(first_week_number, last_week_number + 1)): + yield self.get_week_timetable_infos(i, timeperiods) + +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..1916e58 100644 --- a/tests/test_manager.py +++ b/tests/test_manager.py @@ -1360,3 +1360,71 @@ def test_agenda_day_view_late_meeting(app, admin_user, manager_user, api_user): resp = app.get('/manage/agendas/%s/' % agenda.id, status=302).follow() assert resp.text.count('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 = TimePeriod(desk=desk, weekday=today.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.18.0