Projet

Général

Profil

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

Josué Kouka, 16 juin 2017 12:10

Télécharger (34,1 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                       |  78 +++++++
 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  |  57 +++++
 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                       | 229 ++++++++++++++++++++
 combo/apps/calendar/views.py                       |  42 ++++
 combo/settings.py                                  |   1 +
 tests/settings.py                                  |   5 +
 tests/test_calendar.py                             | 231 +++++++++++++++++++++
 15 files changed, 833 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, get_formdefs
23

  
24

  
25
class BookingCalendarForm(forms.ModelForm):
26

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

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

  
40

  
41
class BookingForm(forms.Form):
42

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

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

  
58
        # check that all slots are part of the same day
59
        for slot in slots:
60
            if parse_datetime(slot).date() != start_dt.date():
61
                raise ValueError(_('Please select slots of the same day'))
62

  
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 + offset
69

  
70
        # check that event booking duration >= minimal booking duration
71
        min_duration = self.cell.minimal_booking_duration
72
        if not (end_dt - start_dt) >= min_duration:
73
            str_min_duration = parse_time(str(min_duration)).strftime('%H:%M')
74
            raise ValueError(_(
75
                'Minimal booking duration is %s' % str_min_duration))
76
        self.cleaned_data['start'] = start_dt
77
        self.cleaned_data['end'] = end_dt
78
        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=b'H:M:S', verbose_name='Slot duration')),
31
                ('minimal_booking_duration', models.DurationField(default=datetime.timedelta(0, 3600), help_text=b'H:M:S', 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='H:M:S')
37
    minimal_booking_duration = models.DurationField(
38
        _('Minimal booking duration'), default=datetime.timedelta(hours=1),
39
        help_text='H:M:S')
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
    {% if calendar.has_other_pages %}
8
        <p class="paginator">
9
        {% if calendar.has_previous %}
10
            <a class="previous" href="?week_{{cell.id}}={{ calendar.previous_page_number }}">{% trans "previous week" %}</a>
11
        {% endif %}
12
        &nbsp;
13
        <span class="current">
14
            {{ calendar.number }} / {{ calendar.paginator.num_pages }}
15
        </span>
16
        &nbsp;
17
        {% if calendar.has_next %}
18
            <a class="next" href="?week_{{cell.id}}={{ calendar.next_page_number }}">{% trans "next week" %}</a>
19
        {% endif %}
20
        </p>
21
    {% endif %}
22

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

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

  
23
from combo.utils import requests
24

  
25

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

  
31

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

  
35

  
36
def get_chrono_service():
37
    for chrono_key, chrono_site in get_services('chrono').iteritems():
38
        if chrono_site.get('secondary', True):
39
            return {chrono_key: chrono_site}
40
    return {}
41

  
42

  
43
def is_chrono_enabled():
44
    return bool(get_chrono_service())
45

  
46

  
47
def is_wcs_enabled():
48
    return bool(get_wcs_services())
49

  
50

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

  
65

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

  
84

  
85
def get_chrono_events(agenda_reference, **kwargs):
86
    chrono_key, chrono_slug = agenda_reference.split(':')
87
    chrono = get_chrono_service().get(chrono_key)
88
    response = requests.get('api/agenda/%s/datetimes/' % chrono_slug, remote_service=chrono, without_user=True)
89
    try:
90
        result = response.json()
91
    except ValueError:
92
        return {}
93
    return result.get('data', [])
94

  
95

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

  
117
    return sorted(calendar.values(), key=lambda x: x.week)
118

  
119

  
120
def get_form_url_with_params(cell, data):
121
    session_vars = {
122
        "session_var_booking_agenda_slug": cell.agenda_reference.split(':')[1],
123
        "session_var_booking_start": data['start'].isoformat(),
124
        "session_var_booking_end": data['end'].isoformat()
125
    }
126
    wcs_key, wcs_slug = cell.formdef_reference.split(':')
127
    wcs = get_wcs_services().get(wcs_key)
128
    response = requests.get('api/formdefs/', remote_service=wcs, without_user=True)
129
    for formdef in response.json():
130
        if formdef['slug'] == wcs_slug:
131
            break
132
    url = '%s/?%s' % (formdef['url'], urllib.urlencode(session_vars))
133
    return url
134

  
135

  
136
class DaySlot(object):
137

  
138
    def __init__(self, date_time, available):
139
        self.date_time = date_time
140
        self.available = available
141

  
142
    def __repr__(self):
143
        return '<DaySlot date_time=%s - available=%s>' % (self.date_time.isoformat(), self.available)
144

  
145
    @property
146
    def label(self):
147
        return '%s' % self.date_time.isoformat()
148

  
149

  
150
class WeekDay(object):
151

  
152
    def __init__(self, date):
153
        self.date = date
154
        self.slots = []
155

  
156
    def __repr__(self):
157
        return '<WeekDay %s >' % self.date.isoformat()
158

  
159
    def add_slots(self, slot):
160
        if slot not in self.slots:
161
            self.slots.append(slot)
162

  
163
    def get_slot(self, slot_time):
164
        for slot in self.slots:
165
            if slot.date_time.time() == slot_time:
166
                return slot
167
        slot_datetime = datetime.datetime.combine(self.date, slot_time)
168
        return DaySlot(slot_datetime, False)
169

  
170
    def get_minimum_slot(self):
171
        return min(self.slots, key=lambda x: x.date_time.time())
172

  
173
    def get_maximum_slot(self):
174
        return max(self.slots, key=lambda x: x.date_time.time())
175

  
176
    def is_available(self, slot_time):
177
        return self.get_slot(slot_time).available
178

  
179

  
180
class WeekCalendar(object):
181

  
182
    def __init__(self, week, offset):
183
        self.week = week
184
        self.offset = offset
185
        self.days = []
186

  
187
    def __repr__(self):
188
        return '<WeekCalendar - %s>' % self.week
189

  
190
    def get_slots(self):
191
        start = self.get_minimum_slot()
192
        end = self.get_maximum_slot()
193
        # offset = self.offset.hour * 60 + self.offset.minute
194
        while start <= end:
195
            yield start
196
            start = datetime.datetime.combine(
197
                datetime.date.today(), start) + self.offset
198
            start = start.time()
199

  
200
    def get_days(self):
201
        if self.days:
202
            base_day = self.days[0].date
203
        else:
204
            base_day = datetime.datetime.today()
205
        # only week days
206
        for index in range(0, 5):
207
            day = base_day + datetime.timedelta(days=index - base_day.weekday())
208
            yield day
209

  
210
    def get_day(self, date):
211
        for day in self.days:
212
            if day.date == date:
213
                return day
214
        return None
215

  
216
    def has_day(self, date):
217
        return bool(self.get_day(date))
218

  
219
    def get_availability(self, slot):
220
        if not self.has_day(slot.date()):
221
            return DaySlot(slot, False)
222
        day = self.get_day(slot.date())
223
        return day.get_slot(slot.time())
224

  
225
    def get_minimum_slot(self):
226
        return min([day.get_minimum_slot().date_time.time() for day in self.days])
227

  
228
    def get_maximum_slot(self):
229
        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
            return HttpResponseRedirect(cell.page.get_online_url())
40
        data = form.cleaned_data
41
        url = get_form_url_with_params(cell, data)
42
        return HttpResponseRedirect(url)
combo/settings.py
78 78
    'combo.apps.search',
79 79
    'combo.apps.usersearch',
80 80
    'combo.apps.maps',
81
    'combo.apps.calendar',
81 82
    'haystack',
82 83
    'xstatic.pkg.chartnew_js',
83 84
    'xstatic.pkg.leaflet',
tests/settings.py
13 13
        'default': {'title': 'test', 'url': 'http://example.org',
14 14
                    'secret': 'combo', 'orig': 'combo',
15 15
                    'backoffice-menu-url': 'http://example.org/manage/',}
16
    },
17
    'chrono': {
18
        'default': {'title': 'test', 'url': 'http://chrono.example.org',
19
                    'secret': 'combo', 'orig': 'combo',
20
                    'backoffice-menu-url': 'http://chrono.example.org/manage/',}
16 21
    }
17 22
}
18 23

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

  
5
import pytest
6
import mock
7

  
8
from django.utils.dateparse import parse_time
9
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
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='wjatever', slug='test', 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
@mock.patch('combo.apps.calendar.utils.requests.get', side_effect=mocked_requests_get)
183
def test_cell_rendering(mocked_get, client, cell):
184
    page = client.get('/test/')
185
    # test without selecting slots
186
    resp = page.form.submit().follow()
187
    assert 'Please select slots' in resp.content
188
    # test with slots from different day
189
    resp.form.set('slots', True, 0)
190
    resp.form.set('slots', True, 1)
191
    resp.form.set('slots', True, 4)
192
    resp = resp.form.submit().follow()
193
    # test with non contiguous slots
194
    assert 'Please select slots of the same day' in resp.content
195
    resp.form.set('slots', True, 0)
196
    resp.form.set('slots', True, 2)
197
    resp = resp.form.submit().follow()
198
    assert 'Please select contiguous slots' in resp.content
199
    # test with invalid booking duration
200
    resp.form.set('slots', True, 0)
201
    resp = resp.form.submit().follow()
202
    assert 'Minimal booking duration is 01:00' in resp.content
203
    # test with valid selected slots
204
    resp.form.set('slots', True, 0)
205
    resp.form.set('slots', True, 1)
206
    resp.form.set('slots', True, 2)
207
    resp = resp.form.submit()
208
    parsed = urlparse.urlparse(resp.url)
209
    qs = urlparse.parse_qs(parsed.query)
210
    assert qs['session_var_booking_agenda_slug'] == ['test']
211
    assert qs['session_var_booking_start'] == ['2017-06-13T08:00:00']
212
    assert qs['session_var_booking_end'] == ['2017-06-13T09:30:00']
213

  
214

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