diff --git a/chrono/manager/static/css/style.scss b/chrono/manager/static/css/style.scss
index 9b42892b..04a57052 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 af5a1cfb..8599a842 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 00000000..b5453b46
--- /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 %}
+ {% if not day.other_month %}{{ day.date|date:"l d" }}{% endif %} |
+ {% endfor %}
+
+ {% for hour in week_days.periods %}
+
+ {{ hour|date:"TIME_FORMAT" }} |
+ {% 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 %}
+
+
+{% endif %}
+
+{% empty %}
+
+ {% trans "No opening hours this month." %}
+
+{% endfor %}
+
+{% endblock %}
diff --git a/chrono/manager/urls.py b/chrono/manager/urls.py
index d9753158..ba893982 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 7a7720a0..bd187a32 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,110 @@ 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:
+ left = 1
+ period_end = period + interval
+ for desk_index, desk in enumerate(desks):
+ width = (98.0 / desks_len) - 1
+ 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': width,
+ 'css_left': left,
+ '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': width,
+ 'css_left': left,
+ })
+ left += width + 1
+ 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 7f3c863e..e064481b 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
|