Projet

Général

Profil

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

Josué Kouka, 08 juin 2017 06:39

Télécharger (31,4 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                      |  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
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 CalendarCell
24
from .utils import get_agendas, get_formdefs
25

  
26

  
27
class CalendarCellForm(forms.ModelForm):
28

  
29
    class Meta:
30
        model = CalendarCell
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(CalendarCellForm, 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='CalendarCell',
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
from django.utils.text import slugify
21

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

  
26
from jsonfield import JSONField
27

  
28

  
29
@register_cell_class
30
class CalendarCell(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(_('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 CalendarCellForm
50
        return CalendarCellForm
51

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

  
56
    def save(self, *args, **kwargs):
57
        if not self.slug:
58
            if self.title and self.title != self.slug:
59
                slug = slugify(self.title)
60
            else:
61
                slug = 'cal-1'
62
            index = 1
63
            while True:
64
                try:
65
                    CalendarCell.objects.get(slug=slug)
66
                except self.DoesNotExist:
67
                    break
68
                slug = 'cal-%s' % index
69
                index += 1
70
            self.slug = slug
71
        return super(CalendarCell, self).save(*args, **kwargs)
72

  
73
    def get_calendar(self):
74
        return get_chrono_events(self.agenda_reference)
75

  
76
    def render(self, context):
77
        return super(CalendarCell, 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 %}
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
            <thead>
28
                {% if calendar_table_headers %}
29
                <tr>
30
                {% for header in calendar_table_headers %}
31
                    <td>{{header}}</td>
32
                {% endfor %}
33
                </tr>
34
                {% endif %}
35
            </thead>
36
            <tbody>
37
                {% for week, slots in calendar %}
38
                {% for slot, choices in slots.items %}
39
                <tr>
40
                    {% for choice in choices %}
41
                    {% if choice.disabled %}
42
                    <td><label>{% trans "Unavailable" %}</label></td>
43
                    {% else %}
44
                    <td>
45
                        <input id="{{week}}_{{choice.value}}" name="slots" value="{{choice.value}}" type="checkbox">
46
                        <label for="{{week}}_{{choice.value}}">{{slot}}</label>
47
                    </td>
48
                    {% endif %}
49
                    {% endfor %}
50
                </tr>
51
                {% endfor %}
52
                {% endfor %}
53
            </tbody>
54
        </table>
55
        <input type="submit" value="{% trans 'Validate' %}">
56
    </form>
57
</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
from combo.apps.calendar.utils import get_calendar
21

  
22
register = template.Library()
23

  
24

  
25
@register.inclusion_tag('calendar/includes/calendar_table.html', takes_context=True)
26
def calendar_table(context, cell):
27
    request = context['request']
28
    page = request.GET.get('week_%s' % cell.slug, 1)
29
    # get calendar
30
    calendar = get_calendar(cell.get_calendar())
31

  
32
    paginator = Paginator(tuple(calendar.items()), 1)
33
    try:
34
        calendar = paginator.page(page)
35
    except PageNotAnInteger:
36
        calendar = paginator.page(1)
37
    except (EmptyPage,):
38
        calendar = paginator.page(paginator.num_pages)
39

  
40
    # build calendar tablle headers
41
    if calendar.object_list:
42
        calendar_table_headers = []
43
        for choice in calendar.object_list[0][1].values()[0]:
44
            calendar_table_headers.append(choice['label'])
45
        context['calendar_table_headers'] = calendar_table_headers
46
    context['calendar'] = calendar
47
    return context
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 datetime
20
import urlparse
21
from collections import OrderedDict
22

  
23

  
24
from django.conf import settings
25
from django.utils.dateparse import parse_datetime
26
from django.utils import formats
27
from django.template import Context, Template
28

  
29
from combo.utils import requests
30

  
31

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

  
39

  
40
def is_chrono_enabled():
41
    return bool(get_service('chrono'))
42

  
43

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

  
55

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

  
72

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

  
81

  
82
def get_calendar(events):
83
    calendar = {}
84
    for event in events:
85
        human_day = formats.date_format(parse_datetime(event['datetime']).date())
86
        event_datetime = parse_datetime(event['datetime'])
87
        event_time = event_datetime.strftime('%H:%M')
88
        # get week ref
89
        week_ref = event_datetime.isocalendar()[1]
90
        if week_ref not in calendar:
91
            calendar[week_ref] = {}
92
        if event_time not in calendar[week_ref]:
93
            calendar[week_ref][event_time] = []
94
        choice = {}
95
        choice['value'] = event_datetime.isoformat()
96
        choice['label'] = human_day
97
        # add disabled if no more slot available for that day
98
        choice['disabled'] = event.get('disabled', False)
99
        calendar[week_ref][event_time].append(choice)
100
    # sort days
101
    for week in calendar:
102
        calendar[week] = OrderedDict(sorted(calendar[week].items()))
103
    # sort weeks
104
    calendar = OrderedDict(sorted(calendar.items()))
105
    return calendar
106

  
107

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

  
119
    url = '%s?%s' % (formdef['url'], urllib.urlencode(session_vars))
120
    return url
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 CalendarCell
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 = CalendarCell
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 = CalendarCell
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 CalendarCell
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 = CalendarCell(
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
-