From 86958927287b49bdafaf45b8d7b58f7310a2c9ae Mon Sep 17 00:00:00 2001 From: Josue Kouka Date: Wed, 17 May 2017 18:19:07 +0200 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 | 77 ++++++++++++ .../calendar/templates/calendar/calendar_cell.html | 12 ++ .../calendar/includes/calendar_table.html | 57 +++++++++ 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 | 120 ++++++++++++++++++ combo/apps/calendar/views.py | 58 +++++++++ combo/settings.py | 1 + tests/settings.py | 5 + tests/test_calendar.py | 137 +++++++++++++++++++++ 16 files changed, 687 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 diff --git a/combo/apps/calendar/README b/combo/apps/calendar/README new file mode 100644 index 0000000..9c206de --- /dev/null +++ b/combo/apps/calendar/README @@ -0,0 +1,4 @@ +Combo calendar cell +=================== + +To be visible, this cell needs a 'chrono' entry in settings.KNOWN_SERVICES diff --git a/combo/apps/calendar/__init__.py b/combo/apps/calendar/__init__.py new file mode 100644 index 0000000..fe39ed0 --- /dev/null +++ b/combo/apps/calendar/__init__.py @@ -0,0 +1,28 @@ +# combo - content management system +# Copyright (C) 2017 Entr'ouvert +# +# This program is free software: you can redistribute it and/or modify it +# under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import django.apps + + +class AppConfig(django.apps.AppConfig): + name = 'combo.apps.calendar' + + def get_before_urls(self): + from . import urls + return urls.urlpatterns + + +default_app_config = 'combo.apps.calendar.AppConfig' diff --git a/combo/apps/calendar/forms.py b/combo/apps/calendar/forms.py new file mode 100644 index 0000000..6aefcce --- /dev/null +++ b/combo/apps/calendar/forms.py @@ -0,0 +1,76 @@ +# combo - content management system +# Copyright (C) 2017 Entr'ouvert +# +# This program is free software: you can redistribute it and/or modify it +# under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import datetime + +from django import forms +from django.utils.translation import ugettext_lazy as _ +from django.utils.dateparse import parse_datetime + +from .models import CalendarCell +from .utils import get_agendas, get_formdefs + + +class CalendarCellForm(forms.ModelForm): + + class Meta: + model = CalendarCell + fields = ( + 'title', 'agenda_reference', 'formdef_reference', + 'formdef_url_params', 'slot_duration', 'minimal_event_duration') + + def __init__(self, *args, **kwargs): + super(CalendarCellForm, self).__init__(*args, **kwargs) + agenda_references = get_agendas() + formdef_references = get_formdefs() + self.fields['agenda_reference'].widget = forms.Select(choices=agenda_references) + self.fields['formdef_reference'].widget = forms.Select(choices=formdef_references) + + +class BookingForm(forms.Form): + + def __init__(self, *args, **kwargs): + self.cell = kwargs.pop('cell') + super(BookingForm, self).__init__(*args, **kwargs) + self.cleaned_data = {} + + def is_valid(self): + slots = getattr(self.data, 'getlist', lambda x: [])('slots') + # check that at least one slot if selected + if not slots: + raise ValueError(_('Please select slots')) + offset = self.cell.slot_duration.hour * 60 + self.cell.slot_duration.minute + start_dt = parse_datetime(slots[0]) + end_dt = parse_datetime(slots[-1]) + datetime.timedelta(minutes=offset) + slots.append(end_dt.isoformat()) + # check that all slots are part of the same day + for slot in slots: + if parse_datetime(slot).date() != start_dt.date(): + raise ValueError(_('Please select slots of the same day')) + # check that slots datetime are contiguous + start = start_dt + while start <= end_dt: + if start.isoformat() not in slots: + raise ValueError(_('Please select contiguous slots')) + start = start + datetime.timedelta(minutes=offset) + # check that event booking duration >= minimal booking duration + min_duration = self.cell.minimal_event_duration.hour * 60 + self.cell.minimal_event_duration.minute + if not (end_dt - start_dt) >= datetime.timedelta(minutes=min_duration): + raise ValueError(_( + 'Minimal booking duration is %s' % self.cell.minimal_event_duration.strftime('%H:%M'))) + self.cleaned_data['start'] = start_dt.isoformat() + self.cleaned_data['end'] = end_dt.isoformat() + return True diff --git a/combo/apps/calendar/migrations/0001_initial.py b/combo/apps/calendar/migrations/0001_initial.py new file mode 100644 index 0000000..82afb6e --- /dev/null +++ b/combo/apps/calendar/migrations/0001_initial.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models +import jsonfield.fields +import datetime + + +class Migration(migrations.Migration): + + dependencies = [ + ('data', '0027_page_picture'), + ('auth', '0006_require_contenttypes_0002'), + ] + + operations = [ + migrations.CreateModel( + name='CalendarCell', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('placeholder', models.CharField(max_length=20)), + ('order', models.PositiveIntegerField()), + ('slug', models.SlugField(verbose_name='Slug', blank=True)), + ('extra_css_class', models.CharField(max_length=100, verbose_name='Extra classes for CSS styling', blank=True)), + ('public', models.BooleanField(default=True, verbose_name='Public')), + ('restricted_to_unlogged', models.BooleanField(default=False, verbose_name='Restrict to unlogged users')), + ('last_update_timestamp', models.DateTimeField(auto_now=True)), + ('title', models.CharField(max_length=128, null=True, verbose_name='Title', blank=True)), + ('agenda_reference', models.CharField(max_length=128, verbose_name='Agenda')), + ('formdef_reference', models.CharField(max_length=128, verbose_name='Form')), + ('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')), + ('slot_duration', models.TimeField(default=datetime.time(0, 30), verbose_name='Slot duration')), + ('minimal_event_duration', models.TimeField(default=datetime.time(1, 0), verbose_name='Minimal event duration')), + ('groups', models.ManyToManyField(to='auth.Group', verbose_name='Groups', blank=True)), + ('page', models.ForeignKey(to='data.Page')), + ], + options={ + 'verbose_name': 'Booking Calendar', + }, + ), + ] diff --git a/combo/apps/calendar/migrations/__init__.py b/combo/apps/calendar/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/combo/apps/calendar/models.py b/combo/apps/calendar/models.py new file mode 100644 index 0000000..4bb83a9 --- /dev/null +++ b/combo/apps/calendar/models.py @@ -0,0 +1,77 @@ +# combo - content management system +# Copyright (C) 2017 Entr'ouvert +# +# This program is free software: you can redistribute it and/or modify it +# under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from django.db import models +from django.utils.translation import ugettext_lazy as _ +from django.utils.dateparse import parse_time +from django.utils.text import slugify + +from combo.data.models import CellBase +from combo.data.library import register_cell_class +from .utils import is_chrono_enabled, get_chrono_events + +from jsonfield import JSONField + + +@register_cell_class +class CalendarCell(CellBase): + + title = models.CharField(_('Title'), max_length=128, blank=True, null=True) + agenda_reference = models.CharField(_('Agenda'), max_length=128) + formdef_reference = models.CharField(_('Form'), max_length=128) + formdef_url_params = JSONField(_('Session vars'), + help_text=_("{{start}} will take booking start datetime and {{end}} will take booking end datetime"), + default={"session_var_start": "{{start}}", "session_var_end": "{{end}}"}) + slot_duration = models.TimeField( + _('Slot duration'), default=parse_time('00:30')) + minimal_event_duration = models.TimeField( + _('Minimal event duration'), default=parse_time('01:00')) + + template_name = 'calendar/calendar_cell.html' + + class Meta: + verbose_name = _('Booking Calendar') + + def get_default_form_class(self): + from .forms import CalendarCellForm + return CalendarCellForm + + @classmethod + def is_enabled(cls): + return is_chrono_enabled() + + def save(self, *args, **kwargs): + if not self.slug: + if self.title and self.title != self.slug: + slug = slugify(self.title) + else: + slug = 'cal-1' + index = 1 + while True: + try: + CalendarCell.objects.get(slug=slug) + except self.DoesNotExist: + break + slug = 'cal-%s' % index + index += 1 + self.slug = slug + return super(CalendarCell, self).save(*args, **kwargs) + + def get_calendar(self): + return get_chrono_events(self.agenda_reference) + + def render(self, context): + return super(CalendarCell, self).render(context) diff --git a/combo/apps/calendar/templates/calendar/calendar_cell.html b/combo/apps/calendar/templates/calendar/calendar_cell.html new file mode 100644 index 0000000..2ff68ee --- /dev/null +++ b/combo/apps/calendar/templates/calendar/calendar_cell.html @@ -0,0 +1,12 @@ +{% load i18n calendar %} + +
+ {% if cell.title %} +

{{cell.title}}

+ {% endif %} +
+ {% block calendar_table %} + {% calendar_table cell=cell %} + {% endblock %} +
+
diff --git a/combo/apps/calendar/templates/calendar/includes/calendar_table.html b/combo/apps/calendar/templates/calendar/includes/calendar_table.html new file mode 100644 index 0000000..2129b47 --- /dev/null +++ b/combo/apps/calendar/templates/calendar/includes/calendar_table.html @@ -0,0 +1,57 @@ +{% load i18n %} + +
+ {% if calendar.has_other_pages %} +

+ {% if calendar.has_previous %} + {% trans "previous week" %} + {% else %} + << + {% endif %} +   + + {{ calendar.number }} / {{ calendar.paginator.num_pages }} + +   + {% if calendar.has_next %} + {% trans "next week" %} + {% else %} + >> + {% endif %} + {% endif %} +

+ +
+
+ + + {% if calendar_table_headers %} + + {% for header in calendar_table_headers %} + + {% endfor %} + + {% endif %} + + + {% for week, slots in calendar %} + {% for slot, choices in slots.items %} + + {% for choice in choices %} + {% if choice.disabled %} + + {% else %} + + {% endif %} + {% endfor %} + + {% endfor %} + {% endfor %} + +
{{header}}
+ + +
+ +
+
diff --git a/combo/apps/calendar/templatetags/__init__.py b/combo/apps/calendar/templatetags/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/combo/apps/calendar/templatetags/calendar.py b/combo/apps/calendar/templatetags/calendar.py new file mode 100644 index 0000000..aba2cb8 --- /dev/null +++ b/combo/apps/calendar/templatetags/calendar.py @@ -0,0 +1,47 @@ +# combo - content management system +# Copyright (C) 2017 Entr'ouvert +# +# This program is free software: you can redistribute it and/or modify it +# under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from django import template +from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger + +from combo.apps.calendar.utils import get_calendar + +register = template.Library() + + +@register.inclusion_tag('calendar/includes/calendar_table.html', takes_context=True) +def calendar_table(context, cell): + request = context['request'] + page = request.GET.get('week_%s' % cell.slug, 1) + # get calendar + calendar = get_calendar(cell.get_calendar()) + + paginator = Paginator(tuple(calendar.items()), 1) + try: + calendar = paginator.page(page) + except PageNotAnInteger: + calendar = paginator.page(1) + except (EmptyPage,): + calendar = paginator.page(paginator.num_pages) + + # build calendar tablle headers + if calendar.object_list: + calendar_table_headers = [] + for choice in calendar.object_list[0][1].values()[0]: + calendar_table_headers.append(choice['label']) + context['calendar_table_headers'] = calendar_table_headers + context['calendar'] = calendar + return context diff --git a/combo/apps/calendar/urls.py b/combo/apps/calendar/urls.py new file mode 100644 index 0000000..da5f077 --- /dev/null +++ b/combo/apps/calendar/urls.py @@ -0,0 +1,24 @@ +# combo - content management system +# Copyright (C) 2017 Entr'ouvert +# +# This program is free software: you can redistribute it and/or modify it +# under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from django.conf.urls import url + +from .views import EventsView, BookingView + +urlpatterns = [ + url(r'^api/calendar/events/(?P[\w,-]+)/', EventsView.as_view(), name='calendar-events'), + url(r'^api/calendar/book/(?P[\w,-]+)/', BookingView.as_view(), name='calendar-booking'), +] diff --git a/combo/apps/calendar/utils.py b/combo/apps/calendar/utils.py new file mode 100644 index 0000000..5728b4a --- /dev/null +++ b/combo/apps/calendar/utils.py @@ -0,0 +1,120 @@ +# combo - content management system +# Copyright (C) 2017 Entr'ouvert +# +# This program is free software: you can redistribute it and/or modify it +# under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import json +import urllib +import datetime +import urlparse +from collections import OrderedDict + + +from django.conf import settings +from django.utils.dateparse import parse_datetime +from django.utils import formats +from django.template import Context, Template + +from combo.utils import requests + + +def get_service(service_name, key=None): + if hasattr(settings, 'KNOWN_SERVICES') and settings.KNOWN_SERVICES.get(service_name): + services = settings.KNOWN_SERVICES[service_name] + if key: + return services.get(key) + return services + + +def is_chrono_enabled(): + return bool(get_service('chrono')) + + +def get_agendas(): + chronos = get_service('chrono') + references = [] + for chrono_key, chrono_site in chronos.items(): + url = urlparse.urljoin(chrono_site['url'], 'api/agenda/') + response = requests.get(url, headers={'accept': 'application/json'}) + for agenda in response.json()['data']: + references.append(( + '%s:%s' % (chrono_key, agenda['slug']), agenda['text'])) + return references + + +def get_formdefs(): + wcs = get_service('wcs') + references = [] + for wcs_key, wcs_site in wcs.items(): + url = urlparse.urljoin(wcs_site['url'], 'api/formdefs/') + response = requests.get(url, headers={'accept': 'application/json'}) + data = response.json() + if isinstance(data, (dict,)): + if data.get('err') == 1: + continue + data = data.get('data') + for form in data: + references.append(( + '%s:%s' % (wcs_key, form['slug']), form['title'])) + return references + + +def get_chrono_events(agenda_reference, **kwargs): + chrono_key, chrono_slug = agenda_reference.split(':') + chrono = get_service('chrono', key=chrono_key) + url = urlparse.urljoin(chrono['url'], 'api/agenda/%s/datetimes/' % chrono_slug) + response = requests.get(url, + headers={'accept': 'application/json'}, **kwargs) + return response.json().get('data', []) + + +def get_calendar(events): + calendar = {} + for event in events: + human_day = formats.date_format(parse_datetime(event['datetime']).date()) + event_datetime = parse_datetime(event['datetime']) + event_time = event_datetime.strftime('%H:%M') + # get week ref + week_ref = event_datetime.isocalendar()[1] + if week_ref not in calendar: + calendar[week_ref] = {} + if event_time not in calendar[week_ref]: + calendar[week_ref][event_time] = [] + choice = {} + choice['value'] = event_datetime.isoformat() + choice['label'] = human_day + # add disabled if no more slot available for that day + choice['disabled'] = event.get('disabled', False) + calendar[week_ref][event_time].append(choice) + # sort days + for week in calendar: + calendar[week] = OrderedDict(sorted(calendar[week].items())) + # sort weeks + calendar = OrderedDict(sorted(calendar.items())) + return calendar + + +def get_form_url_with_params(cell, data): + tpl = Template(json.dumps(cell.formdef_url_params)) + session_vars = json.loads(tpl.render(Context(data))) + wcs_key, wcs_slug = cell.formdef_reference.split(':') + wcs = get_service('wcs', key=wcs_key) + wcs_base_url = urlparse.urljoin(wcs['url'], 'api/formsdef/') + response = requests.get(wcs_base_url, headers={'accept': 'application/json'}) + for formdef in response.json(): + if formdef['slug'] == wcs_slug: + break + + url = '%s?%s' % (formdef['url'], urllib.urlencode(session_vars)) + return url diff --git a/combo/apps/calendar/views.py b/combo/apps/calendar/views.py new file mode 100644 index 0000000..8b46f1e --- /dev/null +++ b/combo/apps/calendar/views.py @@ -0,0 +1,58 @@ +# combo - content management system +# Copyright (C) 2017 Entr'ouvert +# +# This program is free software: you can redistribute it and/or modify it +# under the terms of the GNU Affero General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from django.http import HttpResponseRedirect, JsonResponse +from django.views.generic import View +from django.views.generic.detail import SingleObjectMixin +from django.views.decorators.csrf import csrf_exempt +from django.contrib import messages + +from .models import CalendarCell +from .utils import get_chrono_events, get_form_url_with_params +from .forms import BookingForm + + +class EventsView(SingleObjectMixin, View): + + http_method_names = ['get'] + model = CalendarCell + + def get(self, request, *args, **kwargs): + cell = self.get_object() + data = get_chrono_events(cell.agenda_reference) + return JsonResponse(data, safe=False) + + +class BookingView(SingleObjectMixin, View): + + http_method_names = ['post'] + model = CalendarCell + + @csrf_exempt + def dispatch(self, request, *args, **kwargs): + return super(BookingView, self).dispatch(request, *args, **kwargs) + + def post(self, request, *args, **kwargs): + cell = self.get_object() + form = BookingForm(request.POST, cell=cell) + try: + form.is_valid() + except ValueError as exc: + messages.error(request, exc.message) + return HttpResponseRedirect(cell.page.get_online_url()) + data = form.cleaned_data + url = get_form_url_with_params(cell, data) + return HttpResponseRedirect(url) diff --git a/combo/settings.py b/combo/settings.py index 778b6f0..d0c1e5f 100644 --- a/combo/settings.py +++ b/combo/settings.py @@ -78,6 +78,7 @@ INSTALLED_APPS = ( 'combo.apps.search', 'combo.apps.usersearch', 'combo.apps.maps', + 'combo.apps.calendar', 'haystack', 'xstatic.pkg.chartnew_js', 'xstatic.pkg.leaflet', diff --git a/tests/settings.py b/tests/settings.py index 2ebafa2..e7dfae0 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -13,6 +13,11 @@ KNOWN_SERVICES = { 'default': {'title': 'test', 'url': 'http://example.org', 'secret': 'combo', 'orig': 'combo', 'backoffice-menu-url': 'http://example.org/manage/',} + }, + 'chrono': { + 'default': {'title': 'test', 'url': 'http://example.org', + 'secret': 'combo', 'orig': 'combo', + 'backoffice-menu-url': 'http://example.org/manage/',} } } diff --git a/tests/test_calendar.py b/tests/test_calendar.py new file mode 100644 index 0000000..fe6ec4d --- /dev/null +++ b/tests/test_calendar.py @@ -0,0 +1,137 @@ +import json +import urlparse + +import pytest +import mock + +from django.utils.dateparse import parse_time + +from combo.data.models import Page +from combo.apps.calendar.models import CalendarCell + +pytestmark = pytest.mark.django_db + +CHRONO_EVENTS = { + "data": [ + { + "disabled": True, + "text": "19 mai 2017 08:00", + "api": { + "fillslot_url": "http://example.net/api/agenda/whatever/fillslot/86/" + }, + "id": 86, + "datetime": "2017-05-19 08:00:00" + }, + { + "disabled": True, + "text": "19 mai 2017 08:30", + "api": { + "fillslot_url": "http://example.net/api/agenda/whatever/fillslot/87/" + }, + "id": 87, + "datetime": "2017-05-19 08:30:00" + }, + { + "disabled": True, + "text": "19 mai 2017 09:00", + "api": { + "fillslot_url": "http://example.net/api/agenda/whatever/fillslot/88/" + }, + "id": 88, + "datetime": "2017-05-19 09:00:00" + }, + { + "disabled": True, + "text": "19 mai 2017 09:30", + "api": { + "fillslot_url": "http://example.net/api/agenda/whatever/fillslot/89/" + }, + "id": 89, + "datetime": "2017-05-19 09:30:00" + } + ] +} + + +WCS_FORMDEFS = [ + { + "count": 12, + "category": "common", + "functions": { + "_receiver": { + "label": "Recipient" + }, + }, + "authentication_required": False, + "description": "", + "title": "Demande de place en creche", + "url": "http://example.net/demande-de-place-en-creche/", + "category_slug": "common", + "redirection": False, + "keywords": [], + "slug": "demande-de-place-en-creche" + } +] + + +class MockedRequestResponse(mock.Mock): + + def json(self): + return json.loads(self.content) + + +@pytest.fixture +def cell(db): + page = Page.objects.create() + cell = CalendarCell( + page=page, title='Example Of Calendar', order=1, + agenda_reference='default:whatever', + formdef_reference='default:whatever', + formdef_url_params={ + "session_var_start_dt": "{{start}}", "session_var_end_dt": "{{end}}", + "session_var_whatever_slug": "whatever" + }, + slot_duration=parse_time('00:30'), + minimal_event_duration=parse_time('01:00') + ) + cell.save() + return cell + + +@mock.patch('combo.apps.calendar.utils.requests.get') +def test_get_events(mocked_get, app, cell): + mocked_get.return_value = MockedRequestResponse(content=json.dumps(CHRONO_EVENTS)) + resp = app.get('/api/calendar/events/%s/' % cell.pk) + assert len(resp.json) == 4 + + +@mock.patch('combo.apps.calendar.utils.requests.get') +def test_redirection_session_vars(mocked_get, app, cell, settings): + mocked_get.return_value = MockedRequestResponse(content=json.dumps(WCS_FORMDEFS)) + # test with no slots + params = {'slots': []} + resp = app.post('/api/calendar/book/%s/' % cell.pk, params=params, status=302).follow() + assert 'Please select slots' in resp.content + # test with slots from different day + params = {'slots': ['2017-05-19T10:30:00', '2017-05-19T11:00:00', + '2017-05-20T09:00:00']} + resp = app.post('/api/calendar/book/%s/' % cell.pk, params=params, status=302).follow() + assert 'Please select slots of the same day' in resp.content + # test with non contiguous slots + params = {'slots': ['2017-05-19T10:30:00', '2017-05-19T11:00:00', + '2017-05-19T12:00:00']} + resp = app.post('/api/calendar/book/%s/' % cell.pk, params=params, status=302).follow() + assert 'Please select contiguous slots' in resp.content + # test with invalid booking duration + params = {'slots': ['2017-05-19T10:30:00']} + resp = app.post('/api/calendar/book/%s/' % cell.pk, params=params, status=302).follow() + assert 'Minimal booking duration is 01:00' in resp.content + # test with valid selected slots + params = {'slots': ['2017-05-19T10:30:00', '2017-05-19T11:00:00', + '2017-05-19T11:30:00', '2017-05-19T12:00:00']} + resp = app.post('/api/calendar/book/%s/' % cell.pk, params=params, status=302) + parsed = urlparse.urlparse(resp.url) + qs = urlparse.parse_qs(parsed.query) + assert qs['session_var_whatever_slug'] == ['whatever'] + assert qs['session_var_start_dt'] == ['2017-05-19T10:30:00'] + assert qs['session_var_end_dt'] == ['2017-05-19T12:30:00'] -- 2.11.0