From 9d59020e80e53bfbd516afc340bdc2e0c23b42de Mon Sep 17 00:00:00 2001 From: Josue Kouka Date: Tue, 17 Oct 2017 16:13:45 +0200 Subject: [PATCH] booking calendar cell: display first available slot if any (#19460) --- combo/apps/calendar/static/css/calendar.css | 5 +++ .../templates/calendar/booking_calendar_cell.html | 11 +++++- combo/apps/calendar/utils.py | 26 ++++++++++--- tests/test_calendar.py | 43 ++++++++++++++++++++-- 4 files changed, 76 insertions(+), 9 deletions(-) diff --git a/combo/apps/calendar/static/css/calendar.css b/combo/apps/calendar/static/css/calendar.css index 287f89f..f928e4a 100644 --- a/combo/apps/calendar/static/css/calendar.css +++ b/combo/apps/calendar/static/css/calendar.css @@ -1,3 +1,8 @@ div.cell.bookingcalendar a { cursor: pointer; } + +div.cell.bookingcalendar h2 > span.calinfo { + font-style: italic; + font-size: 80%; +} diff --git a/combo/apps/calendar/templates/calendar/booking_calendar_cell.html b/combo/apps/calendar/templates/calendar/booking_calendar_cell.html index c4649d3..1e0a012 100644 --- a/combo/apps/calendar/templates/calendar/booking_calendar_cell.html +++ b/combo/apps/calendar/templates/calendar/booking_calendar_cell.html @@ -1,7 +1,16 @@ {% load i18n calendar %} {% if cell.title %} -

{{cell.title}}

+

+ {{cell.title}} + {% if calendar %} + + {% with calendar.get_first_available_slot as slot %} + ({% if slot %}{% trans "Next available slot:" %} {{ slot.date_time|date:"DATETIME_FORMAT"}}{% else %}{% trans "No available slots." %}{% endif %}) + {% endwith %} + + {% endif %} +

{% endif %}
{% include 'calendar/booking_calendar_content.html' %} diff --git a/combo/apps/calendar/utils.py b/combo/apps/calendar/utils.py index 177610b..ec84f59 100644 --- a/combo/apps/calendar/utils.py +++ b/combo/apps/calendar/utils.py @@ -22,6 +22,7 @@ import urllib 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 combo.utils import requests @@ -81,7 +82,8 @@ def add_paginated_calendar_to_context(context): request = context['request'] cell = context['cell'] page = request.GET.get('chunk_%s' % cell.pk, 1) - calendar = get_calendar(cell.agenda_reference, cell.slot_duration, cell.days_displayed) + calendar = get_calendar(cell.agenda_reference, cell.slot_duration, cell.days_displayed, + cell.minimal_booking_duration) paginator = Paginator(calendar.get_computed_days(), cell.days_displayed) try: cal_page = paginator.page(page) @@ -95,11 +97,12 @@ def add_paginated_calendar_to_context(context): return context -def get_calendar(agenda_reference, offset, days_displayed): +def get_calendar(agenda_reference, offset, days_displayed, min_duration): if not agenda_reference: return [] events = get_chrono_events(agenda_reference) - calendar = Calendar(offset, days_displayed) + calendar = Calendar(offset, days_displayed, min_duration) + for event in events: event_datetime = parse_datetime(event['datetime']) if not calendar.has_day(event_datetime.date()): @@ -129,7 +132,7 @@ def get_form_url_with_params(cell, data): class DaySlot(object): def __init__(self, date_time, available, exist=True): - self.date_time = date_time + self.date_time = localtime(make_aware(date_time)) self.available = available self.exist = exist @@ -170,14 +173,27 @@ class WeekDay(object): class Calendar(object): - def __init__(self, offset, days_displayed): + def __init__(self, offset, days_displayed, min_duration): self.offset = offset self.days_displayed = days_displayed self.days = [] + self.min_duration = min_duration def __repr__(self): return '' + def get_first_available_slot(self): + """return the first available slot that has enough + consecutive available slots to be allowed for booking + """ + required_contiguous_slots = self.min_duration.seconds / self.offset.seconds + for day in self.days: + slots = day.slots + for idx in range(len(slots) - required_contiguous_slots): + if all([x.available for x in slots[idx:idx+required_contiguous_slots]]): + return slots[idx] + return None + def get_slots(self): start = self.get_minimum_slot() end = self.get_maximum_slot() diff --git a/tests/test_calendar.py b/tests/test_calendar.py index ee8dde0..95c4f92 100644 --- a/tests/test_calendar.py +++ b/tests/test_calendar.py @@ -215,13 +215,13 @@ def test_cell_rendering(mocked_get, client, cell): 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'] + assert qs['session_var_booking_start'] == ['2017-06-13T08:00:00+00:00'] + 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) + cal = get_calendar('default:whatever', cell.slot_duration, 7, cell.minimal_booking_duration) assert len(cal.days) == 3 for day in cal.get_computed_days(): assert day in [ @@ -284,3 +284,40 @@ def test_cell_pagination(mocked_get, client, cell): previous_page_link = links[0] assert previous_page_link.text == 'previous' 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): + 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): + with mock.patch('combo.utils.requests.get') as request_get: + events = CHRONO_EVENTS['data'][::] + for idx in range(2): + events[idx]['disabled'] = True + + def side_effect(*args, **kwargs): + if 'chrono' in kwargs['remote_service']['url']: + return MockedRequestResponse(content=json.dumps({"data": events})) + return MockedRequestResponse(content=json.dumps(WCS_FORMDEFS)) + + request_get.side_effect = side_effect + 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): + 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({"data": []})) + return MockedRequestResponse(content=json.dumps(WCS_FORMDEFS)) + + request_get.side_effect = side_effect + page = client.get('/booking/') + title_info = page.html.h2.find('span', {'class': 'calinfo'}) + assert title_info.text.strip() == '(No available slots.)' -- 2.11.0