api: make shared custody work with recurring events in datetimes (#62598)

This commit is contained in:
Valentin Deniaud 2022-03-08 17:39:11 +01:00
parent 6311c5146a
commit f158dc02ef
2 changed files with 291 additions and 11 deletions

View File

@ -850,7 +850,13 @@ class Agenda(models.Model):
for event in recurring_events:
events.extend(
event.get_recurrences(
min_start, max_start, excluded_datetimes.get(event.pk), exceptions, slug_separator=':'
min_start,
max_start,
excluded_datetimes.get(event.pk),
exceptions,
slug_separator=':',
shared_custody_rules=getattr(self, 'prefetched_custody_rules', None),
shared_custody_periods=getattr(self, 'prefetched_custody_periods', None),
)
)
@ -1084,6 +1090,28 @@ class Agenda(models.Model):
for e in agendas_exceptions
if e.desk_id == desk.pk or e.unavailability_calendar_id in uc_ids
]
if guardian_external_id:
custody_rules = SharedCustodyRule.objects.filter(
guardian__user_external_id=guardian_external_id,
agenda__children__user_external_id=user_external_id,
)
all_periods = SharedCustodyPeriod.objects.filter(
agenda__children__user_external_id=user_external_id
)
if max_start:
all_periods = all_periods.filter(date_start__lte=max_start)
if min_start:
all_periods = all_periods.filter(date_end__gt=min_start)
custody_periods = all_periods.filter(guardian__user_external_id=guardian_external_id)
custody_exceptions = all_periods.exclude(guardian__user_external_id=guardian_external_id)
for agenda in agendas:
agenda.prefetched_custody_rules = custody_rules
agenda.prefetched_custody_periods = custody_periods
agenda.prefetched_exceptions.extend(custody_exceptions)
return agendas
def is_available_for_simple_management(self):
@ -1772,7 +1800,14 @@ class Event(models.Model):
raise e
def get_recurrences(
self, min_datetime, max_datetime, excluded_datetimes=None, exceptions=None, slug_separator='--'
self,
min_datetime,
max_datetime,
excluded_datetimes=None,
exceptions=None,
slug_separator='--',
shared_custody_rules=None,
shared_custody_periods=None,
):
recurrences = []
rrule_set = rruleset()
@ -1781,9 +1816,10 @@ class Event(models.Model):
if exceptions is None:
exceptions = self.agenda.get_recurrence_exceptions(min_datetime, max_datetime)
event_start = localtime(self.start_datetime)
for exception in exceptions:
exception_start = localtime(exception.start_datetime)
event_start = localtime(self.start_datetime)
if event_start.time() < exception_start.time():
exception_start += datetime.timedelta(days=1)
exception_start = exception_start.replace(
@ -1811,11 +1847,32 @@ class Event(models.Model):
url=self.url,
)
if shared_custody_rules:
for rule in shared_custody_rules:
recurrence_rule = self.recurrence_rule
recurrence_rule['byweekday'] = set(recurrence_rule['byweekday']).intersection(
rule.recurrence_rule['byweekday']
)
if not recurrence_rule['byweekday']:
continue
recurrence_rule['byweekno'] = rule.recurrence_rule.get('byweekno')
rrule_set.rrule(rrule(dtstart=make_naive(self.start_datetime), **recurrence_rule))
for period in shared_custody_periods:
period_start = event_start.replace(
year=period.date_start.year, month=period.date_start.month, day=period.date_start.day
)
rule = rrule(dtstart=make_naive(self.start_datetime), **self.recurrence_rule)
for start_datetime in rule.between(
make_naive(period_start), make_naive(period.end_datetime), inc=True
):
rrule_set.rdate(start_datetime)
else:
rrule_set.rrule(rrule(dtstart=make_naive(self.start_datetime), **self.recurrence_rule))
# remove pytz info because dateutil doesn't support DST changes
min_datetime = make_naive(min_datetime)
max_datetime = make_naive(max_datetime)
rrule_set.rrule(rrule(dtstart=make_naive(self.start_datetime), **self.recurrence_rule))
for start_datetime in rrule_set.between(min_datetime, max_datetime, inc=True):
event = copy.copy(event_base)
# add timezone back
@ -3217,6 +3274,15 @@ class SharedCustodyRule(models.Model):
guardian = models.ForeignKey(Person, verbose_name=_('Guardian'), on_delete=models.CASCADE)
def get_slots(self, min_date, max_date):
return [
SharedCustodySlot(self.guardian, dt.date())
for dt in rrule(
dtstart=min_date, until=max_date - datetime.timedelta(days=1), **self.recurrence_rule
)
]
@property
def recurrence_rule(self):
recurrence_rule = {
'freq': WEEKLY,
'byweekday': self.days,
@ -3225,11 +3291,7 @@ class SharedCustodyRule(models.Model):
recurrence_rule['byweekno'] = list(range(1, 55, 2))
elif self.weeks == 'even':
recurrence_rule['byweekno'] = list(range(0, 54, 2))
return [
SharedCustodySlot(self.guardian, dt.date())
for dt in rrule(dtstart=min_date, until=max_date - datetime.timedelta(days=1), **recurrence_rule)
]
return recurrence_rule
@property
def label(self):
@ -3276,3 +3338,11 @@ class SharedCustodyPeriod(models.Model):
date_format(self.date_end, 'SHORT_DATE_FORMAT'),
)
return '%s, %s' % (self.guardian.name, exc_repr)
@property
def start_datetime(self):
return make_aware(datetime.datetime.combine(self.date_start, datetime.time(0, 0)))
@property
def end_datetime(self):
return make_aware(datetime.datetime.combine(self.date_end, datetime.time(0, 0)))

View File

@ -2044,7 +2044,7 @@ def test_datetimes_multiple_agendas_queries(app):
},
)
assert len(resp.json['data']) == 30
assert len(ctx.captured_queries) == 8
assert len(ctx.captured_queries) == 9
@pytest.mark.freeze_time('2021-05-06 14:00')
@ -2498,3 +2498,213 @@ def test_datetimes_multiple_agendas_shared_custody_other_rules(app):
params={'subscribed': 'all', 'user_external_id': 'child_id', 'guardian_external_id': 'mother_id'},
)
assert len(resp.json['data']) == 2
@pytest.mark.freeze_time('2022-03-07 14:00') # Monday of 10th week
def test_datetimes_multiple_agendas_shared_custody_recurring_event(app):
agenda = Agenda.objects.create(label='First agenda', kind='events', maximal_booking_delay=30)
Desk.objects.create(agenda=agenda, slug='_exceptions_holder')
wednesday_event = Event.objects.create(
slug='event-wednesday',
start_datetime=make_aware(datetime.datetime(year=2022, month=3, day=9, hour=14, minute=0)),
recurrence_days=[2],
places=5,
agenda=agenda,
)
thursday_event = Event.objects.create(
slug='event-thursday',
start_datetime=make_aware(datetime.datetime(year=2022, month=3, day=10, hour=14, minute=0)),
recurrence_days=[3],
places=5,
agenda=agenda,
)
Subscription.objects.create(
agenda=agenda,
user_external_id='child_id',
date_start=now(),
date_end=now() + datetime.timedelta(days=30),
)
father = Person.objects.create(user_external_id='father_id', name='John Doe')
mother = Person.objects.create(user_external_id='mother_id', name='Jane Doe')
child = Person.objects.create(user_external_id='child_id', name='James Doe')
agenda = SharedCustodyAgenda.objects.create(first_guardian=father, second_guardian=mother)
agenda.children.add(child)
father_rule = SharedCustodyRule.objects.create(
agenda=agenda, guardian=father, days=list(range(7)), weeks='even'
)
mother_rule = SharedCustodyRule.objects.create(
agenda=agenda, guardian=mother, days=list(range(7)), weeks='odd'
)
resp = app.get(
'/api/agendas/datetimes/',
params={'subscribed': 'all', 'user_external_id': 'child_id'},
)
assert [d['id'] for d in resp.json['data']] == [
'first-agenda@event-wednesday:2022-03-09-1400',
'first-agenda@event-thursday:2022-03-10-1400',
'first-agenda@event-wednesday:2022-03-16-1400',
'first-agenda@event-thursday:2022-03-17-1400',
'first-agenda@event-wednesday:2022-03-23-1400',
'first-agenda@event-thursday:2022-03-24-1400',
'first-agenda@event-wednesday:2022-03-30-1400',
'first-agenda@event-thursday:2022-03-31-1400',
]
resp = app.get(
'/api/agendas/datetimes/',
params={'subscribed': 'all', 'user_external_id': 'child_id', 'guardian_external_id': 'father_id'},
)
assert [d['id'] for d in resp.json['data']] == [
'first-agenda@event-wednesday:2022-03-09-1400',
'first-agenda@event-thursday:2022-03-10-1400',
'first-agenda@event-wednesday:2022-03-23-1400',
'first-agenda@event-thursday:2022-03-24-1400',
]
resp = app.get(
'/api/agendas/datetimes/',
params={'subscribed': 'all', 'user_external_id': 'child_id', 'guardian_external_id': 'mother_id'},
)
assert [d['id'] for d in resp.json['data']] == [
'first-agenda@event-wednesday:2022-03-16-1400',
'first-agenda@event-thursday:2022-03-17-1400',
'first-agenda@event-wednesday:2022-03-30-1400',
'first-agenda@event-thursday:2022-03-31-1400',
]
# add father custody period spanning odd week event
SharedCustodyPeriod.objects.create(
agenda=agenda,
guardian=father,
date_start=datetime.date(2022, 3, 16),
date_end=datetime.date(2022, 3, 18),
)
resp = app.get(
'/api/agendas/datetimes/',
params={'subscribed': 'all', 'user_external_id': 'child_id', 'guardian_external_id': 'father_id'},
)
assert [d['id'] for d in resp.json['data']] == [
'first-agenda@event-wednesday:2022-03-09-1400',
'first-agenda@event-thursday:2022-03-10-1400',
'first-agenda@event-wednesday:2022-03-16-1400',
'first-agenda@event-thursday:2022-03-17-1400',
'first-agenda@event-wednesday:2022-03-23-1400',
'first-agenda@event-thursday:2022-03-24-1400',
]
resp = app.get(
'/api/agendas/datetimes/',
params={'subscribed': 'all', 'user_external_id': 'child_id', 'guardian_external_id': 'mother_id'},
)
assert [d['id'] for d in resp.json['data']] == [
'first-agenda@event-wednesday:2022-03-30-1400',
'first-agenda@event-thursday:2022-03-31-1400',
]
# check date_start/date_end
resp = app.get(
'/api/agendas/datetimes/',
params={
'subscribed': 'all',
'user_external_id': 'child_id',
'guardian_external_id': 'father_id',
'date_start': '2022-03-16',
'date_end': '2022-03-17',
},
)
assert [d['id'] for d in resp.json['data']] == [
'first-agenda@event-wednesday:2022-03-16-1400',
]
resp = app.get(
'/api/agendas/datetimes/',
params={
'subscribed': 'all',
'user_external_id': 'child_id',
'guardian_external_id': 'father_id',
'date_start': '2022-03-17',
'date_end': '2022-03-18',
},
)
assert [d['id'] for d in resp.json['data']] == [
'first-agenda@event-thursday:2022-03-17-1400',
]
# weirder rules
father_rule.days = [0, 1]
father_rule.weeks = ''
father_rule.save()
SharedCustodyRule.objects.create(agenda=agenda, guardian=father, days=[2])
mother_rule.weeks = ''
mother_rule.days = [3, 4, 5, 6]
mother_rule.save()
resp = app.get(
'/api/agendas/datetimes/',
params={'subscribed': 'all', 'user_external_id': 'child_id', 'guardian_external_id': 'father_id'},
)
assert [d['id'] for d in resp.json['data']] == [
'first-agenda@event-wednesday:2022-03-09-1400',
'first-agenda@event-wednesday:2022-03-16-1400',
'first-agenda@event-thursday:2022-03-17-1400',
'first-agenda@event-wednesday:2022-03-23-1400',
'first-agenda@event-wednesday:2022-03-30-1400',
]
resp = app.get(
'/api/agendas/datetimes/',
params={'subscribed': 'all', 'user_external_id': 'child_id', 'guardian_external_id': 'mother_id'},
)
assert [d['id'] for d in resp.json['data']] == [
'first-agenda@event-thursday:2022-03-10-1400',
'first-agenda@event-thursday:2022-03-24-1400',
'first-agenda@event-thursday:2022-03-31-1400',
]
thursday_event.delete()
wednesday_event.recurrence_week_interval = 2
wednesday_event.save()
resp = app.get('/api/agendas/datetimes/', params={'subscribed': 'all', 'user_external_id': 'child_id'})
assert [d['id'] for d in resp.json['data']] == [
'first-agenda@event-wednesday:2022-03-09-1400',
'first-agenda@event-wednesday:2022-03-23-1400',
]
resp = app.get(
'/api/agendas/datetimes/',
params={'subscribed': 'all', 'user_external_id': 'child_id', 'guardian_external_id': 'father_id'},
)
assert [d['id'] for d in resp.json['data']] == [
'first-agenda@event-wednesday:2022-03-09-1400',
'first-agenda@event-wednesday:2022-03-23-1400',
]
# add mother custody period spanning even week event
SharedCustodyPeriod.objects.create(
agenda=agenda,
guardian=mother,
date_start=datetime.date(2022, 3, 9),
date_end=datetime.date(2022, 3, 10),
)
resp = app.get(
'/api/agendas/datetimes/',
params={'subscribed': 'all', 'user_external_id': 'child_id', 'guardian_external_id': 'father_id'},
)
assert [d['id'] for d in resp.json['data']] == [
'first-agenda@event-wednesday:2022-03-23-1400',
]
resp = app.get(
'/api/agendas/datetimes/',
params={'subscribed': 'all', 'user_external_id': 'child_id', 'guardian_external_id': 'mother_id'},
)
assert [d['id'] for d in resp.json['data']] == [
'first-agenda@event-wednesday:2022-03-09-1400',
]