0008-agendas-month-day-agenda-for-resource-38942.patch
chrono/manager/templates/chrono/manager_resource_day_view.html | ||
---|---|---|
1 |
{% extends "chrono/manager_resource_detail.html" %} |
|
2 |
{% load i18n %} |
|
3 | ||
4 |
{% block breadcrumb %} |
|
5 |
{{ block.super }} |
|
6 |
<a>{{ day|date:"SHORT_DATE_FORMAT" }}</a> |
|
7 |
{% endblock %} |
|
8 | ||
9 |
{% block appbar-title %} |
|
10 |
<h2> |
|
11 |
<a href="{{ view.get_previous_day_url }}">←</a> |
|
12 |
<span class="date-title">{{ view.date|date:"l j F Y" }}</span> |
|
13 |
{% with selected_day=view.date|date:"j" selected_month=view.date|date:"n" selected_year=view.date|date:"Y" %} |
|
14 |
<div class="date-picker" style="display: none"> |
|
15 |
<select name="day">{% for day in view.get_days %}<option value="{{ day }}" {% if selected_day == day %}selected{% endif %}>{{day}}</option>{% endfor %}</select> |
|
16 |
<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> |
|
17 |
<select name="year">{% for year in view.get_years %}<option value="{{ year }}" {% if selected_year == year %}selected{% endif %}>{{year}}</option>{% endfor %}</select> |
|
18 |
<button>{% trans 'Set Date' %}</button> |
|
19 |
</div> |
|
20 |
{% endwith %} |
|
21 |
<a href="{{ view.get_next_day_url }}">→</a> |
|
22 |
</h2> |
|
23 |
{% endblock %} |
|
24 |
{% block appbar-extras %} |
|
25 |
<a href="{% url 'chrono-manager-resource-month-view' pk=resource.pk year=view.date|date:"Y" month=view.date|date:"n" %}">{% trans 'Month view' %}</a> |
|
26 |
{% endblock %} |
|
27 | ||
28 |
{% block content %} |
|
29 | ||
30 |
<table class="agenda-table day-view hourspan-{{ hour_span }}"> |
|
31 |
<tbody> |
|
32 |
{% for period, resource_info in view.get_timetable_infos %} |
|
33 |
<tr class="{% cycle 'odd' 'even' %}"> |
|
34 |
<th class="hour">{{ period|date:"TIME_FORMAT" }}</th> |
|
35 |
<td> |
|
36 |
{% for booking in resource_info.bookings %} |
|
37 |
<div class="booking" |
|
38 |
style="height: {{ booking.css_height }}%; min-height: {{ booking.css_height }}%; top: {{ booking.css_top }}%;" |
|
39 |
><span class="start-time">{{ booking.event.start_datetime|date:"TIME_FORMAT" }}</span> |
|
40 |
<a {% if booking.backoffice_url %}href="{{ booking.backoffice_url }}"{% endif %} |
|
41 |
>{% if booking.label or booking.user_name %} |
|
42 |
{{ booking.label }}{% if booking.label and booking.user_name %} - {% endif %} {{ booking.user_name }} |
|
43 |
{% else %}{% trans "booked" %}{% endif %}</a> |
|
44 |
</div> |
|
45 |
{% endfor %} |
|
46 |
</td> |
|
47 |
</tr> |
|
48 |
{% endfor %} |
|
49 |
</tbody> |
|
50 |
</table> |
|
51 | ||
52 |
{% endblock %} |
chrono/manager/templates/chrono/manager_resource_detail.html | ||
---|---|---|
2 | 2 |
{% load i18n %} |
3 | 3 | |
4 | 4 |
{% block page-title-extra-label %} |
5 |
- {{ object.label }}
|
|
5 |
- {{ resource.label }}
|
|
6 | 6 |
{% endblock %} |
7 | 7 | |
8 | 8 |
{% block breadcrumb %} |
9 | 9 |
{{ block.super }} |
10 |
<a href="{% url 'chrono-manager-resource-view' pk=object.pk %}">{{ object }}</a>
|
|
10 |
<a href="{% url 'chrono-manager-resource-view' pk=resource.pk %}">{{ resource.label }}</a>
|
|
11 | 11 |
{% endblock %} |
12 | 12 | |
13 | 13 |
{% block appbar %} |
14 |
<h2>{{ object }}</h2> |
|
14 |
{% block appbar-title %} |
|
15 |
<h2>{{ resource }}</h2> |
|
16 |
{% endblock %} |
|
15 | 17 |
<span class="actions"> |
16 |
<a rel="popup" href="{% url 'chrono-manager-resource-edit' pk=object.pk %}">{% trans 'Edit' %}</a> |
|
17 |
<a rel="popup" href="{% url 'chrono-manager-resource-delete' pk=object.pk %}">{% trans 'Delete' %}</a> |
|
18 |
{% block appbar-extras %} |
|
19 |
<a rel="popup" href="{% url 'chrono-manager-resource-edit' pk=resource.pk %}">{% trans 'Edit' %}</a> |
|
20 |
<a rel="popup" href="{% url 'chrono-manager-resource-delete' pk=resource.pk %}">{% trans 'Delete' %}</a> |
|
21 |
{% now "Y" as today_year %} |
|
22 |
{% now "n" as today_month %} |
|
23 |
{% now "j" as today_day %} |
|
24 |
<a href="{% url 'chrono-manager-resource-month-view' pk=resource.pk year=today_year month=today_month %}">{% trans 'Month view' %}</a> |
|
25 |
<a href="{% url 'chrono-manager-resource-day-view' pk=resource.pk year=today_year month=today_month day=today_day %}">{% trans 'Day view' %}</a> |
|
26 |
{% endblock %} |
|
18 | 27 |
</span> |
19 | 28 |
{% endblock %} |
20 | 29 | |
... | ... | |
23 | 32 |
<div class="section"> |
24 | 33 |
<h3>{% trans 'Used in meetings agendas' %}</h3> |
25 | 34 |
<div> |
26 |
{% with object.agenda_set.all as agendas %}
|
|
35 |
{% with resource.agenda_set.all as agendas %}
|
|
27 | 36 |
{% if agendas %} |
28 | 37 |
<ul class="objects-list single-links"> |
29 | 38 |
{% for agenda in agendas %} |
chrono/manager/templates/chrono/manager_resource_month_view.html | ||
---|---|---|
1 |
{% extends "chrono/manager_resource_detail.html" %} |
|
2 |
{% load i18n %} |
|
3 | ||
4 |
{% block bodyargs %}class="monthview"{% endblock %} |
|
5 | ||
6 |
{% block breadcrumb %} |
|
7 |
{{ block.super }} |
|
8 |
<a>{{ view.date|date:"F Y" }}</a> |
|
9 |
{% endblock %} |
|
10 | ||
11 |
{% block appbar-title %} |
|
12 |
<h2> |
|
13 |
<a href="{{ view.get_previous_month_url }}">←</a> |
|
14 |
<span class="date-title">{{ view.date|date:"F Y" }}</span> |
|
15 |
{% with selected_month=view.date|date:"n" selected_year=view.date|date:"Y" %} |
|
16 |
<div class="date-picker" style="display: none"> |
|
17 |
<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> |
|
18 |
<select name="year">{% for year in view.get_years %}<option value="{{ year }}" {% if selected_year == year %}selected{% endif %}>{{year}}</option>{% endfor %}</select> |
|
19 |
<button>{% trans 'Set Date' %}</button> |
|
20 |
</div> |
|
21 |
{% endwith %} |
|
22 |
<a href="{{ view.get_next_month_url }}">→</a> |
|
23 |
</h2> |
|
24 |
{% endblock %} |
|
25 |
{% block appbar-extras %} |
|
26 |
<a href="{% url 'chrono-manager-resource-day-view' pk=resource.pk year=view.date|date:"Y" month=view.date|date:"n" day=1 %}">{% trans 'Day view' %}</a> |
|
27 |
{% endblock %} |
|
28 | ||
29 |
{% block content %} |
|
30 | ||
31 |
<table class="agenda-table month-view single-desk"> |
|
32 |
<tbody> |
|
33 |
{% for week_days in view.get_timetable_infos %} |
|
34 |
<tr> |
|
35 |
<th></th> |
|
36 |
{% for day in week_days.days %} |
|
37 |
<th class="weekday {% if day.today %}today{% endif %}">{% if not day.other_month %}<a href="{% url 'chrono-manager-resource-day-view' pk=resource.pk year=day.date|date:"Y" month=day.date|date:"m" day=day.date|date:"j" %}">{{ day.date|date:"l j" }}</a>{% endif %}</th> |
|
38 |
{% endfor %} |
|
39 |
</tr> |
|
40 |
{% for hour in week_days.periods %} |
|
41 |
<tr class="{% cycle 'odd' 'even' %}"> |
|
42 |
<th class="hour">{{ hour|date:"TIME_FORMAT" }}</th> |
|
43 |
{% for day in week_days.days %} |
|
44 |
<td class="{% if day.other_month %}other-month{% endif %} {% if day.today %}today{% endif %}"> |
|
45 |
{% if forloop.parentloop.first %} |
|
46 |
{% for slot in day.infos.booked_slots %} |
|
47 |
<div class="booking" style="height:{{ slot.css_height|stringformat:".1f" }}%;min-height:{{ slot.css_height|stringformat:".1f" }}%;top:{{ slot.css_top|stringformat:".1f" }}%"> |
|
48 |
<span class="start-time">{{ slot.booking.event.start_datetime|date:"TIME_FORMAT" }}</span> |
|
49 |
<a {% if slot.booking.backoffice_url %}href="{{ slot.booking.backoffice_url }}"{% endif %} |
|
50 |
>{% if slot.booking.label or slot.booking.user_name %} |
|
51 |
{{ slot.booking.label }}{% if slot.booking.label and slot.booking.user_name %} - {% endif %} {{ slot.booking.user_name }} |
|
52 |
{% else %}{% trans "booked" %}{% endif %}</a> |
|
53 |
</div> |
|
54 |
{% endfor %} |
|
55 |
{% endif %} |
|
56 |
</td> |
|
57 |
{% endfor %} |
|
58 |
</tr> |
|
59 |
{% endfor %} |
|
60 |
{% endfor %} |
|
61 |
</tbody> |
|
62 |
</table> |
|
63 | ||
64 |
{% endblock %} |
chrono/manager/urls.py | ||
---|---|---|
23 | 23 |
url(r'^resources/$', views.resource_list, name='chrono-manager-resource-list'), |
24 | 24 |
url(r'^resource/add/$', views.resource_add, name='chrono-manager-resource-add'), |
25 | 25 |
url(r'^resource/(?P<pk>\d+)/$', views.resource_view, name='chrono-manager-resource-view'), |
26 |
url( |
|
27 |
r'^resource/(?P<pk>\d+)/(?P<year>[0-9]{4})/(?P<month>[0-9]+)/$', |
|
28 |
views.resource_monthly_view, |
|
29 |
name='chrono-manager-resource-month-view', |
|
30 |
), |
|
31 |
url( |
|
32 |
r'^resource/(?P<pk>\d+)/(?P<year>[0-9]{4})/(?P<month>[0-9]+)/(?P<day>[0-9]+)/$', |
|
33 |
views.resource_day_view, |
|
34 |
name='chrono-manager-resource-day-view', |
|
35 |
), |
|
26 | 36 |
url(r'^resource/(?P<pk>\d+)/edit/$', views.resource_edit, name='chrono-manager-resource-edit'), |
27 | 37 |
url(r'^resource/(?P<pk>\d+)/delete/$', views.resource_delete, name='chrono-manager-resource-delete'), |
28 | 38 |
url(r'^agendas/add/$', views.agenda_add, name='chrono-manager-agenda-add'), |
chrono/manager/views.py | ||
---|---|---|
17 | 17 |
import datetime |
18 | 18 |
import itertools |
19 | 19 |
import json |
20 |
import math |
|
20 | 21 | |
21 | 22 |
from django.contrib import messages |
22 | 23 |
from django.core.exceptions import PermissionDenied |
23 | 24 |
from django.db.models import Prefetch |
24 | 25 |
from django.db.models import Q |
26 |
from django.db.models import Min, Max |
|
25 | 27 |
from django.forms import ValidationError |
26 | 28 |
from django.http import Http404, HttpResponse, HttpResponseRedirect |
27 | 29 |
from django.shortcuts import get_object_or_404 |
... | ... | |
113 | 115 |
template_name = 'chrono/manager_resource_detail.html' |
114 | 116 |
model = Resource |
115 | 117 | |
118 |
def get_context_data(self, **kwargs): |
|
119 |
context = super().get_context_data(**kwargs) |
|
120 |
context['resource'] = self.object |
|
121 |
return context |
|
122 | ||
116 | 123 | |
117 | 124 |
resource_view = ResourceDetailView.as_view() |
118 | 125 | |
119 | 126 | |
127 |
class DateMixin(object): |
|
128 |
def get_days(self): |
|
129 |
return [str(x) for x in range(1, 32)] |
|
130 | ||
131 |
def get_months(self): |
|
132 |
return [(str(x), MONTHS[x]) for x in range(1, 13)] |
|
133 | ||
134 |
def get_years(self): |
|
135 |
year = now().year |
|
136 |
return [str(x) for x in range(year - 1, year + 5)] |
|
137 | ||
138 | ||
139 |
class ResourceDayView(DateMixin, DayArchiveView): |
|
140 |
template_name = 'chrono/manager_resource_day_view.html' |
|
141 |
model = Event |
|
142 |
month_format = '%m' |
|
143 |
date_field = 'start_datetime' |
|
144 |
allow_empty = True |
|
145 |
allow_future = True |
|
146 | ||
147 |
def dispatch(self, request, *args, **kwargs): |
|
148 |
self.resource = get_object_or_404(Resource, pk=kwargs['pk']) |
|
149 |
# specify 6am time to get the expected timezone on daylight saving time |
|
150 |
# days. |
|
151 |
try: |
|
152 |
self.date = make_aware( |
|
153 |
datetime.datetime.strptime( |
|
154 |
'%s-%s-%s 06:00' % (self.get_year(), self.get_month(), self.get_day()), '%Y-%m-%d %H:%M' |
|
155 |
) |
|
156 |
) |
|
157 |
except ValueError: # day is out of range for month |
|
158 |
# redirect to last day of month |
|
159 |
date = datetime.date(int(self.get_year()), int(self.get_month()), 1) |
|
160 |
date += datetime.timedelta(days=40) |
|
161 |
date = date.replace(day=1) |
|
162 |
date -= datetime.timedelta(days=1) |
|
163 |
return HttpResponseRedirect( |
|
164 |
reverse( |
|
165 |
'chrono-manager-resource-day-view', |
|
166 |
kwargs={'pk': self.resource.pk, 'year': date.year, 'month': date.month, 'day': date.day}, |
|
167 |
) |
|
168 |
) |
|
169 |
return super().dispatch(request, *args, **kwargs) |
|
170 | ||
171 |
def get_queryset(self): |
|
172 |
queryset = ( |
|
173 |
self.resource.event_set.all().select_related('meeting_type').prefetch_related('booking_set') |
|
174 |
) |
|
175 |
return queryset |
|
176 | ||
177 |
def get_context_data(self, **kwargs): |
|
178 |
context = super().get_context_data(**kwargs) |
|
179 | ||
180 |
context['resource'] = self.resource |
|
181 |
context['hour_span'] = 1 |
|
182 |
durations = MeetingType.objects.filter(agenda__resources=self.resource).values_list( |
|
183 |
'duration', flat=True |
|
184 |
) |
|
185 |
if durations: |
|
186 |
gcd = durations[0] |
|
187 |
for duration in durations[1:]: |
|
188 |
gcd = math.gcd(duration, gcd) |
|
189 |
context['hour_span'] = max(60 // gcd, 1) |
|
190 | ||
191 |
return context |
|
192 | ||
193 |
def get_previous_day_url(self): |
|
194 |
previous_day = self.date.date() - datetime.timedelta(days=1) |
|
195 |
return reverse( |
|
196 |
'chrono-manager-resource-day-view', |
|
197 |
kwargs={ |
|
198 |
'pk': self.resource.pk, |
|
199 |
'year': previous_day.year, |
|
200 |
'month': previous_day.month, |
|
201 |
'day': previous_day.day, |
|
202 |
}, |
|
203 |
) |
|
204 | ||
205 |
def get_next_day_url(self): |
|
206 |
next_day = self.date.date() + datetime.timedelta(days=1) |
|
207 |
return reverse( |
|
208 |
'chrono-manager-resource-day-view', |
|
209 |
kwargs={ |
|
210 |
'pk': self.resource.pk, |
|
211 |
'year': next_day.year, |
|
212 |
'month': next_day.month, |
|
213 |
'day': next_day.day, |
|
214 |
}, |
|
215 |
) |
|
216 | ||
217 |
def get_timetable_infos(self): |
|
218 |
timeperiods = TimePeriod.objects.filter(desk__agenda__resources=self.resource).aggregate( |
|
219 |
Min('start_time'), Max('end_time') |
|
220 |
) |
|
221 |
min_timeperiod = timeperiods['start_time__min'] |
|
222 |
max_timeperiod = timeperiods['end_time__max'] |
|
223 | ||
224 |
interval = datetime.timedelta(minutes=60) |
|
225 |
current_date = self.date.replace(hour=min_timeperiod and min_timeperiod.hour or 0, minute=0) |
|
226 |
max_date = self.date.replace(hour=max_timeperiod and max_timeperiod.hour or 23, minute=0) |
|
227 |
if not max_timeperiod or max_timeperiod.minute != 0: |
|
228 |
# until the end of the last hour. |
|
229 |
max_date += datetime.timedelta(hours=1) |
|
230 | ||
231 |
while current_date < max_date: |
|
232 |
info = {} |
|
233 |
info['bookings'] = bookings = [] # bookings for this resource |
|
234 |
finish_datetime = current_date + interval |
|
235 |
for event in [ |
|
236 |
x |
|
237 |
for x in self.object_list |
|
238 |
if x.start_datetime >= current_date and x.start_datetime < finish_datetime |
|
239 |
]: |
|
240 |
# don't consider cancelled bookings |
|
241 |
for booking in [x for x in event.booking_set.all() if not x.cancellation_datetime]: |
|
242 |
booking.css_top = int(100 * event.start_datetime.minute / 60) |
|
243 |
booking.css_height = int(100 * event.meeting_type.duration / 60) |
|
244 |
bookings.append(booking) |
|
245 |
yield current_date, info |
|
246 |
current_date += interval |
|
247 | ||
248 | ||
249 |
resource_day_view = ResourceDayView.as_view() |
|
250 | ||
251 | ||
252 |
class ResourceMonthView(DateMixin, MonthArchiveView): |
|
253 |
template_name = 'chrono/manager_resource_month_view.html' |
|
254 |
model = Event |
|
255 |
month_format = '%m' |
|
256 |
date_field = 'start_datetime' |
|
257 |
allow_empty = True |
|
258 |
allow_future = True |
|
259 | ||
260 |
def dispatch(self, request, *args, **kwargs): |
|
261 |
self.resource = get_object_or_404(Resource, pk=kwargs['pk']) |
|
262 |
self.date = make_aware( |
|
263 |
datetime.datetime.strptime( |
|
264 |
'%s-%s-%s 06:00' % (self.get_year(), self.get_month(), 1), '%Y-%m-%d %H:%M' |
|
265 |
) |
|
266 |
) |
|
267 |
return super().dispatch(request, *args, **kwargs) |
|
268 | ||
269 |
def get_queryset(self): |
|
270 |
queryset = ( |
|
271 |
self.resource.event_set.all().select_related('meeting_type').prefetch_related('booking_set') |
|
272 |
) |
|
273 |
return queryset |
|
274 | ||
275 |
def get_context_data(self, **kwargs): |
|
276 |
context = super().get_context_data(**kwargs) |
|
277 | ||
278 |
context['resource'] = self.resource |
|
279 | ||
280 |
return context |
|
281 | ||
282 |
def get_previous_month_url(self): |
|
283 |
previous_month = self.get_previous_month(self.date.date()) |
|
284 |
return reverse( |
|
285 |
'chrono-manager-resource-month-view', |
|
286 |
kwargs={'pk': self.resource.pk, 'year': previous_month.year, 'month': previous_month.month}, |
|
287 |
) |
|
288 | ||
289 |
def get_next_month_url(self): |
|
290 |
next_month = self.get_next_month(self.date.date()) |
|
291 |
return reverse( |
|
292 |
'chrono-manager-resource-month-view', |
|
293 |
kwargs={'pk': self.resource.pk, 'year': next_month.year, 'month': next_month.month}, |
|
294 |
) |
|
295 | ||
296 |
def get_timetable_infos(self): |
|
297 |
timeperiods = TimePeriod.objects.filter(desk__agenda__resources=self.resource).aggregate( |
|
298 |
Min('start_time'), Max('end_time') |
|
299 |
) |
|
300 |
self.min_timeperiod = timeperiods['start_time__min'] |
|
301 |
self.max_timeperiod = timeperiods['end_time__max'] |
|
302 | ||
303 |
weekdays = TimePeriod.objects.filter(desk__agenda__resources=self.resource).values_list( |
|
304 |
'weekday', flat=True |
|
305 |
) |
|
306 |
hide_sunday = 6 not in weekdays |
|
307 |
hide_weekend = hide_sunday and 5 not in weekdays |
|
308 |
# avoid displaying empty first week |
|
309 |
first_week_offset = int( |
|
310 |
(hide_sunday and self.date.weekday() == 6) or (hide_weekend and self.date.weekday() == 5) |
|
311 |
) |
|
312 | ||
313 |
first_week_number = self.date.isocalendar()[1] |
|
314 |
if first_week_number >= 52: |
|
315 |
first_week_number = 0 |
|
316 |
last_month_day = self.get_next_month(self.date.date()) - datetime.timedelta(days=1) |
|
317 |
last_week_number = last_month_day.isocalendar()[1] |
|
318 | ||
319 |
if last_week_number < first_week_number: # new year |
|
320 |
last_week_number = 53 |
|
321 | ||
322 |
for week_number in range(first_week_number + first_week_offset, last_week_number + 1): |
|
323 |
yield self.get_week_timetable_infos( |
|
324 |
week_number - first_week_number, week_end_offset=int(hide_sunday) + int(hide_weekend), |
|
325 |
) |
|
326 | ||
327 |
def get_week_timetable_infos(self, week_index, week_end_offset=0): |
|
328 |
date = self.date + datetime.timedelta(week_index * 7) |
|
329 |
year, week_number, dow = date.isocalendar() |
|
330 |
start_date = date - datetime.timedelta(dow) |
|
331 | ||
332 |
interval = datetime.timedelta(minutes=60) |
|
333 | ||
334 |
period = self.date.replace(hour=self.min_timeperiod and self.min_timeperiod.hour or 0, minute=0) |
|
335 |
max_date = self.date.replace(hour=self.max_timeperiod and self.max_timeperiod.hour or 23, minute=0) |
|
336 |
if not self.max_timeperiod or self.max_timeperiod.minute != 0: |
|
337 |
# until the end of the last hour. |
|
338 |
max_date += datetime.timedelta(hours=1) |
|
339 | ||
340 |
periods = [] |
|
341 |
while period < max_date: |
|
342 |
periods.append(period) |
|
343 |
period = period + interval |
|
344 | ||
345 |
return { |
|
346 |
'days': [ |
|
347 |
self.get_day_timetable_infos(start_date + datetime.timedelta(i), interval) |
|
348 |
for i in range(1, 8 - week_end_offset) |
|
349 |
], |
|
350 |
'periods': periods, |
|
351 |
} |
|
352 | ||
353 |
def get_day_timetable_infos(self, day, interval): |
|
354 |
day = make_aware(make_naive(day)) # give day correct timezone |
|
355 |
period = current_date = day.replace( |
|
356 |
hour=self.min_timeperiod and self.min_timeperiod.hour or 0, minute=0 |
|
357 |
) |
|
358 |
max_date = day.replace(hour=self.max_timeperiod and self.max_timeperiod.hour or 23, minute=0) |
|
359 |
if not self.max_timeperiod or self.max_timeperiod.minute != 0: |
|
360 |
# until the end of the last hour. |
|
361 |
max_date += datetime.timedelta(hours=1) |
|
362 | ||
363 |
timetable = { |
|
364 |
'date': current_date, |
|
365 |
'today': day.date() == datetime.date.today(), |
|
366 |
'other_month': day.month != self.date.month, |
|
367 |
'infos': {'booked_slots': []}, |
|
368 |
} |
|
369 | ||
370 |
# compute booking and opening hours only for current month |
|
371 |
if self.date.month != day.month: |
|
372 |
return timetable |
|
373 | ||
374 |
while period <= max_date: |
|
375 |
period_end = period + interval |
|
376 |
for event in [ |
|
377 |
x for x in self.object_list if x.start_datetime >= period and x.start_datetime < period_end |
|
378 |
]: |
|
379 |
# don't consider cancelled bookings |
|
380 |
bookings = [x for x in event.booking_set.all() if not x.cancellation_datetime] |
|
381 |
if not bookings: |
|
382 |
continue |
|
383 |
booking = { |
|
384 |
'css_top': 100 * (event.start_datetime - current_date).seconds // 3600, |
|
385 |
'css_height': 100 * event.meeting_type.duration // 60, |
|
386 |
'booking': bookings[0], |
|
387 |
} |
|
388 |
timetable['infos']['booked_slots'].append(booking) |
|
389 |
period += interval |
|
390 | ||
391 |
return timetable |
|
392 | ||
393 | ||
394 |
resource_monthly_view = ResourceMonthView.as_view() |
|
395 | ||
396 | ||
120 | 397 |
class ResourceAddView(CreateView): |
121 | 398 |
template_name = 'chrono/manager_resource_form.html' |
122 | 399 |
model = Resource |
... | ... | |
355 | 632 |
agenda_view = AgendaView.as_view() |
356 | 633 | |
357 | 634 | |
358 |
class AgendaDateView(ViewableAgendaMixin): |
|
635 |
class AgendaDateView(DateMixin, ViewableAgendaMixin):
|
|
359 | 636 |
model = Event |
360 | 637 |
month_format = '%m' |
361 | 638 |
date_field = 'start_datetime' |
... | ... | |
408 | 685 |
) |
409 | 686 |
return queryset |
410 | 687 | |
411 |
def get_days(self): |
|
412 |
return [str(x) for x in range(1, 32)] |
|
413 | ||
414 |
def get_months(self): |
|
415 |
return [(str(x), MONTHS[x]) for x in range(1, 13)] |
|
416 | ||
417 |
def get_years(self): |
|
418 |
year = now().year |
|
419 |
return [str(x) for x in range(year - 1, year + 5)] |
|
420 | ||
421 | 688 | |
422 | 689 |
class AgendaDayView(AgendaDateView, DayArchiveView): |
423 | 690 |
template_name = 'chrono/manager_agenda_day_view.html' |
tests/test_manager.py | ||
---|---|---|
206 | 206 |
assert '/manage/agendas/%s/settings' % agenda.pk in resp.text |
207 | 207 | |
208 | 208 | |
209 |
def test_resource_day_view(app, admin_user): |
|
210 |
today = datetime.date.today() |
|
211 |
resource = Resource.objects.create(label='Foo bar') |
|
212 |
agenda = Agenda.objects.create(label='Agenda', kind='meetings') |
|
213 |
desk = Desk.objects.create(agenda=agenda, label='Desk') |
|
214 |
meetingtype = MeetingType.objects.create(agenda=agenda, label='Bar', duration=30) |
|
215 |
timeperiod = TimePeriod.objects.create( |
|
216 |
desk=desk, weekday=today.weekday(), start_time=datetime.time(10, 0), end_time=datetime.time(18, 0) |
|
217 |
) |
|
218 | ||
219 |
login(app) |
|
220 |
resp = app.get('/manage/resource/%s/%d/%d/%d/' % (resource.pk, today.year, today.month, today.day)) |
|
221 |
assert 'div class="booking' not in resp.text |
|
222 |
assert resp.text.count('<tr') == 24 # resource in no agenda |
|
223 | ||
224 |
agenda.resources.add(resource) |
|
225 |
resp = app.get('/manage/resource/%s/%d/%d/%d/' % (resource.pk, today.year, today.month, today.day)) |
|
226 |
assert 'div class="booking' not in resp.text |
|
227 |
assert resp.text.count('<tr') == 8 # 10->18 (not included) |
|
228 | ||
229 |
timeperiod.end_time = datetime.time(18, 30) # end during an hour |
|
230 |
timeperiod.save() |
|
231 |
resp = app.get('/manage/resource/%s/%d/%d/%d/' % (resource.pk, today.year, today.month, today.day)) |
|
232 |
assert resp.text.count('<tr') == 9 # 10->18 (included) |
|
233 | ||
234 |
# book some slots |
|
235 |
for hour, minute in [(10, 30), (14, 0)]: |
|
236 |
event = Event.objects.create( |
|
237 |
agenda=agenda, |
|
238 |
places=1, |
|
239 |
desk=desk, |
|
240 |
meeting_type=meetingtype, |
|
241 |
start_datetime=now().replace(hour=hour, minute=minute), |
|
242 |
) |
|
243 |
event.resources.add(resource) |
|
244 |
Booking.objects.create(event=event) |
|
245 | ||
246 |
with CaptureQueriesContext(connection) as ctx: |
|
247 |
resp = app.get('/manage/resource/%s/%d/%d/%d/' % (resource.pk, today.year, today.month, today.day)) |
|
248 |
assert len(ctx.captured_queries) == 7 |
|
249 |
assert resp.text.count('div class="booking') == 2 |
|
250 |
assert 'hourspan-2' in resp.text # table CSS class |
|
251 |
assert 'height: 50%; top: 0%;' in resp.text # booking cells |
|
252 |
assert 'height: 50%; top: 50%;' in resp.text # booking cells |
|
253 | ||
254 |
# create a shorter meeting type, this will change the table CSS class |
|
255 |
# (and visually this will give more room for events) |
|
256 |
meetingtype = MeetingType.objects.create(agenda=agenda, label='Baz', duration=15) |
|
257 |
resp = app.get('/manage/resource/%s/%d/%d/%d/' % (resource.pk, today.year, today.month, today.day)) |
|
258 |
assert resp.text.count('div class="booking') == 2 |
|
259 |
assert 'hourspan-4' in resp.text # table CSS class |
|
260 | ||
261 |
# cancel a booking |
|
262 |
booking = Booking.objects.first() |
|
263 |
booking.cancel() |
|
264 |
resp = app.get('/manage/resource/%s/%d/%d/%d/' % (resource.pk, today.year, today.month, today.day)) |
|
265 |
assert resp.text.count('div class="booking') == 1 |
|
266 | ||
267 | ||
268 |
def test_resource_day_view_late_meeting(app, admin_user): |
|
269 |
today = datetime.date.today() |
|
270 |
resource = Resource.objects.create(label='Foo bar') |
|
271 |
agenda = Agenda.objects.create(label='Agenda', kind='meetings') |
|
272 |
agenda.resources.add(resource) |
|
273 |
desk = Desk.objects.create(agenda=agenda, label='Desk') |
|
274 |
MeetingType.objects.create(agenda=agenda, label='Bar', duration=30) |
|
275 |
TimePeriod.objects.create( |
|
276 |
desk=desk, weekday=today.weekday(), start_time=datetime.time(10, 0), end_time=datetime.time(23, 30) |
|
277 |
) |
|
278 | ||
279 |
login(app) |
|
280 |
resp = app.get('/manage/resource/%s/%d/%d/%d/' % (resource.pk, today.year, today.month, today.day)) |
|
281 |
assert resp.text.count('<tr') == 14 |
|
282 |
assert '<th class="hour">midnight</th>' not in resp.text |
|
283 |
assert '<th class="hour">11 p.m.</th>' in resp.text |
|
284 | ||
285 | ||
286 |
def test_resource_invalid_day_view(app, admin_user): |
|
287 |
resource = Resource.objects.create(label='Foo bar') |
|
288 | ||
289 |
login(app) |
|
290 |
resp = app.get('/manage/resource/%s/%d/%d/%d/' % (resource.pk, 2018, 11, 31), status=302) |
|
291 |
assert resp.location.endswith('2018/11/30/') |
|
292 | ||
293 | ||
294 |
def test_resource_month_view(app, admin_user): |
|
295 |
resource = Resource.objects.create(label='Foo bar') |
|
296 |
agenda = Agenda.objects.create(label='Agenda', kind='meetings') |
|
297 |
agenda.resources.add(resource) |
|
298 |
desk = Desk.objects.create(agenda=agenda, label='Desk') |
|
299 |
meetingtype = MeetingType.objects.create(agenda=agenda, label='Bar', duration=20) |
|
300 |
TimePeriod.objects.create( |
|
301 |
desk=desk, weekday=0, start_time=datetime.time(10, 0), end_time=datetime.time(18, 0) |
|
302 |
) |
|
303 | ||
304 |
login(app) |
|
305 |
today = datetime.date(2018, 11, 10) # fixed day |
|
306 |
resp = app.get('/manage/resource/%s/%s/%s/' % (resource.pk, today.year, today.month)) |
|
307 |
assert '<div class="booking' not in resp.text |
|
308 |
first_month_day = today.replace(day=1) |
|
309 |
last_month_day = today.replace(day=1, month=today.month + 1) - datetime.timedelta(days=1) |
|
310 |
start_week_number = first_month_day.isocalendar()[1] |
|
311 |
end_week_number = last_month_day.isocalendar()[1] |
|
312 |
weeks_number = end_week_number - start_week_number + 1 |
|
313 |
assert resp.text.count('<tr') == 9 * weeks_number |
|
314 | ||
315 |
# book some slots |
|
316 |
for hour, minute in [(10, 30), (14, 0)]: |
|
317 |
event = Event.objects.create( |
|
318 |
agenda=agenda, |
|
319 |
places=1, |
|
320 |
desk=desk, |
|
321 |
meeting_type=meetingtype, |
|
322 |
start_datetime=now().replace(hour=hour, minute=minute), |
|
323 |
) |
|
324 |
event.resources.add(resource) |
|
325 |
Booking.objects.create(event=event) |
|
326 | ||
327 |
today = datetime.date.today() |
|
328 |
with CaptureQueriesContext(connection) as ctx: |
|
329 |
resp = app.get('/manage/resource/%s/%s/%s/' % (resource.pk, today.year, today.month)) |
|
330 |
assert len(ctx.captured_queries) == 8 |
|
331 |
assert resp.text.count('<div class="booking" style="height:33.0%;') == 2 # booking cells |
|
332 | ||
333 |
# cancel booking |
|
334 |
booking = Booking.objects.first() |
|
335 |
booking.cancel() |
|
336 | ||
337 |
# make sure the are not |
|
338 |
resp = app.get('/manage/resource/%s/%s/%s/' % (resource.pk, today.year, today.month)) |
|
339 |
assert resp.text.count('<div class="booking"') == 1 |
|
340 | ||
341 | ||
342 |
def test_resource_month_view_weekend(app, admin_user): |
|
343 |
resource = Resource.objects.create(label='Foo bar') |
|
344 |
agenda = Agenda.objects.create(label='Agenda', kind='meetings') |
|
345 |
agenda.resources.add(resource) |
|
346 |
desk = Desk.objects.create(agenda=agenda, label='Desk') |
|
347 |
MeetingType.objects.create(agenda=agenda, label='Bar', duration=30) |
|
348 |
monday = 0 |
|
349 |
TimePeriod.objects.create( |
|
350 |
desk=desk, weekday=monday, start_time=datetime.time(10, 0), end_time=datetime.time(18, 0) |
|
351 |
) |
|
352 |
month, year = 1, 2019 |
|
353 | ||
354 |
login(app) |
|
355 |
resp = app.get('/manage/resource/%s/%s/%s/' % (resource.pk, year, month)) |
|
356 |
assert 'Sunday' not in resp.text |
|
357 |
assert 'Saturday' not in resp.text |
|
358 |
# No Monday on first row since month starts a Tuesday |
|
359 |
assert len(resp.pyquery.find('tbody tr:first th.weekday:empty')) == 1 |
|
360 | ||
361 |
# When weekend is hidden, do not display an empty first week |
|
362 |
month, year = 12, 2019 # month starts a Sunday |
|
363 |
resp = app.get('/manage/resource/%s/%s/%s/' % (resource.pk, year, month)) |
|
364 |
assert len(resp.pyquery.find('tbody tr:first th.weekday:empty')) == 0 |
|
365 | ||
366 |
month, year = 6, 2019 # month starts a Saturday |
|
367 |
resp = app.get('/manage/resource/%s/%s/%s/' % (resource.pk, year, month)) |
|
368 |
assert len(resp.pyquery.find('tbody tr:first th.weekday:empty')) == 0 |
|
369 | ||
370 |
saturday = 5 |
|
371 |
timeperiod_sat = TimePeriod.objects.create( |
|
372 |
desk=desk, weekday=saturday, start_time=datetime.time(10, 0), end_time=datetime.time(18, 0) |
|
373 |
) |
|
374 |
resp = app.get('/manage/resource/%s/%s/%s/' % (resource.pk, year, month)) |
|
375 |
assert 'Sunday' not in resp.text |
|
376 |
assert 'Saturday' in resp.text |
|
377 |
assert len(resp.pyquery.find('tbody tr:first th.weekday:empty')) == 5 |
|
378 | ||
379 |
sunday = 6 |
|
380 |
TimePeriod.objects.create( |
|
381 |
desk=desk, weekday=sunday, start_time=datetime.time(10, 0), end_time=datetime.time(18, 0) |
|
382 |
) |
|
383 |
resp = app.get('/manage/resource/%s/%s/%s/' % (resource.pk, year, month)) |
|
384 |
assert 'Sunday' in resp.text |
|
385 |
assert 'Saturday' in resp.text |
|
386 | ||
387 |
timeperiod_sat.delete() |
|
388 |
resp = app.get('/manage/resource/%s/%s/%s/' % (resource.pk, year, month)) |
|
389 |
assert 'Sunday' in resp.text |
|
390 |
assert 'Saturday' in resp.text |
|
391 | ||
392 | ||
393 |
def test_resource_month_view_dst_change(app, admin_user): |
|
394 |
resource = Resource.objects.create(label='Foo bar') |
|
395 |
agenda = Agenda.objects.create(label='Agenda', kind='meetings') |
|
396 |
agenda.resources.add(resource) |
|
397 |
desk = Desk.objects.create(agenda=agenda, label='Desk') |
|
398 |
meetingtype = MeetingType.objects.create(agenda=agenda, label='Bar', duration=30) |
|
399 |
TimePeriod.objects.create( |
|
400 |
desk=desk, weekday=0, start_time=datetime.time(9, 0), end_time=datetime.time(12, 0) |
|
401 |
) |
|
402 | ||
403 |
login(app) |
|
404 |
# book some slots |
|
405 |
with freezegun.freeze_time('2019-10-01'): |
|
406 |
for start_datetime in [ |
|
407 |
make_aware(datetime.datetime(2019, 10, 2, 10, 0)), |
|
408 |
make_aware(datetime.datetime(2019, 10, 29, 10, 0)), |
|
409 |
]: |
|
410 |
event = Event.objects.create( |
|
411 |
agenda=agenda, places=1, desk=desk, meeting_type=meetingtype, start_datetime=start_datetime, |
|
412 |
) |
|
413 |
event.resources.add(resource) |
|
414 |
Booking.objects.create(event=event) |
|
415 | ||
416 |
# check booked slots are similarly aligned |
|
417 |
resp = app.get('/manage/resource/%s/2019/10/' % resource.pk) |
|
418 |
assert resp.text.count('height:50.0%;top:100.0%') == 2 |
|
419 | ||
420 | ||
421 |
def test_resource_month_view_januaries(app, admin_user): |
|
422 |
resource = Resource.objects.create(label='Foo bar') |
|
423 | ||
424 |
for year in range(2020, 2030): |
|
425 |
date = datetime.date(year, 1, 1) |
|
426 |
with freezegun.freeze_time(date): |
|
427 |
login(app) |
|
428 |
resp = app.get('/manage/resource/%s/%s/1/' % (resource.pk, date.year)) |
|
429 |
assert resp.text.count('<th></th>') in (4, 5) |
|
430 | ||
431 | ||
209 | 432 |
def test_edit_resource(app, admin_user): |
210 | 433 |
resource = Resource.objects.create(label='Foo bar') |
211 | 434 | |
212 |
- |