0001-add-calendar-cell-model-16393.patch
combo/apps/chrono/README | ||
---|---|---|
1 |
Combo calendar cell |
|
2 |
=================== |
|
3 | ||
4 |
To be visible, this cell needs a 'chrono' entry in settings.KNOWN_SERVICES |
combo/apps/chrono/__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.chrono' |
|
22 | ||
23 |
def get_before_urls(self): |
|
24 |
from . import urls |
|
25 |
return urls.urlpatterns |
|
26 | ||
27 | ||
28 |
default_app_config = 'combo.apps.chrono.AppConfig' |
combo/apps/chrono/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 | ||
20 |
from .models import CalendarCell |
|
21 |
from .utils import (get_agendas, get_formdefs, |
|
22 |
is_datetime_diff_valid) |
|
23 | ||
24 | ||
25 |
class CalendarCellForm(forms.ModelForm): |
|
26 | ||
27 |
class Meta: |
|
28 |
model = CalendarCell |
|
29 |
fields = ( |
|
30 |
'title', 'agenda_reference', 'formdef_reference', |
|
31 |
'formdef_url_params', 'slot_duration', 'minimal_event_duration', |
|
32 |
'business_hour_start', 'business_hour_start') |
|
33 | ||
34 |
def __init__(self, *args, **kwargs): |
|
35 |
super(CalendarCellForm, self).__init__(*args, **kwargs) |
|
36 |
agenda_references = get_agendas() |
|
37 |
formdef_references = get_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 | ||
48 |
def is_valid(self): |
|
49 |
# assure that at least slot is selected |
|
50 |
if len(self.data.keys()) == 1: |
|
51 |
raise ValueError(_('Please select slots')) |
|
52 |
# check that selected slots are part of the same day |
|
53 |
if len(self.data.keys()) > 2: |
|
54 |
raise ValueError(_('Please select slot from the same day')) |
|
55 |
data = {} |
|
56 |
for key in self.data.keys(): |
|
57 |
if key == 'csrfmiddlewaretoken': |
|
58 |
continue |
|
59 |
values = self.data.getlist(key) |
|
60 |
data['start'] = values[0] |
|
61 |
data['end'] = values[-1] |
|
62 | ||
63 |
# check that booking duration is above minimum limitation |
|
64 |
if not is_datetime_diff_valid(data['end'], data['start'], self.cell.minimal_event_duration): |
|
65 |
raise ValueError(_( |
|
66 |
'Minimal booking duration is %s' % self.cell.minimal_event_duration.strftime('%H:%M'))) |
|
67 |
self.cleaned_data = data |
|
68 |
return True |
combo/apps/chrono/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, verbose_name='Title')), |
|
29 |
('agenda_reference', models.URLField(verbose_name='Events source URL')), |
|
30 |
('formdef_reference', models.URLField(verbose_name='Application form URL')), |
|
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(2, 0), verbose_name='Minimal event duration')), |
|
34 |
('business_hour_start', models.TimeField(default=datetime.time(8, 0), verbose_name='Business hour start')), |
|
35 |
('business_hour_end', models.TimeField(default=datetime.time(18, 0), verbose_name='Business hour end')), |
|
36 |
('groups', models.ManyToManyField(to='auth.Group', verbose_name='Groups', blank=True)), |
|
37 |
('page', models.ForeignKey(to='data.Page')), |
|
38 |
], |
|
39 |
options={ |
|
40 |
'verbose_name': 'Calendar', |
|
41 |
}, |
|
42 |
), |
|
43 |
] |
combo/apps/chrono/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) |
|
33 |
agenda_reference = models.URLField(_('Events source URL')) |
|
34 |
formdef_reference = models.URLField(_('Application form URL')) |
|
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('02:00')) |
|
42 |
business_hour_start = models.TimeField( |
|
43 |
_('Business hour start'), default=parse_time('08:00')) |
|
44 |
business_hour_end = models.TimeField( |
|
45 |
_('Business hour end'), default=parse_time('18:00')) |
|
46 | ||
47 |
template_name = 'chrono/calendar_cell.html' |
|
48 | ||
49 |
class Meta: |
|
50 |
verbose_name = _('Calendar') |
|
51 | ||
52 |
def get_default_form_class(self): |
|
53 |
from .forms import CalendarCellForm |
|
54 |
return CalendarCellForm |
|
55 | ||
56 |
@classmethod |
|
57 |
def is_enabled(cls): |
|
58 |
return is_chrono_enabled() |
|
59 | ||
60 |
def save(self, *args, **kwargs): |
|
61 |
if not self.slug: |
|
62 |
self.slug = slugify(self.title) |
|
63 |
return super(CalendarCell, self).save(*args, **kwargs) |
|
64 | ||
65 |
def get_calendar(self): |
|
66 |
return get_chrono_events(self.agenda_reference) |
|
67 | ||
68 |
def render(self, context): |
|
69 |
return super(CalendarCell, self).render(context) |
combo/apps/chrono/templates/chrono/calendar_cell.html | ||
---|---|---|
1 |
{% load i18n chrono %} |
|
2 | ||
3 |
<div id="cal-{{cell.pk}}"> |
|
4 |
<h3>{{cell.title}}</h3> |
|
5 |
<div> |
|
6 |
{% block calendar_table %} |
|
7 |
{% calendar_table cell=cell %} |
|
8 |
{% endblock %} |
|
9 |
</div> |
|
10 |
</div> |
combo/apps/chrono/templates/chrono/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 "previews week" %}</a> |
|
8 |
{% else %} |
|
9 |
<span><<</span> |
|
10 |
{% endif %} |
|
11 |
|
|
12 |
<span class="current"> |
|
13 |
{{ calendar.number }} / {{ calendar.paginator.num_pages }} |
|
14 |
</span> |
|
15 |
|
|
16 |
{% if calendar.has_next %} |
|
17 |
<a href="?week_{{cell.slug}}={{ calendar.next_page_number }}">{% trans "next week" %}</a> |
|
18 |
{% else %} |
|
19 |
<span>>></span> |
|
20 |
{% endif %} |
|
21 |
</div> |
|
22 |
{% endif %} |
|
23 |
</div> |
|
24 | ||
25 |
<div id="calendar_{{cell.pk}}" data-calendar="{{cell.pk}}"> |
|
26 |
<form id="form-cal-{{cell.pk}}" method="POST" action="{% url 'chrono-booking' cell.pk %}"> |
|
27 |
{% csrf_token %} |
|
28 |
<table id="cal-table-{{cell.pk}}"> |
|
29 |
<thead> |
|
30 |
{% if calendar_table_headers %} |
|
31 |
<tr> |
|
32 |
{% for header in calendar_table_headers %} |
|
33 |
<td>{{header}}</td> |
|
34 |
{% endfor %} |
|
35 |
</tr> |
|
36 |
{% endif %} |
|
37 |
</thead> |
|
38 |
<tbody> |
|
39 |
{% for week, slots in calendar %} |
|
40 |
{% for slot, choices in slots.items %} |
|
41 |
<tr> |
|
42 |
{% for choice in choices %} |
|
43 |
<td> |
|
44 |
<input id="{{week}}_{{choice.value}}" name="{{choice.label}}" value="{{choice.value}}" type="checkbox" {{choice.disabled}}> |
|
45 |
<label for="{{week}}_{{choice.value}}">{{slot}}</label> |
|
46 |
</td> |
|
47 |
{% endfor %} |
|
48 |
</tr> |
|
49 |
{% endfor %} |
|
50 |
{% endfor %} |
|
51 |
</tbody> |
|
52 |
</table> |
|
53 |
<input type="submit" value="{% trans 'Validate' %}"> |
|
54 |
</form> |
|
55 |
</div> |
combo/apps/chrono/templatetags/chrono.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.chrono.utils import get_calendar |
|
21 | ||
22 |
register = template.Library() |
|
23 | ||
24 | ||
25 |
@register.inclusion_tag('chrono/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 | ||
47 |
context['calendar'] = calendar |
|
48 |
return context |
combo/apps/chrono/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/chrono/events/(?P<pk>[\w,-]+)/', EventsView.as_view(), name='chrono-events'), |
|
23 |
url(r'^api/chrono/book/(?P<pk>[\w,-]+)/', BookingView.as_view(), name='chrono-booking'), |
|
24 |
] |
combo/apps/chrono/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 |
import json |
|
17 |
from collections import OrderedDict |
|
18 |
import datetime |
|
19 | ||
20 | ||
21 |
from django.conf import settings |
|
22 |
from django.utils.dateparse import parse_datetime |
|
23 |
from django.utils import formats |
|
24 |
from django.template import Context, Template |
|
25 | ||
26 |
from combo.utils import requests |
|
27 | ||
28 | ||
29 |
def get_service(service_name): |
|
30 |
if hasattr(settings, 'KNOWN_SERVICES') and settings.KNOWN_SERVICES.get(service_name): |
|
31 |
services = settings.KNOWN_SERVICES[service_name] |
|
32 |
for service, items in services.items(): |
|
33 |
if not items.get('secondary', False): |
|
34 |
return items |
|
35 | ||
36 | ||
37 |
def is_chrono_enabled(): |
|
38 |
return get_service('chrono') |
|
39 | ||
40 | ||
41 |
def get_agendas(): |
|
42 |
chrono = get_service('chrono') |
|
43 |
url = chrono['url'] + 'api/agenda/' |
|
44 |
response = requests.get(url, headers={'accept': 'application/json'}) |
|
45 |
data = response.json() |
|
46 |
agendas = [(item['api']['datetimes_url'], item['text']) for item in data['data']] |
|
47 |
return agendas |
|
48 | ||
49 | ||
50 |
def get_formdefs(): |
|
51 |
wcs = get_service('wcs') |
|
52 |
url = wcs['url'] + '/api/formdefs/' |
|
53 |
response = requests.get(url, headers={'accept': 'application/json'}) |
|
54 |
data = response.json() |
|
55 |
forms = [(item['url'], item['title']) for item in data] |
|
56 |
return forms |
|
57 | ||
58 | ||
59 |
def get_chrono_events(chrono_url, **kwargs): |
|
60 |
response = requests.get( |
|
61 |
chrono_url, |
|
62 |
headers={'accept': 'application/json'}, |
|
63 |
**kwargs) |
|
64 |
return response.json().get('data', []) |
|
65 | ||
66 | ||
67 |
def get_day_time_slots(start, end, offset): |
|
68 |
dstart = start |
|
69 |
# offset in minute |
|
70 |
offset = offset.hour * 60 + offset.minute |
|
71 |
while dstart <= end: |
|
72 |
yield dstart.strftime('%H:%M') |
|
73 |
dstart = datetime.datetime.combine( |
|
74 |
datetime.date.today(), dstart) + datetime.timedelta(minutes=offset) |
|
75 |
dstart = dstart.time() |
|
76 | ||
77 | ||
78 |
def get_calendar(events): |
|
79 |
calendar = {} |
|
80 |
for event in events: |
|
81 |
human_day = formats.date_format(parse_datetime(event['datetime']).date()) |
|
82 |
event_datetime = parse_datetime(event['datetime']) |
|
83 |
event_time = event_datetime.strftime('%H:%M') |
|
84 |
# get week ref |
|
85 |
week_ref = event_datetime.isocalendar()[1] |
|
86 |
if week_ref not in calendar: |
|
87 |
calendar[week_ref] = {} |
|
88 |
if event_time not in calendar[week_ref]: |
|
89 |
calendar[week_ref][event_time] = [] |
|
90 |
choice = {} |
|
91 |
choice['value'] = event_datetime.isoformat() |
|
92 |
choice['label'] = human_day |
|
93 |
# add disabled if no more slot available for that day |
|
94 |
choice['disabled'] = 'disabled' if event.get('disabled', False) else '' |
|
95 |
calendar[week_ref][event_time].append(choice) |
|
96 |
# sort days |
|
97 |
for week in calendar: |
|
98 |
calendar[week] = OrderedDict(sorted(calendar[week].items())) |
|
99 |
# sort weeks |
|
100 |
calendar = OrderedDict(sorted(calendar.items())) |
|
101 |
return calendar |
|
102 | ||
103 | ||
104 |
def build_session_vars(cell, data): |
|
105 |
tpl = Template(json.dumps(cell.formdef_url_params)) |
|
106 |
session_vars = json.loads(tpl.render(Context(data))) |
|
107 |
return session_vars |
|
108 | ||
109 | ||
110 |
def is_datetime_diff_valid(end, start, valid): |
|
111 |
end = parse_datetime(end) |
|
112 |
start = parse_datetime(start) |
|
113 |
return (end - start) >= datetime.timedelta(hours=valid.hour) |
combo/apps/chrono/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 |
import urllib |
|
18 | ||
19 |
from django.http import HttpResponseRedirect, JsonResponse |
|
20 |
from django.views.decorators.csrf import csrf_exempt |
|
21 |
from django.views.generic import View |
|
22 |
from django.views.generic.detail import SingleObjectMixin |
|
23 |
from django.contrib import messages |
|
24 | ||
25 |
from .models import CalendarCell |
|
26 |
from .utils import get_chrono_events, build_session_vars |
|
27 |
from .forms import BookingForm |
|
28 | ||
29 | ||
30 |
class EventsView(SingleObjectMixin, View): |
|
31 | ||
32 |
http_method_names = ['get'] |
|
33 |
model = CalendarCell |
|
34 | ||
35 |
def get(self, request, *args, **kwargs): |
|
36 |
cell = self.get_object() |
|
37 |
data = get_chrono_events(cell.agenda_reference) |
|
38 |
return JsonResponse(data, safe=False) |
|
39 | ||
40 | ||
41 |
class BookingView(SingleObjectMixin, View): |
|
42 | ||
43 |
http_method_names = ['post'] |
|
44 |
model = CalendarCell |
|
45 | ||
46 |
@csrf_exempt |
|
47 |
def dispatch(self, request, *args, **kwargs): |
|
48 |
return super(BookingView, self).dispatch(request, *args, **kwargs) |
|
49 | ||
50 |
def post(self, request, *args, **kwargs): |
|
51 |
cell = self.get_object() |
|
52 |
form = BookingForm(request.POST, cell=cell) |
|
53 |
try: |
|
54 |
form.is_valid() |
|
55 |
except ValueError as exc: |
|
56 |
messages.error(request, exc.message) |
|
57 |
return HttpResponseRedirect(cell.page.get_online_url()) |
|
58 |
data = form.cleaned_data |
|
59 |
params = build_session_vars(cell, data) |
|
60 |
url = '%s?%s' % (cell.formdef_reference, urllib.urlencode(params)) |
|
61 |
return HttpResponseRedirect(url) |
combo/settings.py | ||
---|---|---|
78 | 78 |
'combo.apps.search', |
79 | 79 |
'combo.apps.usersearch', |
80 | 80 |
'combo.apps.maps', |
81 |
'combo.apps.chrono', |
|
81 | 82 |
'haystack', |
82 | 83 |
'xstatic.pkg.chartnew_js', |
83 | 84 |
'xstatic.pkg.leaflet', |
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.chrono.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 |
class MockedRequestResponse(mock.Mock): |
|
57 | ||
58 |
def json(self): |
|
59 |
return json.loads(self.content) |
|
60 | ||
61 | ||
62 |
@pytest.fixture |
|
63 |
def cell(db): |
|
64 |
page = Page.objects.create() |
|
65 |
cell = CalendarCell( |
|
66 |
page=page, title='Example Of Calendar', order=1, |
|
67 |
agenda_reference='http://example.net/api/events/', |
|
68 |
formdef_reference='http://example.net/form/whatever/', |
|
69 |
formdef_url_params={ |
|
70 |
"session_var_start_dt": "{{start}}", "session_var_end_dt": "{{end}}", |
|
71 |
"session_var_whatever_slug": "whatever" |
|
72 |
}, |
|
73 |
slot_duration=parse_time('00:30'), |
|
74 |
business_hour_start=parse_time('08:00'), |
|
75 |
business_hour_end=parse_time('18:00'), |
|
76 |
minimal_event_duration=parse_time('00:30') |
|
77 |
) |
|
78 |
cell.save() |
|
79 |
return cell |
|
80 | ||
81 | ||
82 |
@mock.patch('combo.apps.chrono.utils.requests.get') |
|
83 |
def test_get_events(mocked_get, app, cell): |
|
84 |
mocked_get.return_value = MockedRequestResponse(content=json.dumps(CHRONO_EVENTS)) |
|
85 |
resp = app.get('/api/chrono/events/%s/' % cell.pk) |
|
86 |
assert len(resp.json) == 4 |
|
87 | ||
88 | ||
89 |
def test_redirection_session_vars(app, cell, settings): |
|
90 |
params = {'19 Mai 2017': ['2017-05-19T10:30:23', '2017-05-19T12:30:14'], |
|
91 |
'csrfmiddleware': 'shgfsufysgfjsvfhsvfgv'} |
|
92 |
resp = app.post('/api/chrono/book/%s/' % cell.pk, params=params, status=302) |
|
93 |
parsed = urlparse.urlparse(resp.url) |
|
94 |
qs = urlparse.parse_qs(parsed.query) |
|
95 |
assert qs['session_var_whatever_slug'] == ['whatever'] |
|
96 |
assert qs['session_var_start_dt'] == [params['19 Mai 2017'][0]] |
|
97 |
assert qs['session_var_end_dt'] == [params['19 Mai 2017'][1]] |
|
0 |
- |