diff --git a/combo/apps/calendar/models.py b/combo/apps/calendar/models.py
index feebea32..f88d8c1f 100644
--- a/combo/apps/calendar/models.py
+++ b/combo/apps/calendar/models.py
@@ -21,8 +21,9 @@ from django.utils.translation import ugettext_lazy as _
from combo.data.models import CellBase
from combo.data.library import register_cell_class
+
from .utils import (is_chrono_enabled, is_wcs_enabled,
- add_paginated_calendar_to_context)
+ get_chrono_events, get_calendar_context_vars)
@register_cell_class
@@ -52,7 +53,15 @@ class BookingCalendar(CellBase):
def is_enabled(cls):
return is_chrono_enabled() and is_wcs_enabled()
- def render(self, context):
- context.update(self.get_cell_extra_context(context))
- context = add_paginated_calendar_to_context(context)
- return super(BookingCalendar, self).render(context)
+ def is_visible(self, user=None):
+ return self.agenda_reference and self.formdef_reference \
+ and super(BookingCalendar, self).is_visible(user=user)
+
+ def get_cell_extra_context(self, context):
+ if context.get('placeholder_search_mode'):
+ return {}
+ extra_context = super(BookingCalendar, self).get_cell_extra_context(context)
+ events_data = get_chrono_events(self.agenda_reference, not(context.get('synchronous')))
+ extra_context.update(get_calendar_context_vars(
+ context['request'], extra_context['cell'], events_data))
+ return extra_context
diff --git a/combo/apps/calendar/templates/calendar/booking_calendar_cell.html b/combo/apps/calendar/templates/calendar/booking_calendar_cell.html
index 099dded7..ac8acc24 100644
--- a/combo/apps/calendar/templates/calendar/booking_calendar_cell.html
+++ b/combo/apps/calendar/templates/calendar/booking_calendar_cell.html
@@ -12,9 +12,14 @@
{% endif %}
{% endif %}
-
-{% include 'calendar/booking_calendar_content.html' %}
-
+
+{% if error %}
+
+{% else %}
+
+ {% include 'calendar/booking_calendar_content.html' %}
+
+{% endif %}
diff --git a/combo/apps/calendar/utils.py b/combo/apps/calendar/utils.py
index ec84f590..846f0d7b 100644
--- a/combo/apps/calendar/utils.py
+++ b/combo/apps/calendar/utils.py
@@ -23,6 +23,7 @@ from django.conf import settings
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.utils.dateparse import parse_datetime
from django.utils.timezone import localtime, make_aware
+from django.utils.translation import ugettext_lazy as _
from combo.utils import requests
@@ -67,22 +68,26 @@ def get_agendas():
return references
-def get_chrono_events(agenda_reference):
+def get_chrono_events(agenda_reference, synchronous):
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)
+ response = requests.get('api/agenda/%s/datetimes/' % chrono_slug, remote_service=chrono,
+ without_user=True, raise_if_not_cached=synchronous)
try:
+ if response.status_code != 200:
+ raise ValueError
result = response.json()
except ValueError:
- return []
- return result.get('data', [])
+ return {'error': _('An error occurred while retrieving calendar\'s availabilities.')}
+ return result
-def add_paginated_calendar_to_context(context):
- request = context['request']
- cell = context['cell']
+def get_calendar_context_vars(request, cell, events_data):
page = request.GET.get('chunk_%s' % cell.pk, 1)
- calendar = get_calendar(cell.agenda_reference, cell.slot_duration, cell.days_displayed,
+ if 'error' in events_data:
+ return events_data
+ events = events_data['data']
+ calendar = get_calendar(events, cell.slot_duration, cell.days_displayed,
cell.minimal_booking_duration)
paginator = Paginator(calendar.get_computed_days(), cell.days_displayed)
try:
@@ -91,16 +96,14 @@ def add_paginated_calendar_to_context(context):
cal_page = paginator.page(1)
except (EmptyPage,):
cal_page = paginator.page(paginator.num_pages)
- context['calendar'] = calendar
- context['calendar_days'] = cal_page
- context['calendar_slots'] = calendar.get_slots()
- return context
+ return {
+ 'calendar': calendar,
+ 'calendar_days': cal_page,
+ 'calendar_slots': calendar.get_slots()
+ }
-def get_calendar(agenda_reference, offset, days_displayed, min_duration):
- if not agenda_reference:
- return []
- events = get_chrono_events(agenda_reference)
+def get_calendar(events, offset, days_displayed, min_duration):
calendar = Calendar(offset, days_displayed, min_duration)
for event in events:
diff --git a/combo/apps/calendar/views.py b/combo/apps/calendar/views.py
index 377c2be9..136279c6 100644
--- a/combo/apps/calendar/views.py
+++ b/combo/apps/calendar/views.py
@@ -21,7 +21,8 @@ from django.views.generic.detail import SingleObjectMixin
from .forms import BookingForm
from .models import BookingCalendar
-from .utils import get_form_url_with_params, add_paginated_calendar_to_context
+from .utils import (get_form_url_with_params, get_chrono_events,
+ get_calendar_context_vars)
class BookingView(SingleObjectMixin, View):
@@ -51,6 +52,6 @@ class CalendarContentAjaxView(DetailView):
def get_context_data(self, **kwargs):
context = super(CalendarContentAjaxView, self).get_context_data(**kwargs)
context['cell'] = self.object
- context['request'] = self.request
- context = add_paginated_calendar_to_context(context)
+ events_data = get_chrono_events(self.object.agenda_reference, context.get('synchronous'))
+ context.update(get_calendar_context_vars(self.request, self.object, events_data))
return context
diff --git a/tests/test_calendar.py b/tests/test_calendar.py
index ec1a92c3..1156fd9e 100644
--- a/tests/test_calendar.py
+++ b/tests/test_calendar.py
@@ -6,10 +6,13 @@ import pytest
import mock
from django.contrib.auth.models import User
+from django.core.cache import cache
+from django.core.urlresolvers import reverse
from combo.data.models import Page
from combo.apps.calendar.models import BookingCalendar
-from combo.apps.calendar.utils import get_calendar, get_chrono_service
+from combo.apps.calendar.utils import (get_calendar, get_chrono_service,
+ get_chrono_events)
pytestmark = pytest.mark.django_db
CHRONO_EVENTS = {
@@ -129,13 +132,14 @@ def str2datetime(sdt):
class MockedRequestResponse(mock.Mock):
+ status_code = 200
+
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']:
+def mocked_requests_send(request, **kwargs):
+ if 'chrono' in request.url:
return MockedRequestResponse(
content=json.dumps(CHRONO_EVENTS))
else:
@@ -143,6 +147,10 @@ def mocked_requests_get(*args, **kwargs):
content=json.dumps(WCS_FORMDEFS))
+def teardown_function(function):
+ cache.clear()
+
+
@pytest.fixture
def admin(db):
return User.objects.create_superuser(username='admin', password='admin', email=None)
@@ -178,6 +186,13 @@ def cell(db):
return cell
+@pytest.fixture
+def async_url(cell):
+ return reverse(
+ 'combo-public-ajax-page-cell',
+ kwargs={'page_pk': cell.page.pk, 'cell_reference': cell.get_reference()})
+
+
def test_get_chrono_service(settings):
service = get_chrono_service()
assert service['title'] == 'test'
@@ -185,8 +200,15 @@ def test_get_chrono_service(settings):
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):
+@mock.patch('combo.apps.calendar.utils.requests.send', side_effect=mocked_requests_send)
+def test_cell_rendering(mocked_send, client, cell, async_url):
+ page = client.get('/booking/')
+ cell_content = page.html.body.find('div', {'class': 'bookingcalendar'})
+ # test async cell loading
+ assert cell_content.text.strip() == 'Loading...'
+ # put data in cache
+ client.get(async_url)
+ # check that data are cached
page = client.get('/booking/')
# test without selecting slots
resp = page.form.submit().follow()
@@ -219,9 +241,9 @@ def test_cell_rendering(mocked_get, client, cell):
assert qs['session_var_booking_end'] == ['2017-06-13T09:30:00+00: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, 7, cell.minimal_booking_duration)
+def test_calendar(cell):
+ events = CHRONO_EVENTS['data']
+ cal = get_calendar(events, cell.slot_duration, 7, cell.minimal_booking_duration)
assert len(cal.days) == 3
for day in cal.get_computed_days():
assert day in [
@@ -238,10 +260,12 @@ def test_calendar(mocked_get, cell):
assert cal.get_day(max_slot.date()).slots[-1].available is False
-@mock.patch('combo.apps.calendar.utils.requests.get', side_effect=mocked_requests_get)
-def test_cell_pagination(mocked_get, client, cell):
+@mock.patch('combo.apps.calendar.utils.requests.send', side_effect=mocked_requests_send)
+def test_cell_pagination(mocked_send, client, cell, async_url):
cell.days_displayed = 2
cell.save()
+ # put data in cache
+ client.get(async_url)
page = client.get('/booking/')
# first page
table = page.html.find('table')
@@ -288,14 +312,17 @@ def test_cell_pagination(mocked_get, client, cell):
assert previous_page_link.attrs['data-content-url'] == '/ajax/calendar/content/%d/?chunk_%d=1' % (cell.pk, cell.pk)
-@mock.patch('combo.apps.calendar.utils.requests.get', side_effect=mocked_requests_get)
-def test_cell_rendering_cal_info(mocked_get, client, cell):
+@mock.patch('combo.apps.calendar.utils.requests.send', side_effect=mocked_requests_send)
+def test_cell_rendering_cal_info(mocked_send, client, cell, async_url):
+ page = client.get('/booking/')
+ # put data in cache
+ client.get(async_url)
page = client.get('/booking/')
title_info = page.html.h2.find('span', {'class': 'calinfo'})
assert title_info.text.strip() == '(Next available slot: June 13, 2017, 8 a.m.)'
-def test_cell_rendering_cal_info_when_available_slots_next_day(client, cell):
+def test_cell_rendering_cal_info_when_available_slots_next_day(client, cell, async_url):
with mock.patch('combo.utils.requests.get') as request_get:
events = CHRONO_EVENTS['data'][::]
for idx in range(2):
@@ -307,12 +334,14 @@ def test_cell_rendering_cal_info_when_available_slots_next_day(client, cell):
return MockedRequestResponse(content=json.dumps(WCS_FORMDEFS))
request_get.side_effect = side_effect
+ # put data in cache
+ client.get(async_url)
page = client.get('/booking/')
title_info = page.html.h2.find('span', {'class': 'calinfo'})
assert title_info.text.strip() == '(Next available slot: June 14, 2017, 9:30 a.m.)'
-def test_cell_rendering_cal_info_when_no_available_slots(client, cell):
+def test_cell_rendering_cal_info_when_no_available_slots(client, cell, async_url):
with mock.patch('combo.utils.requests.get') as request_get:
def side_effect(*args, **kwargs):
if 'chrono' in kwargs['remote_service']['url']:
@@ -320,10 +349,13 @@ def test_cell_rendering_cal_info_when_no_available_slots(client, cell):
return MockedRequestResponse(content=json.dumps(WCS_FORMDEFS))
request_get.side_effect = side_effect
+ # put data in cache
+ client.get(async_url)
page = client.get('/booking/')
title_info = page.html.h2.find('span', {'class': 'calinfo'})
assert title_info.text.strip() == '(No available slots.)'
+
def test_booking_calendar_indexing(cell):
with mock.patch('combo.utils.requests.get') as request_get:
def side_effect(*args, **kwargs):
@@ -333,3 +365,15 @@ def test_booking_calendar_indexing(cell):
request_get.side_effect = side_effect
search_text = cell.render_for_search()
assert 'Example Of Calendar' in search_text
+
+
+def test_cell_async_rendering_failure(client, cell, async_url):
+ with mock.patch('combo.utils.requests.get') as request_get:
+ def side_effect(*args, **kwargs):
+ if 'chrono' in kwargs['remote_service']['url']:
+ return MockedRequestResponse(content=json.dumps({}), status_code=502)
+ return MockedRequestResponse(content=json.dumps(WCS_FORMDEFS))
+
+ request_get.side_effect = side_effect
+ page = client.get(async_url)
+ assert page.html.div.text == "An error occurred while retrieving calendar's availabilities."