Projet

Général

Profil

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

Benjamin Dauvergne, 08 juin 2021 15:17

Télécharger (31 ko)

Voir les différences:

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

 chrono/agendas/migrations/0088_lease.py |  64 +++++++
 chrono/agendas/models.py                |  16 ++
 chrono/api/views.py                     | 142 +++++++++++++--
 chrono/settings.py                      |   3 +
 tests/api/test_locks.py                 | 231 ++++++++++++++++++++++++
 tests/api/test_meetings_datetimes.py    |  10 +-
 tests/test_locks.py                     | 102 +++++++++++
 7 files changed, 547 insertions(+), 21 deletions(-)
 create mode 100644 chrono/agendas/migrations/0088_lease.py
 create mode 100644 tests/api/test_locks.py
 create mode 100644 tests/test_locks.py
chrono/agendas/migrations/0088_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', '0087_booking_user_name'),
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
2646 2646
    @property
2647 2647
    def base_slug(self):
2648 2648
        return slugify(self.label)
2649

  
2650

  
2651
class Lease(models.Model):
2652
    desk = models.ForeignKey(Desk, null=True, on_delete=models.CASCADE)
2653
    resource = models.ForeignKey(Resource, null=True, on_delete=models.CASCADE)
2654
    agenda = models.ForeignKey(Agenda, null=True, on_delete=models.CASCADE)
2655
    lock_code = models.CharField(_('Lock code'), max_length=64, blank=False)
2656
    lock_expiration_datetime = models.DateTimeField(_('Lock expiration time'))
2657
    start_datetime = models.DateTimeField(_('Start'))
2658
    end_datetime = models.DateTimeField(_('End'))
2659

  
2660
    class Meta:
2661
        index_together = (('start_datetime', 'end_datetime'),)
2662

  
2663
    def as_interval(self):
2664
        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 Count, Prefetch, Q
24 25
from django.db.models.functions import TruncDay
25 26
from django.http import Http404, HttpResponse
......
46 47
    Category,
47 48
    Desk,
48 49
    Event,
50
    Lease,
49 51
    MeetingType,
50 52
    TimePeriodException,
51 53
)
......
86 88
    start_datetime=None,
87 89
    end_datetime=None,
88 90
    excluded_user_external_id=None,
91
    lock_code=None,
89 92
):
90 93
    """Get all occupation state of all possible slots for the given agenda (of
91 94
    its real agendas for a virtual agenda) and the given meeting_type.
......
103 106
      min/max_datetime; for each time slot check its status in the exclusion
104 107
      and bookings sets.
105 108
      If it is excluded, ignore it completely.
106
      It if is booked, report the slot as full.
109
      If it is booked, report the slot as full.
110
      If it is booked but match the lock code, report the slot as open.
111

  
107 112
    """
108 113
    resources = resources or []
109 114
    # virtual agendas have one constraint :
......
269 274
            for event_start_datetime, event_duration in booked_events
270 275
        )
271 276

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

  
272 297
    unique_booked = {}
273 298
    for time_period in base_agenda.get_effective_time_periods():
274 299
        duration = (
......
315 340

  
316 341
                    # slot is full if an already booked event overlaps it
317 342
                    # check resources first
318
                    booked = resources_bookings.overlaps(start_datetime, end_datetime)
343
                    booked = False
344
                    if resources:
345
                        if not booked:
346
                            booked = resources_bookings.overlaps(start_datetime, end_datetime)
347
                        if not booked:
348
                            booked = resource_locked_intervals.overlaps(start_datetime, end_datetime)
319 349
                    # then check user boookings
320 350
                    if not booked:
321 351
                        booked = user_bookings.overlaps(start_datetime, end_datetime)
......
324 354
                        booked = desk.id in bookings and bookings[desk.id].overlaps(
325 355
                            start_datetime, end_datetime
326 356
                        )
357
                    # then locks
358
                    if not booked and desk.id in desk_locked_intervals:
359
                        booked = desk_locked_intervals[desk.id].overlaps(start_datetime, end_datetime)
327 360
                    if unique and unique_booked.get(timestamp) is booked:
328 361
                        continue
329 362
                    unique_booked[timestamp] = booked
......
725 758
        start_datetime, end_datetime = get_start_and_end_datetime_from_request(request)
726 759
        user_external_id = request.GET.get('exclude_user_external_id') or None
727 760

  
761
        lock_code = request.GET.get('lock_code', None)
762
        if lock_code == '':
763
            raise APIError(
764
                _('lock_code must not be empty'),
765
                err_class='lock_code must not be empty',
766
                http_status=status.HTTP_400_BAD_REQUEST,
767
            )
768

  
728 769
        # Generate an unique slot for each possible meeting [start_datetime,
729 770
        # end_datetime] range.
730 771
        # First use get_all_slots() to get each possible meeting by desk and
......
747 788
                    start_datetime=start_datetime,
748 789
                    end_datetime=end_datetime,
749 790
                    excluded_user_external_id=user_external_id,
791
                    lock_code=lock_code,
750 792
                )
751 793
            )
752 794
            for slot in sorted(all_slots, key=lambda slot: slot[:3]):
......
944 986
    force_waiting_list = serializers.BooleanField(default=False)
945 987
    use_color_for = serializers.CharField(max_length=250, allow_blank=True)
946 988

  
989
    lock_code = serializers.CharField(max_length=64, required=False, allow_blank=True)
990
    lock_duration = serializers.IntegerField(
991
        min_value=0, default=lambda: settings.CHRONO_LOCK_DURATION
992
    )  # in seconds
993
    confirm_after_lock = serializers.BooleanField(default=False)
994

  
947 995

  
948 996
class StringOrListField(serializers.ListField):
949 997
    def to_internal_value(self, data):
......
1000 1048
            )
1001 1049
        payload = serializer.validated_data
1002 1050

  
1051
        lock_code = payload.get('lock_code')
1052
        if lock_code == '':  # lock_code should be absent or a non-empty string
1053
            raise APIError(
1054
                _('lock_code cannot be empty'),
1055
                err_class='invalid payload',
1056
                errors=serializer.errors,
1057
                http_status=status.HTTP_400_BAD_REQUEST,
1058
            )
1059

  
1003 1060
        if 'slots' in payload:
1004 1061
            slots = payload['slots']
1005 1062
        if not slots:
......
1122 1179
                    meeting_type,
1123 1180
                    resources=resources,
1124 1181
                    excluded_user_external_id=user_external_id if exclude_user else None,
1182
                    lock_code=lock_code,
1125 1183
                ),
1126 1184
                key=lambda slot: slot.start_datetime,
1127 1185
            )
......
1199 1257
            # booking requires real Event objects (not lazy Timeslots);
1200 1258
            # create them now, with data from the slots and the desk we found.
1201 1259
            events = []
1202
            for start_datetime in datetimes:
1203
                event = Event.objects.create(
1204
                    agenda=available_desk.agenda,
1205
                    slug=str(uuid.uuid4()),  # set slug to avoid queries during slug generation
1206
                    meeting_type=meeting_type,
1207
                    start_datetime=start_datetime,
1208
                    full=False,
1209
                    places=1,
1210
                    desk=available_desk,
1211
                )
1212
                if resources:
1213
                    event.resources.add(*resources)
1214
                events.append(event)
1260
            if not lock_code or payload.get('confirm_after_lock'):
1261
                for start_datetime in datetimes:
1262
                    event = Event.objects.create(
1263
                        agenda=available_desk.agenda,
1264
                        slug=str(uuid.uuid4()),  # set slug to avoid queries during slug generation
1265
                        meeting_type=meeting_type,
1266
                        start_datetime=start_datetime,
1267
                        full=False,
1268
                        places=1,
1269
                        desk=available_desk,
1270
                    )
1271
                    if resources:
1272
                        event.resources.add(*resources)
1273
                    events.append(event)
1274
            else:
1275
                # remove existing locks
1276
                Lease.objects.filter(lock_code=lock_code).delete()
1277

  
1278
                # create new locks
1279
                lock_duration = payload.get('lock_duration')
1280
                if not lock_duration or lock_duration < settings.CHRONO_LOCK_DURATION:
1281
                    lock_duration = settings.CHRONO_LOCK_DURATION
1282
                lock_expiration_datetime = now() + datetime.timedelta(seconds=lock_duration)
1283
                meeting_duration = datetime.timedelta(minutes=meeting_type.duration)
1284
                locks = []
1285
                for start_datetime in datetimes:
1286
                    locks.append(
1287
                        Lease(
1288
                            desk=available_desk,
1289
                            agenda=available_desk.agenda,
1290
                            lock_code=lock_code,
1291
                            lock_expiration_datetime=lock_expiration_datetime,
1292
                            start_datetime=start_datetime,
1293
                            end_datetime=start_datetime + meeting_duration,
1294
                        )
1295
                    )
1296
                    for resource in resources:
1297
                        locks.append(
1298
                            Lease(
1299
                                resource=resource,
1300
                                lock_code=lock_code,
1301
                                lock_expiration_datetime=lock_expiration_datetime,
1302
                                start_datetime=start_datetime,
1303
                                end_datetime=start_datetime + meeting_duration,
1304
                            )
1305
                        )
1306
                try:
1307
                    with transaction.atomic():
1308
                        Lease.objects.bulk_create(locks)
1309
                except IntegrityError:
1310
                    raise APIError(
1311
                        _('no more desk available'),
1312
                        err_class='no more desk available',
1313
                    )
1314
                else:
1315
                    return Response({'err': 0})
1215 1316
        else:
1317
            if lock_code:
1318
                raise APIError(
1319
                    _('lock_code does not work with events'),
1320
                    err_class='lock_code does not work with events',
1321
                    http_status=status.HTTP_400_BAD_REQUEST,
1322
                )
1216 1323
            # convert event recurrence identifiers to real event slugs
1217 1324
            for i, slot in enumerate(slots.copy()):
1218 1325
                if ':' not in slot:
......
1278 1385
                cancelled_booking_id = to_cancel_booking.pk
1279 1386
                to_cancel_booking.cancel()
1280 1387

  
1388
            if lock_code:
1389
                Lease.objects.filter(lock_code=lock_code).delete()
1390

  
1281 1391
            # now we have a list of events, book them.
1282 1392
            primary_booking = None
1283 1393
            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/api/test_locks.py
1
import urllib.parse as urlparse
2

  
3
import pytest
4

  
5
from chrono.agendas.models import Booking, Lease, MeetingType, Resource
6

  
7
pytestmark = pytest.mark.django_db
8

  
9

  
10
def test_booking_api_meeting_lock_and_confirm(app, meetings_agenda, user):
11
    agenda_id = meetings_agenda.slug
12
    meeting_type = MeetingType.objects.get(agenda=meetings_agenda)
13

  
14
    # list free slots, with or without a lock
15
    resp = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id)
16
    free_slots = len(resp.json['data'])
17
    resp = app.get('/api/agenda/meetings/%s/datetimes/?lock_code=OTHERLOCK' % meeting_type.id)
18
    assert free_slots == len(resp.json['data'])
19
    resp = app.get('/api/agenda/meetings/%s/datetimes/?lock_code=MYLOCK' % meeting_type.id)
20
    assert free_slots == len(resp.json['data'])
21

  
22
    # lock a slot
23
    event_id = resp.json['data'][2]['id']
24
    assert urlparse.urlparse(
25
        resp.json['data'][2]['api']['fillslot_url']
26
    ).path == '/api/agenda/%s/fillslot/%s/' % (meetings_agenda.slug, event_id)
27
    app.authorization = ('Basic', ('john.doe', 'password'))
28
    app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id), params={'lock_code': 'MYLOCK'})
29
    assert Booking.objects.count() == 0
30
    assert Lease.objects.count() == 1
31
    assert (
32
        Lease.objects.filter(
33
            agenda=meetings_agenda,
34
            desk=meetings_agenda.desk_set.get(),
35
            lock_code='MYLOCK',
36
            lock_expiration_datetime__isnull=False,
37
        ).count()
38
        == 1
39
    )
40

  
41
    # list free slots: one is locked ...
42
    resp2 = app.get('/api/agenda/meetings/%s/datetimes/' % meeting_type.id)
43
    assert free_slots == len([x for x in resp2.json['data']])
44
    assert len([x for x in resp2.json['data'] if x.get('disabled')]) == 1
45

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

  
50
    # ... unless it's MYLOCK
51
    resp2 = app.get('/api/agenda/meetings/%s/datetimes/?lock_code=MYLOCK' % meeting_type.id)
52
    assert free_slots == len([x for x in resp2.json['data']])
53
    assert len([x for x in resp2.json['data'] if x.get('disabled')]) == 0
54

  
55
    # can't lock the same timeslot ...
56
    resp_booking = app.post(
57
        '/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id), params={'lock_code': 'OTHERLOCK'}
58
    )
59
    assert resp_booking.json['err'] == 1
60
    assert resp_booking.json['reason'] == 'no more desk available'
61

  
62
    # ... unless with MYLOCK (aka "relock")
63
    resp_booking = app.post(
64
        '/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id), params={'lock_code': 'MYLOCK'}
65
    )
66
    assert resp_booking.json['err'] == 0
67
    assert Booking.objects.count() == 0
68
    assert Lease.objects.count() == 1
69
    assert (
70
        Lease.objects.filter(
71
            agenda=meetings_agenda,
72
            desk=meetings_agenda.desk_set.get(),
73
            lock_code='MYLOCK',
74
            lock_expiration_datetime__isnull=False,
75
        ).count()
76
        == 1
77
    )
78

  
79
    # can't book the slot ...
80
    resp_booking = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id))
81
    assert resp_booking.json['err'] == 1
82
    assert resp_booking.json['reason'] == 'no more desk available'
83

  
84
    resp_booking = app.post(
85
        '/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id), params={'confirm_after_lock': True}
86
    )
87
    assert resp_booking.json['err'] == 1
88
    assert resp_booking.json['reason'] == 'no more desk available'
89

  
90
    resp_booking = app.post(
91
        '/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id),
92
        params={'lock_code': 'OTHERLOCK', 'confirm_after_lock': True},
93
    )
94
    assert resp_booking.json['err'] == 1
95
    assert resp_booking.json['reason'] == 'no more desk available'
96

  
97
    # ... unless with MYLOCK (aka "confirm")
98
    resp_booking = app.post(
99
        '/api/agenda/%s/fillslot/%s/' % (agenda_id, event_id),
100
        params={'lock_code': 'MYLOCK', 'confirm_after_lock': True},
101
    )
102
    assert resp_booking.json['err'] == 0
103
    assert Booking.objects.count() == 1
104
    assert Lease.objects.count() == 0
105

  
106

  
107
def test_booking_api_meeting_lock_and_confirm_with_resource(app, meetings_agenda, user):
108
    resource1 = Resource.objects.create(label='Resource 1', slug='re1')
109
    resource2 = Resource.objects.create(label='Resource 2', slug='re2')
110
    meetings_agenda.resources.add(resource1, resource2)
111
    agenda_id = meetings_agenda.slug
112
    meeting_type = MeetingType.objects.get(agenda=meetings_agenda)
113

  
114
    # list free slots, with or without a lock
115
    resp = app.get('/api/agenda/meetings/%s/datetimes/?resources=re1' % meeting_type.id)
116
    free_slots = len(resp.json['data'])
117
    resp = app.get('/api/agenda/meetings/%s/datetimes/?lock_code=OTHERLOCK&resources=re1' % meeting_type.id)
118
    assert free_slots == len(resp.json['data'])
119
    resp = app.get('/api/agenda/meetings/%s/datetimes/?lock_code=MYLOCK&resources=re1' % meeting_type.id)
120
    assert free_slots == len(resp.json['data'])
121

  
122
    # lock a slot
123
    event_id = resp.json['data'][2]['id']
124
    assert urlparse.urlparse(
125
        resp.json['data'][2]['api']['fillslot_url']
126
    ).path == '/api/agenda/%s/fillslot/%s/' % (meetings_agenda.slug, event_id)
127
    app.authorization = ('Basic', ('john.doe', 'password'))
128
    app.post(
129
        '/api/agenda/%s/fillslot/%s/?resources=re1' % (agenda_id, event_id), params={'lock_code': 'MYLOCK'}
130
    )
131
    assert Booking.objects.count() == 0
132
    assert Lease.objects.count() == 2
133
    assert (
134
        Lease.objects.filter(
135
            agenda=meetings_agenda,
136
            desk=meetings_agenda.desk_set.get(),
137
            resource__isnull=True,
138
            lock_code='MYLOCK',
139
            lock_expiration_datetime__isnull=False,
140
        ).count()
141
        == 1
142
    )
143
    assert (
144
        Lease.objects.filter(
145
            agenda__isnull=True,
146
            desk__isnull=True,
147
            resource=resource1,
148
            lock_code='MYLOCK',
149
            lock_expiration_datetime__isnull=False,
150
        ).count()
151
        == 1
152
    )
153
    old_lock_ids = set(Lease.objects.values_list('id', flat=True))
154

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

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

  
164
    # ... unless it's MYLOCK
165
    resp2 = app.get('/api/agenda/meetings/%s/datetimes/?lock_code=MYLOCK&resources=re1' % meeting_type.id)
166
    assert free_slots == len([x for x in resp2.json['data']])
167
    assert len([x for x in resp2.json['data'] if x.get('disabled')]) == 0
168

  
169
    # can't lock the same timeslot ...
170
    resp_booking = app.post(
171
        '/api/agenda/%s/fillslot/%s/?resources=re1' % (agenda_id, event_id), params={'lock_code': 'OTHERLOCK'}
172
    )
173
    assert resp_booking.json['err'] == 1
174
    assert resp_booking.json['reason'] == 'no more desk available'
175

  
176
    # ... unless with MYLOCK (aka "relock")
177
    resp_booking = app.post(
178
        '/api/agenda/%s/fillslot/%s/?resources=re1' % (agenda_id, event_id), params={'lock_code': 'MYLOCK'}
179
    )
180
    assert resp_booking.json['err'] == 0
181
    assert Booking.objects.count() == 0
182
    assert Lease.objects.count() == 2
183
    assert (
184
        Lease.objects.filter(
185
            agenda=meetings_agenda,
186
            desk=meetings_agenda.desk_set.get(),
187
            lock_code='MYLOCK',
188
            lock_expiration_datetime__isnull=False,
189
        ).count()
190
        == 1
191
    )
192
    assert (
193
        Lease.objects.filter(
194
            agenda__isnull=True,
195
            desk__isnull=True,
196
            resource=resource1,
197
            lock_code='MYLOCK',
198
            lock_expiration_datetime__isnull=False,
199
        ).count()
200
        == 1
201
    )
202
    new_lock_ids = set(Lease.objects.values_list('id', flat=True))
203
    assert not (old_lock_ids & new_lock_ids)
204

  
205
    # can't book the slot ...
206
    resp_booking = app.post('/api/agenda/%s/fillslot/%s/?resources=re1' % (agenda_id, event_id))
207
    assert resp_booking.json['err'] == 1
208
    assert resp_booking.json['reason'] == 'no more desk available'
209

  
210
    resp_booking = app.post(
211
        '/api/agenda/%s/fillslot/%s/?resources=re1' % (agenda_id, event_id),
212
        params={'confirm_after_lock': True},
213
    )
214
    assert resp_booking.json['err'] == 1
215
    assert resp_booking.json['reason'] == 'no more desk available'
216

  
217
    resp_booking = app.post(
218
        '/api/agenda/%s/fillslot/%s/?resources=re1' % (agenda_id, event_id),
219
        params={'lock_code': 'OTHERLOCK', 'confirm_after_lock': True},
220
    )
221
    assert resp_booking.json['err'] == 1
222
    assert resp_booking.json['reason'] == 'no more desk available'
223

  
224
    # ... unless with MYLOCK (aka "confirm")
225
    resp_booking = app.post(
226
        '/api/agenda/%s/fillslot/%s/?resources=re1' % (agenda_id, event_id),
227
        params={'lock_code': 'MYLOCK', 'confirm_after_lock': True},
228
    )
229
    assert resp_booking.json['err'] == 0
230
    assert Booking.objects.count() == 1
231
    assert Lease.objects.count() == 0
tests/api/test_meetings_datetimes.py
289 289
    )
290 290
    with CaptureQueriesContext(connection) as ctx:
291 291
        resp = app.get(api_url)
292
        assert len(ctx.captured_queries) == 10
292
        assert len(ctx.captured_queries) == 12
293 293
    assert len(resp.json['data']) == 32
294 294
    assert [s['datetime'] for s in resp.json['data'] if s['disabled'] is True] == [
295 295
        '%s 09:00:00' % tomorrow_str,
......
501 501
            '/api/agenda/%s/meetings/%s/datetimes/' % (agenda.slug, meeting_type.slug),
502 502
            params={'exclude_user_external_id': '42'},
503 503
        )
504
        assert len(ctx.captured_queries) == 9
504
        assert len(ctx.captured_queries) == 11
505 505
    assert resp.json['data'][0]['id'] == 'foo-bar:2021-02-26-0900'
506 506
    assert resp.json['data'][0]['disabled'] is True
507 507
    assert resp.json['data'][2]['id'] == 'foo-bar:2021-02-26-1000'
......
1476 1476
    with CaptureQueriesContext(connection) as ctx:
1477 1477
        resp = app.get(api_url)
1478 1478
        assert len(resp.json['data']) == 12
1479
        assert len(ctx.captured_queries) == 10
1479
        assert len(ctx.captured_queries) == 12
1480 1480

  
1481 1481
    # simulate booking
1482 1482
    dt = datetime.datetime.strptime(resp.json['data'][2]['id'].split(':')[1], '%Y-%m-%d-%H%M')
......
1605 1605
            '/api/agenda/%s/meetings/%s/datetimes/' % (virt_agenda.slug, meeting_type.slug),
1606 1606
            params={'exclude_user_external_id': '42'},
1607 1607
        )
1608
        assert len(ctx.captured_queries) == 11
1608
        assert len(ctx.captured_queries) == 13
1609 1609
    assert resp.json['data'][0]['id'] == 'foo-bar:2021-02-26-0900'
1610 1610
    assert resp.json['data'][0]['disabled'] is True
1611 1611
    assert resp.json['data'][2]['id'] == 'foo-bar:2021-02-26-1000'
......
1658 1658
    # 2 slots are gone
1659 1659
    with CaptureQueriesContext(connection) as ctx:
1660 1660
        resp2 = app.get(datetimes_url)
1661
        assert len(ctx.captured_queries) == 10
1661
        assert len(ctx.captured_queries) == 12
1662 1662
    assert len(resp.json['data']) == len(resp2.json['data']) + 2
1663 1663

  
1664 1664
    # add a standard desk exception
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
-