api: add date time period support in datetimes and fillslot (#70185)
This commit is contained in:
parent
bb148dc008
commit
f8e1888144
|
@ -542,43 +542,55 @@ class Agenda(models.Model):
|
|||
self.reminder_settings.duplicate(agenda_target=new_agenda)
|
||||
return new_agenda
|
||||
|
||||
def get_effective_time_periods(self):
|
||||
def get_effective_time_periods(self, min_datetime=None, max_datetime=None):
|
||||
"""Regroup timeperiods by desks.
|
||||
|
||||
List all timeperiods, timeperiods having the same begin_time and
|
||||
end_time are regrouped in a SharedTimePeriod object, which has a
|
||||
list of desks instead of only one desk.
|
||||
"""
|
||||
min_date = min_datetime.date() if min_datetime else None
|
||||
max_date = max_datetime.date() if max_datetime else None
|
||||
if self.kind == 'virtual':
|
||||
return self.get_effective_time_periods_virtual()
|
||||
return self.get_effective_time_periods_virtual(min_date, max_date)
|
||||
elif self.kind == 'meetings':
|
||||
return self.get_effective_time_periods_meetings()
|
||||
return self.get_effective_time_periods_meetings(min_date, max_date)
|
||||
else:
|
||||
raise ValueError('does not work with kind %r' % self.kind)
|
||||
|
||||
def get_effective_time_periods_meetings(self):
|
||||
def get_effective_time_periods_meetings(self, min_date, max_date):
|
||||
"""List timeperiod instances for all desks of the agenda, convert them
|
||||
into an Interval of WeekTime which can be compared and regrouped using
|
||||
itertools.groupby.
|
||||
"""
|
||||
time_periods = TimePeriod.objects.filter(desk__agenda=self)
|
||||
if min_date:
|
||||
time_periods.filter(Q(date__isnull=True) | Q(date__gte=min_date))
|
||||
if max_date:
|
||||
time_periods.filter(Q(date__isnull=True) | Q(date__lte=max_date))
|
||||
|
||||
yield from (
|
||||
SharedTimePeriod.from_weektime_interval(
|
||||
weektime_interval,
|
||||
desks=[time_period.desk for time_period in time_periods],
|
||||
)
|
||||
for weektime_interval, time_periods in itertools.groupby(
|
||||
TimePeriod.objects.filter(desk__agenda=self)
|
||||
.prefetch_related('desk')
|
||||
.order_by('weekday', 'start_time', 'end_time'),
|
||||
time_periods.prefetch_related('desk').order_by('weekday', 'start_time', 'end_time'),
|
||||
key=TimePeriod.as_weektime_interval,
|
||||
)
|
||||
)
|
||||
|
||||
def get_effective_time_periods_virtual(self):
|
||||
def get_effective_time_periods_virtual(self, min_date, max_date):
|
||||
"""List timeperiod instances for all desks of all real agendas of this
|
||||
virtual agenda, convert them into an Interval of WeekTime which can be
|
||||
compared and regrouped using itertools.groupby.
|
||||
"""
|
||||
time_periods = TimePeriod.objects.filter(desk__agenda__virtual_agendas=self)
|
||||
if min_date:
|
||||
time_periods.filter(Q(date__isnull=True) | Q(date__gte=min_date))
|
||||
if max_date:
|
||||
time_periods.filter(Q(date__isnull=True) | Q(date__lte=max_date))
|
||||
|
||||
closed_hours_by_days = IntervalSet.from_ordered(
|
||||
[
|
||||
time_period.as_weektime_interval()
|
||||
|
@ -586,9 +598,7 @@ class Agenda(models.Model):
|
|||
]
|
||||
)
|
||||
for time_period_interval, time_periods in itertools.groupby(
|
||||
TimePeriod.objects.filter(desk__agenda__virtual_agendas=self)
|
||||
.order_by('weekday', 'start_time', 'end_time')
|
||||
.prefetch_related('desk'),
|
||||
time_periods.order_by('weekday', 'start_time', 'end_time').prefetch_related('desk'),
|
||||
key=lambda tp: tp.as_weektime_interval(),
|
||||
):
|
||||
time_periods = list(time_periods)
|
||||
|
@ -1091,14 +1101,17 @@ WEEKDAYS_LIST = sorted(WEEKDAYS.items(), key=lambda x: x[0])
|
|||
class WeekTime(collections.namedtuple('WeekTime', ['weekday', 'time'])):
|
||||
"""Representation of a time point in a weekday, ex.: Monday at 5 o'clock."""
|
||||
|
||||
def __new__(cls, weekday, weekday_indexes, time):
|
||||
def __new__(cls, weekday, weekday_indexes, date, time):
|
||||
if date:
|
||||
weekday = date.weekday()
|
||||
self = super().__new__(cls, weekday, time)
|
||||
self.weekday_indexes = weekday_indexes
|
||||
self.date = date
|
||||
return self
|
||||
|
||||
def __repr__(self):
|
||||
return '%s / %s' % (
|
||||
force_str(WEEKDAYS[self.weekday]),
|
||||
self.date or force_str(WEEKDAYS[self.weekday]),
|
||||
date_format(self.time, 'TIME_FORMAT'),
|
||||
)
|
||||
|
||||
|
@ -1139,12 +1152,15 @@ class TimePeriod(models.Model):
|
|||
]
|
||||
|
||||
def __str__(self):
|
||||
label = force_str(WEEKDAYS[self.weekday])
|
||||
if self.weekday_indexes:
|
||||
label = _('%(weekday)s (%(ordinals)s of the month)') % {
|
||||
'weekday': label,
|
||||
'ordinals': ', '.join(ordinal(i) for i in self.weekday_indexes),
|
||||
}
|
||||
if self.date:
|
||||
label = date_format(self.date, 'l d F Y')
|
||||
else:
|
||||
label = force_str(WEEKDAYS[self.weekday])
|
||||
if self.weekday_indexes:
|
||||
label = _('%(weekday)s (%(ordinals)s of the month)') % {
|
||||
'weekday': label,
|
||||
'ordinals': ', '.join(ordinal(i) for i in self.weekday_indexes),
|
||||
}
|
||||
|
||||
label = '%s / %s → %s' % (
|
||||
label,
|
||||
|
@ -1191,8 +1207,8 @@ class TimePeriod(models.Model):
|
|||
|
||||
def as_weektime_interval(self):
|
||||
return Interval(
|
||||
WeekTime(self.weekday, self.weekday_indexes, self.start_time),
|
||||
WeekTime(self.weekday, self.weekday_indexes, self.end_time),
|
||||
WeekTime(self.weekday, self.weekday_indexes, self.date, self.start_time),
|
||||
WeekTime(self.weekday, self.weekday_indexes, self.date, self.end_time),
|
||||
)
|
||||
|
||||
def as_shared_timeperiods(self):
|
||||
|
@ -1201,6 +1217,7 @@ class TimePeriod(models.Model):
|
|||
weekday_indexes=self.weekday_indexes,
|
||||
start_time=self.start_time,
|
||||
end_time=self.end_time,
|
||||
date=self.date,
|
||||
desks=[self.desk],
|
||||
)
|
||||
|
||||
|
@ -1226,13 +1243,14 @@ class SharedTimePeriod:
|
|||
of get_all_slots() for details).
|
||||
"""
|
||||
|
||||
__slots__ = ['weekday', 'weekday_indexes', 'start_time', 'end_time', 'desks']
|
||||
__slots__ = ['weekday', 'weekday_indexes', 'start_time', 'end_time', 'date', 'desks']
|
||||
|
||||
def __init__(self, weekday, weekday_indexes, start_time, end_time, desks):
|
||||
def __init__(self, weekday, weekday_indexes, start_time, end_time, date, desks):
|
||||
self.weekday = weekday
|
||||
self.weekday_indexes = weekday_indexes
|
||||
self.start_time = start_time
|
||||
self.end_time = end_time
|
||||
self.date = date
|
||||
self.desks = set(desks)
|
||||
|
||||
def __str__(self):
|
||||
|
@ -1243,17 +1261,19 @@ class SharedTimePeriod:
|
|||
)
|
||||
|
||||
def __eq__(self, other):
|
||||
return (self.weekday, self.start_time, self.end_time) == (
|
||||
return (self.weekday, self.start_time, self.end_time, self.date) == (
|
||||
other.weekday,
|
||||
other.start_time,
|
||||
other.end_time,
|
||||
other.date,
|
||||
)
|
||||
|
||||
def __lt__(self, other):
|
||||
return (self.weekday, self.start_time, self.end_time) < (
|
||||
return (self.weekday, self.start_time, self.end_time, self.date) < (
|
||||
other.weekday,
|
||||
other.start_time,
|
||||
other.end_time,
|
||||
other.date,
|
||||
)
|
||||
|
||||
def get_time_slots(self, min_datetime, max_datetime, meeting_duration, base_duration):
|
||||
|
@ -1278,10 +1298,17 @@ class SharedTimePeriod:
|
|||
Generated start_datetime MUST be in the local timezone, and the local
|
||||
timezone must not change, as the API needs it to generate stable ids.
|
||||
"""
|
||||
if self.date and not (min_datetime.date() <= self.date <= max_datetime.date()):
|
||||
return
|
||||
|
||||
meeting_duration = datetime.timedelta(minutes=meeting_duration)
|
||||
duration = datetime.timedelta(minutes=base_duration)
|
||||
|
||||
real_min_datetime = min_datetime + datetime.timedelta(days=self.weekday - min_datetime.weekday())
|
||||
real_min_datetime = (
|
||||
min_datetime + datetime.timedelta(days=self.weekday - min_datetime.weekday())
|
||||
if not self.date
|
||||
else min_datetime
|
||||
)
|
||||
if real_min_datetime < min_datetime:
|
||||
real_min_datetime += datetime.timedelta(days=7)
|
||||
|
||||
|
@ -1293,6 +1320,10 @@ class SharedTimePeriod:
|
|||
event_datetime = make_aware(make_naive(real_min_datetime)).replace(
|
||||
hour=self.start_time.hour, minute=self.start_time.minute, second=0, microsecond=0
|
||||
)
|
||||
if self.date:
|
||||
event_datetime = event_datetime.replace(
|
||||
day=self.date.day, month=self.date.month, year=self.date.year
|
||||
)
|
||||
# don't start before min_datetime
|
||||
event_datetime = max(event_datetime, min_datetime)
|
||||
|
||||
|
@ -1305,6 +1336,10 @@ class SharedTimePeriod:
|
|||
or event_datetime.date() != next_time.date()
|
||||
or (self.weekday_indexes and get_weekday_index(event_datetime) not in self.weekday_indexes)
|
||||
):
|
||||
# if time slot is not repeating, end now
|
||||
if self.date:
|
||||
break
|
||||
|
||||
# switch to naive time for day/week changes
|
||||
event_datetime = make_naive(event_datetime)
|
||||
# back to morning
|
||||
|
@ -1335,6 +1370,7 @@ class SharedTimePeriod:
|
|||
weekday_indexes=begin.weekday_indexes or end.weekday_indexes,
|
||||
start_time=begin.time,
|
||||
end_time=end.time,
|
||||
date=begin.date or end.date,
|
||||
desks=desks,
|
||||
)
|
||||
|
||||
|
|
|
@ -285,7 +285,7 @@ def get_all_slots(
|
|||
)
|
||||
|
||||
unique_booked = {}
|
||||
for time_period in base_agenda.get_effective_time_periods():
|
||||
for time_period in base_agenda.get_effective_time_periods(base_min_datetime, base_max_datetime):
|
||||
duration = (
|
||||
datetime.datetime.combine(base_date, time_period.end_time)
|
||||
- datetime.datetime.combine(base_date, time_period.start_time)
|
||||
|
|
|
@ -2426,3 +2426,91 @@ def test_datetimes_api_meetings_virtual_agenda_weekday_indexes(app):
|
|||
'2022-03-07 11:30:00',
|
||||
'2022-03-14 11:30:00',
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.freeze_time('2022-10-24 10:00')
|
||||
def test_datetimes_api_meetings_agenda_date_time_period(app):
|
||||
agenda = Agenda.objects.create(
|
||||
label='Foo bar', kind='meetings', minimal_booking_delay=0, maximal_booking_delay=8
|
||||
)
|
||||
meeting_type = MeetingType.objects.create(agenda=agenda, label='Plop', duration=30)
|
||||
desk = Desk.objects.create(agenda=agenda, label='desk')
|
||||
|
||||
TimePeriod.objects.create(
|
||||
date=datetime.date(2022, 10, 24),
|
||||
start_time=datetime.time(12, 0),
|
||||
end_time=datetime.time(14, 0),
|
||||
desk=desk,
|
||||
)
|
||||
api_url = '/api/agenda/%s/meetings/%s/datetimes/' % (agenda.slug, meeting_type.slug)
|
||||
|
||||
resp = app.get(api_url)
|
||||
assert [x['datetime'] for x in resp.json['data']] == [
|
||||
'2022-10-24 12:00:00',
|
||||
'2022-10-24 12:30:00',
|
||||
'2022-10-24 13:00:00',
|
||||
'2022-10-24 13:30:00',
|
||||
]
|
||||
|
||||
resp = app.get(api_url, params={'date_start': '2022-10-25'})
|
||||
assert resp.json['data'] == []
|
||||
|
||||
# mix with repeating period
|
||||
TimePeriod.objects.create(
|
||||
weekday=0,
|
||||
start_time=datetime.time(13, 0),
|
||||
end_time=datetime.time(15, 0),
|
||||
desk=desk,
|
||||
)
|
||||
|
||||
resp = app.get(api_url)
|
||||
assert [x['datetime'] for x in resp.json['data']] == [
|
||||
'2022-10-24 12:00:00',
|
||||
'2022-10-24 12:30:00',
|
||||
'2022-10-24 13:00:00',
|
||||
'2022-10-24 13:30:00',
|
||||
'2022-10-24 14:00:00',
|
||||
'2022-10-24 14:30:00',
|
||||
'2022-10-31 13:00:00',
|
||||
'2022-10-31 13:30:00',
|
||||
'2022-10-31 14:00:00',
|
||||
'2022-10-31 14:30:00',
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.freeze_time('2022-10-24 10:00')
|
||||
def test_datetimes_api_meetings_virtual_agenda_date_time_period(app):
|
||||
agenda = Agenda.objects.create(
|
||||
label='Foo bar', kind='meetings', minimal_booking_delay=0, maximal_booking_delay=8
|
||||
)
|
||||
desk = Desk.objects.create(agenda=agenda, label='desk')
|
||||
meeting_type = MeetingType.objects.create(agenda=agenda, label='Plop', duration=30)
|
||||
virtual_agenda = Agenda.objects.create(label='Foo bar Meeting', kind='virtual')
|
||||
virtual_agenda.real_agendas.add(agenda)
|
||||
|
||||
TimePeriod.objects.create(
|
||||
date=datetime.date(2022, 10, 24),
|
||||
start_time=datetime.time(12, 0),
|
||||
end_time=datetime.time(14, 0),
|
||||
desk=desk,
|
||||
)
|
||||
|
||||
api_url = '/api/agenda/%s/meetings/%s/datetimes/' % (virtual_agenda.slug, meeting_type.slug)
|
||||
resp = app.get(api_url)
|
||||
assert [x['datetime'] for x in resp.json['data']] == [
|
||||
'2022-10-24 12:00:00',
|
||||
'2022-10-24 12:30:00',
|
||||
'2022-10-24 13:00:00',
|
||||
'2022-10-24 13:30:00',
|
||||
]
|
||||
|
||||
# add exclusion period on virtual agenda
|
||||
TimePeriod.objects.create(
|
||||
weekday=0, start_time=datetime.time(12, 00), end_time=datetime.time(13, 00), agenda=virtual_agenda
|
||||
)
|
||||
resp = app.get(api_url)
|
||||
resp = app.get(api_url)
|
||||
assert [x['datetime'] for x in resp.json['data']] == [
|
||||
'2022-10-24 13:00:00',
|
||||
'2022-10-24 13:30:00',
|
||||
]
|
||||
|
|
|
@ -995,6 +995,42 @@ def test_booking_api_with_data(app, user):
|
|||
assert Booking.objects.all()[0].extra_data == {'hello': 'world'}
|
||||
|
||||
|
||||
@pytest.mark.freeze_time('2022-10-24 10:00')
|
||||
def test_booking_api_meeting_date_time_period(app, user):
|
||||
agenda = Agenda.objects.create(
|
||||
label='Foo bar', kind='meetings', minimal_booking_delay=0, maximal_booking_delay=8
|
||||
)
|
||||
meeting_type = MeetingType.objects.create(agenda=agenda, label='Plop', duration=30)
|
||||
desk = Desk.objects.create(agenda=agenda, label='desk')
|
||||
|
||||
TimePeriod.objects.create(
|
||||
date=datetime.date(2022, 10, 24),
|
||||
start_time=datetime.time(12, 0),
|
||||
end_time=datetime.time(14, 0),
|
||||
desk=desk,
|
||||
)
|
||||
datetimes_resp = app.get('/api/agenda/%s/meetings/%s/datetimes/' % (agenda.slug, meeting_type.slug))
|
||||
slot = datetimes_resp.json['data'][0]['id']
|
||||
assert slot == 'plop:2022-10-24-1200'
|
||||
|
||||
app.authorization = ('Basic', ('john.doe', 'password'))
|
||||
|
||||
# single booking
|
||||
resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, slot))
|
||||
assert Booking.objects.count() == 1
|
||||
assert resp.json['duration'] == 30
|
||||
|
||||
# multiple slots
|
||||
slots = [datetimes_resp.json['data'][1]['id'], datetimes_resp.json['data'][2]['id']]
|
||||
assert slots == ['plop:2022-10-24-1230', 'plop:2022-10-24-1300']
|
||||
resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': slots})
|
||||
assert Booking.objects.count() == 3
|
||||
|
||||
resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda.slug, slot))
|
||||
assert resp.json['err'] == 1
|
||||
assert resp.json['err_desc'] == 'no more desk available'
|
||||
|
||||
|
||||
def test_booking_api_available(app, user):
|
||||
agenda = Agenda.objects.create(label='Foo bar', kind='events', minimal_booking_delay=0)
|
||||
for i in range(0, 10):
|
||||
|
|
|
@ -491,3 +491,28 @@ def test_time_period_check_constraint():
|
|||
start_time=datetime.time(hour=1, minute=0),
|
||||
end_time=datetime.time(hour=2, minute=0),
|
||||
)
|
||||
|
||||
|
||||
def test_timeperiod_date_time_slots():
|
||||
agenda = Agenda(label='Foo bar', slug='bar')
|
||||
agenda.save()
|
||||
desk = Desk.objects.create(label='Desk 1', agenda=agenda)
|
||||
meeting_type = MeetingType(duration=60, agenda=agenda)
|
||||
meeting_type.save()
|
||||
timeperiod = TimePeriod(
|
||||
desk=desk,
|
||||
date=datetime.date(2022, 10, 24),
|
||||
start_time=datetime.time(9, 0),
|
||||
end_time=datetime.time(12, 0),
|
||||
)
|
||||
events = timeperiod.as_shared_timeperiods().get_time_slots(
|
||||
min_datetime=make_aware(datetime.datetime(2022, 10, 1)),
|
||||
max_datetime=make_aware(datetime.datetime(2022, 11, 1)),
|
||||
meeting_duration=meeting_type.duration,
|
||||
base_duration=agenda.get_base_meeting_duration(),
|
||||
)
|
||||
assert [x.timetuple()[:5] for x in sorted(events)] == [
|
||||
(2022, 10, 24, 9, 0),
|
||||
(2022, 10, 24, 10, 0),
|
||||
(2022, 10, 24, 11, 0),
|
||||
]
|
||||
|
|
Loading…
Reference in New Issue