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 %} +

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