general: add booking calendar cell (#16393)

This commit is contained in:
Josue Kouka 2017-05-17 18:19:07 +02:00 committed by Frédéric Péters
parent 0665cb8bbd
commit 56b47f5a50
14 changed files with 831 additions and 0 deletions

View File

@ -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'

View File

@ -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

View File

@ -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',
},
),
]

View File

@ -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)

View File

@ -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>

View File

@ -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)

View File

@ -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'),
]

View File

@ -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])

View File

@ -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)

View File

@ -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',

View File

@ -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,
}
}
}

239
tests/test_calendar.py Normal file
View File

@ -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