0001-api-handle-lock-on-fillslots-and-datetimes-17685.patch
chrono/agendas/models.py | ||
---|---|---|
355 | 355 |
user_name = models.CharField(max_length=250, blank=True) |
356 | 356 |
backoffice_url = models.URLField(blank=True) |
357 | 357 | |
358 |
lock_code = models.CharField(max_length=64, blank=True) |
|
359 |
lock_expiration_datetime = models.DateTimeField(null=True) |
|
360 | ||
358 | 361 |
def save(self, *args, **kwargs): |
359 | 362 |
with transaction.atomic(): |
360 | 363 |
super(Booking, self).save(*args, **kwargs) |
chrono/api/views.py | ||
---|---|---|
19 | 19 |
import datetime |
20 | 20 |
import operator |
21 | 21 | |
22 |
from django.conf import settings |
|
22 | 23 |
from django.core.urlresolvers import reverse |
23 | 24 |
from django.http import Http404 |
24 | 25 |
from django.shortcuts import get_object_or_404 |
... | ... | |
43 | 44 |
return exceptions_by_desk |
44 | 45 | |
45 | 46 | |
46 |
def get_all_slots(agenda, meeting_type): |
|
47 |
def get_all_slots(agenda, meeting_type, lock_code=''):
|
|
47 | 48 |
min_datetime = now() + datetime.timedelta(days=agenda.minimal_booking_delay) |
48 | 49 |
max_datetime = now() + datetime.timedelta(days=agenda.maximal_booking_delay) |
49 | 50 |
min_datetime = min_datetime.replace(hour=0, minute=0, second=0, microsecond=0) |
... | ... | |
75 | 76 |
begin, end = interval |
76 | 77 |
open_slots_by_desk[desk].remove_overlap(localtime(begin), localtime(end)) |
77 | 78 | |
78 |
for event in agenda.event_set.filter( |
|
79 |
agenda=agenda, start_datetime__gte=min_datetime, |
|
80 |
start_datetime__lte=max_datetime + datetime.timedelta(meeting_type.duration)).select_related( |
|
81 |
'meeting_type').exclude( |
|
82 |
booking__cancellation_datetime__isnull=False): |
|
79 |
concerned_events = agenda.event_set.filter( |
|
80 |
agenda=agenda, |
|
81 |
start_datetime__gte=min_datetime, |
|
82 |
start_datetime__lte=max_datetime + datetime.timedelta(meeting_type.duration) |
|
83 |
).select_related('meeting_type') |
|
84 |
booked_events = concerned_events.exclude(booking__cancellation_datetime__isnull=False) |
|
85 |
if lock_code: |
|
86 |
booked_events = concerned_events.exclude(booking__lock_code=lock_code) |
|
87 |
for event in booked_events: |
|
83 | 88 |
for slot in open_slots_by_desk[event.desk_id].search_data(event.start_datetime, event.end_datetime): |
84 | 89 |
slot.full = True |
85 | 90 | |
... | ... | |
218 | 223 |
except (ValueError, MeetingType.DoesNotExist): |
219 | 224 |
raise Http404() |
220 | 225 | |
226 |
lock_code = request.query_params.get('lock_code', '') |
|
227 | ||
221 | 228 |
agenda = meeting_type.agenda |
222 | 229 | |
223 | 230 |
now_datetime = now() |
224 | 231 | |
225 |
slots = get_all_slots(agenda, meeting_type) |
|
232 |
slots = get_all_slots(agenda, meeting_type, lock_code)
|
|
226 | 233 |
entries = {} |
227 | 234 |
for slot in slots: |
228 | 235 |
if slot.start_datetime < now_datetime: |
... | ... | |
311 | 318 |
backoffice_url = serializers.URLField(allow_blank=True) |
312 | 319 |
count = serializers.IntegerField(min_value=1) |
313 | 320 | |
321 |
lock_code = serializers.CharField(max_length=64, allow_blank=True) |
|
322 |
lock_duration = serializers.IntegerField(min_value=0) # in seconds |
|
323 |
confirm_after_lock = serializers.BooleanField() |
|
324 | ||
314 | 325 | |
315 | 326 |
class SlotsSerializer(SlotSerializer): |
316 | 327 |
''' |
... | ... | |
370 | 381 |
else: |
371 | 382 |
places_count = 1 |
372 | 383 | |
384 |
lock_code = '' |
|
385 |
lock_expiration_datetime = None |
|
386 |
confirm_after_lock = False |
|
387 |
if 'lock_code' in payload: |
|
388 |
lock_code = payload['lock_code'] or '' |
|
389 |
if 'lock_duration' in payload: |
|
390 |
lock_duration = payload['lock_duration'] |
|
391 |
else: |
|
392 |
lock_duration = settings.CHRONO_LOCK_DURATION |
|
393 |
lock_expiration_datetime = now() + datetime.timedelta(seconds=lock_duration) |
|
394 |
confirm_after_lock = payload.get('confirm_after_lock') or False |
|
395 | ||
373 | 396 |
extra_data = {} |
374 | 397 |
for k, v in request.data.items(): |
375 | 398 |
if k not in serializer.validated_data: |
... | ... | |
392 | 415 |
datetimes.add(make_aware(datetime.datetime.strptime(datetime_str, '%Y-%m-%d-%H%M'))) |
393 | 416 | |
394 | 417 |
# get all free slots and separate them by desk |
395 |
all_slots = get_all_slots(agenda, MeetingType.objects.get(id=meeting_type_id)) |
|
418 |
all_slots = get_all_slots(agenda, MeetingType.objects.get(id=meeting_type_id), lock_code)
|
|
396 | 419 |
all_slots = [slot for slot in all_slots if not slot.full] |
397 | 420 |
datetimes_by_desk = defaultdict(set) |
398 | 421 |
for slot in all_slots: |
... | ... | |
438 | 461 | |
439 | 462 |
# now we have a list of events, book them. |
440 | 463 |
primary_booking = None |
464 |
new_bookings = [] |
|
441 | 465 |
for event in events: |
442 | 466 |
for i in range(places_count): |
443 | 467 |
new_booking = Booking(event_id=event.id, |
... | ... | |
448 | 472 |
extra_data=extra_data) |
449 | 473 |
if primary_booking is not None: |
450 | 474 |
new_booking.primary_booking = primary_booking |
475 |
if lock_code and not confirm_after_lock: |
|
476 |
new_booking.lock_code = lock_code |
|
477 |
new_booking.lock_expiration_datetime = lock_expiration_datetime |
|
451 | 478 |
new_booking.save() |
479 |
new_bookings.append(new_booking.id) |
|
452 | 480 |
if primary_booking is None: |
453 | 481 |
primary_booking = new_booking |
454 | 482 | |
483 |
# remove past locks and related fake events |
|
484 |
if lock_code: |
|
485 |
old_bookings = Booking.objects.filter(lock_code=lock_code).exclude(id__in=new_bookings) |
|
486 |
if agenda.kind == 'meetings': |
|
487 |
Event.objects.filter(booking__in=old_bookings).delete() |
|
488 |
old_bookings.delete() |
|
489 | ||
455 | 490 |
response = { |
456 | 491 |
'err': 0, |
457 | 492 |
'in_waiting_list': in_waiting_list, |
chrono/settings.py | ||
---|---|---|
164 | 164 |
# (see http://docs.python-requests.org/en/master/user/advanced/#proxies) |
165 | 165 |
REQUESTS_PROXIES = None |
166 | 166 | |
167 |
# default lock duration, in seconds |
|
168 |
CHRONO_LOCK_DURATION = 10*60 |
|
169 | ||
167 | 170 |
local_settings_file = os.environ.get('CHRONO_SETTINGS_FILE', |
168 | 171 |
os.path.join(os.path.dirname(__file__), 'local_settings.py')) |
169 | 172 |
if os.path.exists(local_settings_file): |
tests/test_api.py | ||
---|---|---|
1541 | 1541 |
# them. |
1542 | 1542 |
resp = app.get(api_url) |
1543 | 1543 |
assert len([x for x in resp.json['data'] if x['disabled']]) == 2 |
1544 | ||
1545 | ||
1546 |
def test_booking_api_meeting_lock_and_confirm(app, meetings_agenda, user): |
|
1547 |
agenda_id = meetings_agenda.slug |
|
1548 |
meeting_type = MeetingType.objects.get(agenda=meetings_agenda) |
|
1549 | ||
1550 |
# list free slots, with or without a lock |
|
1551 |
resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id) |
|
1552 |
free_slots = len(resp.json['data']) |
|
1553 |
resp = app.get('/api/agenda/meetings/%s/datetimes/?lock_code=OTHERLOCK' % meeting_type.id) |
|
1554 |
assert free_slots == len(resp.json['data']) |
|
1555 |
resp = app.get('/api/agenda/meetings/%s/datetimes/?lock_code=MYLOCK' % meeting_type.id) |
|
1556 |
assert free_slots == len(resp.json['data']) |
|
1557 | ||
1558 |
# lock a slot |
|
1559 |
event_id = resp.json['data'][2]['id'] |
|
1560 |
assert urlparse.urlparse(resp.json['data'][2]['api']['fillslot_url'] |
|
1561 |
).path == '/api/agenda/%s/fillslot/%s/' % (meetings_agenda.slug, event_id) |
|
1562 |
app.authorization = ('Basic', ('john.doe', 'password')) |
|
1563 |
resp_lock = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id), |
|
1564 |
params={'lock_code': 'MYLOCK'}) |
|
1565 |
assert Booking.objects.count() == 1 |
|
1566 |
assert Booking.objects.all()[0].lock_code == 'MYLOCK' |
|
1567 |
assert Booking.objects.all()[0].lock_expiration_datetime is not None |
|
1568 | ||
1569 |
# list free slots: one is locked ... |
|
1570 |
resp2 = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id) |
|
1571 |
assert free_slots == len([x for x in resp2.json['data']]) |
|
1572 |
assert len([x for x in resp2.json['data'] if x.get('disabled')]) == 1 |
|
1573 | ||
1574 |
resp2 = app.get('/api/agenda/meetings/%s/datetimes/?lock_code=OTHERLOCK' % meeting_type.id) |
|
1575 |
assert free_slots == len([x for x in resp2.json['data']]) |
|
1576 |
assert len([x for x in resp2.json['data'] if x.get('disabled')]) == 1 |
|
1577 | ||
1578 |
# ... unless it's MYLOCK |
|
1579 |
resp2 = app.get('/api/agenda/meetings/%s/datetimes/?lock_code=MYLOCK' % meeting_type.id) |
|
1580 |
assert free_slots == len([x for x in resp2.json['data']]) |
|
1581 |
assert len([x for x in resp2.json['data'] if x.get('disabled')]) == 0 |
|
1582 | ||
1583 |
# can't lock the same timeslot ... |
|
1584 |
resp_booking = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id), |
|
1585 |
params={'lock_code': 'OTHERLOCK'}) |
|
1586 |
assert resp_booking.json['err'] == 1 |
|
1587 |
assert resp_booking.json['reason'] == 'no more desk available' |
|
1588 | ||
1589 |
# ... unless with MYLOCK (aka "relock") |
|
1590 |
resp_booking = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id), |
|
1591 |
params={'lock_code': 'MYLOCK'}) |
|
1592 |
assert resp_booking.json['err'] == 0 |
|
1593 |
assert Booking.objects.count() == 1 |
|
1594 |
assert Booking.objects.all()[0].lock_code == 'MYLOCK' |
|
1595 |
assert Booking.objects.all()[0].lock_expiration_datetime is not None |
|
1596 | ||
1597 |
# can't book the slot ... |
|
1598 |
resp_booking = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id)) |
|
1599 |
assert resp_booking.json['err'] == 1 |
|
1600 |
assert resp_booking.json['reason'] == 'no more desk available' |
|
1601 | ||
1602 |
resp_booking = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id), |
|
1603 |
params={'confirm_after_lock': True}) |
|
1604 |
assert resp_booking.json['err'] == 1 |
|
1605 |
assert resp_booking.json['reason'] == 'no more desk available' |
|
1606 | ||
1607 |
resp_booking = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id), |
|
1608 |
params={'lock_code': 'OTHERLOCK', 'confirm_after_lock': True}) |
|
1609 |
assert resp_booking.json['err'] == 1 |
|
1610 |
assert resp_booking.json['reason'] == 'no more desk available' |
|
1611 | ||
1612 |
# ... unless with MYLOCK (aka "confirm") |
|
1613 |
resp_booking = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id), |
|
1614 |
params={'lock_code': 'MYLOCK', 'confirm_after_lock': True}) |
|
1615 |
assert resp_booking.json['err'] == 0 |
|
1616 |
assert Booking.objects.count() == 1 |
|
1617 |
assert Booking.objects.all()[0].lock_code == '' |
|
1618 |
assert Booking.objects.all()[0].lock_expiration_datetime is None |
|
1544 |
- |