Projet

Général

Profil

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

Josué Kouka, 17 juin 2017 16:04

Télécharger (34,8 ko)

Voir les différences:

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

 combo/apps/calendar/README                         |   4 +
 combo/apps/calendar/__init__.py                    |  28 +++
 combo/apps/calendar/forms.py                       |  79 +++++++
 combo/apps/calendar/migrations/0001_initial.py     |  39 ++++
 combo/apps/calendar/migrations/__init__.py         |   0
 combo/apps/calendar/models.py                      |  67 ++++++
 .../templates/calendar/booking_calendar_cell.html  |  65 ++++++
 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                       | 208 ++++++++++++++++++
 combo/apps/calendar/views.py                       |  44 ++++
 combo/settings.py                                  |   1 +
 tests/settings.py                                  |  14 ++
 tests/test_calendar.py                             | 239 +++++++++++++++++++++
 15 files changed, 840 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
from django import forms
18
from django.utils.translation import ugettext_lazy as _
19
from django.utils.dateparse import parse_datetime, parse_time
20

  
21
from .models import BookingCalendar
22
from .utils import get_agendas
23
from combo.apps.wcs.utils import get_wcs_options
24

  
25

  
26
class BookingCalendarForm(forms.ModelForm):
27

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

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

  
41

  
42
class BookingForm(forms.Form):
43

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

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

  
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

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

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

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

  
7

  
8
class Migration(migrations.Migration):
9

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

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

  
17
import datetime
18

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

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

  
27

  
28
@register_cell_class
29
class BookingCalendar(CellBase):
30

  
31
    title = models.CharField(_('Title'), max_length=128, blank=True, null=True)
32
    agenda_reference = models.CharField(_('Agenda'), max_length=128)
33
    formdef_reference = models.CharField(_('Form'), max_length=128)
34
    slot_duration = models.DurationField(
35
        _('Slot duration'), default=datetime.timedelta(minutes=30),
36
        help_text=_('Format is hours:minutes:seconds'))
37
    minimal_booking_duration = models.DurationField(
38
        _('Minimal booking duration'), default=datetime.timedelta(hours=1),
39
        help_text=_('Format is hours:minutes:seconds'))
40

  
41
    template_name = 'calendar/booking_calendar_cell.html'
42

  
43
    class Meta:
44
        verbose_name = _('Booking Calendar')
45

  
46
    def get_default_form_class(self):
47
        from .forms import BookingCalendarForm
48
        return BookingCalendarForm
49

  
50
    @classmethod
51
    def is_enabled(cls):
52
        return is_chrono_enabled() and is_wcs_enabled()
53

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

  
3
<div id="cal-{{cell.id}}">
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 class="previous" href="?week_{{cell.id}}={{ calendar.previous_page_number }}">{% trans "previous week" %}</a>
12
            {% else %}
13
                <span class="previous">{% trans "previous week" %}</span>
14
            {% endif %}
15
            <span class="current">
16
                {{ calendar.number }} / {{ calendar.paginator.num_pages }}
17
            </span>
18
            {% if calendar.has_next %}
19
                <a class="next" href="?week_{{cell.id}}={{ calendar.next_page_number }}">{% trans "next week" %}</a>
20
            {% else %}
21
                <span class="next">{% trans "next week" %}</span>
22
            {% endif %}
23
            </p>
24
        {% endif %}
25

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

  
17
from django.conf.urls import url
18

  
19
from .views import BookingView
20

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

  
17
import urllib
18
import urlparse
19
import datetime
20

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

  
24
from combo.utils import requests
25

  
26

  
27
def get_services(service_name):
28
    if hasattr(settings, 'KNOWN_SERVICES') and settings.KNOWN_SERVICES.get(service_name):
29
        return settings.KNOWN_SERVICES[service_name]
30
    return {}
31

  
32

  
33
def get_wcs_services():
34
    return get_services('wcs')
35

  
36

  
37
def get_chrono_service():
38
    for chrono_key, chrono_site in get_services('chrono').iteritems():
39
        if not chrono_site.get('secondary', True):
40
            chrono_site['slug'] = chrono_key
41
            return chrono_site
42
    return {}
43

  
44

  
45
def is_chrono_enabled():
46
    return bool(get_chrono_service())
47

  
48

  
49
def is_wcs_enabled():
50
    return bool(get_wcs_services())
51

  
52

  
53
def get_agendas():
54
    chrono = get_chrono_service()
55
    references = []
56
    response = requests.get('api/agenda/', remote_service=chrono, without_user=True)
57
    try:
58
        result = response.json()
59
    except ValueError:
60
        return references
61
    for agenda in result.get('data'):
62
        references.append((
63
            '%s:%s' % (chrono['slug'], agenda['slug']), agenda['text']))
64
    return references
65

  
66

  
67
def get_chrono_events(agenda_reference):
68
    chrono_key, chrono_slug = agenda_reference.split(':')
69
    chrono = get_chrono_service()
70
    response = requests.get('api/agenda/%s/datetimes/' % chrono_slug, remote_service=chrono, without_user=True)
71
    try:
72
        result = response.json()
73
    except ValueError:
74
        return []
75
    return result.get('data', [])
76

  
77

  
78
def get_calendar(agenda_reference, offset):
79
    if not agenda_reference:
80
        return []
81
    events = get_chrono_events(agenda_reference)
82
    calendar = {}
83
    for event in events:
84
        event_datetime = parse_datetime(event['datetime'])
85
        week_ref = event_datetime.isocalendar()[1]
86
        if week_ref not in calendar:
87
            calendar[week_ref] = WeekCalendar(week_ref, offset)
88
        week_cal = calendar[week_ref]
89
        # add day to week calendar
90
        if not week_cal.has_day(event_datetime.date()):
91
            day = WeekDay(event_datetime.date())
92
            week_cal.days.append(day)
93
        else:
94
            day = week_cal.get_day(event_datetime.date())
95
        # add slots to day
96
        day.add_slots(DaySlot(
97
            event_datetime, True if not event.get('disabled', True) else False))
98

  
99
    return sorted(calendar.values(), key=lambda x: x.week)
100

  
101

  
102
def get_form_url_with_params(cell, data):
103
    session_vars = {
104
        "session_var_booking_agenda_slug": cell.agenda_reference.split(':')[1],
105
        "session_var_booking_start": data['start'].isoformat(),
106
        "session_var_booking_end": data['end'].isoformat()
107
    }
108
    wcs_key, wcs_slug = cell.formdef_reference.split(':')
109
    wcs = get_wcs_services().get(wcs_key)
110
    url = urlparse.urljoin(wcs['url'], wcs_slug)
111
    url += '/?%s' % urllib.urlencode(session_vars)
112
    return url
113

  
114

  
115
class DaySlot(object):
116

  
117
    def __init__(self, date_time, available, exist=True):
118
        self.date_time = date_time
119
        self.available = available
120
        self.exist = exist
121

  
122
    def __repr__(self):
123
        return '<DaySlot date_time=%s - available=%s>' % (self.date_time.isoformat(), self.available)
124

  
125
    @property
126
    def label(self):
127
        return '%s' % self.date_time.isoformat()
128

  
129

  
130
class WeekDay(object):
131

  
132
    def __init__(self, date):
133
        self.date = date
134
        self.slots = []
135

  
136
    def __repr__(self):
137
        return '<WeekDay %s >' % self.date.isoformat()
138

  
139
    def add_slots(self, slot):
140
        if slot not in self.slots:
141
            self.slots.append(slot)
142

  
143
    def get_slot(self, slot_time):
144
        for slot in self.slots:
145
            if slot.date_time.time() == slot_time:
146
                return slot
147
        slot_datetime = datetime.datetime.combine(self.date, slot_time)
148
        return DaySlot(slot_datetime, False, exist=False)
149

  
150
    def get_minimum_slot(self):
151
        return min(self.slots, key=lambda x: x.date_time.time())
152

  
153
    def get_maximum_slot(self):
154
        return max(self.slots, key=lambda x: x.date_time.time())
155

  
156
    def is_available(self, slot_time):
157
        return self.get_slot(slot_time).available
158

  
159

  
160
class WeekCalendar(object):
161

  
162
    def __init__(self, week, offset):
163
        self.week = week
164
        self.offset = offset
165
        self.days = []
166

  
167
    def __repr__(self):
168
        return '<WeekCalendar - %s>' % self.week
169

  
170
    def get_slots(self):
171
        start = self.get_minimum_slot()
172
        end = self.get_maximum_slot()
173
        while start <= end:
174
            yield start
175
            start = datetime.datetime.combine(
176
                datetime.date.today(), start) + self.offset
177
            start = start.time()
178

  
179
    def get_days(self):
180
        if self.days:
181
            base_day = self.days[0].date
182
        else:
183
            base_day = datetime.datetime.today()
184
        # only week days
185
        for index in range(0, 5):
186
            day = base_day + datetime.timedelta(days=index - base_day.weekday())
187
            yield day
188

  
189
    def get_day(self, date):
190
        for day in self.days:
191
            if day.date == date:
192
                return day
193
        return None
194

  
195
    def has_day(self, date):
196
        return bool(self.get_day(date))
197

  
198
    def get_availability(self, slot):
199
        if not self.has_day(slot.date()):
200
            return DaySlot(slot, False, exist=False)
201
        day = self.get_day(slot.date())
202
        return day.get_slot(slot.time())
203

  
204
    def get_minimum_slot(self):
205
        return min([day.get_minimum_slot().date_time.time() for day in self.days])
206

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

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

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

  
26

  
27
class BookingView(SingleObjectMixin, View):
28

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

  
32
    def post(self, request, *args, **kwargs):
33
        cell = self.get_object()
34
        form = BookingForm(request.POST, cell=cell)
35
        try:
36
            form.is_valid()
37
        except ValueError as exc:
38
            messages.error(request, exc.message)
39
            redirect_url = '%s?%s' % (
40
                cell.page.get_online_url(), request.GET.urlencode())
41
            return HttpResponseRedirect(redirect_url)
42
        data = form.cleaned_data
43
        url = get_form_url_with_params(cell, data)
44
        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': {
19
            'title': 'test', 'url': 'http://chrono.example.org',
20
            'secret': 'combo', 'orig': 'combo',
21
            'backoffice-menu-url': 'http://chrono.example.org/manage/',
22
            'secondary': False,
23
        },
24
        'other': {
25
            'title': 'other', 'url': 'http://other.chrono.example.org',
26
            'secret': 'combo', 'orig': 'combo',
27
            'backoffice-menu-url': 'http://other.chrono.example.org/manage/',
28
            'secondary': True,
29
        }
16 30
    }
17 31
}
18 32

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

  
5
import pytest
6
import mock
7

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

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

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

  
91

  
92
    ]
93
}
94

  
95

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

  
116

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

  
126

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

  
130

  
131
class MockedRequestResponse(mock.Mock):
132

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

  
136

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

  
146

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

  
151

  
152
@pytest.fixture
153
def anonymous(app):
154
    return app
155

  
156

  
157
@pytest.fixture
158
def connected(app, admin):
159
    return login(app)
160

  
161

  
162
@pytest.fixture(params=['anonymous', 'connected'])
163
def client(request, anonymous, connected):
164
    return locals().get(request.param)
165

  
166

  
167
@pytest.fixture
168
def cell(db):
169
    page = Page.objects.create(title='whatever', slug='booking', template_name='standard')
170
    cell = BookingCalendar(
171
        page=page, title='Example Of Calendar',
172
        agenda_reference='default:test',
173
        formdef_reference='default:test',
174
        slot_duration=datetime.timedelta(minutes=30),
175
        minimal_booking_duration=datetime.timedelta(hours=1),
176
        placeholder='content', order=0
177
    )
178
    cell.save()
179
    return cell
180

  
181

  
182
def test_get_chrono_service(settings):
183
    service = get_chrono_service()
184
    assert service['title'] == 'test'
185
    assert service['url'] == 'http://chrono.example.org'
186
    assert service['secondary'] is False
187

  
188

  
189
@mock.patch('combo.apps.calendar.utils.requests.get', side_effect=mocked_requests_get)
190
def test_cell_rendering(mocked_get, client, cell):
191
    page = client.get('/booking/')
192
    # test without selecting slots
193
    resp = page.form.submit().follow()
194
    assert 'Please select slots' in resp.content
195
    # test with slots from different day
196
    resp.form.set('slots', True, 0)
197
    resp.form.set('slots', True, 1)
198
    resp.form.set('slots', True, 4)
199
    resp = resp.form.submit().follow()
200
    # test with non contiguous slots
201
    assert 'Please select slots of the same day' in resp.content
202
    resp.form.set('slots', True, 0)
203
    resp.form.set('slots', True, 2)
204
    resp = resp.form.submit().follow()
205
    assert 'Please select contiguous slots' in resp.content
206
    # test with invalid booking duration
207
    resp.form.set('slots', True, 0)
208
    resp = resp.form.submit().follow()
209
    assert 'Minimal booking duration is 01:00' in resp.content
210
    # test with valid selected slots
211
    resp.form.set('slots', True, 0)
212
    resp.form.set('slots', True, 1)
213
    resp.form.set('slots', True, 2)
214
    resp = resp.form.submit()
215
    parsed = urlparse.urlparse(resp.url)
216
    assert parsed.path == '/test/'
217
    qs = urlparse.parse_qs(parsed.query)
218
    assert qs['session_var_booking_agenda_slug'] == ['test']
219
    assert qs['session_var_booking_start'] == ['2017-06-13T08:00:00']
220
    assert qs['session_var_booking_end'] == ['2017-06-13T09:30:00']
221

  
222

  
223
@mock.patch('combo.apps.calendar.utils.requests.get', side_effect=mocked_requests_get)
224
def test_calendar(mocked_get, cell):
225
    cal = get_calendar('default:whatever', cell.slot_duration)[0]
226
    assert len(cal.days) == 3
227
    for day in cal.get_days():
228
        assert day in [
229
            str2datetime('2017-06-12T08:00:00').date() + datetime.timedelta(days=i)
230
            for i in range(0, 5)]
231
    min_slot = str2datetime('2017-06-13T08:00:00')
232
    max_slot = str2datetime('2017-06-14T15:00:00')
233
    for slot in cal.get_slots():
234
        assert (min_slot.time() <= slot <= max_slot.time()) is True
235
    assert cal.has_day(min_slot.date()) is True
236
    assert cal.get_availability(str2datetime('2017-06-14T15:00:00')).available is False
237
    assert cal.get_minimum_slot() == min_slot.time()
238
    assert cal.get_maximum_slot() == max_slot.time()
239
    assert cal.get_day(max_slot.date()).slots[-1].available is False
0
-