0001-booking-calendar-cell-display-first-available-slot-i.patch
combo/apps/calendar/static/css/calendar.css | ||
---|---|---|
1 | 1 |
div.cell.bookingcalendar a { |
2 | 2 |
cursor: pointer; |
3 | 3 |
} |
4 | ||
5 |
div.cell.bookingcalendar h2 > span.calinfo { |
|
6 |
font-style: italic; |
|
7 |
font-size: 13px; |
|
8 |
} |
combo/apps/calendar/templates/calendar/booking_calendar_cell.html | ||
---|---|---|
1 | 1 |
{% load i18n calendar %} |
2 | 2 | |
3 | 3 |
{% if cell.title %} |
4 |
<h2>{{cell.title}}</h2> |
|
4 |
<h2> |
|
5 |
<span>{{cell.title}}</span> |
|
6 |
{% if calendar %} |
|
7 |
<span class="calinfo">{{ calendar.get_info }}</span> |
|
8 |
{% endif %} |
|
9 |
</h2> |
|
5 | 10 |
{% endif %} |
6 | 11 |
<div> |
7 | 12 |
{% include 'calendar/booking_calendar_content.html' %} |
combo/apps/calendar/utils.py | ||
---|---|---|
18 | 18 |
import math |
19 | 19 |
import urllib |
20 | 20 | |
21 | ||
22 | 21 |
from django.conf import settings |
23 | 22 |
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger |
23 |
from django.utils.dateformat import DateFormat |
|
24 | 24 |
from django.utils.dateparse import parse_datetime |
25 |
from django.utils.timezone import now |
|
25 |
from django.utils.formats import get_format |
|
26 |
from django.utils.timezone import localtime, make_aware, now |
|
27 |
from django.utils.translation import ugettext_lazy as _ |
|
26 | 28 | |
27 | 29 | |
28 | 30 |
from combo.utils import requests |
... | ... | |
85 | 87 |
request = context['request'] |
86 | 88 |
cell = context['cell'] |
87 | 89 |
page = request.GET.get('chunk_%s' % cell.pk, 1) |
88 |
calendar = get_calendar(cell.agenda_reference, cell.slot_duration, cell.chunk_size) |
|
90 |
calendar = get_calendar(cell.agenda_reference, cell.slot_duration, cell.chunk_size, cell.minimal_booking_duration)
|
|
89 | 91 |
paginator = Paginator(list(calendar.get_days()), cell.chunk_size) |
90 | 92 |
try: |
91 | 93 |
cal_page = paginator.page(page) |
... | ... | |
99 | 101 |
return context |
100 | 102 | |
101 | 103 | |
102 |
<<<<<<< HEAD |
|
103 |
def get_calendar(agenda_reference, offset, chunk_size): |
|
104 |
def get_calendar(agenda_reference, offset, chunk_size, min_duration): |
|
104 | 105 |
if not agenda_reference: |
105 | 106 |
return [] |
106 | 107 |
events = get_chrono_events(agenda_reference) |
107 |
calendar = Calendar(offset, chunk_size) |
|
108 |
======= |
|
109 |
def get_calendar(agenda_reference, offset): |
|
110 |
if not agenda_reference: |
|
111 |
return [] |
|
112 |
events = get_chrono_events(agenda_reference) |
|
113 |
calendar = Calendar(offset) |
|
114 |
>>>>>>> booking calendar: display availablilities in rolling days (#19368) |
|
108 |
calendar = Calendar(offset, chunk_size, min_duration) |
|
115 | 109 |
for event in events: |
116 | 110 |
event_datetime = parse_datetime(event['datetime']) |
117 | 111 |
if not calendar.has_day(event_datetime.date()): |
... | ... | |
141 | 135 |
class DaySlot(object): |
142 | 136 | |
143 | 137 |
def __init__(self, date_time, available, exist=True): |
144 |
self.date_time = date_time
|
|
138 |
self.date_time = localtime(make_aware(date_time))
|
|
145 | 139 |
self.available = available |
146 | 140 |
self.exist = exist |
147 | 141 | |
148 | 142 |
def __repr__(self): |
149 | 143 |
return '<DaySlot date_time=%s - available=%s>' % (self.date_time.isoformat(), self.available) |
150 | 144 | |
145 |
def __str__(self): |
|
146 |
date = DateFormat(self.date_time).format(get_format('DATE_FORMAT')) |
|
147 |
return _('%s at %s') % (date, self.date_time.strftime('%H:%M')) |
|
148 | ||
151 | 149 |
@property |
152 | 150 |
def label(self): |
153 | 151 |
return '%s' % self.date_time.isoformat() |
... | ... | |
179 | 177 |
def get_maximum_slot(self): |
180 | 178 |
return max(self.slots, key=lambda x: x.date_time.time()) |
181 | 179 | |
180 |
def get_first_available_slot(self, offset, min_duration): |
|
181 |
step = min_duration.seconds / offset.seconds |
|
182 |
for idx in range(len(self.slots)): |
|
183 |
tmp = self.slots[idx:idx + step] |
|
184 |
if (tmp[-1].date_time - tmp[0].date_time) != ((step - 1) * offset): |
|
185 |
continue |
|
186 |
if not all(map(lambda x: x.available, tmp)): |
|
187 |
continue |
|
188 |
return tmp[0] |
|
189 |
return None |
|
190 | ||
182 | 191 | |
183 | 192 |
class Calendar(object): |
184 | 193 | |
185 |
def __init__(self, offset, chunk_size): |
|
194 |
def __init__(self, offset, chunk_size, min_duration):
|
|
186 | 195 |
self.offset = offset |
187 | 196 |
self.chunk_size = chunk_size |
188 | 197 |
self.days = [] |
198 |
self.min_duration = min_duration |
|
189 | 199 | |
190 | 200 |
def __repr__(self): |
191 | 201 |
return '<Calendar>' |
192 | 202 | |
203 |
def get_info(self): |
|
204 |
for day in self.days: |
|
205 |
first_available_slot = day.get_first_available_slot(self.offset, self.min_duration) |
|
206 |
if first_available_slot: |
|
207 |
return _('(Next available slot: %s)') % first_available_slot |
|
208 |
return _('(No slot available)') |
|
209 | ||
193 | 210 |
def get_slots(self): |
194 | 211 |
start = self.get_minimum_slot() |
195 | 212 |
end = self.get_maximum_slot() |
tests/test_calendar.py | ||
---|---|---|
5 | 5 |
import pytest |
6 | 6 |
import mock |
7 | 7 | |
8 |
from django.utils.dateparse import parse_time |
|
9 | 8 |
from django.utils.timezone import now |
10 | 9 |
from django.contrib.auth.models import User |
11 | 10 | |
... | ... | |
222 | 221 |
assert parsed.path == '/test/' |
223 | 222 |
qs = urlparse.parse_qs(parsed.query) |
224 | 223 |
assert qs['session_var_booking_agenda_slug'] == ['test'] |
225 |
assert qs['session_var_booking_start'] == ['2017-06-13T08:00:00'] |
|
226 |
assert qs['session_var_booking_end'] == ['2017-06-13T09:30:00'] |
|
224 |
assert qs['session_var_booking_start'] == ['2017-06-13T08:00:00+00:00']
|
|
225 |
assert qs['session_var_booking_end'] == ['2017-06-13T09:30:00+00:00']
|
|
227 | 226 | |
228 | 227 | |
229 | 228 |
@mock.patch('combo.apps.calendar.utils.now', mocked_now) |
230 | 229 |
@mock.patch('combo.apps.calendar.utils.requests.get', side_effect=mocked_requests_get) |
231 | 230 |
def test_calendar(mocked_get, cell): |
232 |
cal = get_calendar('default:whatever', cell.slot_duration, 7) |
|
231 |
cal = get_calendar('default:whatever', cell.slot_duration, 7, cell.minimal_booking_duration)
|
|
233 | 232 |
assert len(cal.days) == 3 |
234 | 233 |
for day in cal.get_days(): |
235 | 234 |
assert day in [ |
... | ... | |
244 | 243 |
assert cal.get_minimum_slot() == min_slot.time() |
245 | 244 |
assert cal.get_maximum_slot() == max_slot.time() |
246 | 245 |
assert cal.get_day(max_slot.date()).slots[-1].available is False |
246 | ||
247 | ||
248 |
@mock.patch('combo.apps.calendar.utils.requests.get', side_effect=mocked_requests_get) |
|
249 |
def test_cell_rendering_cal_info(mocked_get, client, cell): |
|
250 |
page = client.get('/booking/') |
|
251 |
assert '(Next available slot: June 13' in page.content |
|
252 |
# test when no slot is available |
|
253 |
with mock.patch('combo.utils.requests.get') as request_get: |
|
254 |
def side_effect(*args, **kwargs): |
|
255 |
if 'chrono' in kwargs['remote_service']['url']: |
|
256 |
return MockedRequestResponse(content=json.dumps({"data": []})) |
|
257 |
return MockedRequestResponse(content=json.dumps(WCS_FORMDEFS)) |
|
258 | ||
259 |
request_get.side_effect = side_effect |
|
260 |
page = client.get('/booking/') |
|
261 |
assert '(No slot available)' in page.content |
|
247 |
- |