Projet

Général

Profil

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

Josué Kouka, 15 juin 2017 10:31

Télécharger (33,7 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     |  41 ++++
 combo/apps/calendar/migrations/__init__.py         |   0
 combo/apps/calendar/models.py                      |  73 +++++++
 .../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                             | 192 +++++++++++++++++
 15 files changed, 804 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
            '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

  
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 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={}, help_text="i.e {'session_var_toto' : 'toto'}", 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
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
from jsonfield import JSONField
27

  
28

  
29
@register_cell_class
30
class BookingCalendar(CellBase):
31

  
32
    title = models.CharField(_('Title'), max_length=128, blank=True, null=True)
33
    agenda_reference = models.CharField(_('Agenda'), max_length=128)
34
    formdef_reference = models.CharField(_('Form'), max_length=128)
35
    formdef_url_params = JSONField(
36
        _('Session vars'),
37
        help_text=_("i.e {'session_var_toto' : 'toto'}"),
38
        default={})
39
    slot_duration = models.TimeField(
40
        _('Slot duration'), default=parse_time('00:30'))
41
    minimal_event_duration = models.TimeField(
42
        _('Minimal event duration'), default=parse_time('01:00'))
43

  
44
    template_name = 'calendar/booking_calendar_cell.html'
45

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

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

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

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

  
60
    def render(self, context):
61
        request = context['request']
62
        page = request.GET.get('week_%s' % self.pk, 1)
63
        # get calendar
64
        calendar = self.get_calendar()
65
        paginator = Paginator(calendar, 1)
66
        try:
67
            cal_page = paginator.page(page)
68
        except PageNotAnInteger:
69
            cal_page = paginator.page(1)
70
        except (EmptyPage,):
71
            cal_page = paginator.page(paginator.num_pages)
72
        context['calendar'] = cal_page
73
        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
    </div>
27

  
28
    <div>
29
        <form method="POST" action="{% url 'calendar-booking' cell.pk %}">
30
            {% csrf_token %}
31
            <table id="cal-table-{{cell.pk}}">
32
                {% for cal in calendar %}
33
                <thead>
34
                    <tr>
35
                        <th></th>
36
                        {% for day in cal.get_days %}
37
                        <th>{{day|date:"SHORT_DATE_FORMAT"}}</th>
38
                        {% endfor %}
39
                    </tr>
40
                </thead>
41
                <tbody>
42
                    {% for slot in cal.get_slots %}
43
                    <tr>
44
                        <th>{{slot|date:"TIME_FORMAT"}}</th>
45
                        {% for day in cal.get_days %}
46
                        <td>
47
                            {% cal_slot_available cal day=day slot=slot as value %}
48
                                {% if value.available %}
49
                                    <input type="checkbox" name="slots" value="{{value.label}}" id="{{value.label}}"/>
50
                                {% endif %}
51
                        </td>
52
                        {% endfor %}
53
                    </tr>
54
                    {% endfor %}
55
                </tbody>
56
                {% endfor %}
57
            </table>
58
            <input type="submit" value="{% trans "Book" %}">
59
        </form>
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 json
18
import urllib
19
import datetime
20

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

  
25
from combo.utils import requests
26

  
27

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

  
33

  
34
def get_service(service_name, key):
35
    service = get_services(service_name)
36
    return service.get(key, {})
37

  
38

  
39
def is_chrono_enabled():
40
    return bool(get_services('chrono'))
41

  
42

  
43
def is_wcs_enabled():
44
    return bool(get_services('wcs'))
45

  
46

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

  
61

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

  
80

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

  
91

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

  
111
    return sorted(calendar.values(), key=lambda x: x.week)
112

  
113

  
114
def get_form_url_with_params(cell, data):
115
    tpl = Template(json.dumps(cell.formdef_url_params))
116
    session_vars = {
117
        "session_var_booking_agenda_slug": cell.agenda_reference.split(':')[1],
118
        "session_var_booking_start": data['start'].isoformat(),
119
        "session_var_booking_end": data['end'].isoformat()
120
    }
121
    session_vars.update(json.loads(tpl.render(Context(data))))
122
    wcs_key, wcs_slug = cell.formdef_reference.split(':')
123
    wcs = get_service('wcs', key=wcs_key)
124
    response = requests.get('api/formdefs/', remote_service=wcs)
125
    for formdef in response.json():
126
        if formdef['slug'] == wcs_slug:
127
            break
128
    url = '%s/?%s' % (formdef['url'], urllib.urlencode(session_vars))
129
    print(url)
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)
169

  
170
    def get_maximum_slot(self):
171
        return max(self.slots, key=lambda x: x.date_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().time()
189
        end = self.get_maximum_slot().time()
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 for day in self.days])
224

  
225
    def get_maximum_slot(self):
226
        return max([day.get_maximum_slot().date_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

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

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

  
81
    ]
82
}
83

  
84

  
85
WCS_FORMDEFS = [
86
    {
87
        "count": 12,
88
        "category": "common",
89
        "functions": {
90
            "_receiver": {
91
                "label": "Recipient"
92
            },
93
        },
94
        "authentication_required": False,
95
        "description": "",
96
        "title": "Demande de place en creche",
97
        "url": "http://example.net/demande-de-place-en-creche/",
98
        "category_slug": "common",
99
        "redirection": False,
100
        "keywords": [],
101
        "slug": "demande-de-place-en-creche"
102
    }
103
]
104

  
105

  
106
def str2datetime(sdt):
107
    return datetime.datetime.strptime(sdt, '%Y-%m-%dT%H:%M:%S')
108

  
109

  
110
class MockedRequestResponse(mock.Mock):
111

  
112
    def json(self):
113
        return json.loads(self.content)
114

  
115

  
116
def mocked_requests_get(*args, **kwargs):
117
    remote_service = kwargs.get('remote_service')
118
    if 'chrono' in remote_service['url']:
119
        return MockedRequestResponse(
120
            content=json.dumps(CHRONO_EVENTS))
121
    else:
122
        return MockedRequestResponse(
123
            content=json.dumps(WCS_FORMDEFS))
124

  
125

  
126
@pytest.fixture
127
def cell(db):
128
    page = Page.objects.create(title='wjatever', slug='test', template_name='standard')
129
    cell = BookingCalendar(
130
        page=page, title='Example Of Calendar',
131
        agenda_reference='default:test',
132
        formdef_reference='default:test',
133
        formdef_url_params={"session_var_whatever": "whatever"},
134
        slot_duration=parse_time('00:30'),
135
        minimal_event_duration=parse_time('01:00'),
136
        placeholder='content', order=0
137
    )
138
    cell.save()
139
    return cell
140

  
141

  
142
@mock.patch('combo.apps.calendar.utils.requests.get', side_effect=mocked_requests_get)
143
def test_cell_rendering(mocked_get, app, cell):
144
    page = app.get('/test/')
145
    # test without selecting slots
146
    resp = page.form.submit().follow()
147
    assert 'Please select slots' in resp.content
148
    # test with slots from different day
149
    resp.form.set('slots', True, 0)
150
    resp.form.set('slots', True, 1)
151
    resp.form.set('slots', True, 4)
152
    resp = resp.form.submit().follow()
153
    # test with non contiguous slots
154
    assert 'Please select slots of the same day' in resp.content
155
    resp.form.set('slots', True, 0)
156
    resp.form.set('slots', True, 2)
157
    resp = resp.form.submit().follow()
158
    assert 'Please select contiguous slots' in resp.content
159
    # test with invalid booking duration
160
    resp.form.set('slots', True, 0)
161
    resp = resp.form.submit().follow()
162
    assert 'Minimal booking duration is 01:00' in resp.content
163
    # test with valid selected slots
164
    resp.form.set('slots', True, 0)
165
    resp.form.set('slots', True, 1)
166
    resp.form.set('slots', True, 2)
167
    resp = resp.form.submit()
168
    parsed = urlparse.urlparse(resp.url)
169
    qs = urlparse.parse_qs(parsed.query)
170
    assert qs['session_var_booking_agenda_slug'] == ['test']
171
    assert qs['session_var_booking_start'] == ['2017-06-13T08:00:00']
172
    assert qs['session_var_booking_end'] == ['2017-06-13T09:30:00']
173
    assert qs['session_var_whatever'] == ['whatever']
174

  
175

  
176
@mock.patch('combo.apps.calendar.utils.requests.get', side_effect=mocked_requests_get)
177
def test_calendar(mocked_get, cell):
178
    cal = get_calendar('default:whatever', cell.slot_duration)[0]
179
    assert len(cal.days) == 2
180
    for day in cal.get_days():
181
        assert day in [
182
            str2datetime('2017-06-12T08:00:00').date() + datetime.timedelta(days=i)
183
            for i in range(0, 5)]
184
    min_slot = str2datetime('2017-06-13T08:00:00')
185
    max_slot = str2datetime('2017-06-14T15:00:00')
186
    for slot in cal.get_slots():
187
        assert (min_slot.time() <= slot <= max_slot.time()) is True
188
    assert cal.has_day(min_slot.date()) is True
189
    assert cal.get_availability(str2datetime('2017-06-14T15:00:00')).available is False
190
    assert cal.get_minimum_slot().time() == min_slot.time()
191
    assert cal.get_maximum_slot().time() == max_slot.time()
192
    assert cal.get_day(max_slot.date()).slots[-1].available is False
0
-