Projet

Général

Profil

0001-start-unavailability-calendars-46555.patch

Emmanuel Cazenave, 30 septembre 2020 15:10

Télécharger (59,5 ko)

Voir les différences:

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

 .../0065_unavailability_calendar.py           |  71 ++++
 chrono/agendas/models.py                      |  57 ++-
 chrono/api/views.py                           |  13 +
 chrono/manager/forms.py                       |  47 ++-
 ...r_agenda_unavailability_calendar_form.html |  23 ++
 .../templates/chrono/manager_home.html        |   7 +-
 .../manager_meetings_agenda_settings.html     |  32 ++
 .../manager_time_period_exception_form.html   |   2 +-
 ...anager_unavailability_calendar_detail.html |  53 +++
 .../manager_unavailability_calendar_form.html |  22 ++
 .../manager_unavailability_calendar_list.html |  38 ++
 ...ager_unavailability_calendar_settings.html |  53 +++
 chrono/manager/urls.py                        |  45 +++
 chrono/manager/views.py                       | 233 ++++++++++++-
 chrono/urls_utils.py                          |   9 +-
 tests/test_api.py                             | 130 ++++++-
 tests/test_manager.py                         | 325 +++++++++++++++++-
 17 files changed, 1140 insertions(+), 20 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-09-30 11:52
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
                (
27
                    'agendas',
28
                    models.ManyToManyField(related_name='unavailability_calendars', to='agendas.Agenda'),
29
                ),
30
                (
31
                    'edit_role',
32
                    models.ForeignKey(
33
                        blank=True,
34
                        default=None,
35
                        null=True,
36
                        on_delete=django.db.models.deletion.SET_NULL,
37
                        related_name='+',
38
                        to='auth.Group',
39
                        verbose_name='Edit Role',
40
                    ),
41
                ),
42
                (
43
                    'view_role',
44
                    models.ForeignKey(
45
                        blank=True,
46
                        default=None,
47
                        null=True,
48
                        on_delete=django.db.models.deletion.SET_NULL,
49
                        related_name='+',
50
                        to='auth.Group',
51
                        verbose_name='View Role',
52
                    ),
53
                ),
54
            ],
55
            options={'ordering': ['label'],},
56
        ),
57
        migrations.AlterField(
58
            model_name='timeperiodexception',
59
            name='desk',
60
            field=models.ForeignKey(
61
                null=True, on_delete=django.db.models.deletion.CASCADE, to='agendas.Desk'
62
            ),
63
        ),
64
        migrations.AddField(
65
            model_name='timeperiodexception',
66
            name='unavailability_calendar',
67
            field=models.ForeignKey(
68
                null=True, on_delete=django.db.models.deletion.CASCADE, to='agendas.UnavailabilityCalendar'
69
            ),
70
        ),
71
    ]
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
    agendas = models.ManyToManyField(Agenda, 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__agendas__in=agendas
148
    ).order_by('start_datetime', 'end_datetime'):
149
        for agenda in time_period_exception.unavailability_calendar.agendas.all():
150
            for desk in agenda.desk_set.all():
151
                if desk not in desks_exceptions:
152
                    desks_exceptions[desk] = IntervalSet()
153
                desks_exceptions[desk].add(
154
                    time_period_exception.start_datetime, time_period_exception.end_datetime
155
                )
156

  
144 157
    # compute reduced min/max_datetime windows by desks based on exceptions
145 158
    desk_min_max_datetime = {}
146 159
    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
......
170 190
        self.fields['resource'].queryset = Resource.objects.exclude(agenda=self.initial['agenda'])
171 191

  
172 192

  
193
class AgendaUnavailabilityCalendarForm(forms.Form):
194
    unavailability_calendar = forms.ModelChoiceField(
195
        label=_('UnavailabilityCalendar'), queryset=UnavailabilityCalendar.objects.none()
196
    )
197

  
198
    def __init__(self, *args, **kwargs):
199
        super().__init__(*args, **kwargs)
200
        self.fields['unavailability_calendar'].queryset = UnavailabilityCalendar.objects.exclude(
201
            agendas=self.initial['agenda']
202
        )
203

  
204

  
173 205
class NewMeetingTypeForm(forms.ModelForm):
174 206
    class Meta:
175 207
        model = MeetingType
......
287 319

  
288 320
    class Meta:
289 321
        model = TimePeriodException
290
        fields = ['desk', 'start_datetime', 'end_datetime', 'label']
322
        fields = ['desk', 'start_datetime', 'end_datetime', 'label', 'unavailability_calendar']
291 323
        widgets = {
292 324
            'desk': forms.HiddenInput(),
325
            'unavailability_calendar': forms.HiddenInput(),
293 326
        }
294 327
        field_classes = {
295 328
            'start_datetime': SplitDateTimeField,
......
297 330
        }
298 331

  
299 332
    def __init__(self, *args, **kwargs):
333
        has_desk = kwargs.pop('has_desk', True)
300 334
        super().__init__(*args, **kwargs)
301
        if self.instance.pk is not None:
302
            del self.fields['all_desks']
303
        elif 'desk' in self.initial and self.initial['desk'].agenda.desk_set.count() == 1:
335
        if has_desk:
336
            del self.fields['unavailability_calendar']
337
            if self.instance.pk is not None or (
338
                'desk' in self.initial and self.initial['desk'].agenda.desk_set.count() == 1
339
            ):
340
                del self.fields['all_desks']
341
        else:
342
            del self.fields['desk']
304 343
            del self.fields['all_desks']
305 344

  
306 345
    def clean(self):
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_meetings_agenda_settings.html
14 14

  
15 15
{% block agenda-extra-management-actions %}
16 16
  {% if has_resources %}<a rel="popup" href="{% url 'chrono-manager-agenda-add-resource' pk=object.pk %}">{% trans 'Add resource' %}</a>{% endif %}
17
  {% if has_unavailability_calendars %}<a rel="popup" href="{% url 'chrono-manager-agenda-add-unavailability-calendar' pk=object.pk %}">{% trans 'Add unavailability calendar' %}</a>{% endif %}
17 18
  <a rel="popup" href="{% url 'chrono-manager-agenda-add-meeting-type' pk=object.id %}">{% trans 'New Meeting Type' %}</a>
18 19
  <a rel="popup" href="{% url 'chrono-manager-agenda-add-desk' pk=object.id %}">{% trans 'New Desk' %}</a>
19 20
{% endblock %}
......
98 99
</div>
99 100
</div>
100 101

  
102
{% with object.unavailability_calendars.all as agenda_unavailability_calendars %}
103
{% if has_unavailability_calendars %}
104
<div class="section">
105
<h3>{% trans 'Unavailability calendars' %}</h3>
106
<div>
107
{% if agenda_unavailability_calendars %}
108
  <ul class="objects-list single-links">
109
    {% for unavailability_calendar in agenda_unavailability_calendars %}
110
    <li>
111
        <a href="{% url 'chrono-manager-unavailability-calendar-view' pk=unavailability_calendar.pk %}">
112
            {{ unavailability_calendar.label }}
113
            <span class="identifier">[{% trans "identifier:" %} {{ unavailability_calendar.slug }}]</span>
114
        </a>
115
        <a rel="popup" class="delete" href="{% url 'chrono-manager-agenda-delete-unavailability_calendar' pk=object.pk unavailability_calendar_pk=unavailability_calendar.pk %}">{% trans "remove" %}</a>
116
    </li>
117
    {% endfor %}
118
  </ul>
119
{% else %}
120
<div class="big-msg-info">
121
  {% blocktrans %}
122
  This agenda doesn't have any unavailability calendar yet. Click on the "Add unavailability calendar" button in
123
  the top right of the page to add a first one.
124
  {% endblocktrans %}
125
</div>
126
{% endif %}
127
</div>
128
</div>
129
{% endif %}
130
{% endwith %}
131

  
132

  
101 133
{% with object.resources.all as agenda_resources %}
102 134
{% if has_resources %}
103 135
<div class="section">
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
- {{ 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
{% with unavailability_calendar.agendas.all as agendas %}
32
{% if agendas %}
33
  <ul class="objects-list single-links">
34
    {% for agenda in agendas %}
35
    <li>
36
        <a href="{% url 'chrono-manager-agenda-view' pk=agenda.pk %}">
37
            {{ agenda.label }}
38
        </a>
39
    </li>
40
    {% endfor %}
41
  </ul>
42
{% else %}
43
<div class="big-msg-info">
44
  {% blocktrans %}
45
  This unavailability calendar is not used yet.
46
  {% endblocktrans %}
47
</div>
48
{% endif %}
49
{% endwith %}
50
</div>
51
</div>
52

  
53
{% 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
    <span class="identifier">[{% trans "identifier:" %} {{unavailability_calendar.slug}}]</span>
12
</h2>
13
<span class="actions">
14
  <a class="extra-actions-menu-opener"></a>
15
  {% block agenda-extra-management-actions %}
16
    <a rel="popup" href="{% url 'chrono-manager-unavailability-calendar-add-unavailability' pk=unavailability_calendar.id %}">{% trans 'Add Unavailability' %}</a>
17
  {% endblock %}
18
  <ul class="extra-actions-menu">
19
    <li><a rel="popup" href="{% url 'chrono-manager-unavailability-calendar-edit' pk=unavailability_calendar.id %}">{% trans 'Options' %}</a></li>
20
    {% if user.is_staff %}
21
      <li><a rel="popup" href="{% url 'chrono-manager-unavailability-calendar-delete' pk=unavailability_calendar.id %}">{% trans 'Delete' %}</a></li>
22
    {% endif %}
23
  </ul>
24
</span>
25
{% endblock %}
26

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

  
52

  
53
{% 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'),
......
149 184
        views.virtual_agenda_add_time_period,
150 185
        name='chrono-manager-virtual-agenda-add-time-period',
151 186
    ),
187
    url(
188
        r'^agendas/(?P<pk>\d+)/add-unavailability-calendar$',
189
        views.agenda_add_unavailability_calendar,
190
        name='chrono-manager-agenda-add-unavailability-calendar',
191
    ),
192
    url(
193
        r'^agendas/(?P<pk>\d+)/unavailability-calendar/(?P<unavailability_calendar_pk>\d+)/delete/$',
194
        views.agenda_delete_unavailability_calendar,
195
        name='chrono-manager-agenda-delete-unavailability_calendar',
196
    ),
152 197
    url(r'^timeperiods/(?P<pk>\d+)/edit$', views.time_period_edit, name='chrono-manager-time-period-edit'),
153 198
    url(
154 199
        r'^timeperiods/(?P<pk>\d+)/delete$',
chrono/manager/views.py
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,
100
    AgendaUnavailabilityCalendarForm,
97 101
)
98 102
from .utils import import_site
99 103

  
......
1235 1239
        return reverse('chrono-manager-agenda-settings', kwargs={'pk': self.agenda.id})
1236 1240

  
1237 1241

  
1242
class ManagedTimePeriodExceptionMixin(object):
1243

  
1244
    desk = None
1245
    unavailability_calendar = None
1246

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

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

  
1266
    def get_form_kwargs(self):
1267
        kwargs = super().get_form_kwargs()
1268
        kwargs['has_desk'] = True
1269
        if self.unavailability_calendar:
1270
            kwargs['has_desk'] = False
1271
        return kwargs
1272

  
1273
    def get_success_url(self):
1274
        if self.desk:
1275
            return reverse('chrono-manager-agenda-settings', kwargs={'pk': self.desk.agenda.id})
1276
        elif self.unavailability_calendar:
1277
            return reverse(
1278
                'chrono-manager-unavailability-calendar-settings',
1279
                kwargs={'pk': self.unavailability_calendar.pk},
1280
            )
1281

  
1282

  
1238 1283
class AgendaSettings(ManagedAgendaMixin, DetailView):
1239 1284
    model = Agenda
1240 1285

  
......
1256 1301
            ]
1257 1302
        if self.agenda.kind == 'meetings':
1258 1303
            context['has_resources'] = Resource.objects.exists()
1304
            context['has_unavailability_calendars'] = UnavailabilityCalendar.objects.exists()
1259 1305
        return context
1260 1306

  
1261 1307
    def get_events(self):
......
1530 1576
agenda_delete_resource = AgendaResourceDeleteView.as_view()
1531 1577

  
1532 1578

  
1579
class AgendaAddUnavailabilityCalendarView(ManagedAgendaMixin, FormView):
1580
    template_name = 'chrono/manager_agenda_unavailability_calendar_form.html'
1581
    form_class = AgendaUnavailabilityCalendarForm
1582

  
1583
    def set_agenda(self, **kwargs):
1584
        self.agenda = get_object_or_404(Agenda, id=kwargs.get('pk'), kind='meetings')
1585

  
1586
    def form_valid(self, form):
1587
        self.agenda.unavailability_calendars.add(form.cleaned_data['unavailability_calendar'])
1588
        return super().form_valid(form)
1589

  
1590

  
1591
agenda_add_unavailability_calendar = AgendaAddUnavailabilityCalendarView.as_view()
1592

  
1593

  
1594
class AgendaUnavailabilityCalendarDeleteView(ManagedAgendaMixin, DeleteView):
1595
    template_name = 'chrono/manager_confirm_delete.html'
1596
    model = UnavailabilityCalendar
1597
    pk_url_kwarg = 'unavailability_calendar_pk'
1598

  
1599
    def set_agenda(self, **kwargs):
1600
        self.agenda = get_object_or_404(Agenda, id=kwargs.get('pk'), kind='meetings')
1601

  
1602
    def delete(self, request, *args, **kwargs):
1603
        self.object = self.get_object()
1604
        self.agenda.unavailability_calendars.remove(self.object)
1605
        return HttpResponseRedirect(self.get_success_url())
1606

  
1607

  
1608
agenda_delete_unavailability_calendar = AgendaUnavailabilityCalendarDeleteView.as_view()
1609

  
1610

  
1533 1611
class AgendaAddMeetingTypeView(ManagedAgendaMixin, CreateView):
1534 1612
    template_name = 'chrono/manager_meeting_type_form.html'
1535 1613
    model = Event
......
1736 1814
    model = TimePeriodException
1737 1815
    form_class = TimePeriodExceptionForm
1738 1816

  
1817
    def get_form_kwargs(self):
1818
        kwargs = super().get_form_kwargs()
1819
        kwargs['has_desk'] = True
1820
        return kwargs
1821

  
1739 1822
    def form_valid(self, form):
1740 1823
        result = super().form_valid(form)
1741 1824
        exceptions = [self.object]
......
1766 1849
agenda_add_time_period_exception = AgendaAddTimePeriodExceptionView.as_view()
1767 1850

  
1768 1851

  
1769
class TimePeriodExceptionEditView(ManagedDeskSubobjectMixin, UpdateView):
1852
class TimePeriodExceptionEditView(ManagedTimePeriodExceptionMixin, UpdateView):
1770 1853
    template_name = 'chrono/manager_time_period_exception_form.html'
1771 1854
    model = TimePeriodException
1772 1855
    form_class = TimePeriodExceptionForm
......
1803 1886
time_period_exception_extract_list = TimePeriodExceptionExtractListView.as_view()
1804 1887

  
1805 1888

  
1806
class TimePeriodExceptionDeleteView(ManagedDeskSubobjectMixin, DeleteView):
1889
class TimePeriodExceptionDeleteView(ManagedTimePeriodExceptionMixin, DeleteView):
1807 1890
    template_name = 'chrono/manager_confirm_exception_delete.html'
1808 1891
    model = TimePeriodException
1809 1892

  
1810 1893
    def get_success_url(self):
1811
        referer = self.request.META.get('HTTP_REFERER')
1812
        success_url = reverse('chrono-manager-time-period-exception-list', kwargs={'pk': self.desk.pk})
1813
        if success_url in referer:
1814
            return success_url
1894
        if self.desk:
1895
            referer = self.request.META.get('HTTP_REFERER')
1896
            success_url = reverse('chrono-manager-time-period-exception-list', kwargs={'pk': self.desk.pk})
1897
            if success_url in referer:
1898
                return success_url
1815 1899

  
1816 1900
        success_url = super(TimePeriodExceptionDeleteView, self).get_success_url()
1817
        if 'from_popup' in self.request.GET:
1901
        if self.desk and 'from_popup' in self.request.GET:
1818 1902
            success_url = '{}?display_exceptions={}'.format(success_url, self.desk.pk)
1819 1903
        return success_url
1820 1904

  
......
2073 2157
time_period_exception_source_toggle = TimePeriodExceptionSourceToggleView.as_view()
2074 2158

  
2075 2159

  
2160
class ViewableUnavailabilityCalendarMixin(object):
2161
    unavailability_calendar = None
2162

  
2163
    def set_unavailability_calendar(self, **kwargs):
2164
        self.unavailability_calendar = get_object_or_404(UnavailabilityCalendar, id=kwargs.get('pk'))
2165

  
2166
    def dispatch(self, request, *args, **kwargs):
2167
        self.set_unavailability_calendar(**kwargs)
2168
        if not self.check_permissions(request.user):
2169
            raise PermissionDenied()
2170
        return super().dispatch(request, *args, **kwargs)
2171

  
2172
    def check_permissions(self, user):
2173
        return self.unavailability_calendar.can_be_viewed(user)
2174

  
2175
    def get_context_data(self, **kwargs):
2176
        context = super().get_context_data(**kwargs)
2177
        context['unavailability_calendar'] = self.unavailability_calendar
2178
        context['user_can_manage'] = self.unavailability_calendar.can_be_managed(self.request.user)
2179
        return context
2180

  
2181

  
2182
class ManagedUnavailabilityCalendarMixin(ViewableUnavailabilityCalendarMixin):
2183
    def check_permissions(self, user):
2184
        return self.unavailability_calendar.can_be_managed(user)
2185

  
2186
    def get_initial(self):
2187
        initial = super().get_initial()
2188
        initial['unavailability_calendar'] = self.unavailability_calendar
2189
        return initial
2190

  
2191
    def get_success_url(self):
2192
        return reverse(
2193
            'chrono-manager-unavailability-calendar-settings', kwargs={'pk': self.unavailability_calendar.id}
2194
        )
2195

  
2196

  
2197
class UnavailabilityCalendarListView(ListView):
2198
    template_name = 'chrono/manager_unavailability_calendar_list.html'
2199
    model = UnavailabilityCalendar
2200

  
2201
    def get_queryset(self):
2202
        queryset = super().get_queryset()
2203
        if not self.request.user.is_staff:
2204
            group_ids = [x.id for x in self.request.user.groups.all()]
2205
            queryset = queryset.filter(Q(view_role_id__in=group_ids) | Q(edit_role_id__in=group_ids))
2206
        return queryset.order_by('label')
2207

  
2208

  
2209
unavailability_calendar_list = UnavailabilityCalendarListView.as_view()
2210

  
2211

  
2212
class UnavailabilityCalendarAddView(CreateView):
2213
    template_name = 'chrono/manager_unavailability_calendar_form.html'
2214
    model = UnavailabilityCalendar
2215
    form_class = UnavailabilityCalendarAddForm
2216

  
2217
    def dispatch(self, request, *args, **kwargs):
2218
        if not request.user.is_staff:
2219
            raise PermissionDenied()
2220
        return super().dispatch(request, *args, **kwargs)
2221

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

  
2225

  
2226
unavailability_calendar_add = UnavailabilityCalendarAddView.as_view()
2227

  
2228

  
2229
class UnavailabilityCalendarDetailView(ViewableUnavailabilityCalendarMixin, DetailView):
2230
    template_name = 'chrono/manager_unavailability_calendar_detail.html'
2231
    model = UnavailabilityCalendar
2232

  
2233

  
2234
unavailability_calendar_view = UnavailabilityCalendarDetailView.as_view()
2235

  
2236

  
2237
class UnavailabilityCalendarEditView(ManagedUnavailabilityCalendarMixin, UpdateView):
2238
    template_name = 'chrono/manager_unavailability_calendar_form.html'
2239
    model = UnavailabilityCalendar
2240
    form_class = UnavailabilityCalendarEditForm
2241

  
2242

  
2243
unavailability_calendar_edit = UnavailabilityCalendarEditView.as_view()
2244

  
2245

  
2246
class UnavailabilityCalendarDeleteView(DeleteView):
2247
    template_name = 'chrono/manager_confirm_delete.html'
2248
    model = UnavailabilityCalendar
2249

  
2250
    def dispatch(self, request, *args, **kwargs):
2251
        if not request.user.is_staff:
2252
            raise PermissionDenied()
2253
        return super().dispatch(request, *args, **kwargs)
2254

  
2255
    def get_success_url(self):
2256
        return reverse('chrono-manager-unavailability-calendar-list')
2257

  
2258

  
2259
unavailability_calendar_delete = UnavailabilityCalendarDeleteView.as_view()
2260

  
2261

  
2262
class UnavailabilityCalendarSettings(ManagedUnavailabilityCalendarMixin, DetailView):
2263
    template_name = 'chrono/manager_unavailability_calendar_settings.html'
2264
    model = UnavailabilityCalendar
2265

  
2266
    def get_context_data(self, **kwargs):
2267
        context = super(UnavailabilityCalendarSettings, self).get_context_data(**kwargs)
2268
        context['unavailability_calendar'] = self.object
2269
        return context
2270

  
2271

  
2272
unavailability_calendar_settings = UnavailabilityCalendarSettings.as_view()
2273

  
2274

  
2275
class UnavailabilityCalendarAddUnavailabilityView(ManagedUnavailabilityCalendarMixin, CreateView):
2276
    template_name = 'chrono/manager_time_period_exception_form.html'
2277
    form_class = TimePeriodExceptionForm
2278

  
2279
    def get_form_kwargs(self):
2280
        kwargs = super().get_form_kwargs()
2281
        kwargs['has_desk'] = False
2282
        return kwargs
2283

  
2284
    def get_context_data(self, **kwargs):
2285
        context = super(UnavailabilityCalendarAddUnavailabilityView, self).get_context_data(**kwargs)
2286
        context['cancel_url'] = reverse(
2287
            'chrono-manager-unavailability-calendar-settings', kwargs={'pk': self.unavailability_calendar.pk,}
2288
        )
2289
        return context
2290

  
2291

  
2292
unavailability_calendar_add_unavailability = UnavailabilityCalendarAddUnavailabilityView.as_view()
2293

  
2294

  
2076 2295
def menu_json(request):
2077 2296
    label = _('Agendas')
2078 2297
    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
    datetimes_url = '/api/agenda/%s/meetings/%s/datetimes/' % (meetings_agenda.slug, meeting_type.slug)
4136
    resp = app.get(datetimes_url)
4137
    assert len(resp.json['data']) == 144
4138

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

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

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

  
4163

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

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

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

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

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

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

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

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

  
4249
    # 2 slots are gone
4250
    resp = app.get(api_url)
4251
    data = resp.json['data']
4252
    assert len(data) == 6
4253
    assert data[0]['datetime'] == '2017-05-22 10:00:00'
4254
    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):
......
4366 4367
        'Reminder: you have a booking for event "{{ event_label }}", on 02/06 at 2:30 p.m.. Take ID card.'
4367 4368
        in resp.text
4368 4369
    )
4370

  
4371

  
4372
def test_no_unavailability_calendar(app, admin_user):
4373
    app = login(app)
4374

  
4375
    # empty unavailability calendars list
4376
    resp = app.get('/manage/')
4377
    resp = resp.click('Unavailability calendars')
4378
    assert "This site doesn't have any unavailability calendar yet" in resp.text
4379

  
4380
    # on the agenda settings page, no unavailability calendar reference
4381
    agenda = Agenda.objects.create(label='Agenda', kind='meetings')
4382
    resp = app.get('/manage/agendas/%s/settings' % agenda.pk)
4383
    assert 'unavailability calendar' not in resp.text
4384

  
4385

  
4386
def test_add_unavailability_calendar(app, admin_user):
4387
    app = login(app)
4388
    resp = app.get('/manage/')
4389
    resp = resp.click('Unavailability calendars')
4390
    resp = resp.click('New')
4391
    resp.form['label'] = 'Foo bar'
4392
    resp = resp.form.submit()
4393
    unavailability_calendar = UnavailabilityCalendar.objects.latest('pk')
4394
    assert resp.location.endswith('/manage/unavailability-calendar/%s/' % unavailability_calendar.pk)
4395
    assert unavailability_calendar.label == 'Foo bar'
4396
    assert unavailability_calendar.slug == 'foo-bar'
4397
    resp = resp.follow()
4398
    assert 'This unavailability calendar is not used yet.' in resp.text
4399
    resp = app.get('/manage/unavailability-calendars/')
4400
    assert 'Foo bar' in resp.text
4401
    assert 'foo-bar' in resp.text
4402

  
4403

  
4404
def test_edit_unavailability_calendar(app, admin_user):
4405
    unavailability_calendar = UnavailabilityCalendar.objects.create(label='Foo')
4406
    app = login(app)
4407
    settings_url = '/manage/unavailability-calendar/%s/settings' % unavailability_calendar.pk
4408
    resp = app.get(settings_url)
4409
    resp = resp.click('Options')
4410
    resp.form['label'] = 'Bar'
4411
    resp = resp.form.submit()
4412
    assert resp.location.endswith(settings_url)
4413
    resp = resp.follow()
4414
    assert 'Bar' in resp.text
4415
    unavailability_calendar.refresh_from_db()
4416
    assert unavailability_calendar.label == 'Bar'
4417

  
4418

  
4419
def test_delete_unavailability_calendar(app, admin_user):
4420
    unavailability_calendar = UnavailabilityCalendar.objects.create(label='Foo')
4421
    app = login(app)
4422
    resp = app.get('/manage/unavailability-calendar/%s/settings' % unavailability_calendar.pk)
4423
    resp = resp.click('Delete')
4424
    resp = resp.form.submit()
4425
    assert resp.location.endswith('/manage/unavailability-calendars/')
4426
    resp = resp.follow()
4427
    assert 'Foo' not in resp.text
4428
    assert UnavailabilityCalendar.objects.count() == 0
4429

  
4430

  
4431
def test_unavailability_calendar_add_time_period_exeptions(app, admin_user):
4432
    unavailability_calendar = UnavailabilityCalendar.objects.create(label='Foo')
4433
    app = login(app)
4434
    resp = app.get('/manage/unavailability-calendar/%s/' % unavailability_calendar.pk)
4435
    resp = resp.click('Settings')
4436
    assert 'There is no unavailabilities yet' in resp.text
4437
    resp = resp.click('Add Unavailability')
4438
    today = datetime.datetime.today().replace(hour=0, minute=0, second=0, microsecond=0)
4439
    tomorrow = make_aware(today + datetime.timedelta(days=1))
4440
    resp.form['label'] = 'Exception 1'
4441
    resp.form['start_datetime_0'] = tomorrow.strftime('%Y-%m-%d')
4442
    resp.form['start_datetime_1'] = '08:00'
4443
    resp.form['end_datetime_0'] = tomorrow.strftime('%Y-%m-%d')
4444
    resp.form['end_datetime_1'] = '16:00'
4445
    resp = resp.form.submit().follow()
4446
    assert 'Exception 1' in resp.text
4447

  
4448
    time_period_exception = TimePeriodException.objects.first()
4449
    assert time_period_exception.unavailability_calendar == unavailability_calendar
4450
    assert time_period_exception.desk is None
4451
    assert time_period_exception.label == 'Exception 1'
4452

  
4453

  
4454
def test_unavailability_calendar_edit_time_period_exeptions(app, admin_user):
4455
    unavailability_calendar = UnavailabilityCalendar.objects.create(label='Foo')
4456
    time_period_exception = TimePeriodException.objects.create(
4457
        unavailability_calendar=unavailability_calendar,
4458
        label='Exception 1',
4459
        start_datetime=now() - datetime.timedelta(days=2),
4460
        end_datetime=now() - datetime.timedelta(days=1),
4461
    )
4462

  
4463
    app = login(app)
4464
    resp = app.get('/manage/unavailability-calendar/%s/settings' % unavailability_calendar.pk)
4465
    assert 'Exception 1' in resp.text
4466
    resp = resp.click(href='/manage/time-period-exceptions/%s/edit' % time_period_exception.pk)
4467
    resp.form['label'] = 'Exception foo'
4468
    resp = resp.form.submit().follow()
4469
    assert 'Exception foo' in resp.text
4470
    time_period_exception.refresh_from_db()
4471
    assert 'Exception foo' == time_period_exception.label
4472

  
4473

  
4474
def test_unavailability_calendar_delete_time_period_exeptions(app, admin_user):
4475
    unavailability_calendar = UnavailabilityCalendar.objects.create(label='Foo')
4476
    time_period_exception = TimePeriodException.objects.create(
4477
        unavailability_calendar=unavailability_calendar,
4478
        label='Exception 1',
4479
        start_datetime=now() - datetime.timedelta(days=2),
4480
        end_datetime=now() - datetime.timedelta(days=1),
4481
    )
4482

  
4483
    app = login(app)
4484
    resp = app.get('/manage/unavailability-calendar/%s/settings' % unavailability_calendar.pk)
4485
    assert 'Exception 1' in resp.text
4486
    resp = resp.click(href='/manage/time-period-exceptions/%s/delete' % time_period_exception.pk)
4487
    resp = resp.form.submit().follow()
4488
    assert 'Exception foo' not in resp.text
4489
    assert unavailability_calendar.timeperiodexception_set.count() == 0
4490

  
4491

  
4492
def test_add_unavailability_calendar_in_agenda(app, admin_user):
4493
    app = login(app)
4494
    unavailability_calendar = UnavailabilityCalendar.objects.create(label='Foo')
4495
    agenda = Agenda.objects.create(label='Agenda', kind='meetings')
4496
    settings_url = '/manage/agendas/%s/settings' % agenda.pk
4497
    resp = app.get(settings_url)
4498
    assert "This agenda doesn't have any unavailability calendar yet" in resp.text
4499
    resp = resp.click('Add unavailability calendar')
4500
    resp.form['unavailability_calendar'].select(unavailability_calendar.pk)
4501
    resp = resp.form.submit()
4502
    assert resp.location == settings_url
4503
    resp = resp.follow()
4504
    assert 'Foo' in resp.text
4505
    assert agenda.unavailability_calendars.first() == unavailability_calendar
4506
    resp = resp.click(href='/manage/unavailability-calendar/%s/' % unavailability_calendar.pk)
4507
    assert 'Agenda' in resp.text
4508
    resp = resp.click(href='/manage/agendas/%s/' % agenda.pk)
4509

  
4510

  
4511
def test_remove_unavailability_calendar_in_agenda(app, admin_user):
4512
    app = login(app)
4513
    unavailability_calendar = UnavailabilityCalendar.objects.create(label='Foo')
4514
    agenda = Agenda.objects.create(label='Agenda', kind='meetings')
4515
    unavailability_calendar.agendas.add(agenda)
4516
    settings_url = '/manage/agendas/%s/settings' % agenda.pk
4517
    resp = app.get(settings_url)
4518
    assert "Foo" in resp.text
4519
    resp = resp.click(
4520
        href='/manage/agendas/%s/unavailability-calendar/%s/delete' % (agenda.pk, unavailability_calendar.pk)
4521
    )
4522
    resp = resp.form.submit()
4523
    assert resp.location.endswith(settings_url)
4524
    resp = resp.follow()
4525
    assert "Foo" not in resp.text
4526
    assert agenda.unavailability_calendars.count() == 0
4527

  
4528

  
4529
def test_unavailability_calendar_homepage_permission(app, manager_user):
4530
    unavailability_calendar = UnavailabilityCalendar.objects.create(label='Calendar 1')
4531
    app = login(app, username='manager', password='manager')
4532
    resp = app.get('/manage/', status=403)
4533
    group = manager_user.groups.all()[0]
4534
    unavailability_calendar.view_role = group
4535
    unavailability_calendar.edit_role = None
4536
    unavailability_calendar.save()
4537
    resp = app.get('/manage/')
4538
    resp = resp.click('Unavailability calendars')
4539
    unavailability_calendar.view_role = None
4540
    unavailability_calendar.edit_role = group
4541
    unavailability_calendar.save()
4542
    resp = app.get('/manage/')
4543
    resp = resp.click('Unavailability calendars')
4544

  
4545

  
4546
def test_unavailability_calendar_list_permissions(app, manager_user):
4547
    unavailability_calendar = UnavailabilityCalendar.objects.create(label='Calendar 1')
4548
    app = login(app, username='manager', password='manager')
4549
    app.get('/manage/unavailability-calendars/', status=403)
4550
    group = manager_user.groups.all()[0]
4551
    unavailability_calendar.view_role = group
4552
    unavailability_calendar.edit_role = None
4553
    unavailability_calendar.save()
4554
    resp = app.get('/manage/unavailability-calendars/')
4555
    assert 'Calendar 1' in resp.text
4556
    assert 'New' not in resp.text
4557
    unavailability_calendar.view_role = None
4558
    unavailability_calendar.edit_role = group
4559
    unavailability_calendar.save()
4560
    assert 'Calendar 1' in resp.text
4561
    assert 'New' not in resp.text
4562

  
4563

  
4564
def test_unavailability_calendar_add_permissions(app, manager_user):
4565
    app = login(app, username='manager', password='manager')
4566
    url = '/manage/unavailability-calendar/add/'
4567
    app.get(url, status=403)
4568

  
4569

  
4570
def test_unavailability_calendar_detail_permissions(app, manager_user):
4571
    unavailability_calendar = UnavailabilityCalendar.objects.create(label='Calendar 1')
4572
    app = login(app, username='manager', password='manager')
4573
    detail_url = '/manage/unavailability-calendar/%s/' % unavailability_calendar.pk
4574
    resp = app.get(detail_url, status=403)
4575
    group = manager_user.groups.all()[0]
4576
    unavailability_calendar.view_role = group
4577
    unavailability_calendar.edit_role = None
4578
    unavailability_calendar.save()
4579
    resp = app.get(detail_url)
4580
    assert 'Settings' not in resp.text
4581
    unavailability_calendar.view_role = None
4582
    unavailability_calendar.edit_role = group
4583
    unavailability_calendar.save()
4584
    resp = app.get(detail_url)
4585
    assert 'Settings' in resp.text
4586

  
4587

  
4588
def test_unavailability_calendar_edit_permissions(app, manager_user):
4589
    unavailability_calendar = UnavailabilityCalendar.objects.create(label='Calendar 1')
4590
    app = login(app, username='manager', password='manager')
4591
    url = '/manage/unavailability-calendar/%s/edit/' % unavailability_calendar.pk
4592
    app.get(url, status=403)
4593
    group = manager_user.groups.all()[0]
4594
    unavailability_calendar.view_role = group
4595
    unavailability_calendar.edit_role = None
4596
    unavailability_calendar.save()
4597
    app.get(url, status=403)
4598
    unavailability_calendar.view_role = None
4599
    unavailability_calendar.edit_role = group
4600
    unavailability_calendar.save()
4601
    app.get(url)
4602

  
4603

  
4604
def test_unavailability_calendar_delete_permissions(app, manager_user):
4605
    unavailability_calendar = UnavailabilityCalendar.objects.create(label='Calendar 1')
4606
    app = login(app, username='manager', password='manager')
4607
    url = '/manage/unavailability-calendar/%s/delete/' % unavailability_calendar.pk
4608
    app.get(url, status=403)
4609
    group = manager_user.groups.all()[0]
4610
    unavailability_calendar.view_role = group
4611
    unavailability_calendar.edit_role = None
4612
    unavailability_calendar.save()
4613
    app.get(url, status=403)
4614
    unavailability_calendar.view_role = None
4615
    unavailability_calendar.edit_role = group
4616
    unavailability_calendar.save()
4617
    app.get(url, status=403)
4618

  
4619

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

  
4635

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

  
4651

  
4652
def test_unavailability_calendar_edit_unavailability_permissions(app, manager_user):
4653
    unavailability_calendar = UnavailabilityCalendar.objects.create(label='Calendar 1')
4654
    time_period_exception = TimePeriodException.objects.create(
4655
        unavailability_calendar=unavailability_calendar,
4656
        start_datetime=now() - datetime.timedelta(days=2),
4657
        end_datetime=now() - datetime.timedelta(days=1),
4658
    )
4659
    app = login(app, username='manager', password='manager')
4660
    url = '/manage/time-period-exceptions/%s/edit' % time_period_exception.pk
4661
    app.get(url, status=403)
4662
    group = manager_user.groups.all()[0]
4663
    unavailability_calendar.view_role = group
4664
    unavailability_calendar.edit_role = None
4665
    unavailability_calendar.save()
4666
    app.get(url, status=403)
4667
    unavailability_calendar.view_role = None
4668
    unavailability_calendar.edit_role = group
4669
    unavailability_calendar.save()
4670
    app.get(url)
4671

  
4672

  
4673
def test_unavailability_calendar_delete_unavailability_permissions(app, manager_user):
4674
    unavailability_calendar = UnavailabilityCalendar.objects.create(label='Calendar 1')
4675
    time_period_exception = TimePeriodException.objects.create(
4676
        unavailability_calendar=unavailability_calendar,
4677
        start_datetime=now() - datetime.timedelta(days=2),
4678
        end_datetime=now() - datetime.timedelta(days=1),
4679
    )
4680
    app = login(app, username='manager', password='manager')
4681
    url = '/manage/time-period-exceptions/%s/delete' % time_period_exception.pk
4682
    app.get(url, status=403)
4683
    group = manager_user.groups.all()[0]
4684
    unavailability_calendar.view_role = group
4685
    unavailability_calendar.edit_role = None
4686
    unavailability_calendar.save()
4687
    app.get(url, status=403)
4688
    unavailability_calendar.view_role = None
4689
    unavailability_calendar.edit_role = group
4690
    unavailability_calendar.save()
4691
    app.get(url)
4369
-