Projet

Général

Profil

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

Serghei Mihai, 28 août 2018 11:36

Télécharger (17,2 ko)

Voir les différences:

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
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
		font-weight: normal;
103
	}
104
	th.weekday {
105
		width: 12.5%;
106
		font-weight: normal;
107
		a {
108
			color: #000;
109
			text-decoration: underline;
110
		}
111
		&.current-day {
112
			font-weight: bold;
113
			a {
114
				color: #07C;
115
			}
116
		}
117
	}
118
	td {
119
		position: relative;
120
	}
121
	span.desk {
122
		display: block;
123
		padding: 2px;
124
	}
125
}
126

  
98 127
.dayview thead th {
99 128
	width: 14vw;
100 129
	padding-bottom: 1ex;
......
116 145
}
117 146

  
118 147
.dayview tbody tr:nth-child(2n+1) th,
119
.dayview tbody tr:nth-child(2n+1) td {
148
.monthview tbody tr:nth-child(2n+1) th,
149
.dayview tbody tr:nth-child(2n+1) td,
150
.monthview tbody tr:nth-child(2n+1) td {
120 151
	background: #f0f0f0;
152
	background-clip: padding-box;
121 153
	@media print {
122 154
		border-top: 1px solid #aaa;
123 155
	}
124 156
}
125 157

  
126
.dayview tbody td {
158
.dayview tbody td, .monthview tbody td {
127 159
	padding: 0 1ex;
128 160
	vertical-align: top;
129 161
	position: relative;
130 162
}
131 163

  
164
.monthview tbody tr td {
165
	border-left: 1px solid #d0d0d0;
166
	border-right: 1px solid #d0d0d0;
167
	&.other-month {
168
		background: #fafafa;
169
		background-clip: padding-box;
170
	}
171
}
172

  
132 173
@for $i from 1 through 60 {
133 174
	table.hourspan-#{$i} tbody td {
134 175
		height: calc(#{$i} * 2.5em);
135 176
	}
136 177
}
137 178

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

  
205
.monthview tbody td div.booking {
206
	padding: 0;
207
	transition: width 100ms ease-in, left 100ms ease-in, color 200ms ease-in;
208
	text-indent: -9999px;
209
	&:hover {
210
		text-indent: 0;
211
		color: inherit;
212
		left: 0% !important;
213
		width: 100% !important
214
	}
215
}
216

  
163 217
span.start-time {
164 218
	font-size: 80%;
165 219
}
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.day == view.date.day %} current-day{% endif %}">{% if day.infos.opening_hours %}<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>{% endif %}</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{% if not day.infos.opening_hours %} class="other-month"{% endif %}>
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 datetime.date.today().day
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

  
301
        # compute booking and opening hours only for current month
302
        if self.date.month != day.month:
303
            return timetable
304

  
305
        while period <= max_date:
306
            period_end = period + self.interval
307
            for desk_index, desk in enumerate(desks):
308
                for event in [x for x in self.object_list if x.desk_id == desk.id and
309
                              x.start_datetime >= period and x.start_datetime < period_end]:
310
                    # don't consider cancelled bookings
311
                    bookings = [x for x in event.booking_set.all() if not x.cancellation_datetime]
312
                    if not bookings:
313
                        continue
314
                    booking = {'css_top': 100 * (event.start_datetime - current_date).seconds // 3600,
315
                               'css_height': 100 * event.meeting_type.duration // 60,
316
                               'css_width': 100.0 / desks_len,
317
                               'css_left': 100.0 * desk_index / desks_len,
318
                               'desk': desk,
319
                               'booking': bookings[0]
320
                    }
321
                    timetable['infos']['booked_slots'].append(booking)
322

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

  
334
        return timetable
335

  
336
    def get_week_timetable_infos(self, i, timeperiods):
337
        date = self.date.replace(day=1) + datetime.timedelta(i*7)
338
        year, week_number, dow = date.isocalendar()
339
        start_date = date - datetime.timedelta(dow)
340

  
341
        self.min_timeperiod = min([x.start_time for x in timeperiods])
342
        self.max_timeperiod = max([x.end_time for x in timeperiods])
343
        self.interval = datetime.timedelta(minutes=60)
344

  
345
        period = self.date.replace(hour=self.min_timeperiod.hour, minute=0)
346
        max_date = self.date.replace(hour=self.max_timeperiod.hour, minute=0)
347

  
348
        periods = []
349
        while period < max_date:
350
            periods.append(period)
351
            period = period + self.interval
352

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

  
356
    def get_timetable_infos(self):
357
        timeperiods = TimePeriod.objects.filter(desk__agenda=self.agenda)
358
        if not timeperiods:
359
            return
360

  
361
        first_month_day = self.date.replace(day=1)
362
        first_week_number = first_month_day.isocalendar()[1]
363
        last_month_day = self.get_next_month(first_month_day.date()) - datetime.timedelta(days=1)
364
        last_week_number = last_month_day.isocalendar()[1]
365

  
366
        for i, week_number in enumerate(range(first_week_number, last_week_number + 1)):
367
            yield self.get_week_timetable_infos(i, timeperiods)
368

  
369
agenda_monthly_view = AgendaMonthView.as_view()
370

  
371

  
271 372
class ManagedAgendaMixin(object):
272 373
    agenda = None
273 374

  
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
    first_month_day = today.replace(day=1)
1391
    last_month_day = today.replace(day=1, month=today.month+1) - datetime.timedelta(days=1)
1392
    start_week_number = first_month_day.isocalendar()[1]
1393
    end_week_number = last_month_day.isocalendar()[1]
1394
    weeks_number = end_week_number - start_week_number + 1
1395
    assert resp.text.count('<tr') == 9 * weeks_number
1396

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

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

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

  
1423
    app.reset()
1424
    resp = app.post(booking_url)
1425
    login(app)
1426
    resp = app.get('/manage/agendas/%s/%s/%s/' % (agenda.id, today.year, today.month))
1427
    assert resp.text.count('<div class="opening-hours"') == 8
1428
    current_month = today.strftime('%Y-%m')
1429
    if current_month in booking_url or current_month in booking_url2:
1430
        assert '<div class="booking"' in resp.text
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
-