summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSerghei Mihai <smihai@entrouvert.com>2018-06-20 20:16:14 (GMT)
committerSerghei Mihai <smihai@entrouvert.com>2018-07-19 13:17:18 (GMT)
commitb420dfc64286bbd58b72047bcc3dfbf5f969616b (patch)
tree525ae09f8f6e69b639af63bd7f13fe645124f175
parente1780be59a3e50c1fc874302e15df013fc379f7e (diff)
downloadchrono-wip/vue-mensuelle.zip
chrono-wip/vue-mensuelle.tar.gz
chrono-wip/vue-mensuelle.tar.bz2
manager: add agenda's month and booking view (#21326)wip/vue-mensuelle
-rw-r--r--chrono/manager/static/css/style.scss46
-rw-r--r--chrono/manager/templates/chrono/manager_agenda_day_view.html1
-rw-r--r--chrono/manager/templates/chrono/manager_agenda_month_view.html69
-rw-r--r--chrono/manager/urls.py2
-rw-r--r--chrono/manager/views.py99
5 files changed, 212 insertions, 5 deletions
diff --git a/chrono/manager/static/css/style.scss b/chrono/manager/static/css/style.scss
index 3aebd4a..faea847 100644
--- a/chrono/manager/static/css/style.scss
+++ b/chrono/manager/static/css/style.scss
@@ -91,10 +91,32 @@ a.timeperiod-exception-all {
min-width: 24ex;
}
-.dayview table {
+.dayview table, .monthview table {
border-collapse: collapse;
}
+.monthview table {
+ width: 100%;
+ th {
+ width: 5%;
+ }
+ th.weekday {
+ width: 12.5%;
+ &.previous-month, &.next-month {
+ a {
+ opacity: 0.5;
+ }
+ }
+ }
+ td {
+ position: relative;
+ }
+ span.desk {
+ display: block;
+ padding: 2px;
+ }
+}
+
.dayview thead th {
width: 14vw;
padding-bottom: 1ex;
@@ -116,14 +138,17 @@ 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;
@@ -135,7 +160,7 @@ a.timeperiod-exception-all {
}
}
-.dayview tbody td div {
+.dayview tbody td div, .monthview tbody td div {
box-sizing: border-box;
padding: 1ex;
position: absolute;
@@ -146,6 +171,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 +186,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..cf75bd8 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 @@
<a href="{% url 'chrono-manager-agenda-settings' pk=agenda.id %}">{% trans 'Settings' %}</a>
{% endif %}
<a href="" onclick="window.print()">{% trans 'Print' %}</a>
+<a href="{% url 'chrono-manager-agenda-month-view' pk=agenda.id year=view.date|date:"Y" month=view.date|date:"m" %}">{% trans 'Month view' %}</a>
{% 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..9f66be5
--- /dev/null
+++ b/chrono/manager/templates/chrono/manager_agenda_month_view.html
@@ -0,0 +1,69 @@
+{% extends "chrono/manager_agenda_view.html" %}
+{% load i18n %}
+
+{% block bodyargs %}class="monthview"{% endblock %}
+
+{% block breadcrumb %}
+{{ block.super }}
+<a>{{ view.date|date:"F Y" }}</a>
+{% endblock %}
+
+{% block appbar %}
+<h2>
+ <a href="{{ view.get_previous_month_url }}">←</a>
+ <span class="date-title">{{ view.date|date:"F Y" }}</span>
+ {% with selected_month=view.date|date:"n" selected_year=view.date|date:"Y" %}
+ <div class="date-picker" style="display: none">
+ <select name="month">{% for month, month_label in view.get_months %}<option value="{{ month }}" {% if selected_month == month %}selected{% endif %}>{{ month_label }}</option>{% endfor %}</select>
+ <select name="year">{% for year in view.get_years %}<option value="{{ year }}" {% if selected_year == year %}selected{% endif %}>{{year}}</option>{% endfor %}</select>
+ <button>{% trans 'Set Date' %}</button>
+ </div>
+ {% endwith %}
+ <a href="{{ view.get_next_month_url }}">→</a>
+</h2>
+{% if user_can_manage %}
+ <a href="{% url 'chrono-manager-agenda-settings' pk=agenda.id %}">{% trans 'Settings' %}</a>
+{% endif %}
+<a href="" onclick="window.print()">{% trans 'Print' %}</a>
+<a href="{% url 'chrono-manager-agenda-day-view' pk=agenda.id year=view.date|date:"Y" month=view.date|date:"m" day=view.date|date:"d" %}">{% trans 'Day view' %}</a>
+{% endblock %}
+
+{% block content %}
+<table>
+ <tbody>
+ {% for week_days in view.get_timetable_infos %}
+ <tr>
+ <th></th>
+ {% for day in week_days.days %}
+ <th class="weekday {% if day.date.month < view.date.month %}previous-month{% elif day.date.month > view.date.month %}next-month{% endif %}"><a href="{% url 'chrono-manager-agenda-day-view' pk=agenda.id year=day.date|date:"Y" month=day.date|date:"m" day=day.date|date:"d" %}">{{ day.date|date:"l d" }}</a></th>
+ {% endfor %}
+ </tr>
+ {% for hour in week_days.periods %}
+ <tr>
+ <th>{{ hour|date:"TIME_FORMAT" }}</th>
+ {% for day in week_days.days %}
+ <td>
+ {% if forloop.parentloop.first %}
+ {% for slot in day.infos.opening_hours %}
+ <div class="opening-hours" style="height: {{ slot.css_height }}%; top: {{ slot.css_top }}%;"></div>
+ {% endfor %}
+ {% for slot in day.infos.booked_slots %}
+ <div class="booking" style="left:{{ slot.css_left }}%;height:{{ slot.css_height }}%; top:{{ slot.css_top }}%; width:{{ slot.css_width }}%">
+ <span class="start-time">{{slot.booking.event.start_datetime|date:"TIME_FORMAT"}}</span>
+ <a {% if slot.booking.backoffice_url %}href="{{slot.booking.backoffice_url}}"{% endif %}
+ >{% if slot.booking.label or slot.booking.user_name %}
+ {{slot.booking.label}}{% if slot.booking.label and slot.booking.user_name %} - {% endif %} {{slot.booking.user_name}}
+ {% else %}{% trans "booked" %}{% endif %}</a>
+ <span class="desk">{{ slot.desk }}</span>
+ </div>
+ {% endfor %}
+ {% endif %}
+ </td>
+ {% endfor %}
+ </tr>
+ {% endfor %}
+ {% endfor %}
+ </tbody>
+</table>
+
+{% endblock %}
diff --git a/chrono/manager/urls.py b/chrono/manager/urls.py
index d975315..a243d6c 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<pk>\w+)/$', views.agenda_view,
name='chrono-manager-agenda-view'),
+ url(r'^agendas/(?P<pk>\w+)/(?P<year>[0-9]{4})/(?P<month>[0-9]{1,2})/$', views.agenda_monthly_view,
+ name='chrono-manager-agenda-month-view'),
url(r'^agendas/(?P<pk>\w+)/(?P<year>[0-9]{4})/(?P<month>[0-9]+)/(?P<day>[0-9]+)/$', views.agenda_day_view,
name='chrono-manager-agenda-day-view'),
url(r'^agendas/(?P<pk>\w+)/settings$', views.agenda_settings,
diff --git a/chrono/manager/views.py b/chrono/manager/views.py
index af9d09c..7e2764c 100644
--- a/chrono/manager/views.py
+++ b/chrono/manager/views.py
@@ -29,10 +29,12 @@ 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)
+from chrono.interval import Intervals
from .forms import (AgendaAddForm, AgendaEditForm, EventForm, NewMeetingTypeForm, MeetingTypeForm,
TimePeriodForm, ImportEventsForm, NewDeskForm, DeskForm, TimePeriodExceptionForm,
@@ -268,6 +270,101 @@ 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_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()
+ hours = Intervals()
+ for desk in desks:
+ for opening_hour in desk.get_opening_hours(current_date):
+ hours.add(opening_hour.begin, opening_hour.end)
+
+ # return merged opening hours
+ for hour in hours.iter_merge():
+ timetable['infos']['opening_hours'].append({
+ 'css_top': 100 * (hour.begin - current_date).seconds // 3600,
+ 'css_height': 100 * (hour.end - hour.begin).seconds // 3600,
+ })
+
+ max_date = day.replace(hour=self.max_timeperiod.hour, minute=0)
+ 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 / len(desks),
+ 'css_left': 100 * desk_index / len(desks),
+ 'desk': desk,
+ 'booking': bookings[0]
+ }
+ timetable['infos']['booked_slots'].append(booking)
+ period += self.interval
+
+ return timetable
+
+ def get_week_timetable_infos(self, i, timeperiods):
+ date = self.date + 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 xrange(1, 8)],
+ 'periods': periods}
+
+ 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 i, week_number in enumerate(xrange(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