Projet

Général

Profil

0001-manager-add-view-of-events-agenda-for-restricted-use.patch

Frédéric Péters, 23 décembre 2019 11:37

Télécharger (23,4 ko)

Voir les différences:

Subject: [PATCH 1/4] manager: add view of events agenda for restricted users
 (#20279)

 .../chrono/manager_agenda_month_view.html     |  52 +------
 .../manager_events_agenda_month_view.html     |  53 +++++++
 .../manager_meetings_agenda_month_view.html   |  56 +++++++
 chrono/manager/views.py                       | 144 +++++++++---------
 tests/test_manager.py                         |  55 ++++++-
 5 files changed, 230 insertions(+), 130 deletions(-)
 create mode 100644 chrono/manager/templates/chrono/manager_events_agenda_month_view.html
 create mode 100644 chrono/manager/templates/chrono/manager_meetings_agenda_month_view.html
chrono/manager/templates/chrono/manager_agenda_month_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-day-view' pk=agenda.id year=view.date|date:"Y" month=view.date|date:"m" day=view.date|date:"d" %}">{% trans 'Day view' %}</a>
30
{% endblock %}
29
{% block extra-actions %}{% endblock %}
31 30
</span>
32

  
33
{% block content %}
34
{% for week_days in view.get_timetable_infos %}
35
{% if forloop.first %}
36
<table class="agenda-table month-view">
37
  <tbody>
38
{% endif %}
39
  <tr>
40
    <th></th>
41
    {% for day in week_days.days %}
42
    <th class="weekday {% if day.today %}today{% 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 j" }}</a>{% endif %}</th>
43
    {% endfor %}
44
  </tr>
45
  {% for hour in week_days.periods %}
46
  <tr class="{% cycle 'odd' 'even' %}">
47
    <th class="hour">{{ hour|date:"TIME_FORMAT" }}</th>
48
    {% for day in week_days.days %}
49
    <td class="{% if day.other_month %}other-month{% endif %} {% if day.today %}today{% endif %}">
50
      {% if forloop.parentloop.first %}
51
      {% for slot in day.infos.opening_hours %}
52
      <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>
53
      {% endfor %}
54
      {% for slot in day.infos.booked_slots %}
55
      <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" }}%">
56
        <span class="start-time">{{slot.booking.event.start_datetime|date:"TIME_FORMAT"}}</span>
57
        <a {% if slot.booking.backoffice_url %}href="{{slot.booking.backoffice_url}}"{% endif %}
58
           >{% if slot.booking.label or slot.booking.user_name %}
59
          {{slot.booking.label}}{% if slot.booking.label and slot.booking.user_name %} - {% endif %} {{slot.booking.user_name}}
60
          {% else %}{% trans "booked" %}{% endif %}</a>
61
        <span class="desk">{{ slot.desk }}</span>
62
        </div>
63
      {% endfor %}
64
      {% endif %}
65
    </td>
66
    {% endfor %}
67
  </tr>
68
  {% endfor %}
69
  {% resetcycle %}
70
{% if forloop.last %}
71
  </tbody>
72
</table>
73
{% endif %}
74

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

  
81 31
{% endblock %}
chrono/manager/templates/chrono/manager_events_agenda_month_view.html
1
{% extends "chrono/manager_agenda_month_view.html" %}
2
{% load i18n %}
3

  
4
{% block content %}
5
<div class="section">
6
<h3>{% trans "Events" %}</h3>
7
<div>
8
{% if object_list %}
9
  <ul class="objects-list single-links">
10
  {% for event in object_list %}
11
    <li class="{% if event.booked_places > event.places %}overbooking{% endif %}
12
               {% if event.full %}full{% endif %}
13
               {% if not event.in_bookable_period %}not-{% endif %}bookable"
14
        {% if event.places %}
15
          data-total="{{event.places}}" data-booked="{{event.booked_places}}"
16
        {% elif event.waiting_list_places %}
17
          data-total="{{event.waiting_list_places}}" data-booked="{{event.waiting_list}}"
18
        {% endif %}
19
        ><a>
20
        {% if event.label %}{{event.label}} / {% endif %}
21
        {{ event.start_datetime }}
22
        {% if event.full %}/ <span class="full">{% trans "full" %}</span>{% endif %}
23
        (
24
        {% if event.places %}
25
        {% blocktrans with places=event.places booked_places=event.booked_places %}{{ places }} places, {{ booked_places }} booked places{% endblocktrans %}
26
        {% endif %}
27
        {% if event.places and event.waiting_list_places %} / {% endif %}
28
        {% if event.waiting_list_places %}
29
        {% blocktrans with places=event.waiting_list_places waiting_places=event.waiting_list %}
30
        {{waiting_places}} on {{ places }} in waiting list
31
        {% endblocktrans %}
32
        {% endif %}
33
        )
34
        {% if not event.in_bookable_period %}
35
        ({% trans "out of bookable period" %})
36
        {% endif %}
37
            </a>
38
        <span class="occupation-bar"></span>
39
    </li>
40
    {% endfor %}
41
  </ul>
42
  {% include "gadjo/pagination.html" %}
43
{% else %}
44
<div class="big-msg-info">
45
  {% blocktrans %}
46
  This month doesn't have any event configured.
47
  {% endblocktrans %}
48
</div>
49
{% endif %}
50
</div>
51
</div>
52

  
53
{% endblock %}
chrono/manager/templates/chrono/manager_meetings_agenda_month_view.html
1
{% extends "chrono/manager_agenda_month_view.html" %}
2
{% load i18n %}
3

  
4
{% block extra-actions %}
5
<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>
6
{% endblock %}
7

  
8
{% block content %}
9
{% for week_days in view.get_timetable_infos %}
10
{% if forloop.first %}
11
<table class="agenda-table month-view">
12
  <tbody>
13
{% endif %}
14
  <tr>
15
    <th></th>
16
    {% for day in week_days.days %}
17
    <th class="weekday {% if day.today %}today{% 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 j" }}</a>{% endif %}</th>
18
    {% endfor %}
19
  </tr>
20
  {% for hour in week_days.periods %}
21
  <tr class="{% cycle 'odd' 'even' %}">
22
    <th class="hour">{{ hour|date:"TIME_FORMAT" }}</th>
23
    {% for day in week_days.days %}
24
    <td class="{% if day.other_month %}other-month{% endif %} {% if day.today %}today{% endif %}">
25
      {% if forloop.parentloop.first %}
26
      {% for slot in day.infos.opening_hours %}
27
      <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>
28
      {% endfor %}
29
      {% for slot in day.infos.booked_slots %}
30
      <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" }}%">
31
        <span class="start-time">{{slot.booking.event.start_datetime|date:"TIME_FORMAT"}}</span>
32
        <a {% if slot.booking.backoffice_url %}href="{{slot.booking.backoffice_url}}"{% endif %}
33
           >{% if slot.booking.label or slot.booking.user_name %}
34
          {{slot.booking.label}}{% if slot.booking.label and slot.booking.user_name %} - {% endif %} {{slot.booking.user_name}}
35
          {% else %}{% trans "booked" %}{% endif %}</a>
36
        <span class="desk">{{ slot.desk }}</span>
37
        </div>
38
      {% endfor %}
39
      {% endif %}
40
    </td>
41
    {% endfor %}
42
  </tr>
43
  {% endfor %}
44
  {% resetcycle %}
45
{% if forloop.last %}
46
  </tbody>
47
</table>
48
{% endif %}
49

  
50
{% empty %}
51
<div class="closed-for-the-day">
52
  <p>{% trans "No opening hours this month." %}</p>
53
</div>
54
{% endfor %}
55

  
56
{% endblock %}
chrono/manager/views.py
160 160
agendas_import = AgendasImportView.as_view()
161 161

  
162 162

  
163
class AgendaEditView(UpdateView):
163
class ViewableAgendaMixin(object):
164
    agenda = None
165

  
166
    def dispatch(self, request, *args, **kwargs):
167
        self.agenda = get_object_or_404(Agenda, id=kwargs.get('pk'))
168
        if not self.check_permissions(request.user):
169
            raise PermissionDenied()
170
        return super(ViewableAgendaMixin, self).dispatch(request, *args, **kwargs)
171

  
172
    def check_permissions(self, user):
173
        return self.agenda.can_be_viewed(user)
174

  
175
    def get_context_data(self, **kwargs):
176
        context = super(ViewableAgendaMixin, self).get_context_data(**kwargs)
177
        context['agenda'] = self.agenda
178
        return context
179

  
180

  
181
class ManagedAgendaMixin(ViewableAgendaMixin):
182
    def check_permissions(self, user):
183
        return self.agenda.can_be_managed(user)
184

  
185
    def get_initial(self):
186
        initial = super(ManagedAgendaMixin, self).get_initial()
187
        initial['agenda'] = self.agenda.id
188
        return initial
189

  
190
    def get_success_url(self):
191
        return reverse('chrono-manager-agenda-settings', kwargs={'pk': self.agenda.id})
192

  
193

  
194
class AgendaEditView(ManagedAgendaMixin, UpdateView):
164 195
    template_name = 'chrono/manager_agenda_form.html'
165 196
    model = Agenda
166 197
    form_class = AgendaEditForm
167 198

  
168
    def get_object(self, queryset=None):
169
        obj = super(AgendaEditView, self).get_object(queryset=queryset)
170
        if not obj.can_be_managed(self.request.user):
171
            raise PermissionDenied()
172
        return obj
173

  
174 199
    def get_success_url(self):
175 200
        return reverse('chrono-manager-agenda-settings', kwargs={'pk': self.object.id})
176 201

  
......
208 233
agenda_delete = AgendaDeleteView.as_view()
209 234

  
210 235

  
211
class AgendaView(DetailView):
236
class AgendaView(ViewableAgendaMixin, ListView):
212 237
    model = Agenda
213 238

  
214 239
    def get(self, request, *args, **kwargs):
215
        try:
216
            agenda = Agenda.objects.get(id=kwargs.get('pk'))
217
        except Agenda.DoesNotExist:
218
            raise Http404()
219
        if not agenda.can_be_viewed(self.request.user):
220
            raise PermissionDenied()
221

  
222
        if agenda.kind == 'meetings':
240
        today = datetime.date.today()
241
        if self.agenda.kind == 'meetings':
223 242
            # redirect to today view
224
            today = datetime.date.today()
225 243
            return HttpResponseRedirect(
226 244
                reverse(
227 245
                    'chrono-manager-agenda-day-view',
228
                    kwargs={'pk': agenda.id, 'year': today.year, 'month': today.month, 'day': today.day},
246
                    kwargs={'pk': self.agenda.id, 'year': today.year, 'month': today.month, 'day': today.day},
229 247
                )
230 248
            )
231 249

  
232
        # redirect to settings
233
        return HttpResponseRedirect(reverse('chrono-manager-agenda-settings', kwargs={'pk': agenda.id}))
250
        if self.agenda.kind == 'events':
251
            # redirect to monthly view, to first month where there are events,
252
            # otherwise to latest month with events, otherwise to this month.
253
            event = self.agenda.event_set.filter(
254
                start_datetime__gte=datetime.date(today.year, today.month, 1)
255
            ).first()
256
            if not event:
257
                event = self.agenda.event_set.filter(
258
                    start_datetime__lte=datetime.date(today.year, today.month, 1)
259
                ).last()
260
            if event:
261
                day = event.start_datetime
262
            else:
263
                day = today
264
            return HttpResponseRedirect(
265
                reverse(
266
                    'chrono-manager-agenda-month-view',
267
                    kwargs={'pk': self.agenda.id, 'year': day.year, 'month': day.month},
268
                )
269
            )
234 270

  
235 271

  
236 272
agenda_view = AgendaView.as_view()
237 273

  
238 274

  
239
class AgendaDateView(object):
275
class AgendaDateView(ViewableAgendaMixin):
240 276
    model = Event
241 277
    month_format = '%m'
242 278
    date_field = 'start_datetime'
......
244 280
    allow_future = True
245 281

  
246 282
    def dispatch(self, request, *args, **kwargs):
247
        self.agenda = get_object_or_404(Agenda, id=kwargs.get('pk'))
248
        if self.agenda.kind != 'meetings':
249
            raise Http404()
250
        if not self.agenda.can_be_viewed(request.user):
251
            raise PermissionDenied()
252

  
253 283
        # specify 6am time to get the expected timezone on daylight saving time
254 284
        # days.
255 285
        try:
......
267 297
            return HttpResponseRedirect(
268 298
                reverse(
269 299
                    'chrono-manager-agenda-day-view',
270
                    kwargs={'pk': self.agenda.id, 'year': date.year, 'month': date.month, 'day': date.day},
300
                    kwargs={'pk': kwargs['pk'], 'year': date.year, 'month': date.month, 'day': date.day},
271 301
                )
272 302
            )
273 303
        return super(AgendaDateView, self).dispatch(request, *args, **kwargs)
......
275 305
    def get_context_data(self, **kwargs):
276 306
        context = super(AgendaDateView, self).get_context_data(**kwargs)
277 307
        context['agenda'] = self.agenda
278
        try:
279
            context['hour_span'] = max(60 // self.agenda.get_base_meeting_duration(), 1)
280
        except ValueError:  # no meeting types defined
281
            context['hour_span'] = 1
308
        if self.agenda.kind == 'meetings':
309
            try:
310
                context['hour_span'] = max(60 // self.agenda.get_base_meeting_duration(), 1)
311
            except ValueError:  # no meeting types defined
312
                context['hour_span'] = 1
282 313
        context['user_can_manage'] = self.agenda.can_be_managed(self.request.user)
283 314
        return context
284 315

  
......
301 332
class AgendaDayView(AgendaDateView, DayArchiveView):
302 333
    template_name = 'chrono/manager_agenda_day_view.html'
303 334

  
335
    def dispatch(self, request, *args, **kwargs):
336
        # day view should only exist for meetings kind.
337
        get_object_or_404(Agenda, id=kwargs.get('pk'), kind='meetings')
338
        return super(AgendaDayView, self).dispatch(request, *args, **kwargs)
339

  
304 340
    def get_previous_day_url(self):
305 341
        previous_day = self.date.date() - datetime.timedelta(days=1)
306 342
        return reverse(
......
385 421

  
386 422

  
387 423
class AgendaMonthView(AgendaDateView, MonthArchiveView):
388
    template_name = 'chrono/manager_agenda_month_view.html'
424
    def get_template_names(self):
425
        return ['chrono/manager_%s_agenda_month_view.html' % self.agenda.kind]
389 426

  
390 427
    def get_previous_month_url(self):
391 428
        previous_month = self.get_previous_month(self.date.date())
......
507 544
agenda_monthly_view = AgendaMonthView.as_view()
508 545

  
509 546

  
510
class ManagedAgendaMixin(object):
511
    agenda = None
512

  
513
    def dispatch(self, request, *args, **kwargs):
514
        try:
515
            self.agenda = Agenda.objects.get(id=kwargs.get('pk'))
516
        except Agenda.DoesNotExist:
517
            raise Http404()
518
        if not self.agenda.can_be_managed(request.user):
519
            raise PermissionDenied()
520
        return super(ManagedAgendaMixin, self).dispatch(request, *args, **kwargs)
521

  
522
    def get_context_data(self, **kwargs):
523
        context = super(ManagedAgendaMixin, self).get_context_data(**kwargs)
524
        context['agenda'] = self.agenda
525
        return context
526

  
527
    def get_initial(self):
528
        initial = super(ManagedAgendaMixin, self).get_initial()
529
        initial['agenda'] = self.agenda.id
530
        return initial
531

  
532
    def get_success_url(self):
533
        return reverse('chrono-manager-agenda-settings', kwargs={'pk': self.agenda.id})
534

  
535

  
536 547
class ManagedAgendaSubobjectMixin(object):
537 548
    agenda = None
538 549

  
......
600 611
class AgendaSettings(ManagedAgendaMixin, DetailView):
601 612
    model = Agenda
602 613

  
603
    def dispatch(self, request, *args, **kwargs):
604
        try:
605
            self.agenda = Agenda.objects.get(id=kwargs.get('pk'))
606
        except Agenda.DoesNotExist:
607
            raise Http404()
608
        if not self.agenda.can_be_managed(request.user):
609
            # "events" agendas settings page can be access by user with the
610
            # view permission as there are no other "view" page for this type
611
            # of agenda.
612
            if self.agenda.kind != 'events' or not self.agenda.can_be_viewed(request.user):
613
                raise PermissionDenied()
614
        return super(DetailView, self).dispatch(request, *args, **kwargs)
615

  
616 614
    def get_context_data(self, **kwargs):
617 615
        context = super(AgendaSettings, self).get_context_data(**kwargs)
618
        context['user_can_manage'] = self.get_object().can_be_managed(self.request.user)
616
        context['user_can_manage'] = True
619 617
        return context
620 618

  
621 619
    def get_template_names(self):
tests/test_manager.py
159 159
    # check user doesn't have access
160 160
    app.get('/manage/agendas/%s/' % agenda2.id, status=403)
161 161

  
162
    # check view gives access to the settings page for "events" agenda
163
    resp = app.get('/manage/agendas/%s/settings' % agenda.id, status=200)
164
    # but there's no links to actions
165
    assert not '>New Event<' in resp.text
166
    assert not '>Options<' in resp.text
162
    # check there's no access to the settings page for "events" agenda
163
    resp = app.get('/manage/agendas/%s/settings' % agenda.id, status=403)
167 164
    app.get('/manage/agendas/%s/add-event' % agenda.id, status=403)
168 165
    app.get('/manage/agendas/%s/edit' % agenda.id, status=403)
169 166

  
......
207 204
    app = login(app)
208 205
    resp = app.get('/manage/', status=200)
209 206
    resp = resp.click('Foo bar').follow()
207
    resp = resp.click('Settings')
210 208
    resp = resp.click('Options')
211 209
    assert resp.form['label'].value == 'Foo bar'
212 210
    resp.form['label'] = 'Foo baz'
......
225 223
    resp = app.get('/manage/', status=200)
226 224
    resp = resp.click('Foo bar')
227 225
    assert not 'Settings' in resp.text
228
    resp = app.get('/manage/agendas/%s/settings' % agenda.id, status=200)  # ok for "events" agendas
226
    resp = app.get('/manage/agendas/%s/settings' % agenda.id, status=403)
229 227
    resp = app.get('/manage/agendas/%s/edit' % agenda.id, status=403)
230 228
    agenda.kind = 'meetings'
231 229
    agenda.save()
......
241 239

  
242 240
    resp = app.get('/manage/', status=200)
243 241
    resp = resp.click('Foo bar').follow()
242
    resp = resp.click('Settings')
244 243
    resp = resp.click('Options')
245 244
    assert resp.form['label'].value == 'Foo bar'
246 245
    resp.form['label'] = 'Foo baz'
......
257 256
    app = login(app)
258 257
    resp = app.get('/manage/', status=200)
259 258
    resp = resp.click('Foo bar').follow()
259
    resp = resp.click('Settings')
260 260
    resp = resp.click('Delete')
261 261
    resp = resp.form.submit()
262 262
    assert resp.location.endswith('/manage/')
......
273 273
    app = login(app)
274 274
    resp = app.get('/manage/', status=200)
275 275
    resp = resp.click('Foo bar').follow()
276
    resp = resp.click('Settings')
276 277
    resp = resp.click('Delete')
277 278
    assert 'Are you sure you want to delete this?' in resp.text
278 279

  
......
280 281
    booking.save()
281 282
    resp = app.get('/manage/', status=200)
282 283
    resp = resp.click('Foo bar').follow()
284
    resp = resp.click('Settings')
283 285
    resp = resp.click('Delete')
284 286
    assert 'This cannot be removed' in resp.text
285 287

  
......
287 289
    booking.save()
288 290
    resp = app.get('/manage/', status=200)
289 291
    resp = resp.click('Foo bar').follow()
292
    resp = resp.click('Settings')
290 293
    resp = resp.click('Delete')
291 294
    assert 'Are you sure you want to delete this?' in resp.text
292 295

  
......
304 307
    app = login(app, username='manager', password='manager')
305 308
    resp = app.get('/manage/', status=200)
306 309
    resp = resp.click('Foo bar').follow()
310
    resp = resp.click('Settings')
307 311
    assert 'Options' in resp.text
308 312
    assert 'Delete' not in resp.text
309 313
    resp = app.get('/manage/agendas/%s/delete' % agenda.id, status=403)
......
391 395
    agenda.save()
392 396

  
393 397
    resp = app.get('/manage/agendas/%s/' % agenda.id).follow()
398
    resp = resp.click('Settings')
394 399
    assert '<h2>Settings' in resp.text
395 400
    resp = resp.click('New Event')
396 401
    resp.form['start_datetime'] = '2016-02-15 17:00'
......
1683 1688
    assert resp.location.endswith('2018/11/30/')
1684 1689

  
1685 1690

  
1691
def test_agenda_events_month_view(app, admin_user, manager_user, api_user):
1692
    agenda = Agenda.objects.create(label='Events', kind='events')
1693

  
1694
    login(app)
1695
    resp = app.get('/manage/agendas/%s/' % agenda.id, status=302)
1696
    resp = resp.follow()
1697
    assert "This month doesn't have any event configured." in resp.text
1698
    today = datetime.date.today()
1699
    assert resp.request.url.endswith('%s/%s/' % (today.year, today.month))
1700

  
1701
    # add event in a future month
1702
    event = Event(label='xyz', start_datetime=now() + datetime.timedelta(days=40), places=10, agenda=agenda)
1703
    event.save()
1704
    resp = app.get('/manage/agendas/%s/' % agenda.id).follow()
1705
    assert 'xyz' in resp.text
1706
    day = event.start_datetime
1707
    assert resp.request.url.endswith('%s/%s/' % (day.year, day.month))
1708

  
1709
    # current month still doesn't have events
1710
    resp = app.get('/manage/agendas/%s/%s/%s/' % (agenda.id, today.year, today.month))
1711
    assert "This month doesn't have any event configured." in resp.text
1712

  
1713
    # add event in the past
1714
    event2 = Event(label='zyx', start_datetime=now() - datetime.timedelta(days=40), places=10, agenda=agenda)
1715
    event2.save()
1716
    resp = app.get('/manage/agendas/%s/' % agenda.id).follow()
1717
    assert 'xyz' in resp.text
1718
    day = event.start_datetime  # still the future event
1719
    assert resp.request.url.endswith('%s/%s/' % (day.year, day.month))
1720

  
1721
    # remove future event
1722
    event.delete()
1723
    resp = app.get('/manage/agendas/%s/' % agenda.id).follow()
1724
    assert 'zyx' in resp.text
1725
    day = event2.start_datetime  # now the past event
1726
    assert resp.request.url.endswith('%s/%s/' % (day.year, day.month))
1727

  
1728

  
1686 1729
def test_agenda_month_view(app, admin_user, manager_user, api_user):
1687 1730
    agenda = Agenda.objects.create(label='Passeports', kind='meetings')
1688 1731
    desk = Desk.objects.create(agenda=agenda, label='Desk A')
1689
-