booking calendar: make cell rendering asynchronous (#19949)

This commit is contained in:
Josue Kouka 2017-11-13 10:27:56 +01:00
parent 1936428327
commit 7b22f9fdf7
5 changed files with 104 additions and 42 deletions

View File

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

View File

@ -12,9 +12,14 @@
{% endif %}
</h2>
{% endif %}
<div class="calcontent">
{% include 'calendar/booking_calendar_content.html' %}
</div>
{% if error %}
<div><p>{{ error }}</p></div>
{% else %}
<div class="calcontent">
{% include 'calendar/booking_calendar_content.html' %}
</div>
{% endif %}
<style>.calinfo { font-style: italic; font-size: 80%; }</style>

View File

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

View File

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

View File

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