Projet

Général

Profil

0001-add-calendar-cell-model-16393.patch

Josué Kouka, 15 juin 2017 19:49

Télécharger (35,6 ko)

Voir les différences:

Subject: [PATCH] add calendar cell model (#16393)

 combo/apps/calendar/README                         |   4 +
 combo/apps/calendar/__init__.py                    |  28 +++
 combo/apps/calendar/forms.py                       |  79 +++++++
 combo/apps/calendar/migrations/0001_initial.py     |  39 ++++
 combo/apps/calendar/migrations/__init__.py         |   0
 combo/apps/calendar/models.py                      |  67 ++++++
 .../templates/calendar/booking_calendar_cell.html  |  61 +++++
 combo/apps/calendar/templatetags/__init__.py       |   0
 combo/apps/calendar/templatetags/calendar.py       |  29 +++
 combo/apps/calendar/urls.py                        |  23 ++
 combo/apps/calendar/utils.py                       | 226 +++++++++++++++++++
 combo/apps/calendar/views.py                       |  42 ++++
 combo/settings.py                                  |   1 +
 tests/settings.py                                  |   5 +
 tests/test_calendar.py                             | 251 +++++++++++++++++++++
 15 files changed, 855 insertions(+)
 create mode 100644 combo/apps/calendar/README
 create mode 100644 combo/apps/calendar/__init__.py
 create mode 100644 combo/apps/calendar/forms.py
 create mode 100644 combo/apps/calendar/migrations/0001_initial.py
 create mode 100644 combo/apps/calendar/migrations/__init__.py
 create mode 100644 combo/apps/calendar/models.py
 create mode 100644 combo/apps/calendar/templates/calendar/booking_calendar_cell.html
 create mode 100644 combo/apps/calendar/templatetags/__init__.py
 create mode 100644 combo/apps/calendar/templatetags/calendar.py
 create mode 100644 combo/apps/calendar/urls.py
 create mode 100644 combo/apps/calendar/utils.py
 create mode 100644 combo/apps/calendar/views.py
 create mode 100644 tests/test_calendar.py
combo/apps/calendar/README
1
Combo calendar cell
2
===================
3

  
4
To be visible, this cell needs a 'chrono' entry in settings.KNOWN_SERVICES
combo/apps/calendar/__init__.py
1
# combo - content management system
2
# Copyright (C) 2017  Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
import django.apps
18

  
19

  
20
class AppConfig(django.apps.AppConfig):
21
    name = 'combo.apps.calendar'
22

  
23
    def get_before_urls(self):
24
        from . import urls
25
        return urls.urlpatterns
26

  
27

  
28
default_app_config = 'combo.apps.calendar.AppConfig'
combo/apps/calendar/forms.py
1
# combo - content management system
2
# Copyright (C) 2017  Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
import datetime
18

  
19
from django import forms
20
from django.utils.translation import ugettext_lazy as _
21
from django.utils.dateparse import parse_datetime
22

  
23
from .models import BookingCalendar
24
from .utils import get_agendas, get_formdefs
25

  
26

  
27
class BookingCalendarForm(forms.ModelForm):
28

  
29
    class Meta:
30
        model = BookingCalendar
31
        fields = (
32
            'title', 'agenda_reference', 'formdef_reference',
33
            'slot_duration', 'minimal_event_duration')
34

  
35
    def __init__(self, *args, **kwargs):
36
        super(BookingCalendarForm, self).__init__(*args, **kwargs)
37
        agenda_references = get_agendas()
38
        formdef_references = get_formdefs()
39
        self.fields['agenda_reference'].widget = forms.Select(choices=agenda_references)
40
        self.fields['formdef_reference'].widget = forms.Select(choices=formdef_references)
41

  
42

  
43
class BookingForm(forms.Form):
44

  
45
    def __init__(self, *args, **kwargs):
46
        self.cell = kwargs.pop('cell')
47
        super(BookingForm, self).__init__(*args, **kwargs)
48
        self.cleaned_data = {}
49

  
50
    def is_valid(self):
51
        slots = getattr(self.data, 'getlist', lambda x: [])('slots')
52
        # check that at least one slot if selected
53
        if not slots:
54
            raise ValueError(_('Please select slots'))
55
        offset = self.cell.slot_duration.hour * 60 + self.cell.slot_duration.minute
56
        start_dt = parse_datetime(slots[0])
57
        end_dt = parse_datetime(slots[-1]) + datetime.timedelta(minutes=offset)
58
        slots.append(end_dt.isoformat())
59

  
60
        # check that all slots are part of the same day
61
        for slot in slots:
62
            if parse_datetime(slot).date() != start_dt.date():
63
                raise ValueError(_('Please select slots of the same day'))
64

  
65
        # check that slots datetime are contiguous
66
        start = start_dt
67
        while start <= end_dt:
68
            if start.isoformat() not in slots:
69
                raise ValueError(_('Please select contiguous slots'))
70
            start = start + datetime.timedelta(minutes=offset)
71

  
72
        # check that event booking duration >= minimal booking duration
73
        min_duration = self.cell.minimal_event_duration.hour * 60 + self.cell.minimal_event_duration.minute
74
        if not (end_dt - start_dt) >= datetime.timedelta(minutes=min_duration):
75
            raise ValueError(_(
76
                'Minimal booking duration is %s' % self.cell.minimal_event_duration.strftime('%H:%M')))
77
        self.cleaned_data['start'] = start_dt
78
        self.cleaned_data['end'] = end_dt
79
        return True
combo/apps/calendar/migrations/0001_initial.py
1
# -*- coding: utf-8 -*-
2
from __future__ import unicode_literals
3

  
4
from django.db import migrations, models
5
import datetime
6

  
7

  
8
class Migration(migrations.Migration):
9

  
10
    dependencies = [
11
        ('data', '0027_page_picture'),
12
        ('auth', '0006_require_contenttypes_0002'),
13
    ]
14

  
15
    operations = [
16
        migrations.CreateModel(
17
            name='BookingCalendar',
18
            fields=[
19
                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
20
                ('placeholder', models.CharField(max_length=20)),
21
                ('order', models.PositiveIntegerField()),
22
                ('slug', models.SlugField(verbose_name='Slug', blank=True)),
23
                ('extra_css_class', models.CharField(max_length=100, verbose_name='Extra classes for CSS styling', blank=True)),
24
                ('public', models.BooleanField(default=True, verbose_name='Public')),
25
                ('restricted_to_unlogged', models.BooleanField(default=False, verbose_name='Restrict to unlogged users')),
26
                ('last_update_timestamp', models.DateTimeField(auto_now=True)),
27
                ('title', models.CharField(max_length=128, null=True, verbose_name='Title', blank=True)),
28
                ('agenda_reference', models.CharField(max_length=128, verbose_name='Agenda')),
29
                ('formdef_reference', models.CharField(max_length=128, verbose_name='Form')),
30
                ('slot_duration', models.TimeField(default=datetime.time(0, 30), verbose_name='Slot duration')),
31
                ('minimal_event_duration', models.TimeField(default=datetime.time(1, 0), verbose_name='Minimal event duration')),
32
                ('groups', models.ManyToManyField(to='auth.Group', verbose_name='Groups', blank=True)),
33
                ('page', models.ForeignKey(to='data.Page')),
34
            ],
35
            options={
36
                'verbose_name': 'Booking Calendar',
37
            },
38
        ),
39
    ]
combo/apps/calendar/models.py
1
# combo - content management system
2
# Copyright (C) 2017  Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
from django.db import models
18
from django.utils.translation import ugettext_lazy as _
19
from django.utils.dateparse import parse_time
20
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
21

  
22
from combo.data.models import CellBase
23
from combo.data.library import register_cell_class
24
from .utils import is_chrono_enabled, is_wcs_enabled, get_calendar
25

  
26

  
27
@register_cell_class
28
class BookingCalendar(CellBase):
29

  
30
    title = models.CharField(_('Title'), max_length=128, blank=True, null=True)
31
    agenda_reference = models.CharField(_('Agenda'), max_length=128)
32
    formdef_reference = models.CharField(_('Form'), max_length=128)
33
    slot_duration = models.TimeField(
34
        _('Slot duration'), default=parse_time('00:30'))
35
    minimal_event_duration = models.TimeField(
36
        _('Minimal event duration'), default=parse_time('01:00'))
37

  
38
    template_name = 'calendar/booking_calendar_cell.html'
39

  
40
    class Meta:
41
        verbose_name = _('Booking Calendar')
42

  
43
    def get_default_form_class(self):
44
        from .forms import BookingCalendarForm
45
        return BookingCalendarForm
46

  
47
    @classmethod
48
    def is_enabled(cls):
49
        return is_chrono_enabled() and is_wcs_enabled()
50

  
51
    def get_calendar(self):
52
        return get_calendar(self.agenda_reference, self.slot_duration)
53

  
54
    def render(self, context):
55
        request = context['request']
56
        page = request.GET.get('week_%s' % self.pk, 1)
57
        # get calendar
58
        calendar = self.get_calendar()
59
        paginator = Paginator(calendar, 1)
60
        try:
61
            cal_page = paginator.page(page)
62
        except PageNotAnInteger:
63
            cal_page = paginator.page(1)
64
        except (EmptyPage,):
65
            cal_page = paginator.page(paginator.num_pages)
66
        context['calendar'] = cal_page
67
        return super(BookingCalendar, self).render(context)
combo/apps/calendar/templates/calendar/booking_calendar_cell.html
1
{% load i18n calendar %}
2

  
3
<div id="cal-{{cell.pk}}">
4
    {% if cell.title %}
5
    <h2>{{cell.title}}</h2>
6
    {% endif %}
7
    <div>
8
        {% if calendar.has_other_pages %}
9
        <p class="paginator">
10
        {% if calendar.has_previous %}
11
        <a href="?week_{{cell.pk}}={{ calendar.previous_page_number }}#cal-{{cell.pk}}">{% trans "previous week" %}</a>
12
        {% else %}
13
        <span>&lt;&lt;</span>
14
        {% endif %}
15
        &nbsp;
16
        <span class="current">
17
            {{ calendar.number }} / {{ calendar.paginator.num_pages }}
18
        </span>
19
        &nbsp;
20
        {% if calendar.has_next %}
21
        <a href="?week_{{cell.pk}}={{ calendar.next_page_number }}#cal-{{cell.pk}}">{% trans "next week" %}</a>
22
        {% else %}
23
        <span>&gt;&gt;</span>
24
        {% endif %}
25
        {% endif %}
26

  
27
        {% if calendar %}
28
        <form method="POST" action="{% url 'calendar-booking' cell.pk %}">
29
            {% csrf_token %}
30
            <table id="cal-table-{{cell.pk}}">
31
                {% for cal in calendar %}
32
                <thead>
33
                    <tr>
34
                        <th></th>
35
                        {% for day in cal.get_days %}
36
                        <th>{{day|date:"SHORT_DATE_FORMAT"}}</th>
37
                        {% endfor %}
38
                    </tr>
39
                </thead>
40
                <tbody>
41
                    {% for slot in cal.get_slots %}
42
                    <tr>
43
                        <th>{{slot|date:"TIME_FORMAT"}}</th>
44
                        {% for day in cal.get_days %}
45
                        <td>
46
                            {% cal_slot_available cal day=day slot=slot as value %}
47
                                {% if value.available %}
48
                                    <input type="checkbox" name="slots" value="{{value.label}}" id="{{value.label}}"/>
49
                                {% endif %}
50
                        </td>
51
                        {% endfor %}
52
                    </tr>
53
                    {% endfor %}
54
                </tbody>
55
                {% endfor %}
56
            </table>
57
            <input type="submit" value="{% trans "Book" context "booking" %}">
58
        </form>
59
        {% endif %}
60
    </div>
61
</div>
combo/apps/calendar/templatetags/calendar.py
1
# combo - content management system
2
# Copyright (C) 2017  Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
import datetime
18

  
19
from django import template
20

  
21
register = template.Library()
22

  
23

  
24
@register.assignment_tag
25
def cal_slot_available(cal, *args, **kwargs):
26
    day = kwargs.get('day')
27
    slot = kwargs.get('slot')
28
    time_slot = datetime.datetime.combine(day, slot)
29
    return cal.get_availability(time_slot)
combo/apps/calendar/urls.py
1
# combo - content management system
2
# Copyright (C) 2017  Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
from django.conf.urls import url
18

  
19
from .views import BookingView
20

  
21
urlpatterns = [
22
    url(r'^calendar/book/(?P<pk>[\w,-]+)/', BookingView.as_view(), name='calendar-booking'),
23
]
combo/apps/calendar/utils.py
1
# combo - content management system
2
# Copyright (C) 2017 Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
import urllib
18
import datetime
19

  
20
from django.conf import settings
21
from django.utils.dateparse import parse_datetime
22

  
23
from combo.utils import requests
24

  
25

  
26
def get_services(service_name, exclude_secondary=False):
27
    if hasattr(settings, 'KNOWN_SERVICES') and settings.KNOWN_SERVICES.get(service_name):
28
        services = settings.KNOWN_SERVICES[service_name]
29
        if exclude_secondary:
30
            for service, items in services.items():
31
                if not items.get('secondary', True):
32
                    return {service: items}
33
        return services
34
    return {}
35

  
36

  
37
def get_service(service_name, key, exclude_secondary=False):
38
    service = get_services(service_name, exclude_secondary)
39
    return service.get(key, {})
40

  
41

  
42
def is_chrono_enabled():
43
    return bool(get_services('chrono', exclude_secondary=True))
44

  
45

  
46
def is_wcs_enabled():
47
    return bool(get_services('wcs'))
48

  
49

  
50
def get_agendas():
51
    chronos = get_services('chrono', exclude_secondary=True)
52
    references = []
53
    for chrono_key, chrono_site in chronos.items():
54
        response = requests.get('api/agenda/', remote_service=chrono_site, without_user=True)
55
        try:
56
            result = response.json()
57
        except ValueError:
58
            return references
59
        for agenda in result.get('data'):
60
            references.append((
61
                '%s:%s' % (chrono_key, agenda['slug']), agenda['text']))
62
    return references
63

  
64

  
65
def get_formdefs():
66
    wcs = get_services('wcs')
67
    references = []
68
    for wcs_key, wcs_site in wcs.items():
69
        response = requests.get('api/formdefs/', remote_service=wcs_site, without_user=True)
70
        try:
71
            result = response.json()
72
        except ValueError:
73
            return references
74
        if isinstance(result, (dict,)):
75
            if result.get('err'):
76
                continue
77
            result = result.get('data')
78
        for form in result:
79
            references.append((
80
                '%s:%s' % (wcs_key, form['slug']), form['title']))
81
    return references
82

  
83

  
84
def get_chrono_events(agenda_reference, **kwargs):
85
    chrono_key, chrono_slug = agenda_reference.split(':')
86
    chrono = get_service('chrono', chrono_key, exclude_secondary=True)
87
    response = requests.get('api/agenda/%s/datetimes/' % chrono_slug, remote_service=chrono, without_user=True)
88
    try:
89
        result = response.json()
90
    except ValueError:
91
        return {}
92
    return result.get('data', [])
93

  
94

  
95
def get_calendar(agenda_reference, offset):
96
    calendar = {}
97
    events = get_chrono_events(agenda_reference)
98
    for event in events:
99
        event_datetime = parse_datetime(event['datetime'])
100
        week_ref = event_datetime.isocalendar()[1]
101
        if week_ref not in calendar:
102
            calendar[week_ref] = WeekCalendar(week_ref, offset)
103
        week_cal = calendar[week_ref]
104
        # add day to week calendar
105
        if not week_cal.has_day(event_datetime.date()):
106
            day = WeekDay(event_datetime.date())
107
            week_cal.days.append(day)
108
        else:
109
            day = week_cal.get_day(event_datetime.date())
110
        # add slots to day
111
        day.add_slots(DaySlot(
112
            event_datetime, True if not event.get('disabled', True) else False))
113

  
114
    return sorted(calendar.values(), key=lambda x: x.week)
115

  
116

  
117
def get_form_url_with_params(cell, data):
118
    session_vars = {
119
        "session_var_booking_agenda_slug": cell.agenda_reference.split(':')[1],
120
        "session_var_booking_start": data['start'].isoformat(),
121
        "session_var_booking_end": data['end'].isoformat()
122
    }
123
    wcs_key, wcs_slug = cell.formdef_reference.split(':')
124
    wcs = get_service('wcs', key=wcs_key)
125
    response = requests.get('api/formdefs/', remote_service=wcs, without_user=True)
126
    for formdef in response.json():
127
        if formdef['slug'] == wcs_slug:
128
            break
129
    url = '%s/?%s' % (formdef['url'], urllib.urlencode(session_vars))
130
    return url
131

  
132

  
133
class DaySlot(object):
134

  
135
    def __init__(self, date_time, available):
136
        self.date_time = date_time
137
        self.available = available
138

  
139
    def __repr__(self):
140
        return '<DaySlot date_time=%s - available=%s>' % (self.date_time.isoformat(), self.available)
141

  
142
    @property
143
    def label(self):
144
        return '%s' % self.date_time.isoformat()
145

  
146

  
147
class WeekDay(object):
148

  
149
    def __init__(self, date):
150
        self.date = date
151
        self.slots = []
152

  
153
    def __repr__(self):
154
        return '<WeekDay %s >' % self.date.isoformat()
155

  
156
    def add_slots(self, slot):
157
        if slot not in self.slots:
158
            self.slots.append(slot)
159

  
160
    def get_slot(self, slot_time):
161
        for slot in self.slots:
162
            if slot.date_time.time() == slot_time:
163
                return slot
164
        slot_datetime = datetime.datetime.combine(self.date, slot_time)
165
        return DaySlot(slot_datetime, False)
166

  
167
    def get_minimum_slot(self):
168
        return min(self.slots, key=lambda x: x.date_time.time())
169

  
170
    def get_maximum_slot(self):
171
        return max(self.slots, key=lambda x: x.date_time.time())
172

  
173
    def is_available(self, slot_time):
174
        return self.get_slot(slot_time).available
175

  
176

  
177
class WeekCalendar(object):
178

  
179
    def __init__(self, week, offset):
180
        self.week = week
181
        self.offset = offset
182
        self.days = []
183

  
184
    def __repr__(self):
185
        return '<WeekCalendar - %s>' % self.week
186

  
187
    def get_slots(self):
188
        start = self.get_minimum_slot()
189
        end = self.get_maximum_slot()
190
        offset = self.offset.hour * 60 + self.offset.minute
191
        while start <= end:
192
            yield start
193
            start = datetime.datetime.combine(
194
                datetime.date.today(), start) + datetime.timedelta(minutes=offset)
195
            start = start.time()
196

  
197
    def get_days(self):
198
        if self.days:
199
            fday = self.days[0].date
200
        else:
201
            fday = datetime.datetime.today()
202
        # only week days
203
        for index in range(0, 5):
204
            day = fday + datetime.timedelta(days=index - fday.weekday())
205
            yield day
206

  
207
    def get_day(self, date):
208
        for day in self.days:
209
            if day.date == date:
210
                return day
211
        return None
212

  
213
    def has_day(self, date):
214
        return bool(self.get_day(date))
215

  
216
    def get_availability(self, slot):
217
        if not self.has_day(slot.date()):
218
            return DaySlot(slot, False)
219
        day = self.get_day(slot.date())
220
        return day.get_slot(slot.time())
221

  
222
    def get_minimum_slot(self):
223
        return min([day.get_minimum_slot().date_time.time() for day in self.days])
224

  
225
    def get_maximum_slot(self):
226
        return max([day.get_maximum_slot().date_time.time() for day in self.days])
combo/apps/calendar/views.py
1
# combo - content management system
2
# Copyright (C) 2017  Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
from django.http import HttpResponseRedirect
18
from django.views.generic import View
19
from django.views.generic.detail import SingleObjectMixin
20
from django.contrib import messages
21

  
22
from .models import BookingCalendar
23
from .utils import get_form_url_with_params
24
from .forms import BookingForm
25

  
26

  
27
class BookingView(SingleObjectMixin, View):
28

  
29
    http_method_names = ['post']
30
    model = BookingCalendar
31

  
32
    def post(self, request, *args, **kwargs):
33
        cell = self.get_object()
34
        form = BookingForm(request.POST, cell=cell)
35
        try:
36
            form.is_valid()
37
        except ValueError as exc:
38
            messages.error(request, exc.message)
39
            return HttpResponseRedirect(cell.page.get_online_url())
40
        data = form.cleaned_data
41
        url = get_form_url_with_params(cell, data)
42
        return HttpResponseRedirect(url)
combo/settings.py
78 78
    'combo.apps.search',
79 79
    'combo.apps.usersearch',
80 80
    'combo.apps.maps',
81
    'combo.apps.calendar',
81 82
    'haystack',
82 83
    'xstatic.pkg.chartnew_js',
83 84
    'xstatic.pkg.leaflet',
tests/settings.py
13 13
        'default': {'title': 'test', 'url': 'http://example.org',
14 14
                    'secret': 'combo', 'orig': 'combo',
15 15
                    'backoffice-menu-url': 'http://example.org/manage/',}
16
    },
17
    'chrono': {
18
        'default': {'title': 'test', 'url': 'http://chrono.example.org',
19
                    'secret': 'combo', 'orig': 'combo',
20
                    'backoffice-menu-url': 'http://chrono.example.org/manage/',}
16 21
    }
17 22
}
18 23

  
tests/test_calendar.py
1
import json
2
import urlparse
3
import datetime
4

  
5
import pytest
6
import mock
7

  
8
from django.utils.dateparse import parse_time
9
from django.contrib.auth.models import User
10

  
11
from combo.data.models import Page
12
from combo.apps.calendar.models import BookingCalendar
13
from combo.apps.calendar.utils import get_calendar
14
pytestmark = pytest.mark.django_db
15

  
16
CHRONO_EVENTS = {
17
    "data": [
18
        {
19
            "disabled": False,
20
            "text": "13 juin 2017 08:00",
21
            "api": {
22
                "fillslot_url": "http://example.net/api/agenda/whatever/fillslot/86/"
23
            },
24
            "id": 86,
25
            "datetime": "2017-06-13 08:00:00"
26
        },
27
        {
28
            "disabled": False,
29
            "text": "13 juin 2017 08:30",
30
            "api": {
31
                "fillslot_url": "http://example.net/api/agenda/whatever/fillslot/87/"
32
            },
33
            "id": 87,
34
            "datetime": "2017-06-13 08:30:00"
35
        },
36
        {
37
            "disabled": False,
38
            "text": "13 juin 2017 09:00",
39
            "api": {
40
                "fillslot_url": "http://example.net/api/agenda/whatever/fillslot/88/"
41
            },
42
            "id": 88,
43
            "datetime": "2017-06-13 09:00:00"
44
        },
45
        {
46
            "disabled": False,
47
            "text": "13 juin 2017 09:30",
48
            "api": {
49
                "fillslot_url": "http://example.net/api/agenda/whatever/fillslot/89/"
50
            },
51
            "id": 89,
52
            "datetime": "2017-06-13 09:30:00"
53
        },
54
        {
55
            "disabled": False,
56
            "text": "14 juin 2017 09:30",
57
            "api": {
58
                "fillslot_url": "http://example.net/api/agenda/whatever/fillslot/89/"
59
            },
60
            "id": 90,
61
            "datetime": "2017-06-14 09:30:00"
62
        },
63
        {
64
            "disabled": False,
65
            "text": "14 juin 2017 10:00",
66
            "api": {
67
                "fillslot_url": "http://example.net/api/agenda/whatever/fillslot/89/"
68
            },
69
            "id": 91,
70
            "datetime": "2017-06-14 10:00:00"
71
        },
72
        {
73
            "disabled": True,
74
            "text": "14 juin 2017 15:00",
75
            "api": {
76
                "fillslot_url": "http://example.net/api/agenda/whatever/fillslot/89/"
77
            },
78
            "id": 91,
79
            "datetime": "2017-06-14 15:00:00"
80
        },
81
        {
82
            "disabled": True,
83
            "text": "15 juin 2017 10:00",
84
            "api": {
85
                "fillslot_url": "http://example.net/api/agenda/whatever/fillslot/89/"
86
            },
87
            "id": 92,
88
            "datetime": "2017-06-15 10:00:00"
89
        }
90

  
91

  
92
    ]
93
}
94

  
95

  
96
WCS_FORMDEFS = [
97
    {
98
        "count": 12,
99
        "category": "common",
100
        "functions": {
101
            "_receiver": {
102
                "label": "Recipient"
103
            },
104
        },
105
        "authentication_required": False,
106
        "description": "",
107
        "title": "Demande de place en creche",
108
        "url": "http://example.net/demande-de-place-en-creche/",
109
        "category_slug": "common",
110
        "redirection": False,
111
        "keywords": [],
112
        "slug": "demande-de-place-en-creche"
113
    }
114
]
115

  
116

  
117
def login(app, username='admin', password='admin'):
118
    login_page = app.get('/login/')
119
    login_form = login_page.forms[0]
120
    login_form['username'] = username
121
    login_form['password'] = password
122
    resp = login_form.submit()
123
    assert resp.status_int == 302
124
    return app
125

  
126

  
127
def str2datetime(sdt):
128
    return datetime.datetime.strptime(sdt, '%Y-%m-%dT%H:%M:%S')
129

  
130

  
131
class MockedRequestResponse(mock.Mock):
132

  
133
    def json(self):
134
        return json.loads(self.content)
135

  
136

  
137
def mocked_requests_get(*args, **kwargs):
138
    remote_service = kwargs.get('remote_service')
139
    if 'chrono' in remote_service['url']:
140
        return MockedRequestResponse(
141
            content=json.dumps(CHRONO_EVENTS))
142
    else:
143
        return MockedRequestResponse(
144
            content=json.dumps(WCS_FORMDEFS))
145

  
146

  
147
@pytest.fixture
148
def admin(db):
149
    return User.objects.create_superuser(username='admin', password='admin', email=None)
150

  
151

  
152
@pytest.fixture
153
def cell(db):
154
    page = Page.objects.create(title='wjatever', slug='test', template_name='standard')
155
    cell = BookingCalendar(
156
        page=page, title='Example Of Calendar',
157
        agenda_reference='default:test',
158
        formdef_reference='default:test',
159
        slot_duration=parse_time('00:30'),
160
        minimal_event_duration=parse_time('01:00'),
161
        placeholder='content', order=0
162
    )
163
    cell.save()
164
    return cell
165

  
166

  
167
@mock.patch('combo.apps.calendar.utils.requests.get', side_effect=mocked_requests_get)
168
def test_cell_rendering(mocked_get, app, cell):
169
    page = app.get('/test/')
170
    # test without selecting slots
171
    resp = page.form.submit().follow()
172
    assert 'Please select slots' in resp.content
173
    # test with slots from different day
174
    resp.form.set('slots', True, 0)
175
    resp.form.set('slots', True, 1)
176
    resp.form.set('slots', True, 4)
177
    resp = resp.form.submit().follow()
178
    # test with non contiguous slots
179
    assert 'Please select slots of the same day' in resp.content
180
    resp.form.set('slots', True, 0)
181
    resp.form.set('slots', True, 2)
182
    resp = resp.form.submit().follow()
183
    assert 'Please select contiguous slots' in resp.content
184
    # test with invalid booking duration
185
    resp.form.set('slots', True, 0)
186
    resp = resp.form.submit().follow()
187
    assert 'Minimal booking duration is 01:00' in resp.content
188
    # test with valid selected slots
189
    resp.form.set('slots', True, 0)
190
    resp.form.set('slots', True, 1)
191
    resp.form.set('slots', True, 2)
192
    resp = resp.form.submit()
193
    parsed = urlparse.urlparse(resp.url)
194
    qs = urlparse.parse_qs(parsed.query)
195
    assert qs['session_var_booking_agenda_slug'] == ['test']
196
    assert qs['session_var_booking_start'] == ['2017-06-13T08:00:00']
197
    assert qs['session_var_booking_end'] == ['2017-06-13T09:30:00']
198

  
199

  
200
@mock.patch('combo.apps.calendar.utils.requests.get', side_effect=mocked_requests_get)
201
def test_cell_rendering_when_authenticated(mocked_get, app, cell, admin):
202
    app = login(app, username='admin', password='admin')
203
    # test without selecting slots
204
    page = app.get('/test/')
205
    resp = page.form.submit().follow()
206
    assert 'Please select slots' in resp.content
207
    # test with slots from different day
208
    resp.form.set('slots', True, 0)
209
    resp.form.set('slots', True, 1)
210
    resp.form.set('slots', True, 4)
211
    resp = resp.form.submit().follow()
212
    # test with non contiguous slots
213
    assert 'Please select slots of the same day' in resp.content
214
    resp.form.set('slots', True, 0)
215
    resp.form.set('slots', True, 2)
216
    resp = resp.form.submit().follow()
217
    assert 'Please select contiguous slots' in resp.content
218
    # test with invalid booking duration
219
    resp.form.set('slots', True, 0)
220
    resp = resp.form.submit().follow()
221
    assert 'Minimal booking duration is 01:00' in resp.content
222
    # test with valid selected slots
223
    resp.form.set('slots', True, 0)
224
    resp.form.set('slots', True, 1)
225
    resp.form.set('slots', True, 2)
226
    resp = resp.form.submit()
227
    parsed = urlparse.urlparse(resp.url)
228
    qs = urlparse.parse_qs(parsed.query)
229
    assert qs['session_var_booking_agenda_slug'] == ['test']
230
    assert qs['session_var_booking_start'] == ['2017-06-13T08:00:00']
231
    assert qs['session_var_booking_end'] == ['2017-06-13T09:30:00']
232

  
233

  
234

  
235
@mock.patch('combo.apps.calendar.utils.requests.get', side_effect=mocked_requests_get)
236
def test_calendar(mocked_get, cell):
237
    cal = get_calendar('default:whatever', cell.slot_duration)[0]
238
    assert len(cal.days) == 3
239
    for day in cal.get_days():
240
        assert day in [
241
            str2datetime('2017-06-12T08:00:00').date() + datetime.timedelta(days=i)
242
            for i in range(0, 5)]
243
    min_slot = str2datetime('2017-06-13T08:00:00')
244
    max_slot = str2datetime('2017-06-14T15:00:00')
245
    for slot in cal.get_slots():
246
        assert (min_slot.time() <= slot <= max_slot.time()) is True
247
    assert cal.has_day(min_slot.date()) is True
248
    assert cal.get_availability(str2datetime('2017-06-14T15:00:00')).available is False
249
    assert cal.get_minimum_slot() == min_slot.time()
250
    assert cal.get_maximum_slot() == max_slot.time()
251
    assert cal.get_day(max_slot.date()).slots[-1].available is False
0
-