Projet

Général

Profil

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

Serghei Mihai, 31 août 2018 17:54

Télécharger (17,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          |  62 ++++++++++-
 .../chrono/manager_agenda_day_view.html       |   1 +
 .../chrono/manager_agenda_month_view.html     |  78 +++++++++++++
 chrono/manager/urls.py                        |   2 +
 chrono/manager/views.py                       | 104 +++++++++++++++++-
 tests/test_manager.py                         |  76 +++++++++++++
 tox.ini                                       |   3 +-
 7 files changed, 320 insertions(+), 6 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
		padding: 0.75em 1em;
106
		a {
107
			color: #000;
108
			border: 0;
109
		}
110
		&.current-day {
111
			font-weight: bold;
112
		}
113
	}
114
	td {
115
		position: relative;
116
	}
117
	span.desk {
118
		display: block;
119
		padding: 2px;
120
	}
121
}
122

  
98 123
.dayview thead th {
99 124
	width: 14vw;
100 125
	padding-bottom: 1ex;
......
116 141
}
117 142

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

  
126
.dayview tbody td {
151
.dayview tbody tr:nth-child(2n+1) td,
152
.monthview tbody tr:nth-child(2n+1) td {
153
	background: #f0f0f0;
154
	@media print {
155
		border-top: 1px solid #aaa;
156
	}
157
}
158

  
159

  
160

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

  
167
.monthview tbody tr td {
168
	&.other-month {
169
		background: transparent;;
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 not day.other_month %}<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 day.other_month %} 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 '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
                     'other_month': day.month != self.date.month,
296
                     'infos': {'opening_hours': [], 'booked_slots': []}}
297

  
298
        desks = self.agenda.desk_set.all()
299
        desks_len = len(desks)
300
        max_date = day.replace(hour=self.max_timeperiod.hour, minute=0)
301

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

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

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

  
335
        return timetable
336

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

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

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

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

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

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

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

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

  
370
agenda_monthly_view = AgendaMonthView.as_view()
371

  
372

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

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

  
1370
def test_agenda_month_view(app, admin_user, manager_user, api_user):
1371
    agenda = Agenda.objects.create(label='Passeports', kind='meetings')
1372
    desk = Desk.objects.create(agenda=agenda, label='Desk A')
1373

  
1374
    meetingtype = MeetingType(agenda=agenda, label='passeport', duration=20)
1375
    meetingtype.save()
1376

  
1377
    login(app)
1378
    resp = app.get('/manage/agendas/%s/' % agenda.id, status=302)
1379
    resp = resp.follow()
1380
    assert 'Month view' in resp.text
1381
    resp = resp.click('Month view')
1382
    today = datetime.date.today()
1383
    assert resp.request.url.endswith('%s/%s/' % (today.year, today.month))
1384

  
1385
    assert 'Day view' in resp.text # date view link should be present
1386
    assert 'No opening hours this month.' in resp.text
1387

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

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

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

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

  
1428
    app.reset()
1429
    resp = app.post(booking_url)
1430
    login(app)
1431
    resp = app.get('/manage/agendas/%s/%s/%s/' % (agenda.id, today.year, today.month))
1432

  
1433
    # count occurences of timeperiod weekday in current month
1434
    d = first_month_day
1435
    weekdays = 0
1436
    while d <= last_month_day:
1437
        if d.weekday() == timeperiod_weekday:
1438
            weekdays += 1
1439
        d += datetime.timedelta(days=1)
1440

  
1441
    assert resp.text.count('<div class="opening-hours"') == 2 * weekdays
1442
    current_month = today.strftime('%Y-%m')
1443
    if current_month in booking_url or current_month in booking_url2:
1444
        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 29
commands =
29 30
  py2: ./getlasso.sh
30 31
  py3: ./getlasso3.sh
31
  py.test {env:COVERAGE:} {posargs:tests/}
32
  py.test {env:COVERAGE:} {posargs:tests/test_manager.py}
32 33
  pylint: ./pylint.sh chrono/
33
-