Projet

Général

Profil

0001-start-unavailability-calendars-46555.patch

Emmanuel Cazenave, 06 octobre 2020 15:55

Télécharger (59,5 ko)

Voir les différences:

Subject: [PATCH] start unavailability calendars (#46555)

 .../0065_unavailability_calendar.py           |  68 ++++
 chrono/agendas/models.py                      |  57 ++-
 chrono/api/views.py                           |  12 +
 chrono/manager/forms.py                       |  22 ++
 ...r_agenda_unavailability_calendar_form.html |  23 ++
 .../templates/chrono/manager_home.html        |   7 +-
 .../chrono/manager_import_exceptions.html     |   8 +-
 .../manager_time_period_exception_form.html   |   2 +-
 ...anager_unavailability_calendar_detail.html |  51 +++
 .../manager_unavailability_calendar_form.html |  22 ++
 .../manager_unavailability_calendar_list.html |  38 ++
 ...ager_unavailability_calendar_settings.html |  52 +++
 chrono/manager/urls.py                        |  40 ++
 chrono/manager/views.py                       | 255 ++++++++++++-
 chrono/urls_utils.py                          |   9 +-
 tests/test_api.py                             | 131 ++++++-
 tests/test_manager.py                         | 346 +++++++++++++++++-
 17 files changed, 1124 insertions(+), 19 deletions(-)
 create mode 100644 chrono/agendas/migrations/0065_unavailability_calendar.py
 create mode 100644 chrono/manager/templates/chrono/manager_agenda_unavailability_calendar_form.html
 create mode 100644 chrono/manager/templates/chrono/manager_unavailability_calendar_detail.html
 create mode 100644 chrono/manager/templates/chrono/manager_unavailability_calendar_form.html
 create mode 100644 chrono/manager/templates/chrono/manager_unavailability_calendar_list.html
 create mode 100644 chrono/manager/templates/chrono/manager_unavailability_calendar_settings.html
chrono/agendas/migrations/0065_unavailability_calendar.py
1
# -*- coding: utf-8 -*-
2
# Generated by Django 1.11.18 on 2020-10-05 12:37
3
from __future__ import unicode_literals
4

  
5
from django.db import migrations, models
6
import django.db.models.deletion
7

  
8

  
9
class Migration(migrations.Migration):
10

  
11
    dependencies = [
12
        ('auth', '0008_alter_user_username_max_length'),
13
        ('agendas', '0064_booking_form_url'),
14
    ]
15

  
16
    operations = [
17
        migrations.CreateModel(
18
            name='UnavailabilityCalendar',
19
            fields=[
20
                (
21
                    'id',
22
                    models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
23
                ),
24
                ('label', models.CharField(max_length=150, verbose_name='Label')),
25
                ('slug', models.SlugField(max_length=160, unique=True, verbose_name='Identifier')),
26
                ('desks', models.ManyToManyField(related_name='unavailability_calendars', to='agendas.Desk')),
27
                (
28
                    'edit_role',
29
                    models.ForeignKey(
30
                        blank=True,
31
                        default=None,
32
                        null=True,
33
                        on_delete=django.db.models.deletion.SET_NULL,
34
                        related_name='+',
35
                        to='auth.Group',
36
                        verbose_name='Edit Role',
37
                    ),
38
                ),
39
                (
40
                    'view_role',
41
                    models.ForeignKey(
42
                        blank=True,
43
                        default=None,
44
                        null=True,
45
                        on_delete=django.db.models.deletion.SET_NULL,
46
                        related_name='+',
47
                        to='auth.Group',
48
                        verbose_name='View Role',
49
                    ),
50
                ),
51
            ],
52
            options={'ordering': ['label'],},
53
        ),
54
        migrations.AlterField(
55
            model_name='timeperiodexception',
56
            name='desk',
57
            field=models.ForeignKey(
58
                null=True, on_delete=django.db.models.deletion.CASCADE, to='agendas.Desk'
59
            ),
60
        ),
61
        migrations.AddField(
62
            model_name='timeperiodexception',
63
            name='unavailability_calendar',
64
            field=models.ForeignKey(
65
                null=True, on_delete=django.db.models.deletion.CASCADE, to='agendas.UnavailabilityCalendar'
66
            ),
67
        ),
68
    ]
chrono/agendas/models.py
1486 1486
        self.save()
1487 1487

  
1488 1488

  
1489
class UnavailabilityCalendar(models.Model):
1490
    label = models.CharField(_('Label'), max_length=150)
1491
    slug = models.SlugField(_('Identifier'), max_length=160, unique=True)
1492
    desks = models.ManyToManyField(Desk, related_name='unavailability_calendars')
1493
    edit_role = models.ForeignKey(
1494
        Group,
1495
        blank=True,
1496
        null=True,
1497
        default=None,
1498
        related_name='+',
1499
        verbose_name=_('Edit Role'),
1500
        on_delete=models.SET_NULL,
1501
    )
1502
    view_role = models.ForeignKey(
1503
        Group,
1504
        blank=True,
1505
        null=True,
1506
        default=None,
1507
        related_name='+',
1508
        verbose_name=_('View Role'),
1509
        on_delete=models.SET_NULL,
1510
    )
1511

  
1512
    class Meta:
1513
        ordering = ['label']
1514

  
1515
    def __str__(self):
1516
        return self.label
1517

  
1518
    @property
1519
    def base_slug(self):
1520
        return slugify(self.label)
1521

  
1522
    def save(self, *args, **kwargs):
1523
        if not self.slug:
1524
            self.slug = generate_slug(self)
1525
        super(UnavailabilityCalendar, self).save(*args, **kwargs)
1526

  
1527
    def can_be_managed(self, user):
1528
        if user.is_staff:
1529
            return True
1530
        group_ids = [x.id for x in user.groups.all()]
1531
        return bool(self.edit_role_id in group_ids)
1532

  
1533
    def can_be_viewed(self, user):
1534
        if self.can_be_managed(user):
1535
            return True
1536
        group_ids = [x.id for x in user.groups.all()]
1537
        return bool(self.view_role_id in group_ids)
1538

  
1539
    def get_absolute_url(self):
1540
        return reverse('chrono-manager-unavailability-calendar-view', kwargs={'pk': self.id})
1541

  
1542

  
1489 1543
class TimePeriodException(models.Model):
1490
    desk = models.ForeignKey(Desk, on_delete=models.CASCADE)
1544
    desk = models.ForeignKey(Desk, on_delete=models.CASCADE, null=True)
1545
    unavailability_calendar = models.ForeignKey(UnavailabilityCalendar, on_delete=models.CASCADE, null=True)
1491 1546
    source = models.ForeignKey(TimePeriodExceptionSource, on_delete=models.CASCADE, null=True)
1492 1547
    label = models.CharField(_('Optional Label'), max_length=150, blank=True, null=True)
1493 1548
    start_datetime = models.DateTimeField(_('Exception start time'))
chrono/api/views.py
141 141
            key=lambda time_period: time_period.desk,
142 142
        )
143 143
    }
144

  
145
    # add exceptions from unavailability calendar
146
    for time_period_exception in TimePeriodException.objects.filter(
147
        unavailability_calendar__desks__agenda__in=agendas
148
    ).order_by('start_datetime', 'end_datetime'):
149
        for desk in time_period_exception.unavailability_calendar.desks.all():
150
            if desk not in desks_exceptions:
151
                desks_exceptions[desk] = IntervalSet()
152
            desks_exceptions[desk].add(
153
                time_period_exception.start_datetime, time_period_exception.end_datetime
154
            )
155

  
144 156
    # compute reduced min/max_datetime windows by desks based on exceptions
145 157
    desk_min_max_datetime = {}
146 158
    for desk, desk_exception in desks_exceptions.items():
chrono/manager/forms.py
44 44
    AgendaNotificationsSettings,
45 45
    AgendaReminderSettings,
46 46
    WEEKDAYS_LIST,
47
    UnavailabilityCalendar,
47 48
)
48 49

  
49 50
from . import widgets
......
89 90
            del self.fields['booking_form_url']
90 91

  
91 92

  
93
class UnavailabilityCalendarAddForm(forms.ModelForm):
94
    class Meta:
95
        model = UnavailabilityCalendar
96
        fields = ['label', 'edit_role', 'view_role']
97

  
98
    edit_role = forms.ModelChoiceField(
99
        label=_('Edit Role'), required=False, queryset=Group.objects.all().order_by('name')
100
    )
101
    view_role = forms.ModelChoiceField(
102
        label=_('View Role'), required=False, queryset=Group.objects.all().order_by('name')
103
    )
104

  
105

  
106
class UnavailabilityCalendarEditForm(UnavailabilityCalendarAddForm):
107
    class Meta:
108
        model = UnavailabilityCalendar
109
        fields = ['label', 'slug', 'edit_role', 'view_role']
110

  
111

  
92 112
class ResourceAddForm(forms.ModelForm):
93 113
    class Meta:
94 114
        model = Resource
......
272 292
        super().__init__(*args, **kwargs)
273 293
        if self.instance.pk is not None:
274 294
            del self.fields['all_desks']
295
        elif self.instance.unavailability_calendar:
296
            del self.fields['all_desks']
275 297
        elif self.instance.desk_id and self.instance.desk.agenda.desk_set.count() == 1:
276 298
            del self.fields['all_desks']
277 299

  
chrono/manager/templates/chrono/manager_agenda_unavailability_calendar_form.html
1
{% extends "chrono/manager_agenda_view.html" %}
2
{% load i18n %}
3

  
4
{% block breadcrumb %}
5
{{ block.super }}
6
<a href="">{% trans "Add unavailability calendar" %}</a>
7
{% endblock %}
8

  
9
{% block appbar %}
10
<h2>{% trans "Add unavailability calendar" %}</h2>
11
{% endblock %}
12

  
13
{% block content %}
14

  
15
<form method="post" enctype="multipart/form-data">
16
  {% csrf_token %}
17
  {{ form.as_p }}
18
  <div class="buttons">
19
    <button class="submit-button">{% trans "Save" %}</button>
20
    <a class="cancel" href="{% url 'chrono-manager-agenda-settings' pk=agenda.pk %}">{% trans 'Cancel' %}</a>
21
  </div>
22
</form>
23
{% endblock %}
chrono/manager/templates/chrono/manager_home.html
3 3

  
4 4
{% block appbar %}
5 5
<h2>{% trans 'Agendas' %}</h2>
6
{% if user.is_staff %}
7 6
<span class="actions">
7
{% if user.is_staff %}
8 8
<a href="{% url 'chrono-manager-category-list' %}">{% trans 'Categories' %}</a>
9
{% endif %}
10
<a href="{% url 'chrono-manager-unavailability-calendar-list' %}">{% trans 'Unavailability calendars' %}</a>
11
{% if user.is_staff %}
9 12
<a href="{% url 'chrono-manager-resource-list' %}">{% trans 'Resources' %}</a>
10 13
<a rel="popup" href="{% url 'chrono-manager-agendas-import' %}">{% trans 'Import' %}</a>
11 14
<a rel="popup" href="{% url 'chrono-manager-agenda-add' %}">{% trans 'New' %}</a>
12
</span>
13 15
{% endif %}
16
</span>
14 17
{% endblock %}
15 18

  
16 19
{% block content %}
chrono/manager/templates/chrono/manager_import_exceptions.html
13 13
{% block content %}
14 14

  
15 15
<form method="post" enctype="multipart/form-data">
16
  {% if exception_sources %}
16
  {% if exception_sources or unavailability_calendars %}
17 17
  <ul class="objects-list single-links">
18 18
      {% for object in exception_sources %}
19 19
        <li>
......
30 30
          {% endif %}
31 31
        </li>
32 32
      {% endfor %}
33
      {% for unavailability_calendar in unavailability_calendars %}
34
        <li>
35
          <a {% if not unavailability_calendar.enabled %}class="disabled"{% endif %} title="{{ unavailability_calendar }}" href="{{ unavailability_calendar.get_absolute_url }}">{{ unavailability_calendar|truncatechars:50 }}</a>
36
          <a class="link-action-text" href="{% url 'chrono-manager-unavailability-calendar-toggle-view' desk.pk unavailability_calendar.pk %}">({{ unavailability_calendar.enabled|yesno:_("disable,enable") }})</a>
37
        </li>
38
      {% endfor %}
33 39
  </ul>
34 40
  {% endif %}
35 41

  
chrono/manager/templates/chrono/manager_time_period_exception_form.html
40 40
  {% endfor %}
41 41
  <div class="buttons">
42 42
    <button class="submit-button">{% trans "Save" %}</button>
43
    <a class="cancel" href="{% url 'chrono-manager-agenda-settings' pk=desk.agenda.id %}">{% trans 'Cancel' %}</a>
43
    <a class="cancel" href="{{ cancel_url }}">{% trans 'Cancel' %}</a>
44 44
  </div>
45 45

  
46 46
  <script>
chrono/manager/templates/chrono/manager_unavailability_calendar_detail.html
1
{% extends "chrono/manager_unavailability_calendar_list.html" %}
2
{% load i18n %}
3

  
4
{% block page-title-extra-label %}
5
- {{ unavailability_calendar.label }}
6
{% endblock %}
7

  
8
{% block breadcrumb %}
9
{{ block.super }}
10
<a href="{% url 'chrono-manager-unavailability-calendar-view' pk=unavailability_calendar.pk %}">{{ unavailability_calendar.label }}</a>
11
{% endblock %}
12

  
13
{% block appbar %}
14
{% block appbar-title %}
15
<h2>{{ unavailability_calendar }}</h2>
16
{% endblock %}
17
{% if user_can_manage %}
18
<span class="actions">
19
{% block appbar-extras %}
20
<a href="{% url 'chrono-manager-unavailability-calendar-settings' pk=unavailability_calendar.pk %}">{% trans 'Settings' %}</a>
21
{% endblock %}
22
</span>
23
{% endif %}
24
{% endblock %}
25

  
26
{% block content %}
27

  
28
<div class="section">
29
<h3>{% trans 'Used in meetings agendas' %}</h3>
30
<div>
31
{% if agendas %}
32
  <ul class="objects-list single-links">
33
    {% for agenda in agendas %}
34
    <li>
35
        <a href="{% url 'chrono-manager-agenda-view' pk=agenda.pk %}">
36
            {{ agenda.label }}
37
        </a>
38
    </li>
39
    {% endfor %}
40
  </ul>
41
{% else %}
42
<div class="big-msg-info">
43
  {% blocktrans %}
44
  This unavailability calendar is not used yet.
45
  {% endblocktrans %}
46
</div>
47
{% endif %}
48
</div>
49
</div>
50

  
51
{% endblock %}
chrono/manager/templates/chrono/manager_unavailability_calendar_form.html
1
{% extends "chrono/manager_home.html" %}
2
{% load i18n %}
3

  
4
{% block appbar %}
5
{% if object.id %}
6
<h2>{% trans "Edit Unavailability Calendar" %}</h2>
7
{% else %}
8
<h2>{% trans "New Unavailability Calendar" %}</h2>
9
{% endif %}
10
{% endblock %}
11

  
12
{% block content %}
13

  
14
<form method="post" enctype="multipart/form-data">
15
  {% csrf_token %}
16
  {{ form.as_p }}
17
  <div class="buttons">
18
    <button class="submit-button">{% trans "Save" %}</button>
19
    <a class="cancel" href="{% url 'chrono-manager-unavailability-calendar-list' %}">{% trans 'Cancel' %}</a>
20
  </div>
21
</form>
22
{% endblock %}
chrono/manager/templates/chrono/manager_unavailability_calendar_list.html
1
{% extends "chrono/manager_base.html" %}
2
{% load i18n %}
3

  
4
{% block breadcrumb %}
5
{{ block.super }}
6
<a href="{% url 'chrono-manager-unavailability-calendar-list' %}">{% trans "Unavailability Calendars" %}</a>
7
{% endblock %}
8

  
9
{% block appbar %}
10
<h2>{% trans 'Unavailability Calendars' %}</h2>
11
{% if user.is_staff %}
12
<span class="actions">
13
<a rel="popup" href="{% url 'chrono-manager-unavailability-calendar-add' %}">{% trans 'New' %}</a>
14
</span>
15
{% endif %}
16
{% endblock %}
17

  
18

  
19
{% block content %}
20
{% if object_list %}
21
<div>
22
  <ul class="objects-list single-links">
23
    {% for object in object_list %}
24
    <li>
25
        <a href="{% url 'chrono-manager-unavailability-calendar-view' pk=object.pk %}">{{ object.label }} ({{ object.slug }})</a>
26
    </li>
27
    {% endfor %}
28
  </ul>
29
</div>
30
{% else %}
31
<div class="big-msg-info">
32
  {% blocktrans %}
33
  This site doesn't have any unavailability calendar yet. Click on the "New" button in the top
34
  right of the page to add a first one.
35
  {% endblocktrans %}
36
</div>
37
{% endif %}
38
{% endblock %}
chrono/manager/templates/chrono/manager_unavailability_calendar_settings.html
1
{% extends "chrono/manager_unavailability_calendar_detail.html" %}
2
{% load i18n %}
3

  
4
{% block breadcrumb %}
5
{{ block.super }}
6
<a href=".">{% trans "Settings" %}</a>
7
{% endblock %}
8

  
9
{% block appbar %}
10
<h2>{% trans "Settings" %}
11
</h2>
12
<span class="actions">
13
  <a class="extra-actions-menu-opener"></a>
14
  {% block agenda-extra-management-actions %}
15
    <a rel="popup" href="{% url 'chrono-manager-unavailability-calendar-add-unavailability' pk=unavailability_calendar.id %}">{% trans 'Add Unavailability' %}</a>
16
  {% endblock %}
17
  <ul class="extra-actions-menu">
18
    <li><a rel="popup" href="{% url 'chrono-manager-unavailability-calendar-edit' pk=unavailability_calendar.id %}">{% trans 'Options' %}</a></li>
19
    {% if user.is_staff %}
20
      <li><a rel="popup" href="{% url 'chrono-manager-unavailability-calendar-delete' pk=unavailability_calendar.id %}">{% trans 'Delete' %}</a></li>
21
    {% endif %}
22
  </ul>
23
</span>
24
{% endblock %}
25

  
26
{% block content %}
27
<div class="section">
28
<h3>{% trans 'Unavailabilities' %}</h3>
29
<div>
30
{% block agenda-settings %}
31
{% if unavailability_calendar.timeperiodexception_set.count %}
32
  <ul class="objects-list single-links">
33
    {% for time_period_exception in unavailability_calendar.timeperiodexception_set.all %}
34
    <li><a rel="popup" href="{% url 'chrono-manager-time-period-exception-edit' pk=time_period_exception.id %}">{{ time_period_exception }}</a>
35
      <a rel="popup" class="delete" href="{% url 'chrono-manager-time-period-exception-delete' pk=time_period_exception.id %}">{% trans "remove" %}</a>
36
    </li>
37
    {% endfor %}
38
  </ul>
39
{% else %}
40
<div class="big-msg-info">
41
  {% blocktrans %}
42
  There is no unavailabilities yet. Click on the "Add Unavailabilty" button in
43
    the top right of the page to add a first one.
44
  {% endblocktrans %}
45
</div>
46
{% endif %}
47
</div>
48
</div>
49
{% endblock %}
50

  
51

  
52
{% endblock %}
chrono/manager/urls.py
20 20

  
21 21
urlpatterns = [
22 22
    url(r'^$', views.homepage, name='chrono-manager-homepage'),
23
    url(
24
        r'^unavailability-calendars/$',
25
        views.unavailability_calendar_list,
26
        name='chrono-manager-unavailability-calendar-list',
27
    ),
28
    url(
29
        r'^unavailability-calendar/add/$',
30
        views.unavailability_calendar_add,
31
        name='chrono-manager-unavailability-calendar-add',
32
    ),
33
    url(
34
        r'^unavailability-calendar/(?P<pk>\d+)/$',
35
        views.unavailability_calendar_view,
36
        name='chrono-manager-unavailability-calendar-view',
37
    ),
38
    url(
39
        r'^unavailability-calendar/(?P<pk>\d+)/edit/$',
40
        views.unavailability_calendar_edit,
41
        name='chrono-manager-unavailability-calendar-edit',
42
    ),
43
    url(
44
        r'^unavailability-calendar/(?P<pk>\d+)/delete/$',
45
        views.unavailability_calendar_delete,
46
        name='chrono-manager-unavailability-calendar-delete',
47
    ),
48
    url(
49
        r'^unavailability-calendar/(?P<pk>\d+)/settings$',
50
        views.unavailability_calendar_settings,
51
        name='chrono-manager-unavailability-calendar-settings',
52
    ),
53
    url(
54
        r'^unavailability-calendar/(?P<pk>\d+)/add-unavailability$',
55
        views.unavailability_calendar_add_unavailability,
56
        name='chrono-manager-unavailability-calendar-add-unavailability',
57
    ),
23 58
    url(r'^resources/$', views.resource_list, name='chrono-manager-resource-list'),
24 59
    url(r'^resource/add/$', views.resource_add, name='chrono-manager-resource-add'),
25 60
    url(r'^resource/(?P<pk>\d+)/$', views.resource_view, name='chrono-manager-resource-view'),
......
158 193
    url(r'^agendas/(?P<pk>\d+)/add-desk$', views.agenda_add_desk, name='chrono-manager-agenda-add-desk'),
159 194
    url(r'^desks/(?P<pk>\d+)/edit$', views.desk_edit, name='chrono-manager-desk-edit'),
160 195
    url(r'^desks/(?P<pk>\d+)/delete$', views.desk_delete, name='chrono-manager-desk-delete'),
196
    url(
197
        r'^desk/(?P<pk>\d+)/unavailability-calendar/(?P<unavailability_calendar_pk>\d+)/toogle/$',
198
        views.unavailability_calendar_toggle_view,
199
        name='chrono-manager-unavailability-calendar-toggle-view',
200
    ),
161 201
    url(
162 202
        r'^agendas/(?P<agenda_pk>\d+)/desk/(?P<pk>\d+)/add-time-period-exception$',
163 203
        views.agenda_add_time_period_exception,
chrono/manager/views.py
23 23

  
24 24
from django.contrib import messages
25 25
from django.core.exceptions import PermissionDenied
26
from django.db.models import Q
26
from django.db.models import Q, Value, BooleanField
27 27
from django.db.models import Min, Max
28 28
from django.http import Http404, HttpResponse, HttpResponseRedirect
29 29
from django.shortcuts import get_object_or_404
......
65 65
    EventCancellationReport,
66 66
    AgendaNotificationsSettings,
67 67
    AgendaReminderSettings,
68
    UnavailabilityCalendar,
68 69
)
69 70

  
70 71
from .forms import (
......
94 95
    EventCancelForm,
95 96
    AgendaNotificationsForm,
96 97
    AgendaReminderForm,
98
    UnavailabilityCalendarAddForm,
99
    UnavailabilityCalendarEditForm,
97 100
)
98 101
from .utils import import_site
99 102

  
......
1237 1240
        return reverse('chrono-manager-agenda-settings', kwargs={'pk': self.agenda.id})
1238 1241

  
1239 1242

  
1243
class ManagedTimePeriodExceptionMixin(object):
1244

  
1245
    desk = None
1246
    unavailability_calendar = None
1247

  
1248
    def dispatch(self, request, *args, **kwargs):
1249
        object_ = self.get_object()
1250
        if object_.desk:
1251
            self.desk = self.get_object().desk
1252
            if not self.desk.agenda.can_be_managed(request.user):
1253
                raise PermissionDenied()
1254
        elif object_.unavailability_calendar:
1255
            self.unavailability_calendar = object_.unavailability_calendar
1256
            if not self.unavailability_calendar.can_be_managed(request.user):
1257
                raise PermissionDenied()
1258
        return super().dispatch(request, *args, **kwargs)
1259

  
1260
    def get_context_data(self, **kwargs):
1261
        context = super().get_context_data(**kwargs)
1262
        if self.desk:
1263
            context['desk'] = self.object.desk
1264
            context['agenda'] = self.object.desk.agenda
1265
        return context
1266

  
1267
    def get_success_url(self):
1268
        if self.desk:
1269
            return reverse('chrono-manager-agenda-settings', kwargs={'pk': self.desk.agenda.id})
1270
        elif self.unavailability_calendar:
1271
            return reverse(
1272
                'chrono-manager-unavailability-calendar-settings',
1273
                kwargs={'pk': self.unavailability_calendar.pk},
1274
            )
1275

  
1276

  
1240 1277
class AgendaSettings(ManagedAgendaMixin, DetailView):
1241 1278
    model = Agenda
1242 1279

  
......
1258 1295
            ]
1259 1296
        if self.agenda.kind == 'meetings':
1260 1297
            context['has_resources'] = Resource.objects.exists()
1298
            context['has_unavailability_calendars'] = UnavailabilityCalendar.objects.exists()
1261 1299
        return context
1262 1300

  
1263 1301
    def get_events(self):
......
1538 1576
agenda_delete_resource = AgendaResourceDeleteView.as_view()
1539 1577

  
1540 1578

  
1579
class UnavailabilityCalendarToggleView(ManagedDeskMixin, DetailView):
1580
    model = UnavailabilityCalendar
1581
    pk_url_kwarg = 'unavailability_calendar_pk'
1582

  
1583
    def get(self, request, *args, **kwargs):
1584
        unavailability_calendar = self.get_object()
1585
        try:
1586
            self.desk.unavailability_calendars.get(pk=unavailability_calendar.pk)
1587
            self.desk.unavailability_calendars.remove(unavailability_calendar)
1588
            message = _(
1589
                'Unavailability calendar %(unavailability_calendar)s has been disabled on desk %(desk)s.'
1590
            )
1591

  
1592
        except UnavailabilityCalendar.DoesNotExist:
1593
            self.desk.unavailability_calendars.add(unavailability_calendar)
1594
            message = _(
1595
                'Unavailability calendar %(unavailability_calendar)s has been enabled on desk %(desk)s.'
1596
            )
1597
        messages.info(
1598
            self.request, message % {'unavailability_calendar': unavailability_calendar, 'desk': self.desk}
1599
        )
1600
        return HttpResponseRedirect(
1601
            reverse('chrono-manager-agenda-settings', kwargs={'pk': self.desk.agenda_id})
1602
        )
1603

  
1604

  
1605
unavailability_calendar_toggle_view = UnavailabilityCalendarToggleView.as_view()
1606

  
1607

  
1541 1608
class AgendaAddMeetingTypeView(ManagedAgendaMixin, CreateView):
1542 1609
    template_name = 'chrono/manager_meeting_type_form.html'
1543 1610
    model = MeetingType
......
1745 1812
    model = TimePeriodException
1746 1813
    form_class = TimePeriodExceptionForm
1747 1814

  
1815
    def get_context_data(self, **kwargs):
1816
        context = super().get_context_data(**kwargs)
1817
        context['cancel_url'] = reverse('chrono-manager-agenda-settings', kwargs={'pk': self.desk.agenda.pk})
1818
        return context
1819

  
1748 1820
    def form_valid(self, form):
1749 1821
        result = super().form_valid(form)
1750 1822
        exceptions = [self.object]
......
1775 1847
agenda_add_time_period_exception = AgendaAddTimePeriodExceptionView.as_view()
1776 1848

  
1777 1849

  
1778
class TimePeriodExceptionEditView(ManagedDeskSubobjectMixin, UpdateView):
1850
class TimePeriodExceptionEditView(ManagedTimePeriodExceptionMixin, UpdateView):
1779 1851
    template_name = 'chrono/manager_time_period_exception_form.html'
1780 1852
    model = TimePeriodException
1781 1853
    form_class = TimePeriodExceptionForm
1782 1854

  
1855
    def get_context_data(self):
1856
        context = super().get_context_data()
1857
        if self.desk:
1858
            context['cancel_url'] = reverse(
1859
                'chrono-manager-agenda-settings', kwargs={'pk': self.desk.agenda.pk}
1860
            )
1861
        elif self.unavailability_calendar:
1862
            context['cancel_url'] = reverse(
1863
                'chrono-manager-unavailability-calendar-settings',
1864
                kwargs={'pk': self.unavailability_calendar.pk},
1865
            )
1866
        return context
1867

  
1783 1868

  
1784 1869
time_period_exception_edit = TimePeriodExceptionEditView.as_view()
1785 1870

  
......
1812 1897
time_period_exception_extract_list = TimePeriodExceptionExtractListView.as_view()
1813 1898

  
1814 1899

  
1815
class TimePeriodExceptionDeleteView(ManagedDeskSubobjectMixin, DeleteView):
1900
class TimePeriodExceptionDeleteView(ManagedTimePeriodExceptionMixin, DeleteView):
1816 1901
    template_name = 'chrono/manager_confirm_exception_delete.html'
1817 1902
    model = TimePeriodException
1818 1903

  
1819 1904
    def get_success_url(self):
1820
        referer = self.request.META.get('HTTP_REFERER')
1821
        success_url = reverse('chrono-manager-time-period-exception-list', kwargs={'pk': self.desk.pk})
1822
        if success_url in referer:
1823
            return success_url
1905
        if self.desk:
1906
            referer = self.request.META.get('HTTP_REFERER')
1907
            success_url = reverse('chrono-manager-time-period-exception-list', kwargs={'pk': self.desk.pk})
1908
            if success_url in referer:
1909
                return success_url
1824 1910

  
1825 1911
        success_url = super(TimePeriodExceptionDeleteView, self).get_success_url()
1826
        if 'from_popup' in self.request.GET:
1912
        if self.desk and 'from_popup' in self.request.GET:
1827 1913
            success_url = '{}?display_exceptions={}'.format(success_url, self.desk.pk)
1828 1914
        return success_url
1829 1915

  
......
1841 1927

  
1842 1928
    def get_context_data(self, **kwargs):
1843 1929
        context = super(DeskImportTimePeriodExceptionsView, self).get_context_data(**kwargs)
1844
        context['exception_sources'] = self.get_object().timeperiodexceptionsource_set.all()
1930
        desk = self.get_object()
1931
        context['exception_sources'] = desk.timeperiodexceptionsource_set.all()
1932
        context['desk'] = desk
1933
        context['unavailability_calendars'] = (
1934
            UnavailabilityCalendar.objects.filter(desks=desk)
1935
            .annotate(enabled=Value(True, BooleanField()))
1936
            .union(
1937
                UnavailabilityCalendar.objects.exclude(desks=desk).annotate(
1938
                    enabled=Value(False, BooleanField())
1939
                )
1940
            )
1941
        )
1845 1942
        return context
1846 1943

  
1847 1944
    def form_valid(self, form):
......
2082 2179
time_period_exception_source_toggle = TimePeriodExceptionSourceToggleView.as_view()
2083 2180

  
2084 2181

  
2182
class ViewableUnavailabilityCalendarMixin(object):
2183
    unavailability_calendar = None
2184

  
2185
    def set_unavailability_calendar(self, **kwargs):
2186
        self.unavailability_calendar = get_object_or_404(UnavailabilityCalendar, id=kwargs.get('pk'))
2187

  
2188
    def dispatch(self, request, *args, **kwargs):
2189
        self.set_unavailability_calendar(**kwargs)
2190
        if not self.check_permissions(request.user):
2191
            raise PermissionDenied()
2192
        return super().dispatch(request, *args, **kwargs)
2193

  
2194
    def check_permissions(self, user):
2195
        return self.unavailability_calendar.can_be_viewed(user)
2196

  
2197
    def get_context_data(self, **kwargs):
2198
        context = super().get_context_data(**kwargs)
2199
        context['unavailability_calendar'] = self.unavailability_calendar
2200
        context['user_can_manage'] = self.unavailability_calendar.can_be_managed(self.request.user)
2201
        return context
2202

  
2203

  
2204
class ManagedUnavailabilityCalendarMixin(ViewableUnavailabilityCalendarMixin):
2205
    def check_permissions(self, user):
2206
        return self.unavailability_calendar.can_be_managed(user)
2207

  
2208
    def get_success_url(self):
2209
        return reverse(
2210
            'chrono-manager-unavailability-calendar-settings', kwargs={'pk': self.unavailability_calendar.id}
2211
        )
2212

  
2213

  
2214
class UnavailabilityCalendarListView(ListView):
2215
    template_name = 'chrono/manager_unavailability_calendar_list.html'
2216
    model = UnavailabilityCalendar
2217

  
2218
    def get_queryset(self):
2219
        queryset = super().get_queryset()
2220
        if not self.request.user.is_staff:
2221
            group_ids = [x.id for x in self.request.user.groups.all()]
2222
            queryset = queryset.filter(Q(view_role_id__in=group_ids) | Q(edit_role_id__in=group_ids))
2223
        return queryset.order_by('label')
2224

  
2225

  
2226
unavailability_calendar_list = UnavailabilityCalendarListView.as_view()
2227

  
2228

  
2229
class UnavailabilityCalendarAddView(CreateView):
2230
    template_name = 'chrono/manager_unavailability_calendar_form.html'
2231
    model = UnavailabilityCalendar
2232
    form_class = UnavailabilityCalendarAddForm
2233

  
2234
    def dispatch(self, request, *args, **kwargs):
2235
        if not request.user.is_staff:
2236
            raise PermissionDenied()
2237
        return super().dispatch(request, *args, **kwargs)
2238

  
2239
    def get_success_url(self):
2240
        return reverse('chrono-manager-unavailability-calendar-view', kwargs={'pk': self.object.id})
2241

  
2242

  
2243
unavailability_calendar_add = UnavailabilityCalendarAddView.as_view()
2244

  
2245

  
2246
class UnavailabilityCalendarDetailView(ViewableUnavailabilityCalendarMixin, DetailView):
2247
    template_name = 'chrono/manager_unavailability_calendar_detail.html'
2248
    model = UnavailabilityCalendar
2249

  
2250
    def get_context_data(self, **kwargs):
2251
        context = super().get_context_data(**kwargs)
2252
        context['agendas'] = Agenda.objects.filter(
2253
            desk__unavailability_calendars__pk=self.unavailability_calendar.pk
2254
        ).distinct()
2255
        return context
2256

  
2257

  
2258
unavailability_calendar_view = UnavailabilityCalendarDetailView.as_view()
2259

  
2260

  
2261
class UnavailabilityCalendarEditView(ManagedUnavailabilityCalendarMixin, UpdateView):
2262
    template_name = 'chrono/manager_unavailability_calendar_form.html'
2263
    model = UnavailabilityCalendar
2264
    form_class = UnavailabilityCalendarEditForm
2265

  
2266

  
2267
unavailability_calendar_edit = UnavailabilityCalendarEditView.as_view()
2268

  
2269

  
2270
class UnavailabilityCalendarDeleteView(DeleteView):
2271
    template_name = 'chrono/manager_confirm_delete.html'
2272
    model = UnavailabilityCalendar
2273

  
2274
    def dispatch(self, request, *args, **kwargs):
2275
        if not request.user.is_staff:
2276
            raise PermissionDenied()
2277
        return super().dispatch(request, *args, **kwargs)
2278

  
2279
    def get_success_url(self):
2280
        return reverse('chrono-manager-unavailability-calendar-list')
2281

  
2282

  
2283
unavailability_calendar_delete = UnavailabilityCalendarDeleteView.as_view()
2284

  
2285

  
2286
class UnavailabilityCalendarSettings(ManagedUnavailabilityCalendarMixin, DetailView):
2287
    template_name = 'chrono/manager_unavailability_calendar_settings.html'
2288
    model = UnavailabilityCalendar
2289

  
2290
    def get_context_data(self, **kwargs):
2291
        context = super(UnavailabilityCalendarSettings, self).get_context_data(**kwargs)
2292
        context['unavailability_calendar'] = self.object
2293
        return context
2294

  
2295

  
2296
unavailability_calendar_settings = UnavailabilityCalendarSettings.as_view()
2297

  
2298

  
2299
class UnavailabilityCalendarAddUnavailabilityView(ManagedUnavailabilityCalendarMixin, CreateView):
2300
    template_name = 'chrono/manager_time_period_exception_form.html'
2301
    form_class = TimePeriodExceptionForm
2302
    model = TimePeriodException
2303

  
2304
    def get_form_kwargs(self):
2305
        kwargs = super().get_form_kwargs()
2306
        if not kwargs.get('instance'):
2307
            kwargs['instance'] = self.model()
2308
        kwargs['instance'].unavailability_calendar = self.unavailability_calendar
2309
        return kwargs
2310

  
2311
    def get_context_data(self, **kwargs):
2312
        context = super(UnavailabilityCalendarAddUnavailabilityView, self).get_context_data(**kwargs)
2313
        context['cancel_url'] = reverse(
2314
            'chrono-manager-unavailability-calendar-settings', kwargs={'pk': self.unavailability_calendar.pk,}
2315
        )
2316
        return context
2317

  
2318

  
2319
unavailability_calendar_add_unavailability = UnavailabilityCalendarAddUnavailabilityView.as_view()
2320

  
2321

  
2085 2322
def menu_json(request):
2086 2323
    label = _('Agendas')
2087 2324
    json_str = json.dumps(
chrono/urls_utils.py
22 22
from django.core.exceptions import PermissionDenied
23 23
from django.db.models import Q
24 24

  
25
from .agendas.models import Agenda
25
from .agendas.models import Agenda, UnavailabilityCalendar
26 26

  
27 27
if django.VERSION < (2, 0, 0):
28 28
    from django.urls.resolvers import RegexURLPattern as URLPattern
......
55 55
        if user and not user.is_anonymous:
56 56
            # /manage/ is open to anyone authorized to view or edit an agenda.
57 57
            group_ids = [x.id for x in user.groups.all()]
58
            if Agenda.objects.filter(Q(view_role_id__in=group_ids) | Q(edit_role_id__in=group_ids)).exists():
58
            if (
59
                Agenda.objects.filter(Q(view_role_id__in=group_ids) | Q(edit_role_id__in=group_ids)).exists()
60
                or UnavailabilityCalendar.objects.filter(
61
                    Q(view_role_id__in=group_ids) | Q(edit_role_id__in=group_ids)
62
                ).exists()
63
            ):
59 64
                return True
60 65
            raise PermissionDenied()
61 66
        # As the last resort, show the login form
tests/test_api.py
21 21
    Resource,
22 22
    TimePeriod,
23 23
    TimePeriodException,
24
    UnavailabilityCalendar,
24 25
    VirtualMember,
25 26
)
26 27
import chrono.api.views
......
643 644
    )
644 645
    with CaptureQueriesContext(connection) as ctx:
645 646
        resp = app.get(api_url)
646
        assert len(ctx.captured_queries) == 9
647
        assert len(ctx.captured_queries) == 10
647 648
    assert len(resp.json['data']) == 32
648 649
    assert [s['datetime'] for s in resp.json['data'] if s['disabled'] is True] == [
649 650
        '%s 09:00:00' % tomorrow_str,
......
3802 3803
    with CaptureQueriesContext(connection) as ctx:
3803 3804
        resp = app.get(api_url)
3804 3805
        assert len(resp.json['data']) == 12
3805
        assert len(ctx.captured_queries) == 11
3806
        assert len(ctx.captured_queries) == 12
3806 3807

  
3807 3808
    # simulate booking
3808 3809
    dt = datetime.datetime.strptime(resp.json['data'][2]['id'].split(':')[1], '%Y-%m-%d-%H%M')
......
4126 4127
    ics = app.get(resp.json['api']['ics_url']).text
4127 4128
    assert 'DTSTART:20170519T231200Z' in ics
4128 4129
    assert 'DTEND:20170520T004200Z' in ics
4130

  
4131

  
4132
def test_unavailabilitycalendar_meetings_datetimes(app, user, meetings_agenda):
4133
    app.authorization = ('Basic', ('john.doe', 'password'))
4134
    meeting_type = meetings_agenda.meetingtype_set.first()
4135
    desk = meetings_agenda.desk_set.first()
4136
    datetimes_url = '/api/agenda/%s/meetings/%s/datetimes/' % (meetings_agenda.slug, meeting_type.slug)
4137
    resp = app.get(datetimes_url)
4138
    assert len(resp.json['data']) == 144
4139

  
4140
    # create an unvalailability calendar
4141
    unavailability_calendar = UnavailabilityCalendar.objects.create(label='foo holydays')
4142
    TimePeriodException.objects.create(
4143
        unavailability_calendar=unavailability_calendar,
4144
        start_datetime=make_aware(datetime.datetime(2017, 5, 22, 10, 0)),
4145
        end_datetime=make_aware(datetime.datetime(2017, 5, 22, 11, 0)),
4146
    )
4147
    unavailability_calendar.desks.add(desk)
4148

  
4149
    # 2 slots are gone
4150
    resp2 = app.get(datetimes_url)
4151
    assert len(resp.json['data']) == len(resp2.json['data']) + 2
4152

  
4153
    # add a standard desk exception
4154
    desk = meetings_agenda.desk_set.first()
4155
    TimePeriodException.objects.create(
4156
        desk=desk,
4157
        start_datetime=make_aware(datetime.datetime(2017, 5, 22, 11, 0)),
4158
        end_datetime=make_aware(datetime.datetime(2017, 5, 22, 12, 0)),
4159
    )
4160
    # 4 slots are gone
4161
    resp3 = app.get(datetimes_url)
4162
    assert len(resp.json['data']) == len(resp3.json['data']) + 4
4163

  
4164

  
4165
def test_unavailabilitycalendar_on_virtual_datetimes(app, user, mock_now):
4166
    foo_agenda = Agenda.objects.create(label='Foo Meeting', kind='meetings', maximal_booking_delay=7)
4167
    MeetingType.objects.create(agenda=foo_agenda, label='Meeting Type', duration=30)
4168
    foo_desk_1 = Desk.objects.create(agenda=foo_agenda, label='Foo desk 1')
4169
    TimePeriod.objects.create(
4170
        weekday=0, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0), desk=foo_desk_1,
4171
    )
4172
    TimePeriod.objects.create(
4173
        weekday=1, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0), desk=foo_desk_1,
4174
    )
4175
    virt_agenda = Agenda.objects.create(label='Virtual Agenda', kind='virtual')
4176
    VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=foo_agenda)
4177

  
4178
    api_url = '/api/agenda/%s/meetings/meeting-type/datetimes/' % (virt_agenda.slug)
4179
    resp = app.get(api_url)
4180
    # 8 slots
4181
    data = resp.json['data']
4182
    assert len(data) == 8
4183
    assert data[0]['datetime'] == '2017-05-22 10:00:00'
4184
    assert data[1]['datetime'] == '2017-05-22 10:30:00'
4185
    assert data[2]['datetime'] == '2017-05-22 11:00:00'
4186

  
4187
    # exclude one hour the first day through an unvalailability calendar on the foo agenda
4188
    unavailability_calendar = UnavailabilityCalendar.objects.create(label='foo holydays')
4189
    TimePeriodException.objects.create(
4190
        unavailability_calendar=unavailability_calendar,
4191
        start_datetime=make_aware(datetime.datetime(2017, 5, 22, 11, 0)),
4192
        end_datetime=make_aware(datetime.datetime(2017, 5, 22, 12, 0)),
4193
    )
4194
    unavailability_calendar.desks.add(foo_desk_1)
4195

  
4196
    resp = app.get(api_url)
4197
    data = resp.json['data']
4198
    assert len(data) == 6
4199
    assert data[0]['datetime'] == '2017-05-22 10:00:00'
4200
    assert data[1]['datetime'] == '2017-05-22 10:30:00'
4201
    # no more slots the 22 thanks to the unavailability calendar
4202
    assert data[2]['datetime'] == '2017-05-23 10:00:00'
4203

  
4204
    # exclude the second day
4205
    TimePeriodException.objects.create(
4206
        unavailability_calendar=unavailability_calendar,
4207
        start_datetime=make_aware(datetime.datetime(2017, 5, 23, 9, 0)),
4208
        end_datetime=make_aware(datetime.datetime(2017, 5, 23, 18, 0)),
4209
    )
4210
    resp = app.get(api_url)
4211
    data = resp.json['data']
4212
    assert len(data) == 2
4213
    assert data[0]['datetime'] == '2017-05-22 10:00:00'
4214
    assert data[1]['datetime'] == '2017-05-22 10:30:00'
4215

  
4216
    # add a second real agenda
4217
    bar_agenda = Agenda.objects.create(label='Bar Meeting', kind='meetings', maximal_booking_delay=7)
4218
    VirtualMember.objects.create(virtual_agenda=virt_agenda, real_agenda=bar_agenda)
4219
    MeetingType.objects.create(agenda=bar_agenda, label='Meeting Type', duration=30)
4220
    bar_desk_1 = Desk.objects.create(agenda=bar_agenda, label='Bar desk 1')
4221
    bar_desk_2 = Desk.objects.create(agenda=bar_agenda, label='Bar desk 2')
4222
    TimePeriod.objects.create(
4223
        weekday=0, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0), desk=bar_desk_1,
4224
    )
4225
    TimePeriod.objects.create(
4226
        weekday=1, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0), desk=bar_desk_1,
4227
    )
4228
    TimePeriod.objects.create(
4229
        weekday=0, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0), desk=bar_desk_2,
4230
    )
4231
    TimePeriod.objects.create(
4232
        weekday=1, start_time=datetime.time(10, 0), end_time=datetime.time(12, 0), desk=bar_desk_2,
4233
    )
4234

  
4235
    # bar_agenda has the same time periods than foo_agenda, but no unavailability calendar
4236
    # so we are back at the start : 8 slots
4237
    resp = app.get(api_url)
4238
    data = resp.json['data']
4239
    assert len(data) == 8
4240

  
4241
    # exclude one hour the second day through another unvalailability calendar on the bar agenda
4242
    unavailability_calendar = UnavailabilityCalendar.objects.create(label='bar holydays')
4243
    TimePeriodException.objects.create(
4244
        unavailability_calendar=unavailability_calendar,
4245
        start_datetime=make_aware(datetime.datetime(2017, 5, 23, 11, 0)),
4246
        end_datetime=make_aware(datetime.datetime(2017, 5, 23, 12, 0)),
4247
    )
4248
    unavailability_calendar.desks.add(bar_desk_1, bar_desk_2)
4249

  
4250
    # 2 slots are gone
4251
    resp = app.get(api_url)
4252
    data = resp.json['data']
4253
    assert len(data) == 6
4254
    assert data[0]['datetime'] == '2017-05-22 10:00:00'
4255
    assert data[-1]['datetime'] == '2017-05-23 10:30:00'
tests/test_manager.py
35 35
    TimePeriodExceptionSource,
36 36
    VirtualMember,
37 37
    AgendaReminderSettings,
38
    UnavailabilityCalendar,
38 39
)
39 40
from chrono.manager.forms import TimePeriodExceptionForm
40 41
from chrono.utils.signature import check_query
......
1000 1001
    app = login(app)
1001 1002
    with CaptureQueriesContext(connection) as ctx:
1002 1003
        app.get('/manage/agendas/%s/settings' % agenda.pk)
1003
        assert len(ctx.captured_queries) == 9
1004
        assert len(ctx.captured_queries) == 10
1004 1005

  
1005 1006

  
1006 1007
def test_agenda_resources(app, admin_user):
......
4376 4377
        'Reminder: you have a booking for event "{{ event_label }}", on 02/06 at 2:30 p.m.. Take ID card.'
4377 4378
        in resp.text
4378 4379
    )
4380

  
4381

  
4382
def test_no_unavailability_calendar(app, admin_user):
4383
    app = login(app)
4384

  
4385
    # empty unavailability calendars list
4386
    resp = app.get('/manage/')
4387
    resp = resp.click('Unavailability calendars')
4388
    assert "This site doesn't have any unavailability calendar yet" in resp.text
4389

  
4390
    # on the agenda settings page, no unavailability calendar reference
4391
    agenda = Agenda.objects.create(label='Agenda', kind='meetings')
4392
    resp = app.get('/manage/agendas/%s/settings' % agenda.pk)
4393
    assert 'unavailability calendar' not in resp.text
4394

  
4395

  
4396
def test_add_unavailability_calendar(app, admin_user):
4397
    app = login(app)
4398
    resp = app.get('/manage/')
4399
    resp = resp.click('Unavailability calendars')
4400
    resp = resp.click('New')
4401
    resp.form['label'] = 'Foo bar'
4402
    resp = resp.form.submit()
4403
    unavailability_calendar = UnavailabilityCalendar.objects.latest('pk')
4404
    assert resp.location.endswith('/manage/unavailability-calendar/%s/' % unavailability_calendar.pk)
4405
    assert unavailability_calendar.label == 'Foo bar'
4406
    assert unavailability_calendar.slug == 'foo-bar'
4407
    resp = resp.follow()
4408
    assert 'This unavailability calendar is not used yet.' in resp.text
4409
    resp = app.get('/manage/unavailability-calendars/')
4410
    assert 'Foo bar' in resp.text
4411
    assert 'foo-bar' in resp.text
4412

  
4413

  
4414
def test_used_unavailability_calendar(app, admin_user):
4415
    unavailability_calendar = UnavailabilityCalendar.objects.create(label='Calendar')
4416
    agenda = Agenda.objects.create(label='Foo', kind='meetings')
4417
    desk = Desk.objects.create(agenda=agenda, label='desk')
4418

  
4419
    app = login(app)
4420
    url = '/manage/unavailability-calendar/%s/' % unavailability_calendar.pk
4421
    resp = app.get(url)
4422
    assert 'This unavailability calendar is not used yet.' in resp.text
4423
    assert 'Foo' not in resp.text
4424

  
4425
    desk.unavailability_calendars.add(unavailability_calendar)
4426
    resp = app.get(url)
4427
    assert 'This unavailability calendar is not used yet.' not in resp.text
4428
    assert 'Foo' in resp.text
4429

  
4430

  
4431
def test_edit_unavailability_calendar(app, admin_user):
4432
    unavailability_calendar = UnavailabilityCalendar.objects.create(label='Foo')
4433
    app = login(app)
4434
    settings_url = '/manage/unavailability-calendar/%s/settings' % unavailability_calendar.pk
4435
    resp = app.get(settings_url)
4436
    resp = resp.click('Options')
4437
    resp.form['label'] = 'Bar'
4438
    resp = resp.form.submit()
4439
    assert resp.location.endswith(settings_url)
4440
    resp = resp.follow()
4441
    assert 'Bar' in resp.text
4442
    unavailability_calendar.refresh_from_db()
4443
    assert unavailability_calendar.label == 'Bar'
4444

  
4445

  
4446
def test_delete_unavailability_calendar(app, admin_user):
4447
    unavailability_calendar = UnavailabilityCalendar.objects.create(label='Foo')
4448
    app = login(app)
4449
    resp = app.get('/manage/unavailability-calendar/%s/settings' % unavailability_calendar.pk)
4450
    resp = resp.click('Delete')
4451
    resp = resp.form.submit()
4452
    assert resp.location.endswith('/manage/unavailability-calendars/')
4453
    resp = resp.follow()
4454
    assert 'Foo' not in resp.text
4455
    assert UnavailabilityCalendar.objects.count() == 0
4456

  
4457

  
4458
def test_unavailability_calendar_add_time_period_exeptions(app, admin_user):
4459
    unavailability_calendar = UnavailabilityCalendar.objects.create(label='Foo')
4460
    app = login(app)
4461
    resp = app.get('/manage/unavailability-calendar/%s/' % unavailability_calendar.pk)
4462
    resp = resp.click('Settings')
4463
    assert 'There is no unavailabilities yet' in resp.text
4464
    resp = resp.click('Add Unavailability')
4465
    assert 'all_desks' not in resp.form.fields
4466
    today = datetime.datetime.today().replace(hour=0, minute=0, second=0, microsecond=0)
4467
    tomorrow = make_aware(today + datetime.timedelta(days=1))
4468
    resp.form['label'] = 'Exception 1'
4469
    resp.form['start_datetime_0'] = tomorrow.strftime('%Y-%m-%d')
4470
    resp.form['start_datetime_1'] = '08:00'
4471
    resp.form['end_datetime_0'] = tomorrow.strftime('%Y-%m-%d')
4472
    resp.form['end_datetime_1'] = '16:00'
4473
    resp = resp.form.submit().follow()
4474
    assert 'Exception 1' in resp.text
4475

  
4476
    time_period_exception = TimePeriodException.objects.first()
4477
    assert time_period_exception.unavailability_calendar == unavailability_calendar
4478
    assert time_period_exception.desk is None
4479
    assert time_period_exception.label == 'Exception 1'
4480

  
4481

  
4482
def test_unavailability_calendar_edit_time_period_exeptions(app, admin_user):
4483
    unavailability_calendar = UnavailabilityCalendar.objects.create(label='Foo')
4484
    time_period_exception = TimePeriodException.objects.create(
4485
        unavailability_calendar=unavailability_calendar,
4486
        label='Exception 1',
4487
        start_datetime=now() - datetime.timedelta(days=2),
4488
        end_datetime=now() - datetime.timedelta(days=1),
4489
    )
4490

  
4491
    app = login(app)
4492
    resp = app.get('/manage/unavailability-calendar/%s/settings' % unavailability_calendar.pk)
4493
    assert 'Exception 1' in resp.text
4494
    resp = resp.click(href='/manage/time-period-exceptions/%s/edit' % time_period_exception.pk)
4495
    assert 'all_desks' not in resp.form.fields
4496
    resp.form['label'] = 'Exception foo'
4497
    resp = resp.form.submit().follow()
4498
    assert 'Exception foo' in resp.text
4499
    time_period_exception.refresh_from_db()
4500
    assert 'Exception foo' == time_period_exception.label
4501

  
4502

  
4503
def test_unavailability_calendar_delete_time_period_exeptions(app, admin_user):
4504
    unavailability_calendar = UnavailabilityCalendar.objects.create(label='Foo')
4505
    time_period_exception = TimePeriodException.objects.create(
4506
        unavailability_calendar=unavailability_calendar,
4507
        label='Exception 1',
4508
        start_datetime=now() - datetime.timedelta(days=2),
4509
        end_datetime=now() - datetime.timedelta(days=1),
4510
    )
4511

  
4512
    app = login(app)
4513
    resp = app.get('/manage/unavailability-calendar/%s/settings' % unavailability_calendar.pk)
4514
    assert 'Exception 1' in resp.text
4515
    resp = resp.click(href='/manage/time-period-exceptions/%s/delete' % time_period_exception.pk)
4516
    resp = resp.form.submit().follow()
4517
    assert 'Exception foo' not in resp.text
4518
    assert unavailability_calendar.timeperiodexception_set.count() == 0
4519

  
4520

  
4521
def test_activate_unavailability_calendar_in_desk(app, admin_user):
4522
    app = login(app)
4523
    unavailability_calendar = UnavailabilityCalendar.objects.create(label='Calendar')
4524
    agenda = Agenda.objects.create(label='Agenda', kind='meetings')
4525
    desk = Desk.objects.create(agenda=agenda, label='desk')
4526
    exceptions_url = '/manage/agendas/desk/%s/import-exceptions-from-ics/' % desk.pk
4527
    resp = app.get(exceptions_url)
4528
    assert 'calendar' in resp.text
4529
    assert 'enable' in resp.text
4530
    resp = resp.click(
4531
        href='/manage/desk/%s/unavailability-calendar/%s/toogle/' % (desk.pk, unavailability_calendar.pk)
4532
    )
4533
    assert resp.location.endswith('/manage/agendas/%s/settings' % agenda.pk)
4534
    resp = app.get(exceptions_url)
4535
    assert 'calendar' in resp.text
4536
    assert 'disable' in resp.text
4537
    assert desk.unavailability_calendars.get(pk=unavailability_calendar.pk)
4538

  
4539

  
4540
def test_deactivate_unavailability_calendar_in_desk(app, admin_user):
4541
    app = login(app)
4542
    unavailability_calendar = UnavailabilityCalendar.objects.create(label='Calendar')
4543
    agenda = Agenda.objects.create(label='Agenda', kind='meetings')
4544
    desk = Desk.objects.create(agenda=agenda, label='desk')
4545
    desk.unavailability_calendars.add(unavailability_calendar)
4546
    exceptions_url = '/manage/agendas/desk/%s/import-exceptions-from-ics/' % desk.pk
4547
    resp = app.get(exceptions_url)
4548
    assert 'calendar' in resp.text
4549
    assert 'disable' in resp.text
4550
    resp = resp.click(
4551
        href='/manage/desk/%s/unavailability-calendar/%s/toogle/' % (desk.pk, unavailability_calendar.pk)
4552
    )
4553
    assert resp.location.endswith('/manage/agendas/%s/settings' % agenda.pk)
4554
    resp = app.get(exceptions_url)
4555
    assert 'calendar' in resp.text
4556
    assert 'enable' in resp.text
4557
    assert desk.unavailability_calendars.count() == 0
4558

  
4559

  
4560
def test_unavailability_calendar_homepage_permission(app, manager_user):
4561
    unavailability_calendar = UnavailabilityCalendar.objects.create(label='Calendar 1')
4562
    app = login(app, username='manager', password='manager')
4563
    resp = app.get('/manage/', status=403)
4564
    group = manager_user.groups.all()[0]
4565
    unavailability_calendar.view_role = group
4566
    unavailability_calendar.edit_role = None
4567
    unavailability_calendar.save()
4568
    resp = app.get('/manage/')
4569
    resp = resp.click('Unavailability calendars')
4570
    unavailability_calendar.view_role = None
4571
    unavailability_calendar.edit_role = group
4572
    unavailability_calendar.save()
4573
    resp = app.get('/manage/')
4574
    resp = resp.click('Unavailability calendars')
4575

  
4576

  
4577
def test_unavailability_calendar_list_permissions(app, manager_user):
4578
    unavailability_calendar = UnavailabilityCalendar.objects.create(label='Calendar 1')
4579
    app = login(app, username='manager', password='manager')
4580
    app.get('/manage/unavailability-calendars/', status=403)
4581
    group = manager_user.groups.all()[0]
4582
    unavailability_calendar.view_role = group
4583
    unavailability_calendar.edit_role = None
4584
    unavailability_calendar.save()
4585
    resp = app.get('/manage/unavailability-calendars/')
4586
    assert 'Calendar 1' in resp.text
4587
    assert 'New' not in resp.text
4588
    unavailability_calendar.view_role = None
4589
    unavailability_calendar.edit_role = group
4590
    unavailability_calendar.save()
4591
    assert 'Calendar 1' in resp.text
4592
    assert 'New' not in resp.text
4593

  
4594

  
4595
def test_unavailability_calendar_add_permissions(app, manager_user):
4596
    app = login(app, username='manager', password='manager')
4597
    url = '/manage/unavailability-calendar/add/'
4598
    app.get(url, status=403)
4599

  
4600

  
4601
def test_unavailability_calendar_detail_permissions(app, manager_user):
4602
    unavailability_calendar = UnavailabilityCalendar.objects.create(label='Calendar 1')
4603
    app = login(app, username='manager', password='manager')
4604
    detail_url = '/manage/unavailability-calendar/%s/' % unavailability_calendar.pk
4605
    resp = app.get(detail_url, status=403)
4606
    group = manager_user.groups.all()[0]
4607
    unavailability_calendar.view_role = group
4608
    unavailability_calendar.edit_role = None
4609
    unavailability_calendar.save()
4610
    resp = app.get(detail_url)
4611
    assert 'Settings' not in resp.text
4612
    unavailability_calendar.view_role = None
4613
    unavailability_calendar.edit_role = group
4614
    unavailability_calendar.save()
4615
    resp = app.get(detail_url)
4616
    assert 'Settings' in resp.text
4617

  
4618

  
4619
def test_unavailability_calendar_edit_permissions(app, manager_user):
4620
    unavailability_calendar = UnavailabilityCalendar.objects.create(label='Calendar 1')
4621
    app = login(app, username='manager', password='manager')
4622
    url = '/manage/unavailability-calendar/%s/edit/' % unavailability_calendar.pk
4623
    app.get(url, status=403)
4624
    group = manager_user.groups.all()[0]
4625
    unavailability_calendar.view_role = group
4626
    unavailability_calendar.edit_role = None
4627
    unavailability_calendar.save()
4628
    app.get(url, status=403)
4629
    unavailability_calendar.view_role = None
4630
    unavailability_calendar.edit_role = group
4631
    unavailability_calendar.save()
4632
    app.get(url)
4633

  
4634

  
4635
def test_unavailability_calendar_delete_permissions(app, manager_user):
4636
    unavailability_calendar = UnavailabilityCalendar.objects.create(label='Calendar 1')
4637
    app = login(app, username='manager', password='manager')
4638
    url = '/manage/unavailability-calendar/%s/delete/' % unavailability_calendar.pk
4639
    app.get(url, status=403)
4640
    group = manager_user.groups.all()[0]
4641
    unavailability_calendar.view_role = group
4642
    unavailability_calendar.edit_role = None
4643
    unavailability_calendar.save()
4644
    app.get(url, status=403)
4645
    unavailability_calendar.view_role = None
4646
    unavailability_calendar.edit_role = group
4647
    unavailability_calendar.save()
4648
    app.get(url, status=403)
4649

  
4650

  
4651
def test_unavailability_calendar_settings_permissions(app, manager_user):
4652
    unavailability_calendar = UnavailabilityCalendar.objects.create(label='Calendar 1')
4653
    app = login(app, username='manager', password='manager')
4654
    settings_url = '/manage/unavailability-calendar/%s/settings' % unavailability_calendar.pk
4655
    app.get(settings_url, status=403)
4656
    group = manager_user.groups.all()[0]
4657
    unavailability_calendar.view_role = group
4658
    unavailability_calendar.edit_role = None
4659
    unavailability_calendar.save()
4660
    app.get(settings_url, status=403)
4661
    unavailability_calendar.view_role = None
4662
    unavailability_calendar.edit_role = group
4663
    unavailability_calendar.save()
4664
    app.get(settings_url)
4665

  
4666

  
4667
def test_unavailability_calendar_add_unavailability_permissions(app, manager_user):
4668
    unavailability_calendar = UnavailabilityCalendar.objects.create(label='Calendar 1')
4669
    app = login(app, username='manager', password='manager')
4670
    url = '/manage/unavailability-calendar/%s/add-unavailability' % unavailability_calendar.pk
4671
    app.get(url, status=403)
4672
    group = manager_user.groups.all()[0]
4673
    unavailability_calendar.view_role = group
4674
    unavailability_calendar.edit_role = None
4675
    unavailability_calendar.save()
4676
    app.get(url, status=403)
4677
    unavailability_calendar.view_role = None
4678
    unavailability_calendar.edit_role = group
4679
    unavailability_calendar.save()
4680
    app.get(url)
4681

  
4682

  
4683
def test_unavailability_calendar_edit_unavailability_permissions(app, manager_user):
4684
    unavailability_calendar = UnavailabilityCalendar.objects.create(label='Calendar 1')
4685
    time_period_exception = TimePeriodException.objects.create(
4686
        unavailability_calendar=unavailability_calendar,
4687
        start_datetime=now() - datetime.timedelta(days=2),
4688
        end_datetime=now() - datetime.timedelta(days=1),
4689
    )
4690
    app = login(app, username='manager', password='manager')
4691
    url = '/manage/time-period-exceptions/%s/edit' % time_period_exception.pk
4692
    app.get(url, status=403)
4693
    group = manager_user.groups.all()[0]
4694
    unavailability_calendar.view_role = group
4695
    unavailability_calendar.edit_role = None
4696
    unavailability_calendar.save()
4697
    app.get(url, status=403)
4698
    unavailability_calendar.view_role = None
4699
    unavailability_calendar.edit_role = group
4700
    unavailability_calendar.save()
4701
    app.get(url)
4702

  
4703

  
4704
def test_unavailability_calendar_delete_unavailability_permissions(app, manager_user):
4705
    unavailability_calendar = UnavailabilityCalendar.objects.create(label='Calendar 1')
4706
    time_period_exception = TimePeriodException.objects.create(
4707
        unavailability_calendar=unavailability_calendar,
4708
        start_datetime=now() - datetime.timedelta(days=2),
4709
        end_datetime=now() - datetime.timedelta(days=1),
4710
    )
4711
    app = login(app, username='manager', password='manager')
4712
    url = '/manage/time-period-exceptions/%s/delete' % time_period_exception.pk
4713
    app.get(url, status=403)
4714
    group = manager_user.groups.all()[0]
4715
    unavailability_calendar.view_role = group
4716
    unavailability_calendar.edit_role = None
4717
    unavailability_calendar.save()
4718
    app.get(url, status=403)
4719
    unavailability_calendar.view_role = None
4720
    unavailability_calendar.edit_role = group
4721
    unavailability_calendar.save()
4722
    app.get(url)
4379
-