booking calendar: make cell rendering asynchronous (#19949)
This commit is contained in:
parent
1936428327
commit
7b22f9fdf7
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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."
|
||||
|
|
Loading…
Reference in New Issue