Projet

Général

Profil

0001-Revert-api-add-lock_code-parameter-to-fillslot-and-d.patch

Benjamin Dauvergne, 17 juin 2021 17:41

Télécharger (30,6 ko)

Voir les différences:

Subject: [PATCH 1/2] Revert "api: add lock_code parameter to fillslot and
 datetimes" (#54956)

This reverts commit 2f9bf16a57bab183b3f2650d882ecf37348ed053.
 chrono/agendas/migrations/0091_lease.py |  64 -------
 chrono/agendas/models.py                |  13 --
 chrono/api/views.py                     | 130 ++-----------
 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, 21 insertions(+), 532 deletions(-)
 delete mode 100644 chrono/agendas/migrations/0091_lease.py
 delete mode 100644 tests/api/test_locks.py
 delete mode 100644 tests/test_locks.py
chrono/agendas/migrations/0091_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
create_gist_constraints_on_leases = """
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
drop_gist_constraints_on_leases = """
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', '0090_default_view'),
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=create_gist_constraints_on_leases, reverse_sql=drop_gist_constraints_on_leases),
64
    ]
chrono/agendas/models.py
2673 2673
    @property
2674 2674
    def base_slug(self):
2675 2675
        return slugify(self.label)
2676

  
2677

  
2678
class Lease(models.Model):
2679
    desk = models.ForeignKey(Desk, null=True, on_delete=models.CASCADE)
2680
    resource = models.ForeignKey(Resource, null=True, on_delete=models.CASCADE)
2681
    agenda = models.ForeignKey(Agenda, null=True, on_delete=models.CASCADE)
2682
    lock_code = models.CharField(_('Lock code'), max_length=64, blank=False)
2683
    lock_expiration_datetime = models.DateTimeField(_('Lock expiration time'))
2684
    start_datetime = models.DateTimeField(_('Start'))
2685
    end_datetime = models.DateTimeField(_('End'))
2686

  
2687
    class Meta:
2688
        index_together = (('start_datetime', 'end_datetime'),)
chrono/api/views.py
19 19
import itertools
20 20
import uuid
21 21

  
22
from django.conf import settings
23
from django.db import IntegrityError, transaction
22
from django.db import transaction
24 23
from django.db.models import Count, Prefetch, Q
25 24
from django.db.models.functions import TruncDay
26 25
from django.http import Http404, HttpResponse
......
47 46
    Category,
48 47
    Desk,
49 48
    Event,
50
    Lease,
51 49
    MeetingType,
52 50
    TimePeriodException,
53 51
)
......
88 86
    start_datetime=None,
89 87
    end_datetime=None,
90 88
    excluded_user_external_id=None,
91
    lock_code=None,
92 89
):
93 90
    """Get all occupation state of all possible slots for the given agenda (of
94 91
    its real agendas for a virtual agenda) and the given meeting_type.
......
106 103
      min/max_datetime; for each time slot check its status in the exclusion
107 104
      and bookings sets.
108 105
      If it is excluded, ignore it completely.
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.
106
      It if is booked, report the slot as full.
111 107
    """
112 108
    resources = resources or []
113 109
    # virtual agendas have one constraint :
......
273 269
            for event_start_datetime, event_duration in booked_events
274 270
        )
275 271

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

  
292 272
    unique_booked = {}
293 273
    for time_period in base_agenda.get_effective_time_periods():
294 274
        duration = (
......
335 315

  
336 316
                    # slot is full if an already booked event overlaps it
337 317
                    # check resources first
338
                    booked = False
339
                    if resources:
340
                        if not booked:
341
                            booked = resources_bookings.overlaps(start_datetime, end_datetime)
342
                        if not booked:
343
                            booked = resource_locked_intervals.overlaps(start_datetime, end_datetime)
318
                    booked = resources_bookings.overlaps(start_datetime, end_datetime)
344 319
                    # then check user boookings
345 320
                    if not booked:
346 321
                        booked = user_bookings.overlaps(start_datetime, end_datetime)
......
349 324
                        booked = desk.id in bookings and bookings[desk.id].overlaps(
350 325
                            start_datetime, end_datetime
351 326
                        )
352
                    # then locks
353
                    if not booked and desk.id in desk_locked_intervals:
354
                        booked = desk_locked_intervals[desk.id].overlaps(start_datetime, end_datetime)
355 327
                    if unique and unique_booked.get(timestamp) is booked:
356 328
                        continue
357 329
                    unique_booked[timestamp] = booked
......
754 726
        start_datetime, end_datetime = get_start_and_end_datetime_from_request(request)
755 727
        user_external_id = request.GET.get('exclude_user_external_id') or None
756 728

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

  
765 729
        # Generate an unique slot for each possible meeting [start_datetime,
766 730
        # end_datetime] range.
767 731
        # First use get_all_slots() to get each possible meeting by desk and
......
784 748
                    start_datetime=start_datetime,
785 749
                    end_datetime=end_datetime,
786 750
                    excluded_user_external_id=user_external_id,
787
                    lock_code=lock_code,
788 751
                )
789 752
            )
790 753
            for slot in sorted(all_slots, key=lambda slot: slot[:3]):
......
982 945
    force_waiting_list = serializers.BooleanField(default=False)
983 946
    use_color_for = serializers.CharField(max_length=250, allow_blank=True)
984 947

  
985
    lock_code = serializers.CharField(max_length=64, required=False)
986
    lock_duration = serializers.IntegerField(
987
        min_value=0, default=lambda: settings.CHRONO_LOCK_DURATION
988
    )  # in seconds
989
    confirm_after_lock = serializers.BooleanField(default=False)
990

  
991 948

  
992 949
class StringOrListField(serializers.ListField):
993 950
    def to_internal_value(self, data):
......
1044 1001
            )
1045 1002
        payload = serializer.validated_data
1046 1003

  
1047
        lock_code = payload.get('lock_code')
1048

  
1049 1004
        if 'slots' in payload:
1050 1005
            slots = payload['slots']
1051 1006
        if not slots:
......
1168 1123
                    meeting_type,
1169 1124
                    resources=resources,
1170 1125
                    excluded_user_external_id=user_external_id if exclude_user else None,
1171
                    lock_code=lock_code,
1172 1126
                ),
1173 1127
                key=lambda slot: slot.start_datetime,
1174 1128
            )
......
1246 1200
            # booking requires real Event objects (not lazy Timeslots);
1247 1201
            # create them now, with data from the slots and the desk we found.
1248 1202
            events = []
1249
            if not lock_code or payload.get('confirm_after_lock'):
1250
                for start_datetime in datetimes:
1251
                    event = Event.objects.create(
1252
                        agenda=available_desk.agenda,
1253
                        slug=str(uuid.uuid4()),  # set slug to avoid queries during slug generation
1254
                        meeting_type=meeting_type,
1255
                        start_datetime=start_datetime,
1256
                        full=False,
1257
                        places=1,
1258
                        desk=available_desk,
1259
                    )
1260
                    if resources:
1261
                        event.resources.add(*resources)
1262
                    events.append(event)
1263
            else:
1264
                # remove existing locks
1265
                Lease.objects.filter(lock_code=lock_code).delete()
1266

  
1267
                # create new locks
1268
                lock_duration = payload.get('lock_duration')
1269
                if not lock_duration or lock_duration < settings.CHRONO_LOCK_DURATION:
1270
                    lock_duration = settings.CHRONO_LOCK_DURATION
1271
                lock_expiration_datetime = now() + datetime.timedelta(seconds=lock_duration)
1272
                meeting_duration = datetime.timedelta(minutes=meeting_type.duration)
1273
                locks = []
1274
                for start_datetime in datetimes:
1275
                    locks.append(
1276
                        Lease(
1277
                            desk=available_desk,
1278
                            agenda=available_desk.agenda,
1279
                            lock_code=lock_code,
1280
                            lock_expiration_datetime=lock_expiration_datetime,
1281
                            start_datetime=start_datetime,
1282
                            end_datetime=start_datetime + meeting_duration,
1283
                        )
1284
                    )
1285
                    for resource in resources:
1286
                        locks.append(
1287
                            Lease(
1288
                                resource=resource,
1289
                                lock_code=lock_code,
1290
                                lock_expiration_datetime=lock_expiration_datetime,
1291
                                start_datetime=start_datetime,
1292
                                end_datetime=start_datetime + meeting_duration,
1293
                            )
1294
                        )
1295
                try:
1296
                    with transaction.atomic():
1297
                        Lease.objects.bulk_create(locks)
1298
                except IntegrityError:
1299
                    raise APIError(
1300
                        _('no more desk available'),
1301
                        err_class='no more desk available',
1302
                    )
1303
                else:
1304
                    return Response({'err': 0})
1305
        else:
1306
            if lock_code:
1307
                raise APIError(
1308
                    _('lock_code does not work with events'),
1309
                    err_class='lock_code does not work with events',
1310
                    http_status=status.HTTP_400_BAD_REQUEST,
1203
            for start_datetime in datetimes:
1204
                event = Event.objects.create(
1205
                    agenda=available_desk.agenda,
1206
                    slug=str(uuid.uuid4()),  # set slug to avoid queries during slug generation
1207
                    meeting_type=meeting_type,
1208
                    start_datetime=start_datetime,
1209
                    full=False,
1210
                    places=1,
1211
                    desk=available_desk,
1311 1212
                )
1213
                if resources:
1214
                    event.resources.add(*resources)
1215
                events.append(event)
1216
        else:
1312 1217
            # convert event recurrence identifiers to real event slugs
1313 1218
            for i, slot in enumerate(slots.copy()):
1314 1219
                if ':' not in slot:
......
1374 1279
                cancelled_booking_id = to_cancel_booking.pk
1375 1280
                to_cancel_booking.cancel()
1376 1281

  
1377
            if lock_code:
1378
                Lease.objects.filter(lock_code=lock_code).delete()
1379

  
1380 1282
            # now we have a list of events, book them.
1381 1283
            primary_booking = None
1382 1284
            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

  
175 172
# timeout used in python-requests call, in seconds
176 173
# we use 28s by default: timeout just before web server, which is usually 30s
177 174
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) == 12
292
        assert len(ctx.captured_queries) == 10
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) == 11
504
        assert len(ctx.captured_queries) == 9
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) == 12
1479
        assert len(ctx.captured_queries) == 10
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) == 13
1608
        assert len(ctx.captured_queries) == 11
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) == 12
1661
        assert len(ctx.captured_queries) == 10
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
            )
103
-