Projet

Général

Profil

0001-api-handle-lock-on-fillslots-and-datetimes-17685.patch

Thomas Noël, 05 juin 2018 09:32

Télécharger (11,1 ko)

Voir les différences:

Subject: [PATCH] api: handle lock on fillslots and datetimes (#17685)

 chrono/agendas/models.py |  3 ++
 chrono/api/views.py      | 51 ++++++++++++++++++++++-----
 chrono/settings.py       |  3 ++
 tests/test_api.py        | 75 ++++++++++++++++++++++++++++++++++++++++
 4 files changed, 124 insertions(+), 8 deletions(-)
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
-