Projet

Général

Profil

0003-manager-add-agenda-s-month-and-booking-view-21326.patch

Serghei Mihai, 23 août 2018 15:45

Télécharger (16,6 ko)

Voir les différences:

Subject: [PATCH 3/3] manager: add agenda's month and booking view (#21326)

 chrono/manager/static/css/style.scss          | 51 +++++++++-
 .../chrono/manager_agenda_day_view.html       |  1 +
 .../chrono/manager_agenda_month_view.html     | 78 +++++++++++++++
 chrono/manager/urls.py                        |  2 +
 chrono/manager/views.py                       | 97 ++++++++++++++++++-
 tests/test_manager.py                         | 65 +++++++++++++
 tox.ini                                       |  1 +
 7 files changed, 290 insertions(+), 5 deletions(-)
 create mode 100644 chrono/manager/templates/chrono/manager_agenda_month_view.html
chrono/manager/static/css/style.scss
91 91
	min-width: 24ex;
92 92
}
93 93

  
94
.dayview table {
94
.dayview table, .monthview table {
95 95
	border-collapse: collapse;
96 96
}
97 97

  
98
.monthview table {
99
	width: 100%;
100
	th.hour {
101
		width: 5%;
102
	}
103
	th.weekday {
104
		width: 12.5%;
105
		&.previous-month, &.next-month {
106
			a {
107
				opacity: 0.5;
108
			}
109
		}
110
	}
111
	td {
112
		position: relative;
113
	}
114
	span.desk {
115
		display: block;
116
		padding: 2px;
117
	}
118
}
119

  
98 120
.dayview thead th {
99 121
	width: 14vw;
100 122
	padding-bottom: 1ex;
......
116 138
}
117 139

  
118 140
.dayview tbody tr:nth-child(2n+1) th,
119
.dayview tbody tr:nth-child(2n+1) td {
141
.monthview tbody tr:nth-child(2n+1) th,
142
.dayview tbody tr:nth-child(2n+1) td,
143
.monthview tbody tr:nth-child(2n+1) td {
120 144
	background: #f0f0f0;
145
	background-clip: padding-box;
121 146
	@media print {
122 147
		border-top: 1px solid #aaa;
123 148
	}
124 149
}
125 150

  
126
.dayview tbody td {
151
.dayview tbody td, .monthview tbody td {
127 152
	padding: 0 1ex;
128 153
	vertical-align: top;
129 154
	position: relative;
130 155
}
131 156

  
157
.monthview tbody td {
158
	border-left: 1px solid #d0d0d0;
159
	border-right: 1px solid #d0d0d0;
160
}
161

  
132 162
@for $i from 1 through 60 {
133 163
	table.hourspan-#{$i} tbody td {
134 164
		height: calc(#{$i} * 2.5em);
135 165
	}
136 166
}
137 167

  
138
.dayview tbody td div {
168
.dayview tbody td div, .monthview tbody td div {
139 169
	box-sizing: border-box;
140 170
	padding: 1ex;
141 171
	position: absolute;
......
146 176
		opacity: 0.3;
147 177
		left: 0.5ex;
148 178
		width: calc(100% - 1ex);
179
		position: absolute;
149 180
	}
150 181
	&.booking {
151 182
		background: #eef linear-gradient(135deg, #eef 0%, #ddf 100%);
......
160 191
	}
161 192
}
162 193

  
194
.monthview tbody td div.booking {
195
	padding: 0;
196
	transition: width 100ms ease-in, left 100ms ease-in, color 200ms ease-in;
197
	text-indent: -9999px;
198
	&:hover {
199
		text-indent: 0;
200
		color: inherit;
201
		left: 0% !important;
202
		width: 100% !important
203
	}
204
}
205

  
163 206
span.start-time {
164 207
	font-size: 80%;
165 208
}
chrono/manager/templates/chrono/manager_agenda_day_view.html
26 26
  <a href="{% url 'chrono-manager-agenda-settings' pk=agenda.id %}">{% trans 'Settings' %}</a>
27 27
{% endif %}
28 28
<a href="" onclick="window.print()">{% trans 'Print' %}</a>
29
<a href="{% url 'chrono-manager-agenda-month-view' pk=agenda.id year=view.date|date:"Y" month=view.date|date:"n" %}">{% trans 'Month view' %}</a>
29 30
{% endblock %}
30 31

  
31 32
{% block content %}
chrono/manager/templates/chrono/manager_agenda_month_view.html
1
{% extends "chrono/manager_agenda_view.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 %}
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
{% if user_can_manage %}
25
  <a href="{% url 'chrono-manager-agenda-settings' pk=agenda.id %}">{% trans 'Settings' %}</a>
26
{% endif %}
27
<a href="" onclick="window.print()">{% trans 'Print' %}</a>
28
<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>
29
{% endblock %}
30

  
31
{% block content %}
32
{% for week_days in view.get_timetable_infos %}
33
{% if forloop.first %}
34
<table>
35
  <tbody>
36
{% endif %}
37
  <tr>
38
    <th></th>
39
    {% for day in week_days.days %}
40
    <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>
41
    {% endfor %}
42
  </tr>
43
  {% for hour in week_days.periods %}
44
  <tr>
45
    <th class="hour">{{ hour|date:"TIME_FORMAT" }}</th>
46
    {% for day in week_days.days %}
47
    <td>
48
      {% if forloop.parentloop.first %}
49
      {% for slot in day.infos.opening_hours %}
50
      <div class="opening-hours" style="height:{{ slot.css_height|stringformat:".1f" }}%;top:{{ slot.css_top|stringformat:".1f" }}%;width:{{ slot.css_width|stringformat:".1f" }}%;left:{{ slot.css_left|stringformat:".1f" }}%;"></div>
51
      {% endfor %}
52
      {% for slot in day.infos.booked_slots %}
53
      <div class="booking" style="left:{{ slot.css_left|stringformat:".1f" }}%;height:{{ slot.css_height|stringformat:".1f" }}%;top:{{ slot.css_top|stringformat:".1f" }}%;width:{{ slot.css_width|stringformat:".1f" }}%">
54
        <span class="start-time">{{slot.booking.event.start_datetime|date:"TIME_FORMAT"}}</span>
55
        <a {% if slot.booking.backoffice_url %}href="{{slot.booking.backoffice_url}}"{% endif %}
56
           >{% if slot.booking.label or slot.booking.user_name %}
57
          {{slot.booking.label}}{% if slot.booking.label and slot.booking.user_name %} - {% endif %} {{slot.booking.user_name}}
58
          {% else %}{% trans "booked" %}{% endif %}</a>
59
        <span class="desk">{{ slot.desk }}</span>
60
        </div>
61
      {% endfor %}
62
      {% endif %}
63
    </td>
64
    {% endfor %}
65
  </tr>
66
  {% endfor %}
67
{% if forloop.last %}
68
  </tbody>
69
</table>
70
{% endif %}
71

  
72
{% empty %}
73
<div class="closed-for-the-day">
74
  <p>{% trans "No opening hours this month." %}</p>
75
</div>
76
{% endfor %}
77

  
78
{% endblock %}
chrono/manager/urls.py
24 24
            name='chrono-manager-agenda-add'),
25 25
        url(r'^agendas/(?P<pk>\w+)/$', views.agenda_view,
26 26
            name='chrono-manager-agenda-view'),
27
        url(r'^agendas/(?P<pk>\w+)/(?P<year>[0-9]{4})/(?P<month>[0-9]+)/$', views.agenda_monthly_view,
28
            name='chrono-manager-agenda-month-view'),
27 29
        url(r'^agendas/(?P<pk>\w+)/(?P<year>[0-9]{4})/(?P<month>[0-9]+)/(?P<day>[0-9]+)/$', views.agenda_day_view,
28 30
            name='chrono-manager-agenda-day-view'),
29 31
        url(r'^agendas/(?P<pk>\w+)/settings$', views.agenda_settings,
chrono/manager/views.py
29 29
from django.utils.translation import ungettext
30 30
from django.utils.encoding import force_text
31 31
from django.views.generic import (DetailView, CreateView, UpdateView,
32
        ListView, DeleteView, FormView, TemplateView, DayArchiveView)
32
        ListView, DeleteView, FormView, TemplateView, DayArchiveView,
33
        MonthArchiveView)
33 34

  
34 35
from chrono.agendas.models import (Agenda, Event, MeetingType, TimePeriod,
35 36
                                   Booking, Desk, TimePeriodException, ICSError)
......
268 269
agenda_day_view = AgendaDayView.as_view()
269 270

  
270 271

  
272
class AgendaMonthView(AgendaDateView, MonthArchiveView):
273
    template_name = 'chrono/manager_agenda_month_view.html'
274

  
275
    def get_previous_month_url(self):
276
        previous_month = self.get_previous_month(self.date.date())
277
        return reverse('chrono-manager-agenda-month-view',
278
                kwargs={'pk': self.agenda.id,
279
                        'year': previous_month.year,
280
                        'month': previous_month.month})
281

  
282
    def get_next_month_url(self):
283
        next_month = self.get_next_month(self.date.date())
284
        return reverse('chrono-manager-agenda-month-view',
285
                kwargs={'pk': self.agenda.id,
286
                        'year': next_month.year,
287
                        'month': next_month.month})
288

  
289
    def get_day(self):
290
        return 1
291

  
292
    def get_day_timetable_infos(self, day):
293
        period = current_date = day.replace(hour=self.min_timeperiod.hour, minute=0)
294
        timetable = {'date': current_date,
295
                     'infos': {'opening_hours': [], 'booked_slots': []}}
296

  
297
        desks = self.agenda.desk_set.all()
298
        desks_len = len(desks)
299
        max_date = day.replace(hour=self.max_timeperiod.hour, minute=0)
300
        while period <= max_date:
301
            period_end = period + self.interval
302
            for desk_index, desk in enumerate(desks):
303
                for event in [x for x in self.object_list if x.desk_id == desk.id and
304
                              x.start_datetime >= period and x.start_datetime < period_end]:
305
                    # don't consider cancelled bookings
306
                    bookings = [x for x in event.booking_set.all() if not x.cancellation_datetime]
307
                    if not bookings:
308
                        continue
309
                    booking = {'css_top': 100 * (event.start_datetime - current_date).seconds // 3600,
310
                               'css_height': 100 * event.meeting_type.duration // 60,
311
                               'css_width': 100.0 / desks_len,
312
                               'css_left': 100.0 * desk_index / desks_len,
313
                               'desk': desk,
314
                               'booking': bookings[0]
315
                    }
316
                    timetable['infos']['booked_slots'].append(booking)
317

  
318
                # get desks opening hours on last period iteration
319
                if period == max_date:
320
                    for hour in desk.get_opening_hours(current_date):
321
                        timetable['infos']['opening_hours'].append({
322
                            'css_top': 100 * (hour.begin - current_date).seconds // 3600,
323
                            'css_height': 100 * (hour.end - hour.begin).seconds // 3600,
324
                            'css_width': 100.0 / desks_len,
325
                            'css_left': 100.0 * desk_index / desks_len
326
                        })
327
            period += self.interval
328

  
329
        return timetable
330

  
331
    def get_week_timetable_infos(self, i, timeperiods):
332
        date = self.date + datetime.timedelta(i*7)
333
        year, week_number, dow = date.isocalendar()
334
        start_date = date - datetime.timedelta(dow)
335

  
336
        self.min_timeperiod = min([x.start_time for x in timeperiods])
337
        self.max_timeperiod = max([x.end_time for x in timeperiods])
338
        self.interval = datetime.timedelta(minutes=60)
339

  
340
        period = self.date.replace(hour=self.min_timeperiod.hour, minute=0)
341
        max_date = self.date.replace(hour=self.max_timeperiod.hour, minute=0)
342

  
343
        periods = []
344
        while period < max_date:
345
            periods.append(period)
346
            period = period + self.interval
347

  
348
        return {'days': [self.get_day_timetable_infos(start_date + datetime.timedelta(i)) for i in range(1, 8)],
349
                'periods': periods}
350

  
351
    def get_timetable_infos(self):
352
        timeperiods = TimePeriod.objects.filter(desk__agenda=self.agenda)
353
        if not timeperiods:
354
            return
355

  
356
        first_week_number = self.date.isocalendar()[1]
357
        last_month_day = self.get_next_month(self.date.date()) - datetime.timedelta(days=1)
358
        last_week_number = last_month_day.isocalendar()[1]
359

  
360
        for i, week_number in enumerate(range(first_week_number, last_week_number + 1)):
361
            yield self.get_week_timetable_infos(i, timeperiods)
362

  
363
agenda_monthly_view = AgendaMonthView.as_view()
364

  
365

  
271 366
class ManagedAgendaMixin(object):
272 367
    agenda = None
273 368

  
tests/test_manager.py
1360 1360
    resp = app.get('/manage/agendas/%s/' % agenda.id, status=302).follow()
1361 1361
    assert resp.text.count('<tr') == 15
1362 1362
    assert '<th>11 p.m.</th>' in resp.text
1363

  
1364

  
1365
def test_agenda_month_view(app, admin_user, manager_user, api_user):
1366
    agenda = Agenda.objects.create(label='Passeports', kind='meetings')
1367
    desk = Desk.objects.create(agenda=agenda, label='Desk A')
1368

  
1369
    meetingtype = MeetingType(agenda=agenda, label='passeport', duration=20)
1370
    meetingtype.save()
1371

  
1372
    login(app)
1373
    resp = app.get('/manage/agendas/%s/' % agenda.id, status=302)
1374
    resp = resp.follow()
1375
    assert 'Month view' in resp.text
1376
    resp = resp.click('Month view')
1377
    today = datetime.date.today()
1378
    assert resp.request.url.endswith('%s/%s/' % (today.year, today.month))
1379

  
1380
    assert 'Day view' in resp.text # date view link should be present
1381
    assert 'No opening hours this month.' in resp.text
1382

  
1383
    timeperiod = TimePeriod(desk=desk, weekday=today.weekday(),
1384
            start_time=datetime.time(10, 0),
1385
            end_time=datetime.time(18, 0))
1386
    timeperiod.save()
1387
    resp = app.get('/manage/agendas/%s/%s/%s/' % (agenda.id, today.year, today.month))
1388
    assert not 'No opening hours this month.' in resp.text
1389
    assert not '<div class="booking' in resp.text
1390
    start_week_number = today.replace(day=1).isocalendar()[1]
1391
    end_week_number = (today.replace(day=1, month=today.month+1) - datetime.timedelta(days=1)).isocalendar()[1]
1392
    weeks_number = end_week_number - start_week_number + 1
1393
    assert resp.text.count('<tr') == 9 * weeks_number
1394

  
1395
    # check opening hours cells
1396
    assert '<div class="opening-hours"' in resp.text
1397
    assert 'style="height:800.0%;top:0.0%;width:100.0%;left:0.0%' in resp.text
1398

  
1399
    # book some slots
1400
    app.reset()
1401
    app.authorization = ('Basic', ('john.doe', 'password'))
1402
    resp = app.get('/api/agenda/%s/meetings/%s/datetimes/' % (agenda.slug, meetingtype.slug))
1403
    booking_url = resp.json['data'][0]['api']['fillslot_url']
1404
    booking_url2 = resp.json['data'][2]['api']['fillslot_url']
1405
    resp = app.post(booking_url)
1406
    resp = app.post_json(booking_url2,
1407
            params={'label': 'foo', 'user': 'bar', 'url': 'http://baz/'})
1408

  
1409
    app.reset()
1410
    login(app)
1411
    date = Booking.objects.all()[0].event.start_datetime
1412
    resp = app.get('/manage/agendas/%s/%d/%d/' % (
1413
        agenda.id, date.year, date.month))
1414
    assert resp.text.count('<div class="booking" style="left:0.0%;height:33.0%;') == 2 # booking cells
1415
    desk = Desk.objects.create(agenda=agenda, label='Desk B')
1416
    timeperiod = TimePeriod(desk=desk, weekday=today.weekday(),
1417
            start_time=datetime.time(10, 0),
1418
            end_time=datetime.time(18, 0))
1419
    timeperiod.save()
1420

  
1421
    app.reset()
1422
    resp = app.post(booking_url)
1423
    login(app)
1424
    resp = app.get('/manage/agendas/%s/%s/%s/' % (agenda.id, today.year, today.month))
1425
    assert resp.text.count('<div class="opening-hours"') == 2 * weeks_number
1426
    assert resp.text.count('<div class="booking" style="left:0.0%;height:33.0%;') == 2
1427
    assert resp.text.count('<div class="booking" style="left:50.0%;height:33.0%;') == 1
tox.ini
22 22
  pylint<1.8
23 23
  pylint-django<0.9
24 24
  django-webtest<1.9.3
25
  pytz
25 26
  py2: django-mellon
26 27
  py3: django-mellon>=1.2.35
27 28
  pytest-freezegun
28
-