Projet

Général

Profil

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

Josué Kouka, 13 juin 2017 20:02

Télécharger (33 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                       |  76 ++++++++
 combo/apps/calendar/migrations/0001_initial.py     |  41 ++++
 combo/apps/calendar/migrations/__init__.py         |   0
 combo/apps/calendar/models.py                      |  60 ++++++
 .../calendar/templates/calendar/calendar_cell.html |  12 ++
 .../calendar/includes/calendar_table.html          |  55 ++++++
 combo/apps/calendar/templatetags/__init__.py       |   0
 combo/apps/calendar/templatetags/calendar.py       |  47 +++++
 combo/apps/calendar/urls.py                        |  24 +++
 combo/apps/calendar/utils.py                       | 210 +++++++++++++++++++++
 combo/apps/calendar/views.py                       |  58 ++++++
 combo/settings.py                                  |   1 +
 tests/settings.py                                  |   5 +
 tests/test_calendar.py                             | 137 ++++++++++++++
 16 files changed, 758 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/calendar_cell.html
 create mode 100644 combo/apps/calendar/templates/calendar/includes/calendar_table.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
            'formdef_url_params', '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
        # check that all slots are part of the same day
60
        for slot in slots:
61
            if parse_datetime(slot).date() != start_dt.date():
62
                raise ValueError(_('Please select slots of the same day'))
63
        # check that slots datetime are contiguous
64
        start = start_dt
65
        while start <= end_dt:
66
            if start.isoformat() not in slots:
67
                raise ValueError(_('Please select contiguous slots'))
68
            start = start + datetime.timedelta(minutes=offset)
69
        # check that event booking duration >= minimal booking duration
70
        min_duration = self.cell.minimal_event_duration.hour * 60 + self.cell.minimal_event_duration.minute
71
        if not (end_dt - start_dt) >= datetime.timedelta(minutes=min_duration):
72
            raise ValueError(_(
73
                'Minimal booking duration is %s' % self.cell.minimal_event_duration.strftime('%H:%M')))
74
        self.cleaned_data['start'] = start_dt.isoformat()
75
        self.cleaned_data['end'] = end_dt.isoformat()
76
        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 jsonfield.fields
6
import datetime
7

  
8

  
9
class Migration(migrations.Migration):
10

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

  
16
    operations = [
17
        migrations.CreateModel(
18
            name='BookingCalendar',
19
            fields=[
20
                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
21
                ('placeholder', models.CharField(max_length=20)),
22
                ('order', models.PositiveIntegerField()),
23
                ('slug', models.SlugField(verbose_name='Slug', blank=True)),
24
                ('extra_css_class', models.CharField(max_length=100, verbose_name='Extra classes for CSS styling', blank=True)),
25
                ('public', models.BooleanField(default=True, verbose_name='Public')),
26
                ('restricted_to_unlogged', models.BooleanField(default=False, verbose_name='Restrict to unlogged users')),
27
                ('last_update_timestamp', models.DateTimeField(auto_now=True)),
28
                ('title', models.CharField(max_length=128, null=True, verbose_name='Title', blank=True)),
29
                ('agenda_reference', models.CharField(max_length=128, verbose_name='Agenda')),
30
                ('formdef_reference', models.CharField(max_length=128, verbose_name='Form')),
31
                ('formdef_url_params', jsonfield.fields.JSONField(default={b'session_var_end': b'{{end}}', b'session_var_start': b'{{start}}'}, help_text='{{start}} will take booking start datetime and {{end}} will take booking end datetime', verbose_name='Session vars')),
32
                ('slot_duration', models.TimeField(default=datetime.time(0, 30), verbose_name='Slot duration')),
33
                ('minimal_event_duration', models.TimeField(default=datetime.time(1, 0), verbose_name='Minimal event duration')),
34
                ('groups', models.ManyToManyField(to='auth.Group', verbose_name='Groups', blank=True)),
35
                ('page', models.ForeignKey(to='data.Page')),
36
            ],
37
            options={
38
                'verbose_name': 'Booking Calendar',
39
            },
40
        ),
41
    ]
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

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

  
25
from jsonfield import JSONField
26

  
27

  
28
@register_cell_class
29
class BookingCalendar(CellBase):
30

  
31
    title = models.CharField(_('Title'), max_length=128, blank=True, null=True)
32
    agenda_reference = models.CharField(_('Agenda'), max_length=128)
33
    formdef_reference = models.CharField(_('Form'), max_length=128)
34
    formdef_url_params = JSONField(
35
        _('Session vars'),
36
        help_text=_("{{start}} will take booking start datetime and {{end}} will take booking end datetime"),
37
        default={"session_var_start": "{{start}}", "session_var_end": "{{end}}"})
38
    slot_duration = models.TimeField(
39
        _('Slot duration'), default=parse_time('00:30'))
40
    minimal_event_duration = models.TimeField(
41
        _('Minimal event duration'), default=parse_time('01:00'))
42

  
43
    template_name = 'calendar/calendar_cell.html'
44

  
45
    class Meta:
46
        verbose_name = _('Booking Calendar')
47

  
48
    def get_default_form_class(self):
49
        from .forms import BookingCalendarForm
50
        return BookingCalendarForm
51

  
52
    @classmethod
53
    def is_enabled(cls):
54
        return is_chrono_enabled() and is_wcs_enabled()
55

  
56
    def get_calendar(self):
57
        return get_calendar(self.agenda_reference, self.slot_duration)
58

  
59
    def render(self, context):
60
        return super(BookingCalendar, self).render(context)
combo/apps/calendar/templates/calendar/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
        {% block calendar_table %}
9
        {% calendar_table cell=cell %}
10
        {% endblock %}
11
    </div>
12
</div>
combo/apps/calendar/templates/calendar/includes/calendar_table.html
1
{% load i18n calendar %}
2

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

  
24
<div id="calendar_{{cell.pk}}" data-calendar="{{cell.pk}}">
25
    <form id="form-cal-{{cell.pk}}" method="POST" action="{% url 'calendar-booking' cell.pk %}">
26
        <table id="cal-table-{{cell.pk}}">
27
            {% for cal in calendar %}
28
            <thead>
29
                <tr>
30
                {% for day in cal.days %}
31
                    <th>{{day.date|date:"SHORT_DATE_FORMAT"}}</th>
32
                {% endfor %}
33
                </tr>
34
            </thead>
35
            <tbody>
36
                {% for slot in cal.get_slots %}
37
                <tr>
38
                    {% for day in cal.days %}
39
                        <td>
40
                            {% if day|available:slot %}
41
                            {% with value=day|get_slot:slot %}
42
                            <input type="checkbox" name="slots" value="{{value.label}}" id="{{value.label}}"/>
43
                            <label for="{{value.label}}">{{slot}}</label>
44
                            {% endwith %}
45
                            {% endif %}
46
                        </td>
47
                    {% endfor %}
48
                </tr>
49
                {% endfor %}
50
            </tbody>
51
            {% endfor %}
52
        </table>
53
        <input type="submit" value="{% trans 'Validate' %}">
54
    </form>
55
</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
from django import template
18
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
19

  
20
register = template.Library()
21

  
22

  
23
@register.inclusion_tag('calendar/includes/calendar_table.html', takes_context=True)
24
def calendar_table(context, cell):
25
    request = context['request']
26
    page = request.GET.get('week_%s' % cell.slug, 1)
27
    # get calendar
28
    calendar = cell.get_calendar()
29
    paginator = Paginator(calendar.values(), 1)
30
    try:
31
        cal_page = paginator.page(page)
32
    except PageNotAnInteger:
33
        cal_page = paginator.page(1)
34
    except (EmptyPage,):
35
        cal_page = paginator.page(paginator.num_pages)
36
    context['calendar'] = cal_page
37
    return context
38

  
39

  
40
@register.filter
41
def available(day, slot):
42
    return day.is_available(slot)
43

  
44

  
45
@register.filter
46
def get_slot(day, slot):
47
    return day.get_slot(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 EventsView, BookingView
20

  
21
urlpatterns = [
22
    url(r'^api/calendar/events/(?P<pk>[\w,-]+)/', EventsView.as_view(), name='calendar-events'),
23
    url(r'^api/calendar/book/(?P<pk>[\w,-]+)/', BookingView.as_view(), name='calendar-booking'),
24
]
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 json
18
import urllib
19
import urlparse
20
import datetime
21

  
22
from django.conf import settings
23
from django.utils.dateparse import parse_datetime
24
from django.template import Context, Template
25

  
26
from combo.utils import requests
27

  
28

  
29
def get_service(service_name, key=None):
30
    if hasattr(settings, 'KNOWN_SERVICES') and settings.KNOWN_SERVICES.get(service_name):
31
        services = settings.KNOWN_SERVICES[service_name]
32
        if key:
33
            return services.get(key)
34
        return services
35

  
36

  
37
def is_chrono_enabled():
38
    return bool(get_service('chrono'))
39

  
40

  
41
def is_wcs_enabled():
42
    return bool(get_service('wcs'))
43

  
44

  
45
def get_agendas():
46
    chronos = get_service('chrono')
47
    references = []
48
    for chrono_key, chrono_site in chronos.items():
49
        url = urlparse.urljoin(chrono_site['url'], 'api/agenda/')
50
        response = requests.get(url, headers={'accept': 'application/json'})
51
        for agenda in response.json()['data']:
52
            references.append((
53
                '%s:%s' % (chrono_key, agenda['slug']), agenda['text']))
54
            return references
55

  
56

  
57
def get_formdefs():
58
    wcs = get_service('wcs')
59
    references = []
60
    for wcs_key, wcs_site in wcs.items():
61
        url = urlparse.urljoin(wcs_site['url'], 'api/formdefs/')
62
        response = requests.get(url, headers={'accept': 'application/json'})
63
        data = response.json()
64
        if isinstance(data, (dict,)):
65
            if data.get('err') == 1:
66
                continue
67
            data = data.get('data')
68
        for form in data:
69
            references.append((
70
                '%s:%s' % (wcs_key, form['slug']), form['title']))
71
            return references
72

  
73

  
74
def get_chrono_events(agenda_reference, **kwargs):
75
    chrono_key, chrono_slug = agenda_reference.split(':')
76
    chrono = get_service('chrono', key=chrono_key)
77
    url = urlparse.urljoin(chrono['url'], 'api/agenda/%s/datetimes/' % chrono_slug)
78
    response = requests.get(url, headers={'accept': 'application/json'}, **kwargs)
79
    return response.json().get('data', [])
80

  
81

  
82
def get_slots_range(start, end, offset):
83
    dstart = start
84
    # offset in minute
85
    offset = offset.hour * 60 + offset.minute
86
    while dstart <= end:
87
        yield dstart.strftime('%H:%M')
88
        dstart = datetime.datetime.combine(
89
            datetime.date.today(), dstart) + datetime.timedelta(minutes=offset)
90
        dstart = dstart.time()
91

  
92

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

  
112

  
113
def get_form_url_with_params(cell, data):
114
    tpl = Template(json.dumps(cell.formdef_url_params))
115
    session_vars = json.loads(tpl.render(Context(data)))
116
    wcs_key, wcs_slug = cell.formdef_reference.split(':')
117
    wcs = get_service('wcs', key=wcs_key)
118
    wcs_base_url = urlparse.urljoin(wcs['url'], 'api/formdefs/')
119
    response = requests.get(wcs_base_url, headers={'accept': 'application/json'})
120
    for formdef in response.json():
121
        if formdef['slug'] == wcs_slug:
122
            break
123

  
124
    url = '%s?%s' % (formdef['url'], urllib.urlencode(session_vars))
125
    return url
126

  
127

  
128
class DaySlot(object):
129

  
130
    def __init__(self, date_time, available):
131
        self.date_time = date_time
132
        self.available = available
133

  
134
    def __repr__(self):
135
        return '%s - %s' % (self.date_time.isoformat(), self.available)
136

  
137
    @property
138
    def label(self):
139
        return self.date_time.isoformat()
140

  
141

  
142
class WeekDay(object):
143

  
144
    def __init__(self, date):
145
        self.date = date
146
        self.slots = []
147

  
148
    def __repr__(self):
149
        return self.date.isoformat()
150

  
151
    def add_slots(self, slot):
152
        if slot not in self.slots:
153
            self.slots.append(slot)
154

  
155
    def get_slot(self, slot_time):
156
        exp = datetime.datetime.strptime(slot_time, '%H:%M')
157
        for slot in self.slots:
158
            if slot.date_time.time() == exp.time():
159
                return slot
160
        return DaySlot(exp, False)
161

  
162
    def get_minimum_slot(self):
163
        return min(self.slots, key=lambda x: x.date_time)
164

  
165
    def get_maximum_slot(self):
166
        return max(self.slots, key=lambda x: x.date_time)
167

  
168
    def is_available(self, slot_time):
169
        return self.get_slot(slot_time).available
170

  
171

  
172
class WeekCalendar(object):
173

  
174
    def __init__(self, year=None, week=None, offset=None):
175
        self.year = year
176
        self.week = week
177
        self.days = []
178
        self.slots = []
179
        self.offset = offset
180

  
181
    def __repr__(self):
182
        return 'Cal - %s' % self.week
183

  
184
    def get_slots(self):
185
        start = self.get_minimum_slot().date_time.time()
186
        end = self.get_maximum_slot().date_time.time()
187
        offset = self.offset.hour * 60 + self.offset.minute
188
        while start <= end:
189
            yield start.strftime('%H:%M')
190
            start = datetime.datetime.combine(
191
                datetime.date.today(), start) + datetime.timedelta(minutes=offset)
192
            start = start.time()
193

  
194
    def has_day(self, date):
195
        return bool([day for day in self.days if day.date == date])
196

  
197
    def get_day(self, date):
198
        for day in self.days:
199
            if day.date == date:
200
                return day
201
        return None
202

  
203
    def get_days(self):
204
        return self.days
205

  
206
    def get_minimum_slot(self):
207
        return min([day.get_minimum_slot() for day in self.days])
208

  
209
    def get_maximum_slot(self):
210
        return max([day.get_maximum_slot() 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, JsonResponse
18
from django.views.generic import View
19
from django.views.generic.detail import SingleObjectMixin
20
from django.views.decorators.csrf import csrf_exempt
21
from django.contrib import messages
22

  
23
from .models import BookingCalendar
24
from .utils import get_chrono_events, get_form_url_with_params
25
from .forms import BookingForm
26

  
27

  
28
class EventsView(SingleObjectMixin, View):
29

  
30
    http_method_names = ['get']
31
    model = BookingCalendar
32

  
33
    def get(self, request, *args, **kwargs):
34
        cell = self.get_object()
35
        data = get_chrono_events(cell.agenda_reference)
36
        return JsonResponse(data, safe=False)
37

  
38

  
39
class BookingView(SingleObjectMixin, View):
40

  
41
    http_method_names = ['post']
42
    model = BookingCalendar
43

  
44
    @csrf_exempt
45
    def dispatch(self, request, *args, **kwargs):
46
        return super(BookingView, self).dispatch(request, *args, **kwargs)
47

  
48
    def post(self, request, *args, **kwargs):
49
        cell = self.get_object()
50
        form = BookingForm(request.POST, cell=cell)
51
        try:
52
            form.is_valid()
53
        except ValueError as exc:
54
            messages.error(request, exc.message)
55
            return HttpResponseRedirect(cell.page.get_online_url())
56
        data = form.cleaned_data
57
        url = get_form_url_with_params(cell, data)
58
        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://example.org',
19
                    'secret': 'combo', 'orig': 'combo',
20
                    'backoffice-menu-url': 'http://example.org/manage/',}
16 21
    }
17 22
}
18 23

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

  
4
import pytest
5
import mock
6

  
7
from django.utils.dateparse import parse_time
8

  
9
from combo.data.models import Page
10
from combo.apps.calendar.models import BookingCalendar
11

  
12
pytestmark = pytest.mark.django_db
13

  
14
CHRONO_EVENTS = {
15
    "data": [
16
        {
17
            "disabled": True,
18
            "text": "19 mai 2017 08:00",
19
            "api": {
20
                "fillslot_url": "http://example.net/api/agenda/whatever/fillslot/86/"
21
            },
22
            "id": 86,
23
            "datetime": "2017-05-19 08:00:00"
24
        },
25
        {
26
            "disabled": True,
27
            "text": "19 mai 2017 08:30",
28
            "api": {
29
                "fillslot_url": "http://example.net/api/agenda/whatever/fillslot/87/"
30
            },
31
            "id": 87,
32
            "datetime": "2017-05-19 08:30:00"
33
        },
34
        {
35
            "disabled": True,
36
            "text": "19 mai 2017 09:00",
37
            "api": {
38
                "fillslot_url": "http://example.net/api/agenda/whatever/fillslot/88/"
39
            },
40
            "id": 88,
41
            "datetime": "2017-05-19 09:00:00"
42
        },
43
        {
44
            "disabled": True,
45
            "text": "19 mai 2017 09:30",
46
            "api": {
47
                "fillslot_url": "http://example.net/api/agenda/whatever/fillslot/89/"
48
            },
49
            "id": 89,
50
            "datetime": "2017-05-19 09:30:00"
51
        }
52
    ]
53
}
54

  
55

  
56
WCS_FORMDEFS = [
57
    {
58
        "count": 12,
59
        "category": "common",
60
        "functions": {
61
            "_receiver": {
62
                "label": "Recipient"
63
            },
64
        },
65
        "authentication_required": False,
66
        "description": "",
67
        "title": "Demande de place en creche",
68
        "url": "http://example.net/demande-de-place-en-creche/",
69
        "category_slug": "common",
70
        "redirection": False,
71
        "keywords": [],
72
        "slug": "demande-de-place-en-creche"
73
    }
74
]
75

  
76

  
77
class MockedRequestResponse(mock.Mock):
78

  
79
    def json(self):
80
        return json.loads(self.content)
81

  
82

  
83
@pytest.fixture
84
def cell(db):
85
    page = Page.objects.create()
86
    cell = BookingCalendar(
87
        page=page, title='Example Of Calendar', order=1,
88
        agenda_reference='default:whatever',
89
        formdef_reference='default:whatever',
90
        formdef_url_params={
91
            "session_var_start_dt": "{{start}}", "session_var_end_dt": "{{end}}",
92
            "session_var_whatever_slug": "whatever"
93
        },
94
        slot_duration=parse_time('00:30'),
95
        minimal_event_duration=parse_time('01:00')
96
    )
97
    cell.save()
98
    return cell
99

  
100

  
101
@mock.patch('combo.apps.calendar.utils.requests.get')
102
def test_get_events(mocked_get, app, cell):
103
    mocked_get.return_value = MockedRequestResponse(content=json.dumps(CHRONO_EVENTS))
104
    resp = app.get('/api/calendar/events/%s/' % cell.pk)
105
    assert len(resp.json) == 4
106

  
107

  
108
@mock.patch('combo.apps.calendar.utils.requests.get')
109
def test_redirection_session_vars(mocked_get, app, cell, settings):
110
    mocked_get.return_value = MockedRequestResponse(content=json.dumps(WCS_FORMDEFS))
111
    # test with no slots
112
    params = {'slots': []}
113
    resp = app.post('/api/calendar/book/%s/' % cell.pk, params=params, status=302).follow()
114
    assert 'Please select slots' in resp.content
115
    # test with slots from different day
116
    params = {'slots': ['2017-05-19T10:30:00', '2017-05-19T11:00:00',
117
                        '2017-05-20T09:00:00']}
118
    resp = app.post('/api/calendar/book/%s/' % cell.pk, params=params, status=302).follow()
119
    assert 'Please select slots of the same day' in resp.content
120
    # test with non contiguous slots
121
    params = {'slots': ['2017-05-19T10:30:00', '2017-05-19T11:00:00',
122
                        '2017-05-19T12:00:00']}
123
    resp = app.post('/api/calendar/book/%s/' % cell.pk, params=params, status=302).follow()
124
    assert 'Please select contiguous slots' in resp.content
125
    # test with invalid booking duration
126
    params = {'slots': ['2017-05-19T10:30:00']}
127
    resp = app.post('/api/calendar/book/%s/' % cell.pk, params=params, status=302).follow()
128
    assert 'Minimal booking duration is 01:00' in resp.content
129
    # test with valid selected slots
130
    params = {'slots': ['2017-05-19T10:30:00', '2017-05-19T11:00:00',
131
                        '2017-05-19T11:30:00', '2017-05-19T12:00:00']}
132
    resp = app.post('/api/calendar/book/%s/' % cell.pk, params=params, status=302)
133
    parsed = urlparse.urlparse(resp.url)
134
    qs = urlparse.parse_qs(parsed.query)
135
    assert qs['session_var_whatever_slug'] == ['whatever']
136
    assert qs['session_var_start_dt'] == ['2017-05-19T10:30:00']
137
    assert qs['session_var_end_dt'] == ['2017-05-19T12:30:00']
0
-