Projet

Général

Profil

0001-api-add-lock_code-parameter-to-fillslot-and-datetime.patch

Benjamin Dauvergne, 18 mai 2021 20:02

Télécharger (30,9 ko)

Voir les différences:

Subject: [PATCH] api: add lock_code parameter to fillslot and datetimes
 (#17685)

 chrono/agendas/migrations/0087_lease.py |  64 +++++++
 chrono/agendas/models.py                |  16 ++
 chrono/api/views.py                     | 142 ++++++++++++--
 chrono/settings.py                      |   3 +
 tests/test_api.py                       | 235 +++++++++++++++++++++++-
 tests/test_locks.py                     | 102 ++++++++++
 6 files changed, 541 insertions(+), 21 deletions(-)
 create mode 100644 chrono/agendas/migrations/0087_lease.py
 create mode 100644 tests/test_locks.py
chrono/agendas/migrations/0087_lease.py
1
# Generated by Django 2.2.19 on 2021-03-16 13:44
2

  
3
import django.db.models.deletion
4
from django.db import migrations, models
5

  
6
sql_forwards = """
7
ALTER TABLE agendas_lease
8
ADD CONSTRAINT lease_desk_constraint
9
EXCLUDE USING GIST(desk_id WITH =, tstzrange(start_datetime, end_datetime) WITH &&)
10
    WHERE (desk_id IS NOT NULL);
11
ALTER TABLE agendas_lease
12
ADD CONSTRAINT lease_resource_constraint
13
EXCLUDE USING GIST(resource_id WITH =, tstzrange(start_datetime, end_datetime) WITH &&)
14
    WHERE (resource_id IS NOT NULL);
15
"""
16

  
17
sql_backwards = """
18
ALTER TABLE agendas_lease DROP CONSTRAINT lease_desk_constraint;
19
ALTER TABLE agendas_lease DROP CONSTRAINT lease_resource_constraint;
20
"""
21

  
22

  
23
class Migration(migrations.Migration):
24
    dependencies = [
25
        ('agendas', '0086_booking_user_block_template'),
26
    ]
27

  
28
    operations = [
29
        migrations.CreateModel(
30
            name='Lease',
31
            fields=[
32
                (
33
                    'id',
34
                    models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
35
                ),
36
                ('lock_code', models.CharField(max_length=64, verbose_name='Lock code')),
37
                ('lock_expiration_datetime', models.DateTimeField(verbose_name='Lock expiration time')),
38
                ('start_datetime', models.DateTimeField(verbose_name='Start')),
39
                ('end_datetime', models.DateTimeField(verbose_name='End')),
40
                (
41
                    'agenda',
42
                    models.ForeignKey(
43
                        null=True, on_delete=django.db.models.deletion.CASCADE, to='agendas.Agenda'
44
                    ),
45
                ),
46
                (
47
                    'desk',
48
                    models.ForeignKey(
49
                        null=True, on_delete=django.db.models.deletion.CASCADE, to='agendas.Desk'
50
                    ),
51
                ),
52
                (
53
                    'resource',
54
                    models.ForeignKey(
55
                        null=True, on_delete=django.db.models.deletion.CASCADE, to='agendas.Resource'
56
                    ),
57
                ),
58
            ],
59
            options={
60
                'index_together': {('start_datetime', 'end_datetime')},
61
            },
62
        ),
63
        migrations.RunSQL(sql=sql_forwards, reverse_sql=sql_backwards),
64
    ]
chrono/agendas/models.py
2615 2615
    @property
2616 2616
    def base_slug(self):
2617 2617
        return slugify(self.label)
2618

  
2619

  
2620
class Lease(models.Model):
2621
    desk = models.ForeignKey(Desk, null=True, on_delete=models.CASCADE)
2622
    resource = models.ForeignKey(Resource, null=True, on_delete=models.CASCADE)
2623
    agenda = models.ForeignKey(Agenda, null=True, on_delete=models.CASCADE)
2624
    lock_code = models.CharField(_('Lock code'), max_length=64, blank=False)
2625
    lock_expiration_datetime = models.DateTimeField(_('Lock expiration time'))
2626
    start_datetime = models.DateTimeField(_('Start'))
2627
    end_datetime = models.DateTimeField(_('End'))
2628

  
2629
    class Meta:
2630
        index_together = (('start_datetime', 'end_datetime'),)
2631

  
2632
    def as_interval(self):
2633
        return Interval(self.start_datetime, self.end_datetime)
chrono/api/views.py
19 19
import itertools
20 20
import uuid
21 21

  
22
from django.db import transaction
22
from django.conf import settings
23
from django.db import IntegrityError, transaction
23 24
from django.db.models import Prefetch, Q
24 25
from django.http import Http404, HttpResponse
25 26
from django.shortcuts import get_object_or_404
......
45 46
    BookingColor,
46 47
    Desk,
47 48
    Event,
49
    Lease,
48 50
    MeetingType,
49 51
    TimePeriodException,
50 52
)
......
84 86
    start_datetime=None,
85 87
    end_datetime=None,
86 88
    excluded_user_external_id=None,
89
    lock_code=None,
87 90
):
88 91
    """Get all occupation state of all possible slots for the given agenda (of
89 92
    its real agendas for a virtual agenda) and the given meeting_type.
......
101 104
      min/max_datetime; for each time slot check its status in the exclusion
102 105
      and bookings sets.
103 106
      If it is excluded, ignore it completely.
104
      It if is booked, report the slot as full.
107
      If it is booked, report the slot as full.
108
      If it is booked but match the lock code, report the slot as open.
109

  
105 110
    """
106 111
    resources = resources or []
107 112
    # virtual agendas have one constraint :
......
267 272
            for event_start_datetime, event_duration in booked_events
268 273
        )
269 274

  
275
    # delete old locks
276
    Lease.objects.filter(lock_expiration_datetime__lt=now()).delete()
277
    # aggregate non-expired locked time slots
278
    desk_locked_intervals = collections.defaultdict(lambda: IntervalSet())
279
    resource_locked_intervals = IntervalSet()
280
    q = Q(agenda__in=agendas)
281
    if resources:
282
        q |= Q(resource__in=resources)
283
    for lock in (
284
        Lease.objects
285
        # only lock related to on of the agenda or the resource
286
        .filter(q)
287
        .exclude(lock_code=lock_code)
288
        .order_by('start_datetime', 'end_datetime')
289
    ):
290
        if lock.desk:
291
            desk_locked_intervals[lock.desk_id].add(lock.start_datetime, lock.end_datetime)
292
        if resources and lock.resource:
293
            resource_locked_intervals.add(lock.start_datetime, lock.end_datetime)
294

  
270 295
    unique_booked = {}
271 296
    for time_period in base_agenda.get_effective_time_periods():
272 297
        duration = (
......
313 338

  
314 339
                    # slot is full if an already booked event overlaps it
315 340
                    # check resources first
316
                    booked = resources_bookings.overlaps(start_datetime, end_datetime)
341
                    booked = False
342
                    if resources:
343
                        if not booked:
344
                            booked = resources_bookings.overlaps(start_datetime, end_datetime)
345
                        if not booked:
346
                            booked = resource_locked_intervals.overlaps(start_datetime, end_datetime)
317 347
                    # then check user boookings
318 348
                    if not booked:
319 349
                        booked = user_bookings.overlaps(start_datetime, end_datetime)
......
322 352
                        booked = desk.id in bookings and bookings[desk.id].overlaps(
323 353
                            start_datetime, end_datetime
324 354
                        )
355
                    # then locks
356
                    if not booked and desk.id in desk_locked_intervals:
357
                        booked = desk_locked_intervals[desk.id].overlaps(start_datetime, end_datetime)
325 358
                    if unique and unique_booked.get(timestamp) is booked:
326 359
                        continue
327 360
                    unique_booked[timestamp] = booked
......
718 751
        start_datetime, end_datetime = get_start_and_end_datetime_from_request(request)
719 752
        user_external_id = request.GET.get('exclude_user_external_id') or None
720 753

  
754
        lock_code = request.GET.get('lock_code', None)
755
        if lock_code == '':
756
            raise APIError(
757
                _('lock_code must not be empty'),
758
                err_class='lock_code must not be empty',
759
                http_status=status.HTTP_400_BAD_REQUEST,
760
            )
761

  
721 762
        # Generate an unique slot for each possible meeting [start_datetime,
722 763
        # end_datetime] range.
723 764
        # First use get_all_slots() to get each possible meeting by desk and
......
740 781
                    start_datetime=start_datetime,
741 782
                    end_datetime=end_datetime,
742 783
                    excluded_user_external_id=user_external_id,
784
                    lock_code=lock_code,
743 785
                )
744 786
            )
745 787
            for slot in sorted(all_slots, key=lambda slot: slot[:3]):
......
937 979
    force_waiting_list = serializers.BooleanField(default=False)
938 980
    use_color_for = serializers.CharField(max_length=250, allow_blank=True)
939 981

  
982
    lock_code = serializers.CharField(max_length=64, required=False, allow_blank=True)
983
    lock_duration = serializers.IntegerField(
984
        min_value=0, default=lambda: settings.CHRONO_LOCK_DURATION
985
    )  # in seconds
986
    confirm_after_lock = serializers.BooleanField(default=False)
987

  
940 988

  
941 989
class StringOrListField(serializers.ListField):
942 990
    def to_internal_value(self, data):
......
982 1030
            )
983 1031
        payload = serializer.validated_data
984 1032

  
1033
        lock_code = payload.get('lock_code')
1034
        if lock_code == '':  # lock_code should be absent or a non-empty string
1035
            raise APIError(
1036
                _('lock_code cannot be empty'),
1037
                err_class='invalid payload',
1038
                errors=serializer.errors,
1039
                http_status=status.HTTP_400_BAD_REQUEST,
1040
            )
1041

  
985 1042
        if 'slots' in payload:
986 1043
            slots = payload['slots']
987 1044
        if not slots:
......
1104 1161
                    meeting_type,
1105 1162
                    resources=resources,
1106 1163
                    excluded_user_external_id=user_external_id if exclude_user else None,
1164
                    lock_code=lock_code,
1107 1165
                ),
1108 1166
                key=lambda slot: slot.start_datetime,
1109 1167
            )
......
1181 1239
            # booking requires real Event objects (not lazy Timeslots);
1182 1240
            # create them now, with data from the slots and the desk we found.
1183 1241
            events = []
1184
            for start_datetime in datetimes:
1185
                event = Event.objects.create(
1186
                    agenda=available_desk.agenda,
1187
                    slug=str(uuid.uuid4()),  # set slug to avoid queries during slug generation
1188
                    meeting_type=meeting_type,
1189
                    start_datetime=start_datetime,
1190
                    full=False,
1191
                    places=1,
1192
                    desk=available_desk,
1193
                )
1194
                if resources:
1195
                    event.resources.add(*resources)
1196
                events.append(event)
1242
            if not lock_code or payload.get('confirm_after_lock'):
1243
                for start_datetime in datetimes:
1244
                    event = Event.objects.create(
1245
                        agenda=available_desk.agenda,
1246
                        slug=str(uuid.uuid4()),  # set slug to avoid queries during slug generation
1247
                        meeting_type=meeting_type,
1248
                        start_datetime=start_datetime,
1249
                        full=False,
1250
                        places=1,
1251
                        desk=available_desk,
1252
                    )
1253
                    if resources:
1254
                        event.resources.add(*resources)
1255
                    events.append(event)
1256
            else:
1257
                # remove existing locks
1258
                Lease.objects.filter(lock_code=lock_code).delete()
1259

  
1260
                # create new locks
1261
                lock_duration = payload.get('lock_duration')
1262
                if not lock_duration or lock_duration < settings.CHRONO_LOCK_DURATION:
1263
                    lock_duration = settings.CHRONO_LOCK_DURATION
1264
                lock_expiration_datetime = now() + datetime.timedelta(seconds=lock_duration)
1265
                meeting_duration = datetime.timedelta(minutes=meeting_type.duration)
1266
                locks = []
1267
                for start_datetime in datetimes:
1268
                    locks.append(
1269
                        Lease(
1270
                            desk=available_desk,
1271
                            agenda=available_desk.agenda,
1272
                            lock_code=lock_code,
1273
                            lock_expiration_datetime=lock_expiration_datetime,
1274
                            start_datetime=start_datetime,
1275
                            end_datetime=start_datetime + meeting_duration,
1276
                        )
1277
                    )
1278
                    for resource in resources:
1279
                        locks.append(
1280
                            Lease(
1281
                                resource=resource,
1282
                                lock_code=lock_code,
1283
                                lock_expiration_datetime=lock_expiration_datetime,
1284
                                start_datetime=start_datetime,
1285
                                end_datetime=start_datetime + meeting_duration,
1286
                            )
1287
                        )
1288
                try:
1289
                    with transaction.atomic():
1290
                        Lease.objects.bulk_create(locks)
1291
                except IntegrityError:
1292
                    raise APIError(
1293
                        _('no more desk available'),
1294
                        err_class='no more desk available',
1295
                    )
1296
                else:
1297
                    return Response({'err': 0})
1197 1298
        else:
1299
            if lock_code:
1300
                raise APIError(
1301
                    _('lock_code does not work with events'),
1302
                    err_class='lock_code does not work with events',
1303
                    http_status=status.HTTP_400_BAD_REQUEST,
1304
                )
1198 1305
            # convert event recurrence identifiers to real event slugs
1199 1306
            for i, slot in enumerate(slots.copy()):
1200 1307
                if ':' not in slot:
......
1260 1367
                cancelled_booking_id = to_cancel_booking.pk
1261 1368
                to_cancel_booking.cancel()
1262 1369

  
1370
            if lock_code:
1371
                Lease.objects.filter(lock_code=lock_code).delete()
1372

  
1263 1373
            # now we have a list of events, book them.
1264 1374
            primary_booking = None
1265 1375
            for event in events:
chrono/settings.py
169 169
# (see http://docs.python-requests.org/en/master/user/advanced/#proxies)
170 170
REQUESTS_PROXIES = None
171 171

  
172
# default lock duration, in seconds
173
CHRONO_LOCK_DURATION = 10 * 60
174

  
172 175
# timeout used in python-requests call, in seconds
173 176
# we use 28s by default: timeout just before web server, which is usually 30s
174 177
REQUESTS_TIMEOUT = 28
tests/test_api.py
20 20
    Category,
21 21
    Desk,
22 22
    Event,
23
    Lease,
23 24
    MeetingType,
24 25
    Resource,
25 26
    TimePeriod,
......
948 949
    )
949 950
    with CaptureQueriesContext(connection) as ctx:
950 951
        resp = app.get(api_url)
951
        assert len(ctx.captured_queries) == 10
952
        assert len(ctx.captured_queries) == 12
952 953
    assert len(resp.json['data']) == 32
953 954
    assert [s['datetime'] for s in resp.json['data'] if s['disabled'] is True] == [
954 955
        '%s 09:00:00' % tomorrow_str,
......
1160 1161
            '/api/agenda/%s/meetings/%s/datetimes/' % (agenda.slug, meeting_type.slug),
1161 1162
            params={'exclude_user_external_id': '42'},
1162 1163
        )
1163
        assert len(ctx.captured_queries) == 9
1164
        assert len(ctx.captured_queries) == 11
1164 1165
    assert resp.json['data'][0]['id'] == 'foo-bar:2021-02-26-0900'
1165 1166
    assert resp.json['data'][0]['disabled'] is True
1166 1167
    assert resp.json['data'][2]['id'] == 'foo-bar:2021-02-26-1000'
......
5167 5168
    with CaptureQueriesContext(connection) as ctx:
5168 5169
        resp = app.get(api_url)
5169 5170
        assert len(resp.json['data']) == 12
5170
        assert len(ctx.captured_queries) == 10
5171
        assert len(ctx.captured_queries) == 12
5171 5172

  
5172 5173
    # simulate booking
5173 5174
    dt = datetime.datetime.strptime(resp.json['data'][2]['id'].split(':')[1], '%Y-%m-%d-%H%M')
......
5296 5297
            '/api/agenda/%s/meetings/%s/datetimes/' % (virt_agenda.slug, meeting_type.slug),
5297 5298
            params={'exclude_user_external_id': '42'},
5298 5299
        )
5299
        assert len(ctx.captured_queries) == 11
5300
        assert len(ctx.captured_queries) == 13
5300 5301
    assert resp.json['data'][0]['id'] == 'foo-bar:2021-02-26-0900'
5301 5302
    assert resp.json['data'][0]['disabled'] is True
5302 5303
    assert resp.json['data'][2]['id'] == 'foo-bar:2021-02-26-1000'
......
5696 5697
    # 2 slots are gone
5697 5698
    with CaptureQueriesContext(connection) as ctx:
5698 5699
        resp2 = app.get(datetimes_url)
5699
        assert len(ctx.captured_queries) == 10
5700
        assert len(ctx.captured_queries) == 12
5700 5701
    assert len(resp.json['data']) == len(resp2.json['data']) + 2
5701 5702

  
5702 5703
    # add a standard desk exception
......
6449 6450
    app.authorization = ('Basic', ('john.doe', 'password'))
6450 6451
    resp = app.post(fillslot_url, status=400)
6451 6452
    assert resp.json['err'] == 1
6453

  
6454

  
6455
def test_booking_api_meeting_lock_and_confirm(app, meetings_agenda, user):
6456
    agenda_id = meetings_agenda.slug
6457
    meeting_type = MeetingType.objects.get(agenda=meetings_agenda)
6458

  
6459
    # list free slots, with or without a lock
6460
    resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id)
6461
    free_slots = len(resp.json['data'])
6462
    resp = app.get('/api/agenda/meetings/%s/datetimes/?lock_code=OTHERLOCK' % meeting_type.id)
6463
    assert free_slots == len(resp.json['data'])
6464
    resp = app.get('/api/agenda/meetings/%s/datetimes/?lock_code=MYLOCK' % meeting_type.id)
6465
    assert free_slots == len(resp.json['data'])
6466

  
6467
    # lock a slot
6468
    event_id = resp.json['data'][2]['id']
6469
    assert urlparse.urlparse(
6470
        resp.json['data'][2]['api']['fillslot_url']
6471
    ).path == '/api/agenda/%s/fillslot/%s/' % (meetings_agenda.slug, event_id)
6472
    app.authorization = ('Basic', ('john.doe', 'password'))
6473
    app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id), params={'lock_code': 'MYLOCK'})
6474
    assert Booking.objects.count() == 0
6475
    assert Lease.objects.count() == 1
6476
    assert (
6477
        Lease.objects.filter(
6478
            agenda=meetings_agenda,
6479
            desk=meetings_agenda.desk_set.get(),
6480
            lock_code='MYLOCK',
6481
            lock_expiration_datetime__isnull=False,
6482
        ).count()
6483
        == 1
6484
    )
6485

  
6486
    # list free slots: one is locked ...
6487
    resp2 = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id)
6488
    assert free_slots == len([x for x in resp2.json['data']])
6489
    assert len([x for x in resp2.json['data'] if x.get('disabled')]) == 1
6490

  
6491
    resp2 = app.get('/api/agenda/meetings/%s/datetimes/?lock_code=OTHERLOCK' % meeting_type.id)
6492
    assert free_slots == len([x for x in resp2.json['data']])
6493
    assert len([x for x in resp2.json['data'] if x.get('disabled')]) == 1
6494

  
6495
    # ... unless it's MYLOCK
6496
    resp2 = app.get('/api/agenda/meetings/%s/datetimes/?lock_code=MYLOCK' % meeting_type.id)
6497
    assert free_slots == len([x for x in resp2.json['data']])
6498
    assert len([x for x in resp2.json['data'] if x.get('disabled')]) == 0
6499

  
6500
    # can't lock the same timeslot ...
6501
    resp_booking = app.post(
6502
        '/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id), params={'lock_code': 'OTHERLOCK'}
6503
    )
6504
    assert resp_booking.json['err'] == 1
6505
    assert resp_booking.json['reason'] == 'no more desk available'
6506

  
6507
    # ... unless with MYLOCK (aka "relock")
6508
    resp_booking = app.post(
6509
        '/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id), params={'lock_code': 'MYLOCK'}
6510
    )
6511
    assert resp_booking.json['err'] == 0
6512
    assert Booking.objects.count() == 0
6513
    assert Lease.objects.count() == 1
6514
    assert (
6515
        Lease.objects.filter(
6516
            agenda=meetings_agenda,
6517
            desk=meetings_agenda.desk_set.get(),
6518
            lock_code='MYLOCK',
6519
            lock_expiration_datetime__isnull=False,
6520
        ).count()
6521
        == 1
6522
    )
6523

  
6524
    # can't book the slot ...
6525
    resp_booking = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id))
6526
    assert resp_booking.json['err'] == 1
6527
    assert resp_booking.json['reason'] == 'no more desk available'
6528

  
6529
    resp_booking = app.post(
6530
        '/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id), params={'confirm_after_lock': True}
6531
    )
6532
    assert resp_booking.json['err'] == 1
6533
    assert resp_booking.json['reason'] == 'no more desk available'
6534

  
6535
    resp_booking = app.post(
6536
        '/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id),
6537
        params={'lock_code': 'OTHERLOCK', 'confirm_after_lock': True},
6538
    )
6539
    assert resp_booking.json['err'] == 1
6540
    assert resp_booking.json['reason'] == 'no more desk available'
6541

  
6542
    # ... unless with MYLOCK (aka "confirm")
6543
    resp_booking = app.post(
6544
        '/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id),
6545
        params={'lock_code': 'MYLOCK', 'confirm_after_lock': True},
6546
    )
6547
    assert resp_booking.json['err'] == 0
6548
    assert Booking.objects.count() == 1
6549
    assert Lease.objects.count() == 0
6550

  
6551

  
6552
def test_booking_api_meeting_lock_and_confirm_with_resource(app, meetings_agenda, user):
6553
    resource1 = Resource.objects.create(label='Resource 1', slug='re1')
6554
    resource2 = Resource.objects.create(label='Resource 2', slug='re2')
6555
    meetings_agenda.resources.add(resource1, resource2)
6556
    agenda_id = meetings_agenda.slug
6557
    meeting_type = MeetingType.objects.get(agenda=meetings_agenda)
6558

  
6559
    # list free slots, with or without a lock
6560
    resp = app.get('/api/agenda/meetings/%s/datetimes/?resources=re1' % meeting_type.id)
6561
    free_slots = len(resp.json['data'])
6562
    resp = app.get('/api/agenda/meetings/%s/datetimes/?lock_code=OTHERLOCK&resources=re1' % meeting_type.id)
6563
    assert free_slots == len(resp.json['data'])
6564
    resp = app.get('/api/agenda/meetings/%s/datetimes/?lock_code=MYLOCK&resources=re1' % meeting_type.id)
6565
    assert free_slots == len(resp.json['data'])
6566

  
6567
    # lock a slot
6568
    event_id = resp.json['data'][2]['id']
6569
    assert urlparse.urlparse(
6570
        resp.json['data'][2]['api']['fillslot_url']
6571
    ).path == '/api/agenda/%s/fillslot/%s/' % (meetings_agenda.slug, event_id)
6572
    app.authorization = ('Basic', ('john.doe', 'password'))
6573
    app.post(
6574
        '/api/agenda/%s/fillslot/%s/?resources=re1' % (agenda_id, event_id), params={'lock_code': 'MYLOCK'}
6575
    )
6576
    assert Booking.objects.count() == 0
6577
    assert Lease.objects.count() == 2
6578
    assert (
6579
        Lease.objects.filter(
6580
            agenda=meetings_agenda,
6581
            desk=meetings_agenda.desk_set.get(),
6582
            resource__isnull=True,
6583
            lock_code='MYLOCK',
6584
            lock_expiration_datetime__isnull=False,
6585
        ).count()
6586
        == 1
6587
    )
6588
    assert (
6589
        Lease.objects.filter(
6590
            agenda__isnull=True,
6591
            desk__isnull=True,
6592
            resource=resource1,
6593
            lock_code='MYLOCK',
6594
            lock_expiration_datetime__isnull=False,
6595
        ).count()
6596
        == 1
6597
    )
6598
    old_lock_ids = set(Lease.objects.values_list('id', flat=True))
6599

  
6600
    # list free slots: one is locked ...
6601
    resp2 = app.get('/api/agenda/meetings/%s/datetimes/?resources=re1' % meeting_type.id)
6602
    assert free_slots == len([x for x in resp2.json['data']])
6603
    assert len([x for x in resp2.json['data'] if x.get('disabled')]) == 1
6604

  
6605
    resp2 = app.get('/api/agenda/meetings/%s/datetimes/?lock_code=OTHERLOCK&resources=re1' % meeting_type.id)
6606
    assert free_slots == len([x for x in resp2.json['data']])
6607
    assert len([x for x in resp2.json['data'] if x.get('disabled')]) == 1
6608

  
6609
    # ... unless it's MYLOCK
6610
    resp2 = app.get('/api/agenda/meetings/%s/datetimes/?lock_code=MYLOCK&resources=re1' % meeting_type.id)
6611
    assert free_slots == len([x for x in resp2.json['data']])
6612
    assert len([x for x in resp2.json['data'] if x.get('disabled')]) == 0
6613

  
6614
    # can't lock the same timeslot ...
6615
    resp_booking = app.post(
6616
        '/api/agenda/%s/fillslot/%s/?resources=re1' % (agenda_id, event_id), params={'lock_code': 'OTHERLOCK'}
6617
    )
6618
    assert resp_booking.json['err'] == 1
6619
    assert resp_booking.json['reason'] == 'no more desk available'
6620

  
6621
    # ... unless with MYLOCK (aka "relock")
6622
    resp_booking = app.post(
6623
        '/api/agenda/%s/fillslot/%s/?resources=re1' % (agenda_id, event_id), params={'lock_code': 'MYLOCK'}
6624
    )
6625
    assert resp_booking.json['err'] == 0
6626
    assert Booking.objects.count() == 0
6627
    assert Lease.objects.count() == 2
6628
    assert (
6629
        Lease.objects.filter(
6630
            agenda=meetings_agenda,
6631
            desk=meetings_agenda.desk_set.get(),
6632
            lock_code='MYLOCK',
6633
            lock_expiration_datetime__isnull=False,
6634
        ).count()
6635
        == 1
6636
    )
6637
    assert (
6638
        Lease.objects.filter(
6639
            agenda__isnull=True,
6640
            desk__isnull=True,
6641
            resource=resource1,
6642
            lock_code='MYLOCK',
6643
            lock_expiration_datetime__isnull=False,
6644
        ).count()
6645
        == 1
6646
    )
6647
    new_lock_ids = set(Lease.objects.values_list('id', flat=True))
6648
    assert not (old_lock_ids & new_lock_ids)
6649

  
6650
    # can't book the slot ...
6651
    resp_booking = app.post('/api/agenda/%s/fillslot/%s/?resources=re1' % (agenda_id, event_id))
6652
    assert resp_booking.json['err'] == 1
6653
    assert resp_booking.json['reason'] == 'no more desk available'
6654

  
6655
    resp_booking = app.post(
6656
        '/api/agenda/%s/fillslot/%s/?resources=re1' % (agenda_id, event_id),
6657
        params={'confirm_after_lock': True},
6658
    )
6659
    assert resp_booking.json['err'] == 1
6660
    assert resp_booking.json['reason'] == 'no more desk available'
6661

  
6662
    resp_booking = app.post(
6663
        '/api/agenda/%s/fillslot/%s/?resources=re1' % (agenda_id, event_id),
6664
        params={'lock_code': 'OTHERLOCK', 'confirm_after_lock': True},
6665
    )
6666
    assert resp_booking.json['err'] == 1
6667
    assert resp_booking.json['reason'] == 'no more desk available'
6668

  
6669
    # ... unless with MYLOCK (aka "confirm")
6670
    resp_booking = app.post(
6671
        '/api/agenda/%s/fillslot/%s/?resources=re1' % (agenda_id, event_id),
6672
        params={'lock_code': 'MYLOCK', 'confirm_after_lock': True},
6673
    )
6674
    assert resp_booking.json['err'] == 0
6675
    assert Booking.objects.count() == 1
6676
    assert Lease.objects.count() == 0
tests/test_locks.py
1
import datetime
2
from argparse import Namespace
3

  
4
import pytest
5
from django.db import IntegrityError, transaction
6
from django.utils.timezone import now
7

  
8
from chrono.agendas.models import Agenda, Desk, Lease, MeetingType, Resource
9

  
10

  
11
@pytest.fixture
12
def lock(db):
13
    agenda = Agenda.objects.create(
14
        label=u'Foo bar Meeting', kind='meetings', minimal_booking_delay=1, maximal_booking_delay=56
15
    )
16
    meeting_type = MeetingType(agenda=agenda, label='Blah', duration=30)
17
    meeting_type.save()
18
    desk1 = Desk.objects.create(agenda=agenda, label='Desk 1')
19
    desk2 = Desk.objects.create(agenda=agenda, label='Desk 2')
20
    resource = Resource.objects.create(label='re', description='re')
21
    return Namespace(**locals())
22

  
23

  
24
def test_lock_constraint_desk(lock):
25
    Lease.objects.create(
26
        agenda=lock.agenda,
27
        desk=lock.desk1,
28
        lock_code='1',
29
        lock_expiration_datetime=now() + datetime.timedelta(minutes=5),
30
        start_datetime=now(),
31
        end_datetime=now() + datetime.timedelta(minutes=5),
32
    )
33

  
34
    Lease.objects.create(
35
        agenda=lock.agenda,
36
        desk=lock.desk2,
37
        lock_code='2',
38
        lock_expiration_datetime=now() + datetime.timedelta(minutes=5),
39
        start_datetime=now(),
40
        end_datetime=now() + datetime.timedelta(minutes=5),
41
    )
42

  
43
    Lease.objects.create(
44
        resource=lock.resource,
45
        lock_code='3',
46
        lock_expiration_datetime=now() + datetime.timedelta(minutes=5),
47
        start_datetime=now(),
48
        end_datetime=now() + datetime.timedelta(minutes=5),
49
    )
50

  
51
    with pytest.raises(IntegrityError):
52
        # prevent IntegrityError to break the current transaction
53
        with transaction.atomic():
54
            Lease.objects.create(
55
                agenda=lock.agenda,
56
                desk=lock.desk1,
57
                lock_code='4',
58
                lock_expiration_datetime=now() + datetime.timedelta(minutes=5),
59
                # interval overlaps interval of first lock
60
                start_datetime=now() + datetime.timedelta(minutes=4),
61
                end_datetime=now() + datetime.timedelta(minutes=6),
62
            )
63

  
64

  
65
def test_lock_constraint_resource(lock):
66
    Lease.objects.create(
67
        agenda=lock.agenda,
68
        desk=lock.desk1,
69
        lock_code='1',
70
        lock_expiration_datetime=now() + datetime.timedelta(minutes=5),
71
        start_datetime=now(),
72
        end_datetime=now() + datetime.timedelta(minutes=5),
73
    )
74

  
75
    Lease.objects.create(
76
        agenda=lock.agenda,
77
        desk=lock.desk2,
78
        lock_code='2',
79
        lock_expiration_datetime=now() + datetime.timedelta(minutes=5),
80
        start_datetime=now(),
81
        end_datetime=now() + datetime.timedelta(minutes=5),
82
    )
83

  
84
    Lease.objects.create(
85
        resource=lock.resource,
86
        lock_code='3',
87
        lock_expiration_datetime=now() + datetime.timedelta(minutes=5),
88
        start_datetime=now(),
89
        end_datetime=now() + datetime.timedelta(minutes=5),
90
    )
91

  
92
    with pytest.raises(IntegrityError):
93
        # prevent IntegrityError to break the current transaction
94
        with transaction.atomic():
95
            Lease.objects.create(
96
                resource=lock.resource,
97
                lock_code='4',
98
                lock_expiration_datetime=now() + datetime.timedelta(minutes=5),
99
                # interval overlaps interval of first lock
100
                start_datetime=now() + datetime.timedelta(minutes=4),
101
                end_datetime=now() + datetime.timedelta(minutes=6),
102
            )
0
-