general: add booking calendar cell (#16393)
This commit is contained in:
parent
0665cb8bbd
commit
56b47f5a50
|
@ -0,0 +1,28 @@
|
|||
# combo - content management system
|
||||
# Copyright (C) 2017 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import django.apps
|
||||
|
||||
|
||||
class AppConfig(django.apps.AppConfig):
|
||||
name = 'combo.apps.calendar'
|
||||
|
||||
def get_before_urls(self):
|
||||
from . import urls
|
||||
return urls.urlpatterns
|
||||
|
||||
|
||||
default_app_config = 'combo.apps.calendar.AppConfig'
|
|
@ -0,0 +1,79 @@
|
|||
# combo - content management system
|
||||
# Copyright (C) 2017 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from django import forms
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.dateparse import parse_datetime, parse_time
|
||||
|
||||
from .models import BookingCalendar
|
||||
from .utils import get_agendas
|
||||
from combo.apps.wcs.utils import get_wcs_options
|
||||
|
||||
|
||||
class BookingCalendarForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = BookingCalendar
|
||||
fields = (
|
||||
'title', 'agenda_reference', 'formdef_reference',
|
||||
'slot_duration', 'minimal_booking_duration')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(BookingCalendarForm, self).__init__(*args, **kwargs)
|
||||
agenda_references = get_agendas()
|
||||
formdef_references = get_wcs_options('/api/formdefs/')
|
||||
self.fields['agenda_reference'].widget = forms.Select(choices=agenda_references)
|
||||
self.fields['formdef_reference'].widget = forms.Select(choices=formdef_references)
|
||||
|
||||
|
||||
class BookingForm(forms.Form):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.cell = kwargs.pop('cell')
|
||||
super(BookingForm, self).__init__(*args, **kwargs)
|
||||
self.cleaned_data = {}
|
||||
|
||||
def is_valid(self):
|
||||
slots = getattr(self.data, 'getlist', lambda x: [])('slots')
|
||||
# check that at least one slot if selected
|
||||
if not slots:
|
||||
raise ValueError(_('Please select slots'))
|
||||
offset = self.cell.slot_duration
|
||||
start_dt = parse_datetime(slots[0])
|
||||
end_dt = parse_datetime(slots[-1]) + offset
|
||||
slots.append(end_dt.isoformat())
|
||||
|
||||
# check that all slots are part of the same day
|
||||
for slot in slots:
|
||||
if parse_datetime(slot).date() != start_dt.date():
|
||||
raise ValueError(_('Please select slots of the same day'))
|
||||
|
||||
# check that slots datetime are contiguous
|
||||
start = start_dt
|
||||
while start <= end_dt:
|
||||
if start.isoformat() not in slots:
|
||||
raise ValueError(_('Please select contiguous slots'))
|
||||
start = start + offset
|
||||
|
||||
# check that event booking duration >= minimal booking duration
|
||||
min_duration = self.cell.minimal_booking_duration
|
||||
if not (end_dt - start_dt) >= min_duration:
|
||||
str_min_duration = parse_time(str(min_duration)).strftime('%H:%M')
|
||||
message = _("Minimal booking duration is %s") % str_min_duration
|
||||
raise ValueError(message)
|
||||
self.cleaned_data['start'] = start_dt
|
||||
self.cleaned_data['end'] = end_dt
|
||||
return True
|
|
@ -0,0 +1,39 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import datetime
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('data', '0027_page_picture'),
|
||||
('auth', '0006_require_contenttypes_0002'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='BookingCalendar',
|
||||
fields=[
|
||||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
|
||||
('placeholder', models.CharField(max_length=20)),
|
||||
('order', models.PositiveIntegerField()),
|
||||
('slug', models.SlugField(verbose_name='Slug', blank=True)),
|
||||
('extra_css_class', models.CharField(max_length=100, verbose_name='Extra classes for CSS styling', blank=True)),
|
||||
('public', models.BooleanField(default=True, verbose_name='Public')),
|
||||
('restricted_to_unlogged', models.BooleanField(default=False, verbose_name='Restrict to unlogged users')),
|
||||
('last_update_timestamp', models.DateTimeField(auto_now=True)),
|
||||
('title', models.CharField(max_length=128, null=True, verbose_name='Title', blank=True)),
|
||||
('agenda_reference', models.CharField(max_length=128, verbose_name='Agenda')),
|
||||
('formdef_reference', models.CharField(max_length=128, verbose_name='Form')),
|
||||
('slot_duration', models.DurationField(default=datetime.timedelta(0, 1800), help_text='Format is hours:minutes:seconds', verbose_name='Slot duration')),
|
||||
('minimal_booking_duration', models.DurationField(default=datetime.timedelta(0, 3600), help_text='Format is hours:minutes:seconds', verbose_name='Minimal booking duration')),
|
||||
('groups', models.ManyToManyField(to='auth.Group', verbose_name='Groups', blank=True)),
|
||||
('page', models.ForeignKey(to='data.Page')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Booking Calendar',
|
||||
},
|
||||
),
|
||||
]
|
|
@ -0,0 +1,67 @@
|
|||
# combo - content management system
|
||||
# Copyright (C) 2017 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import datetime
|
||||
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
|
||||
|
||||
from combo.data.models import CellBase
|
||||
from combo.data.library import register_cell_class
|
||||
from .utils import is_chrono_enabled, is_wcs_enabled, get_calendar
|
||||
|
||||
|
||||
@register_cell_class
|
||||
class BookingCalendar(CellBase):
|
||||
|
||||
title = models.CharField(_('Title'), max_length=128, blank=True, null=True)
|
||||
agenda_reference = models.CharField(_('Agenda'), max_length=128)
|
||||
formdef_reference = models.CharField(_('Form'), max_length=128)
|
||||
slot_duration = models.DurationField(
|
||||
_('Slot duration'), default=datetime.timedelta(minutes=30),
|
||||
help_text=_('Format is hours:minutes:seconds'))
|
||||
minimal_booking_duration = models.DurationField(
|
||||
_('Minimal booking duration'), default=datetime.timedelta(hours=1),
|
||||
help_text=_('Format is hours:minutes:seconds'))
|
||||
|
||||
template_name = 'calendar/booking_calendar_cell.html'
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Booking Calendar')
|
||||
|
||||
def get_default_form_class(self):
|
||||
from .forms import BookingCalendarForm
|
||||
return BookingCalendarForm
|
||||
|
||||
@classmethod
|
||||
def is_enabled(cls):
|
||||
return is_chrono_enabled() and is_wcs_enabled()
|
||||
|
||||
def render(self, context):
|
||||
request = context['request']
|
||||
page = request.GET.get('week_%s' % self.pk, 1)
|
||||
# get calendar
|
||||
calendar = get_calendar(self.agenda_reference, self.slot_duration)
|
||||
paginator = Paginator(calendar, 1)
|
||||
try:
|
||||
cal_page = paginator.page(page)
|
||||
except PageNotAnInteger:
|
||||
cal_page = paginator.page(1)
|
||||
except (EmptyPage,):
|
||||
cal_page = paginator.page(paginator.num_pages)
|
||||
context['calendar'] = cal_page
|
||||
return super(BookingCalendar, self).render(context)
|
|
@ -0,0 +1,65 @@
|
|||
{% load i18n calendar %}
|
||||
|
||||
<div id="cal-{{cell.id}}">
|
||||
{% if cell.title %}
|
||||
<h2>{{cell.title}}</h2>
|
||||
{% endif %}
|
||||
<div>
|
||||
{% if calendar.has_other_pages %}
|
||||
<p class="paginator">
|
||||
{% if calendar.has_previous %}
|
||||
<a class="previous" href="?week_{{cell.id}}={{ calendar.previous_page_number }}">{% trans "previous week" %}</a>
|
||||
{% else %}
|
||||
<span class="previous">{% trans "previous week" %}</span>
|
||||
{% endif %}
|
||||
<span class="current">
|
||||
{{ calendar.number }} / {{ calendar.paginator.num_pages }}
|
||||
</span>
|
||||
{% if calendar.has_next %}
|
||||
<a class="next" href="?week_{{cell.id}}={{ calendar.next_page_number }}">{% trans "next week" %}</a>
|
||||
{% else %}
|
||||
<span class="next">{% trans "next week" %}</span>
|
||||
{% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
{% if calendar %}
|
||||
<form method="POST" action="{% url 'calendar-booking' cell.id %}?week_{{cell.id}}={{calendar.number}}">
|
||||
{% csrf_token %}
|
||||
<table id="cal-table-{{cell.id}}">
|
||||
{% for cal in calendar %}
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
{% for day in cal.get_days %}
|
||||
<th>{{day|date:"SHORT_DATE_FORMAT"}}</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for slot in cal.get_slots %}
|
||||
<tr>
|
||||
<th>{{slot|date:"TIME_FORMAT"}}</th>
|
||||
{% for day in cal.get_days %}
|
||||
{% get_day_slot cal day=day slot=slot as value %}
|
||||
{% if not value.exist %}
|
||||
<td class="absent"></td>
|
||||
{% elif value.available %}
|
||||
<td class="available">
|
||||
<input type="checkbox" name="slots" value="{{value.label}}" id="slot-{{cell.id}}-{{value.label}}"/>
|
||||
<label for="slot-{{cell.id}}-{{value.label}}"></label>
|
||||
</td>
|
||||
{% else %}
|
||||
<td class="unavailable"></td>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
{% endfor %}
|
||||
</table>
|
||||
<button class="submit-button">{% trans "Book" context "booking" %}</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,29 @@
|
|||
# combo - content management system
|
||||
# Copyright (C) 2017 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import datetime
|
||||
|
||||
from django import template
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.assignment_tag
|
||||
def get_day_slot(cal, *args, **kwargs):
|
||||
day = kwargs.get('day')
|
||||
slot = kwargs.get('slot')
|
||||
time_slot = datetime.datetime.combine(day, slot)
|
||||
return cal.get_availability(time_slot)
|
|
@ -0,0 +1,23 @@
|
|||
# combo - content management system
|
||||
# Copyright (C) 2017 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from django.conf.urls import url
|
||||
|
||||
from .views import BookingView
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^calendar/book/(?P<pk>[\w,-]+)/', BookingView.as_view(), name='calendar-booking'),
|
||||
]
|
|
@ -0,0 +1,203 @@
|
|||
# combo - content management system
|
||||
# Copyright (C) 2017 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import urllib
|
||||
import datetime
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils.dateparse import parse_datetime
|
||||
|
||||
from combo.utils import requests
|
||||
|
||||
|
||||
def get_services(service_name):
|
||||
if hasattr(settings, 'KNOWN_SERVICES') and settings.KNOWN_SERVICES.get(service_name):
|
||||
return settings.KNOWN_SERVICES[service_name]
|
||||
return {}
|
||||
|
||||
|
||||
def get_wcs_services():
|
||||
return get_services('wcs')
|
||||
|
||||
|
||||
def get_chrono_service():
|
||||
for chrono_key, chrono_site in get_services('chrono').iteritems():
|
||||
if not chrono_site.get('secondary', True):
|
||||
chrono_site['slug'] = chrono_key
|
||||
return chrono_site
|
||||
return {}
|
||||
|
||||
|
||||
def is_chrono_enabled():
|
||||
return bool(get_chrono_service())
|
||||
|
||||
|
||||
def is_wcs_enabled():
|
||||
return bool(get_wcs_services())
|
||||
|
||||
|
||||
def get_agendas():
|
||||
chrono = get_chrono_service()
|
||||
references = []
|
||||
response = requests.get('api/agenda/', remote_service=chrono, without_user=True)
|
||||
try:
|
||||
result = response.json()
|
||||
except ValueError:
|
||||
return references
|
||||
for agenda in result.get('data'):
|
||||
references.append((
|
||||
'%s:%s' % (chrono['slug'], agenda['slug']), agenda['text']))
|
||||
return references
|
||||
|
||||
|
||||
def get_chrono_events(agenda_reference):
|
||||
chrono_key, chrono_slug = agenda_reference.split(':')
|
||||
chrono = get_chrono_service()
|
||||
response = requests.get('api/agenda/%s/datetimes/' % chrono_slug, remote_service=chrono, without_user=True)
|
||||
try:
|
||||
result = response.json()
|
||||
except ValueError:
|
||||
return []
|
||||
return result.get('data', [])
|
||||
|
||||
|
||||
def get_calendar(agenda_reference, offset):
|
||||
if not agenda_reference:
|
||||
return []
|
||||
events = get_chrono_events(agenda_reference)
|
||||
calendar = {}
|
||||
for event in events:
|
||||
event_datetime = parse_datetime(event['datetime'])
|
||||
week_ref = event_datetime.isocalendar()[1]
|
||||
if week_ref not in calendar:
|
||||
calendar[week_ref] = WeekCalendar(week_ref, offset)
|
||||
week_cal = calendar[week_ref]
|
||||
# add day to week calendar
|
||||
if not week_cal.has_day(event_datetime.date()):
|
||||
day = WeekDay(event_datetime.date())
|
||||
week_cal.days.append(day)
|
||||
else:
|
||||
day = week_cal.get_day(event_datetime.date())
|
||||
# add slots to day
|
||||
day.add_slots(DaySlot(
|
||||
event_datetime, True if not event.get('disabled', True) else False))
|
||||
|
||||
return sorted(calendar.values(), key=lambda x: x.week)
|
||||
|
||||
|
||||
def get_form_url_with_params(cell, data):
|
||||
session_vars = {
|
||||
"session_var_booking_agenda_slug": cell.agenda_reference.split(':')[1],
|
||||
"session_var_booking_start": data['start'].isoformat(),
|
||||
"session_var_booking_end": data['end'].isoformat()
|
||||
}
|
||||
wcs_key, wcs_slug = cell.formdef_reference.split(':')
|
||||
wcs = get_wcs_services().get(wcs_key)
|
||||
url = '%s%s/?%s' % (wcs['url'], wcs_slug, urllib.urlencode(session_vars))
|
||||
return url
|
||||
|
||||
|
||||
class DaySlot(object):
|
||||
|
||||
def __init__(self, date_time, available, exist=True):
|
||||
self.date_time = date_time
|
||||
self.available = available
|
||||
self.exist = exist
|
||||
|
||||
def __repr__(self):
|
||||
return '<DaySlot date_time=%s - available=%s>' % (self.date_time.isoformat(), self.available)
|
||||
|
||||
@property
|
||||
def label(self):
|
||||
return '%s' % self.date_time.isoformat()
|
||||
|
||||
|
||||
class WeekDay(object):
|
||||
|
||||
def __init__(self, date):
|
||||
self.date = date
|
||||
self.slots = []
|
||||
|
||||
def __repr__(self):
|
||||
return '<WeekDay %s >' % self.date.isoformat()
|
||||
|
||||
def add_slots(self, slot):
|
||||
if slot not in self.slots:
|
||||
self.slots.append(slot)
|
||||
|
||||
def get_slot(self, slot_time):
|
||||
for slot in self.slots:
|
||||
if slot.date_time.time() == slot_time:
|
||||
return slot
|
||||
slot_datetime = datetime.datetime.combine(self.date, slot_time)
|
||||
return DaySlot(slot_datetime, False, exist=False)
|
||||
|
||||
def get_minimum_slot(self):
|
||||
return min(self.slots, key=lambda x: x.date_time.time())
|
||||
|
||||
def get_maximum_slot(self):
|
||||
return max(self.slots, key=lambda x: x.date_time.time())
|
||||
|
||||
|
||||
class WeekCalendar(object):
|
||||
|
||||
def __init__(self, week, offset):
|
||||
self.week = week
|
||||
self.offset = offset
|
||||
self.days = []
|
||||
|
||||
def __repr__(self):
|
||||
return '<WeekCalendar - %s>' % self.week
|
||||
|
||||
def get_slots(self):
|
||||
start = self.get_minimum_slot()
|
||||
end = self.get_maximum_slot()
|
||||
while start <= end:
|
||||
yield start
|
||||
start = datetime.datetime.combine(
|
||||
datetime.date.today(), start) + self.offset
|
||||
start = start.time()
|
||||
|
||||
def get_days(self):
|
||||
if self.days:
|
||||
base_day = self.days[0].date
|
||||
else:
|
||||
base_day = datetime.datetime.today()
|
||||
# only week days
|
||||
for index in range(0, 5):
|
||||
day = base_day + datetime.timedelta(days=index - base_day.weekday())
|
||||
yield day
|
||||
|
||||
def get_day(self, date):
|
||||
for day in self.days:
|
||||
if day.date == date:
|
||||
return day
|
||||
return None
|
||||
|
||||
def has_day(self, date):
|
||||
return bool(self.get_day(date))
|
||||
|
||||
def get_availability(self, slot):
|
||||
if not self.has_day(slot.date()):
|
||||
return DaySlot(slot, False, exist=False)
|
||||
day = self.get_day(slot.date())
|
||||
return day.get_slot(slot.time())
|
||||
|
||||
def get_minimum_slot(self):
|
||||
return min([day.get_minimum_slot().date_time.time() for day in self.days])
|
||||
|
||||
def get_maximum_slot(self):
|
||||
return max([day.get_maximum_slot().date_time.time() for day in self.days])
|
|
@ -0,0 +1,44 @@
|
|||
# combo - content management system
|
||||
# Copyright (C) 2017 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.views.generic import View
|
||||
from django.views.generic.detail import SingleObjectMixin
|
||||
from django.contrib import messages
|
||||
|
||||
from .models import BookingCalendar
|
||||
from .utils import get_form_url_with_params
|
||||
from .forms import BookingForm
|
||||
|
||||
|
||||
class BookingView(SingleObjectMixin, View):
|
||||
|
||||
http_method_names = ['post']
|
||||
model = BookingCalendar
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
cell = self.get_object()
|
||||
form = BookingForm(request.POST, cell=cell)
|
||||
try:
|
||||
form.is_valid()
|
||||
except ValueError as exc:
|
||||
messages.error(request, exc.message)
|
||||
redirect_url = '%s?%s' % (
|
||||
cell.page.get_online_url(), request.GET.urlencode())
|
||||
return HttpResponseRedirect(redirect_url)
|
||||
data = form.cleaned_data
|
||||
url = get_form_url_with_params(cell, data)
|
||||
return HttpResponseRedirect(url)
|
|
@ -78,6 +78,7 @@ INSTALLED_APPS = (
|
|||
'combo.apps.search',
|
||||
'combo.apps.usersearch',
|
||||
'combo.apps.maps',
|
||||
'combo.apps.calendar',
|
||||
'haystack',
|
||||
'xstatic.pkg.chartnew_js',
|
||||
'xstatic.pkg.leaflet',
|
||||
|
|
|
@ -13,6 +13,20 @@ KNOWN_SERVICES = {
|
|||
'default': {'title': 'test', 'url': 'http://example.org',
|
||||
'secret': 'combo', 'orig': 'combo',
|
||||
'backoffice-menu-url': 'http://example.org/manage/',}
|
||||
},
|
||||
'chrono': {
|
||||
'default': {
|
||||
'title': 'test', 'url': 'http://chrono.example.org',
|
||||
'secret': 'combo', 'orig': 'combo',
|
||||
'backoffice-menu-url': 'http://chrono.example.org/manage/',
|
||||
'secondary': False,
|
||||
},
|
||||
'other': {
|
||||
'title': 'other', 'url': 'http://other.chrono.example.org',
|
||||
'secret': 'combo', 'orig': 'combo',
|
||||
'backoffice-menu-url': 'http://other.chrono.example.org/manage/',
|
||||
'secondary': True,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,239 @@
|
|||
import json
|
||||
import urlparse
|
||||
import datetime
|
||||
|
||||
import pytest
|
||||
import mock
|
||||
|
||||
from django.utils.dateparse import parse_time
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from combo.data.models import Page
|
||||
from combo.apps.calendar.models import BookingCalendar
|
||||
from combo.apps.calendar.utils import get_calendar, get_chrono_service
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
CHRONO_EVENTS = {
|
||||
"data": [
|
||||
{
|
||||
"disabled": False,
|
||||
"text": "13 juin 2017 08:00",
|
||||
"api": {
|
||||
"fillslot_url": "http://example.net/api/agenda/whatever/fillslot/86/"
|
||||
},
|
||||
"id": 86,
|
||||
"datetime": "2017-06-13 08:00:00"
|
||||
},
|
||||
{
|
||||
"disabled": False,
|
||||
"text": "13 juin 2017 08:30",
|
||||
"api": {
|
||||
"fillslot_url": "http://example.net/api/agenda/whatever/fillslot/87/"
|
||||
},
|
||||
"id": 87,
|
||||
"datetime": "2017-06-13 08:30:00"
|
||||
},
|
||||
{
|
||||
"disabled": False,
|
||||
"text": "13 juin 2017 09:00",
|
||||
"api": {
|
||||
"fillslot_url": "http://example.net/api/agenda/whatever/fillslot/88/"
|
||||
},
|
||||
"id": 88,
|
||||
"datetime": "2017-06-13 09:00:00"
|
||||
},
|
||||
{
|
||||
"disabled": False,
|
||||
"text": "13 juin 2017 09:30",
|
||||
"api": {
|
||||
"fillslot_url": "http://example.net/api/agenda/whatever/fillslot/89/"
|
||||
},
|
||||
"id": 89,
|
||||
"datetime": "2017-06-13 09:30:00"
|
||||
},
|
||||
{
|
||||
"disabled": False,
|
||||
"text": "14 juin 2017 09:30",
|
||||
"api": {
|
||||
"fillslot_url": "http://example.net/api/agenda/whatever/fillslot/89/"
|
||||
},
|
||||
"id": 90,
|
||||
"datetime": "2017-06-14 09:30:00"
|
||||
},
|
||||
{
|
||||
"disabled": False,
|
||||
"text": "14 juin 2017 10:00",
|
||||
"api": {
|
||||
"fillslot_url": "http://example.net/api/agenda/whatever/fillslot/89/"
|
||||
},
|
||||
"id": 91,
|
||||
"datetime": "2017-06-14 10:00:00"
|
||||
},
|
||||
{
|
||||
"disabled": True,
|
||||
"text": "14 juin 2017 15:00",
|
||||
"api": {
|
||||
"fillslot_url": "http://example.net/api/agenda/whatever/fillslot/89/"
|
||||
},
|
||||
"id": 91,
|
||||
"datetime": "2017-06-14 15:00:00"
|
||||
},
|
||||
{
|
||||
"disabled": True,
|
||||
"text": "15 juin 2017 10:00",
|
||||
"api": {
|
||||
"fillslot_url": "http://example.net/api/agenda/whatever/fillslot/89/"
|
||||
},
|
||||
"id": 92,
|
||||
"datetime": "2017-06-15 10:00:00"
|
||||
}
|
||||
|
||||
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
WCS_FORMDEFS = [
|
||||
{
|
||||
"count": 12,
|
||||
"category": "common",
|
||||
"functions": {
|
||||
"_receiver": {
|
||||
"label": "Recipient"
|
||||
},
|
||||
},
|
||||
"authentication_required": False,
|
||||
"description": "",
|
||||
"title": "Demande de place en creche",
|
||||
"url": "http://example.net/demande-de-place-en-creche/",
|
||||
"category_slug": "common",
|
||||
"redirection": False,
|
||||
"keywords": [],
|
||||
"slug": "demande-de-place-en-creche"
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def login(app, username='admin', password='admin'):
|
||||
login_page = app.get('/login/')
|
||||
login_form = login_page.forms[0]
|
||||
login_form['username'] = username
|
||||
login_form['password'] = password
|
||||
resp = login_form.submit()
|
||||
assert resp.status_int == 302
|
||||
return app
|
||||
|
||||
|
||||
def str2datetime(sdt):
|
||||
return datetime.datetime.strptime(sdt, '%Y-%m-%dT%H:%M:%S')
|
||||
|
||||
|
||||
class MockedRequestResponse(mock.Mock):
|
||||
|
||||
def json(self):
|
||||
return json.loads(self.content)
|
||||
|
||||
|
||||
def mocked_requests_get(*args, **kwargs):
|
||||
remote_service = kwargs.get('remote_service')
|
||||
if 'chrono' in remote_service['url']:
|
||||
return MockedRequestResponse(
|
||||
content=json.dumps(CHRONO_EVENTS))
|
||||
else:
|
||||
return MockedRequestResponse(
|
||||
content=json.dumps(WCS_FORMDEFS))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def admin(db):
|
||||
return User.objects.create_superuser(username='admin', password='admin', email=None)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def anonymous(app):
|
||||
return app
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def connected(app, admin):
|
||||
return login(app)
|
||||
|
||||
|
||||
@pytest.fixture(params=['anonymous', 'connected'])
|
||||
def client(request, anonymous, connected):
|
||||
return locals().get(request.param)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def cell(db):
|
||||
page = Page.objects.create(title='whatever', slug='booking', template_name='standard')
|
||||
cell = BookingCalendar(
|
||||
page=page, title='Example Of Calendar',
|
||||
agenda_reference='default:test',
|
||||
formdef_reference='default:test',
|
||||
slot_duration=datetime.timedelta(minutes=30),
|
||||
minimal_booking_duration=datetime.timedelta(hours=1),
|
||||
placeholder='content', order=0
|
||||
)
|
||||
cell.save()
|
||||
return cell
|
||||
|
||||
|
||||
def test_get_chrono_service(settings):
|
||||
service = get_chrono_service()
|
||||
assert service['title'] == 'test'
|
||||
assert service['url'] == 'http://chrono.example.org'
|
||||
assert service['secondary'] is False
|
||||
|
||||
|
||||
@mock.patch('combo.apps.calendar.utils.requests.get', side_effect=mocked_requests_get)
|
||||
def test_cell_rendering(mocked_get, client, cell):
|
||||
page = client.get('/booking/')
|
||||
# test without selecting slots
|
||||
resp = page.form.submit().follow()
|
||||
assert 'Please select slots' in resp.content
|
||||
# test with slots from different day
|
||||
resp.form.set('slots', True, 0)
|
||||
resp.form.set('slots', True, 1)
|
||||
resp.form.set('slots', True, 4)
|
||||
resp = resp.form.submit().follow()
|
||||
# test with non contiguous slots
|
||||
assert 'Please select slots of the same day' in resp.content
|
||||
resp.form.set('slots', True, 0)
|
||||
resp.form.set('slots', True, 2)
|
||||
resp = resp.form.submit().follow()
|
||||
assert 'Please select contiguous slots' in resp.content
|
||||
# test with invalid booking duration
|
||||
resp.form.set('slots', True, 0)
|
||||
resp = resp.form.submit().follow()
|
||||
assert 'Minimal booking duration is 01:00' in resp.content
|
||||
# test with valid selected slots
|
||||
resp.form.set('slots', True, 0)
|
||||
resp.form.set('slots', True, 1)
|
||||
resp.form.set('slots', True, 2)
|
||||
resp = resp.form.submit()
|
||||
parsed = urlparse.urlparse(resp.url)
|
||||
assert parsed.path == '/test/'
|
||||
qs = urlparse.parse_qs(parsed.query)
|
||||
assert qs['session_var_booking_agenda_slug'] == ['test']
|
||||
assert qs['session_var_booking_start'] == ['2017-06-13T08:00:00']
|
||||
assert qs['session_var_booking_end'] == ['2017-06-13T09:30:00']
|
||||
|
||||
|
||||
@mock.patch('combo.apps.calendar.utils.requests.get', side_effect=mocked_requests_get)
|
||||
def test_calendar(mocked_get, cell):
|
||||
cal = get_calendar('default:whatever', cell.slot_duration)[0]
|
||||
assert len(cal.days) == 3
|
||||
for day in cal.get_days():
|
||||
assert day in [
|
||||
str2datetime('2017-06-12T08:00:00').date() + datetime.timedelta(days=i)
|
||||
for i in range(0, 5)]
|
||||
min_slot = str2datetime('2017-06-13T08:00:00')
|
||||
max_slot = str2datetime('2017-06-14T15:00:00')
|
||||
for slot in cal.get_slots():
|
||||
assert (min_slot.time() <= slot <= max_slot.time()) is True
|
||||
assert cal.has_day(min_slot.date()) is True
|
||||
assert cal.get_availability(str2datetime('2017-06-14T15:00:00')).available is False
|
||||
assert cal.get_minimum_slot() == min_slot.time()
|
||||
assert cal.get_maximum_slot() == max_slot.time()
|
||||
assert cal.get_day(max_slot.date()).slots[-1].available is False
|
Loading…
Reference in New Issue