implement locking for meeting's agendas (#17685)
* add a Lease model to associate a lock_code to a booking, * add a new command "clean_leases" run by cron every 5 minutes to clean expired leases, * add new parameter lock_code to get_all_slots() and exclude conflicting booking linked to this lock_code if provided, * accept new lock_code query string parameter in the datetimes endpoints (to see available slot minus the locked ones, if the user want to change the chosen slot) * add new parameters lock_code and confirm_after_lock to the fillslot endpoint: - when lock_code is used without confirm_after_lock: 1. look for available slots excluding events/booking pairs associated with the given lock_code, by passing lock_code to get_all_slots 2. before creating the new event/booking pair, clean existing pairs associated to the lock code, 3. after creating the new pair, create a new Lease object with the lock code - when lock_code is used with confirm_after_lock do all previous steps but 3., making a normal meeting booking. * add tests with lock_code on meeting's datetimes and fillslot use, checking exclusion by resources or user_id works with lock_code
This commit is contained in:
parent
2d8912c0a3
commit
d6a5861876
|
@ -0,0 +1,26 @@
|
|||
# chrono - agendas system
|
||||
# Copyright (C) 2020 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from chrono.agendas.models import Lease
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Clean expired leases and related bookings and events'
|
||||
|
||||
def handle(self, **options):
|
||||
Lease.clean()
|
|
@ -0,0 +1,41 @@
|
|||
# Generated by Django 3.2.18 on 2023-08-22 08:19
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
from chrono.agendas.models import get_lease_expiration
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('agendas', '0165_booking_revert'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Lease',
|
||||
fields=[
|
||||
(
|
||||
'id',
|
||||
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
('lock_code', models.CharField(max_length=64, verbose_name='Lock code')),
|
||||
(
|
||||
'expiration_datetime',
|
||||
models.DateTimeField(verbose_name='Lease expiration time', default=get_lease_expiration),
|
||||
),
|
||||
(
|
||||
'booking',
|
||||
models.OneToOneField(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to='agendas.booking',
|
||||
verbose_name='Booking',
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Lease',
|
||||
'verbose_name_plural': 'Leases',
|
||||
},
|
||||
),
|
||||
]
|
|
@ -1208,6 +1208,7 @@ class Agenda(models.Model):
|
|||
start_datetime=None,
|
||||
end_datetime=None,
|
||||
user_external_id=None,
|
||||
lock_code=None,
|
||||
):
|
||||
"""Get all occupation state of all possible slots for the given agenda (of
|
||||
its real agendas for a virtual agenda) and the given meeting_type.
|
||||
|
@ -1226,6 +1227,7 @@ class Agenda(models.Model):
|
|||
and bookings sets.
|
||||
If it is excluded, ignore it completely.
|
||||
It if is booked, report the slot as full.
|
||||
If it is booked but match the lock code, report the slot as open.
|
||||
"""
|
||||
resources = resources or []
|
||||
# virtual agendas have one constraint :
|
||||
|
@ -1329,6 +1331,8 @@ class Agenda(models.Model):
|
|||
.order_by('desk_id', 'start_datetime', 'meeting_type__duration')
|
||||
.values_list('desk_id', 'start_datetime', 'meeting_type__duration')
|
||||
)
|
||||
if lock_code:
|
||||
booked_events = booked_events.exclude(booking__lease__lock_code=lock_code)
|
||||
# compute exclusion set by desk from all bookings, using
|
||||
# itertools.groupby() to group them by desk_id
|
||||
bookings.update(
|
||||
|
@ -1362,6 +1366,8 @@ class Agenda(models.Model):
|
|||
.order_by('start_datetime', 'meeting_type__duration')
|
||||
.values_list('start_datetime', 'meeting_type__duration')
|
||||
)
|
||||
if lock_code:
|
||||
booked_events = booked_events.exclude(booking__lease__lock_code=lock_code)
|
||||
# compute exclusion set
|
||||
resources_bookings = IntervalSet.from_ordered(
|
||||
(event_start_datetime, event_start_datetime + datetime.timedelta(minutes=event_duration))
|
||||
|
@ -1387,6 +1393,8 @@ class Agenda(models.Model):
|
|||
.order_by('start_datetime', 'meeting_type__duration')
|
||||
.values_list('start_datetime', 'meeting_type__duration')
|
||||
)
|
||||
if lock_code:
|
||||
booked_events = booked_events.exclude(booking__lease__lock_code=lock_code)
|
||||
# compute exclusion set by desk from all bookings, using
|
||||
# itertools.groupby() to group them by desk_id
|
||||
user_bookings = IntervalSet.from_ordered(
|
||||
|
@ -4633,3 +4641,26 @@ class SharedCustodySettings(models.Model):
|
|||
return cls.objects.get()
|
||||
except cls.DoesNotExist:
|
||||
return cls()
|
||||
|
||||
|
||||
def get_lease_expiration():
|
||||
return now() + datetime.timedelta(seconds=settings.CHRONO_LOCK_DURATION)
|
||||
|
||||
|
||||
class Lease(models.Model):
|
||||
booking = models.OneToOneField(Booking, on_delete=models.CASCADE, verbose_name=_('Booking'))
|
||||
lock_code = models.CharField(verbose_name=_('Lock code'), max_length=64, blank=False)
|
||||
expiration_datetime = models.DateTimeField(
|
||||
verbose_name=_('Lease expiration time'), default=get_lease_expiration
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Lease')
|
||||
verbose_name_plural = _('Leases')
|
||||
|
||||
@classmethod
|
||||
def clean(cls):
|
||||
'''Clean objects linked to leases.'''
|
||||
|
||||
# Delete expired meeting's events, bookings and leases.'''
|
||||
Event.objects.filter(agenda__kind='meetings', booking__lease__expiration_datetime__lt=now()).delete()
|
||||
|
|
|
@ -107,6 +107,8 @@ class FillSlotSerializer(serializers.Serializer):
|
|||
check_overlaps = serializers.BooleanField(default=False)
|
||||
start_time = serializers.TimeField(required=False, allow_null=True)
|
||||
end_time = serializers.TimeField(required=False, allow_null=True)
|
||||
lock_code = serializers.CharField(max_length=64, required=False, allow_blank=False, trim_whitespace=True)
|
||||
confirm_after_lock = serializers.BooleanField(default=False)
|
||||
|
||||
def validate(self, attrs):
|
||||
super().validate(attrs)
|
||||
|
|
|
@ -50,6 +50,7 @@ from chrono.agendas.models import (
|
|||
BookingColor,
|
||||
Desk,
|
||||
Event,
|
||||
Lease,
|
||||
MeetingType,
|
||||
SharedCustodyAgenda,
|
||||
Subscription,
|
||||
|
@ -823,6 +824,12 @@ class MeetingDatetimes(APIView):
|
|||
N_('user_external_id and exclude_user_external_id have different values')
|
||||
)
|
||||
|
||||
lock_code = request.GET.get('lock_code', None)
|
||||
if lock_code is not None:
|
||||
lock_code = lock_code.strip()
|
||||
if lock_code == '':
|
||||
raise APIErrorBadRequest(_('lock_code must not be empty'))
|
||||
|
||||
# Generate an unique slot for each possible meeting [start_datetime,
|
||||
# end_datetime] range.
|
||||
# First use get_all_slots() to get each possible meeting by desk and
|
||||
|
@ -846,6 +853,7 @@ class MeetingDatetimes(APIView):
|
|||
start_datetime=start_datetime,
|
||||
end_datetime=end_datetime,
|
||||
user_external_id=booked_user_external_id or excluded_user_external_id,
|
||||
lock_code=lock_code,
|
||||
)
|
||||
)
|
||||
for slot in sorted(all_slots, key=lambda slot: slot[:3]):
|
||||
|
@ -867,6 +875,7 @@ class MeetingDatetimes(APIView):
|
|||
start_datetime=start_datetime,
|
||||
end_datetime=end_datetime,
|
||||
user_external_id=booked_user_external_id or excluded_user_external_id,
|
||||
lock_code=lock_code,
|
||||
)
|
||||
)
|
||||
last_slot, slot_agendas = None, set()
|
||||
|
@ -1368,6 +1377,11 @@ class MeetingsAgendaFillslot(APIView):
|
|||
raise APIErrorBadRequest(N_('invalid payload'), errors=serializer.errors)
|
||||
payload = serializer.validated_data
|
||||
|
||||
lock_code = payload.get('lock_code')
|
||||
if lock_code:
|
||||
# cannot apply a default value on the serializer bcause it is used with partial=True
|
||||
confirm_after_lock = payload.get('confirm_after_lock') or False
|
||||
|
||||
to_cancel_booking = None
|
||||
cancel_booking_id = None
|
||||
if payload.get('cancel_booking_id'):
|
||||
|
@ -1416,6 +1430,7 @@ class MeetingsAgendaFillslot(APIView):
|
|||
meeting_type = agenda.get_meetingtype(id_=meeting_type_id)
|
||||
except (MeetingType.DoesNotExist, ValueError):
|
||||
raise APIErrorBadRequest(N_('invalid meeting type id: %s'), meeting_type_id)
|
||||
|
||||
all_slots = sorted(
|
||||
agenda.get_all_slots(
|
||||
meeting_type,
|
||||
|
@ -1423,6 +1438,7 @@ class MeetingsAgendaFillslot(APIView):
|
|||
user_external_id=user_external_id if exclude_user else None,
|
||||
start_datetime=slot_datetime,
|
||||
end_datetime=slot_datetime + datetime.timedelta(minutes=meeting_type.duration),
|
||||
lock_code=lock_code,
|
||||
),
|
||||
key=lambda slot: slot.start_datetime,
|
||||
)
|
||||
|
@ -1499,6 +1515,12 @@ class MeetingsAgendaFillslot(APIView):
|
|||
|
||||
try:
|
||||
with transaction.atomic():
|
||||
if lock_code:
|
||||
# delete events/bookings with the same lock code in the
|
||||
# same agenda(s) to allow rebooking or confirming
|
||||
Event.objects.filter(
|
||||
agenda__in=agenda.get_real_agendas(), booking__lease__lock_code=lock_code
|
||||
).delete()
|
||||
if to_cancel_booking:
|
||||
cancelled_booking_id = to_cancel_booking.pk
|
||||
to_cancel_booking.cancel()
|
||||
|
@ -1514,6 +1536,12 @@ class MeetingsAgendaFillslot(APIView):
|
|||
color=color,
|
||||
)
|
||||
booking.save()
|
||||
if lock_code and not confirm_after_lock:
|
||||
Lease.objects.create(
|
||||
booking=booking,
|
||||
lock_code=lock_code,
|
||||
)
|
||||
|
||||
except IntegrityError as e:
|
||||
if 'tstzrange_constraint' in str(e):
|
||||
# "optimistic concurrency control", between our availability
|
||||
|
|
|
@ -178,6 +178,9 @@ MELLON_IDENTITY_PROVIDERS = []
|
|||
# (see http://docs.python-requests.org/en/master/user/advanced/#proxies)
|
||||
REQUESTS_PROXIES = None
|
||||
|
||||
# default in seconds
|
||||
CHRONO_LOCK_DURATION = 10 * 60
|
||||
|
||||
# timeout used in python-requests call, in seconds
|
||||
# we use 28s by default: timeout just before web server, which is usually 30s
|
||||
REQUESTS_TIMEOUT = 28
|
||||
|
|
|
@ -21,6 +21,7 @@ spooler-max-tasks = 20
|
|||
cron2 = minute=-5,unique=1 /usr/bin/chrono-manage tenant_command cancel_events --all-tenants -v0
|
||||
cron2 = minute=-5,unique=1 /usr/bin/chrono-manage tenant_command send_email_notifications --all-tenants -v0
|
||||
cron2 = minute=-5,unique=1 /usr/bin/chrono-manage tenant_command update_event_recurrences --all-tenants -v0
|
||||
cron2 = minute=-5,unique=1 /usr/bin/chrono-manage tenant_command clean_leases --all-tenants -v0
|
||||
# every fifteen minutes
|
||||
cron2 = minute=-15,unique=1 /usr/bin/chrono-manage tenant_command sync-ants-hub --all-tenants
|
||||
# hourly
|
||||
|
|
|
@ -0,0 +1,274 @@
|
|||
# chrono - agendas system
|
||||
# Copyright (C) 2023 Entr'ouvert
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Affero General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import datetime
|
||||
|
||||
import pytest
|
||||
from django.core.management import call_command
|
||||
|
||||
from chrono.agendas.models import Booking, Lease
|
||||
from tests.utils import build_meetings_agenda, build_virtual_agenda
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
def test_meetings_agenda(app, user):
|
||||
'''Test fillslot on meetings agenda with lock_code'''
|
||||
agenda = build_meetings_agenda(
|
||||
'Agenda',
|
||||
resources=['Re1'],
|
||||
meeting_types=(30,),
|
||||
desks={'desk-1': ['monday-friday 9:00-12:00 14:00-17:00']},
|
||||
)
|
||||
|
||||
datetimes_url = agenda._mt_30.get_datetimes_url()
|
||||
|
||||
# list free slots, with or without a lock
|
||||
resp = app.get(datetimes_url + '?lock_code=MYLOCK')
|
||||
free_slots = len(resp.json['data'])
|
||||
resp = app.get(datetimes_url + '?lock_code=OTHERLOCK')
|
||||
assert free_slots == len(resp.json['data'])
|
||||
resp = app.get(datetimes_url)
|
||||
assert free_slots == len(resp.json['data'])
|
||||
|
||||
# lock a slot
|
||||
fillslot_url = resp.json['data'][2]['api']['fillslot_url']
|
||||
app.authorization = ('Basic', ('john.doe', 'password'))
|
||||
app.post_json(fillslot_url, params={'lock_code': 'MYLOCK'})
|
||||
assert Booking.objects.count() == 1
|
||||
assert Lease.objects.get().lock_code == 'MYLOCK'
|
||||
|
||||
# list free slots: one is locked ...
|
||||
resp = app.get(datetimes_url)
|
||||
assert len([x for x in resp.json['data'] if x.get('disabled')]) == 1
|
||||
|
||||
resp = app.get(datetimes_url, params={'lock_code': 'OTHERLOCK'})
|
||||
assert len([x for x in resp.json['data'] if x.get('disabled')]) == 1
|
||||
|
||||
# ... unless it's MYLOCK
|
||||
resp = app.get(datetimes_url, params={'lock_code': 'MYLOCK'})
|
||||
assert len([x for x in resp.json['data'] if x.get('disabled')]) == 0
|
||||
|
||||
# can't lock the same timeslot ...
|
||||
resp_booking = app.post_json(fillslot_url, params={'lock_code': 'OTHERLOCK'})
|
||||
assert resp_booking.json['err'] == 1
|
||||
assert resp_booking.json['reason'] == 'no more desk available'
|
||||
|
||||
# ... unless with MYLOCK (aka "relock")
|
||||
resp_booking = app.post_json(fillslot_url, params={'lock_code': 'MYLOCK'})
|
||||
assert resp_booking.json['err'] == 0
|
||||
assert Booking.objects.count() == 1
|
||||
assert Lease.objects.get().lock_code == 'MYLOCK'
|
||||
|
||||
# can't book the slot ...
|
||||
resp_booking = app.post_json(fillslot_url)
|
||||
assert resp_booking.json['err'] == 1
|
||||
assert resp_booking.json['reason'] == 'no more desk available'
|
||||
|
||||
resp_booking = app.post_json(fillslot_url, params={'confirm_after_lock': True})
|
||||
assert resp_booking.json['err'] == 1
|
||||
assert resp_booking.json['reason'] == 'no more desk available'
|
||||
|
||||
resp_booking = app.post_json(fillslot_url, params={'lock_code': 'OTHERLOCK', 'confirm_after_lock': True})
|
||||
assert resp_booking.json['err'] == 1
|
||||
assert resp_booking.json['reason'] == 'no more desk available'
|
||||
|
||||
# ... unless with MYLOCK (aka "confirm")
|
||||
resp_booking = app.post_json(fillslot_url, params={'lock_code': 'MYLOCK', 'confirm_after_lock': True})
|
||||
assert resp_booking.json['err'] == 0
|
||||
assert Booking.objects.count() == 1
|
||||
assert Lease.objects.count() == 0
|
||||
|
||||
|
||||
def test_meetings_agenda_expiration(app, user, freezer):
|
||||
'''Test fillslot on meetings agenda with lock_code'''
|
||||
agenda = build_meetings_agenda(
|
||||
'Agenda',
|
||||
resources=['Re1'],
|
||||
meeting_types=(30,),
|
||||
desks={'desk-1': ['monday-friday 9:00-12:00 14:00-17:00']},
|
||||
)
|
||||
datetimes_url = agenda._mt_30.get_datetimes_url()
|
||||
|
||||
# list free slots
|
||||
resp = app.get(datetimes_url)
|
||||
|
||||
# lock a slot
|
||||
fillslot_url = resp.json['data'][2]['api']['fillslot_url']
|
||||
app.authorization = ('Basic', ('john.doe', 'password'))
|
||||
app.post_json(fillslot_url, params={'lock_code': 'MYLOCK'})
|
||||
assert Booking.objects.count() == 1
|
||||
assert Lease.objects.get().lock_code == 'MYLOCK'
|
||||
|
||||
# list free slots: one is locked ...
|
||||
resp = app.get(datetimes_url)
|
||||
assert len([x for x in resp.json['data'] if x.get('disabled')]) == 1
|
||||
|
||||
# after 30 minutes it is not locked anymore
|
||||
freezer.move_to(datetime.timedelta(minutes=30))
|
||||
call_command('clean_leases')
|
||||
|
||||
resp = app.get(datetimes_url)
|
||||
assert len([x for x in resp.json['data'] if x.get('disabled')]) == 0
|
||||
|
||||
|
||||
def test_meetings_agenda_with_resource_exclusion(app, user):
|
||||
'''Test fillslot on meetings agenda with lock_code and ressources'''
|
||||
agenda1 = build_meetings_agenda(
|
||||
'Agenda 1',
|
||||
resources=['Re1'],
|
||||
meeting_types=(30,),
|
||||
desks={'desk-1': ['monday-friday 9:00-12:00 14:00-17:00']},
|
||||
)
|
||||
agenda2 = build_meetings_agenda(
|
||||
'Agenda 2',
|
||||
resources=['Re1'],
|
||||
meeting_types=(30,),
|
||||
desks={'desk-1': ['monday-friday 9:00-12:00 14:00-17:00']},
|
||||
)
|
||||
resource = agenda2._re_re1
|
||||
agenda1_datetimes_url = agenda1._mt_30.get_datetimes_url()
|
||||
agenda2_datetimes_url = agenda2._mt_30.get_datetimes_url()
|
||||
|
||||
# list free slots, with or without a lock
|
||||
resp = app.get(agenda1_datetimes_url, params={'lock_code': 'OTHERLOCK', 'resources': resource.slug})
|
||||
free_slots = len(resp.json['data'])
|
||||
resp = app.get(agenda1_datetimes_url, params={'lock_code': 'MYLOCK', 'resources': resource.slug})
|
||||
assert free_slots == len(resp.json['data'])
|
||||
resp = app.get(agenda1_datetimes_url, params={'resources': resource.slug})
|
||||
assert free_slots == len(resp.json['data'])
|
||||
resp = app.get(agenda1_datetimes_url)
|
||||
assert free_slots == len(resp.json['data'])
|
||||
|
||||
# lock a slot
|
||||
fillslot_url = resp.json['data'][2]['api']['fillslot_url']
|
||||
app.authorization = ('Basic', ('john.doe', 'password'))
|
||||
app.post_json(fillslot_url + '?resources=re1', params={'lock_code': 'MYLOCK'})
|
||||
assert Booking.objects.count() == 1
|
||||
assert Lease.objects.get().lock_code == 'MYLOCK'
|
||||
|
||||
# list free slots: one is locked ...
|
||||
resp = app.get(agenda1_datetimes_url, params={'resources': resource.slug})
|
||||
assert free_slots == len(resp.json['data'])
|
||||
assert len([x for x in resp.json['data'] if x.get('disabled')]) == 1
|
||||
|
||||
resp = app.get(agenda1_datetimes_url, params={'lock_code': 'OTHERLOCK', 'resources': resource.slug})
|
||||
assert len([x for x in resp.json['data'] if x.get('disabled')]) == 1
|
||||
|
||||
# ... unless it's MYLOCK
|
||||
resp = app.get(agenda1_datetimes_url, params={'lock_code': 'MYLOCK', 'resources': resource.slug})
|
||||
assert free_slots == len(resp.json['data'])
|
||||
assert len([x for x in resp.json['data'] if x.get('disabled')]) == 0
|
||||
|
||||
# check slot is also disabled on another agenda with same resource
|
||||
resp = app.get(agenda2_datetimes_url)
|
||||
assert len([x for x in resp.json['data'] if x.get('disabled')]) == 0
|
||||
resp = app.get(agenda2_datetimes_url, params={'resources': resource.slug})
|
||||
assert len([x for x in resp.json['data'] if x.get('disabled')]) == 1
|
||||
|
||||
# can't lock the same timeslot ...
|
||||
resp_booking = app.post_json(fillslot_url + '?resources=re1', params={'lock_code': 'OTHERLOCK'})
|
||||
assert resp_booking.json['err'] == 1
|
||||
assert resp_booking.json['reason'] == 'no more desk available'
|
||||
|
||||
# ... unless with MYLOCK (aka "relock")
|
||||
resp_booking = app.post_json(fillslot_url + '?resources=re1', params={'lock_code': 'MYLOCK'})
|
||||
assert resp_booking.json['err'] == 0
|
||||
assert Booking.objects.count() == 1
|
||||
assert Lease.objects.get().lock_code == 'MYLOCK'
|
||||
|
||||
# can't book the slot ...
|
||||
resp_booking = app.post_json(fillslot_url + '?resources=re1')
|
||||
assert resp_booking.json['err'] == 1
|
||||
assert resp_booking.json['reason'] == 'no more desk available'
|
||||
|
||||
resp_booking = app.post_json(fillslot_url + '?resources=re1', params={'confirm_after_lock': True})
|
||||
assert resp_booking.json['err'] == 1
|
||||
assert resp_booking.json['reason'] == 'no more desk available'
|
||||
|
||||
resp_booking = app.post_json(
|
||||
fillslot_url + '?resources=re1', params={'lock_code': 'OTHERLOCK', 'confirm_after_lock': True}
|
||||
)
|
||||
assert resp_booking.json['err'] == 1
|
||||
assert resp_booking.json['reason'] == 'no more desk available'
|
||||
|
||||
# unless with MYLOCK (aka "confirm")
|
||||
resp_booking = app.post_json(
|
||||
fillslot_url + '?resources=re1', params={'lock_code': 'MYLOCK', 'confirm_after_lock': True}
|
||||
)
|
||||
assert resp_booking.json['err'] == 0
|
||||
assert Booking.objects.count() == 1
|
||||
assert Lease.objects.count() == 0
|
||||
|
||||
|
||||
def test_virtual_agenda_with_external_user_id_exclusion(app, user):
|
||||
'''Test lock_code use when excluding an external_user_id'''
|
||||
agenda = build_virtual_agenda(
|
||||
meeting_types=(30,),
|
||||
agendas={
|
||||
'Agenda 1': {
|
||||
'desks': {
|
||||
'desk': 'monday-friday 08:00-12:00 14:00-17:00',
|
||||
},
|
||||
},
|
||||
'Agenda 2': {
|
||||
'desks': {
|
||||
'desk': 'monday-friday 09:00-12:00',
|
||||
},
|
||||
},
|
||||
'Agenda 3': {
|
||||
'desks': {
|
||||
'desk': 'monday-friday 15:00-17:00',
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
datetimes_url = agenda._mt_30.get_datetimes_url()
|
||||
|
||||
resp = app.get(datetimes_url)
|
||||
slots = resp.json['data']
|
||||
# get first slot between 11 and 11:30
|
||||
slot = [slot for slot in slots if ' 11:00:00' in slot['datetime']][0]
|
||||
fillslot_url = slot['api']['fillslot_url']
|
||||
|
||||
app.authorization = ('Basic', ('john.doe', 'password'))
|
||||
resp = app.post_json(fillslot_url, params={'lock_code': 'MYLOCK', 'user_external_id': 'abcd'})
|
||||
assert resp.json['err'] == 0
|
||||
|
||||
# check the lease was created
|
||||
assert Booking.objects.filter(user_external_id='abcd', lease__lock_code='MYLOCK').count() == 1
|
||||
|
||||
# check 11:00 slot is still available
|
||||
resp = app.get(datetimes_url)
|
||||
slots = resp.json['data']
|
||||
assert any(
|
||||
s['datetime'] == slot['datetime'] for s in slots if not s['disabled']
|
||||
), f"slot {slot['datetime']} should be available"
|
||||
|
||||
# check 11:00 slot is unavailable when tested with user_external_id
|
||||
resp = app.get(datetimes_url, params={'user_external_id': 'abcd'})
|
||||
slots = resp.json['data']
|
||||
assert not any(
|
||||
s['datetime'] == slot['datetime'] for s in slots if not s['disabled']
|
||||
), f"slot {slot['datetime']} should not be available"
|
||||
|
||||
# check 11:00 slot is available if tested with user_external_id *AND* lock_code
|
||||
resp = app.get(datetimes_url, params={'lock_code': 'MYLOCK', 'user_external_id': 'abcd'})
|
||||
slots = resp.json['data']
|
||||
assert any(
|
||||
s['datetime'] == slot['datetime'] for s in slots if not s['disabled']
|
||||
), f"slot {slot['datetime']} should be available"
|
Loading…
Reference in New Issue