agendas: add shared custody holiday rules (#62801)

This commit is contained in:
Valentin Deniaud 2022-03-24 17:05:17 +01:00
parent dcec0f2d3f
commit 7d0511aa78
9 changed files with 817 additions and 4 deletions

View File

@ -0,0 +1,80 @@
# Generated by Django 2.2.19 on 2022-03-24 16:02
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('agendas', '0113_auto_20220323_1708'),
]
operations = [
migrations.CreateModel(
name='SharedCustodyHolidayRule',
fields=[
(
'id',
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
(
'years',
models.CharField(
blank=True,
choices=[('', 'All'), ('even', 'Even'), ('odd', 'Odd')],
max_length=16,
verbose_name='Years',
),
),
(
'periodicity',
models.CharField(
blank=True,
choices=[
('first-half', 'First half'),
('second-half', 'Second half'),
('first-and-third-quarters', 'First and third quarters'),
('second-and-fourth-quarters', 'Second and fourth quarters'),
],
max_length=32,
verbose_name='Periodicity',
),
),
(
'agenda',
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name='holiday_rules',
to='agendas.SharedCustodyAgenda',
),
),
(
'guardian',
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to='agendas.Person',
verbose_name='Guardian',
),
),
(
'holiday',
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to='agendas.TimePeriodExceptionGroup',
verbose_name='Holiday',
),
),
],
options={
'ordering': ['holiday__label', 'guardian', 'years', 'periodicity'],
},
),
migrations.AddField(
model_name='sharedcustodyperiod',
name='holiday_rule',
field=models.ForeignKey(
null=True, on_delete=django.db.models.deletion.CASCADE, to='agendas.SharedCustodyHolidayRule'
),
),
]

View File

@ -27,6 +27,7 @@ from dataclasses import dataclass, field
import requests
import vobject
from dateutil.relativedelta import SU, relativedelta
from dateutil.rrule import DAILY, WEEKLY, rrule, rruleset
from django.conf import settings
from django.contrib.auth.models import Group
@ -3149,8 +3150,27 @@ class SharedCustodyAgenda(models.Model):
qs = qs.filter(days__overlap=days)
return qs.exists()
def holiday_rule_overlaps(self, holiday, years, periodicity, instance=None):
qs = self.holiday_rules.filter(holiday=holiday)
if hasattr(instance, 'pk'):
qs = qs.exclude(pk=instance.pk)
if years:
qs = qs.filter(Q(years='') | Q(years=years))
if periodicity == 'first-half':
qs = qs.exclude(periodicity='second-half')
elif periodicity == 'second-half':
qs = qs.exclude(periodicity='first-half')
elif periodicity == 'first-and-third-quarters':
qs = qs.exclude(periodicity='second-and-fourth-quarters')
elif periodicity == 'second-and-fourth-quarters':
qs = qs.exclude(periodicity='first-and-third-quarters')
return qs.exists()
def period_overlaps(self, date_start, date_end, instance=None):
qs = self.periods
qs = self.periods.filter(holiday_rule__isnull=True)
if hasattr(instance, 'pk'):
qs = qs.exclude(pk=instance.pk)
@ -3218,9 +3238,124 @@ class SharedCustodyRule(models.Model):
ordering = ['days__0', 'weeks']
class SharedCustodyHolidayRule(models.Model):
YEAR_CHOICES = [
('', pgettext_lazy('years', 'All')),
('even', pgettext_lazy('years', 'Even')),
('odd', pgettext_lazy('years', 'Odd')),
]
PERIODICITY_CHOICES = [
('first-half', _('First half')),
('second-half', _('Second half')),
('first-and-third-quarters', _('First and third quarters')),
('second-and-fourth-quarters', _('Second and fourth quarters')),
]
agenda = models.ForeignKey(SharedCustodyAgenda, on_delete=models.CASCADE, related_name='holiday_rules')
holiday = models.ForeignKey(TimePeriodExceptionGroup, verbose_name=_('Holiday'), on_delete=models.CASCADE)
years = models.CharField(_('Years'), choices=YEAR_CHOICES, blank=True, max_length=16)
periodicity = models.CharField(_('Periodicity'), choices=PERIODICITY_CHOICES, blank=True, max_length=32)
guardian = models.ForeignKey(Person, verbose_name=_('Guardian'), on_delete=models.CASCADE)
def update_or_create_periods(self):
shared_custody_periods = []
for exception in self.holiday.exceptions.all():
date_start = localtime(exception.start_datetime).date()
if self.years == 'even' and date_start.year % 2:
continue
if self.years == 'odd' and not date_start.year % 2:
continue
date_start_sunday = date_start + relativedelta(weekday=SU)
date_end = localtime(exception.end_datetime).date()
number_of_weeks = (date_end - date_start_sunday).days // 7
periods = []
if self.periodicity == 'first-half':
date_end = date_start_sunday + datetime.timedelta(days=7 * (number_of_weeks // 2))
periods = [(date_start, date_end)]
elif self.periodicity == 'second-half':
date_start = date_start_sunday + datetime.timedelta(days=7 * (number_of_weeks // 2))
periods = [(date_start, date_end)]
elif self.periodicity == 'first-and-third-quarters' and number_of_weeks >= 4:
weeks_in_quarters = round(number_of_weeks / 4)
first_quarters_date_end = date_start_sunday + datetime.timedelta(days=7 * weeks_in_quarters)
third_quarters_date_start = date_start_sunday + datetime.timedelta(
days=7 * weeks_in_quarters * 2
)
third_quarters_date_end = date_start_sunday + datetime.timedelta(
days=7 * weeks_in_quarters * 3
)
periods = [
(date_start, first_quarters_date_end),
(third_quarters_date_start, third_quarters_date_end),
]
elif self.periodicity == 'second-and-fourth-quarters' and number_of_weeks >= 4:
weeks_in_quarters = round(number_of_weeks / 4)
second_quarters_date_start = date_start_sunday + datetime.timedelta(
days=7 * weeks_in_quarters
)
second_quarters_date_end = date_start_sunday + datetime.timedelta(
days=7 * weeks_in_quarters * 2
)
fourth_quarters_date_start = date_start_sunday + datetime.timedelta(
days=7 * weeks_in_quarters * 3
)
periods = [
(second_quarters_date_start, second_quarters_date_end),
(fourth_quarters_date_start, date_end),
]
elif not self.periodicity:
periods = [(date_start, date_end)]
for date_start, date_end in periods:
shared_custody_periods.append(
SharedCustodyPeriod(
guardian=self.guardian,
agenda=self.agenda,
holiday_rule=self,
date_start=date_start,
date_end=date_end,
)
)
with transaction.atomic():
SharedCustodyPeriod.objects.filter(
guardian=self.guardian, agenda=self.agenda, holiday_rule=self
).delete()
SharedCustodyPeriod.objects.bulk_create(shared_custody_periods)
@property
def label(self):
label = self.holiday.label
if self.periodicity == 'first-half':
label = '%s, %s' % (label, _('the first half'))
elif self.periodicity == 'second-half':
label = '%s, %s' % (label, _('the second half'))
elif self.periodicity == 'first-and-third-quarters':
label = '%s, %s' % (label, _('the first and third quarters'))
elif self.periodicity == 'second-and-fourth-quarters':
label = '%s, %s' % (label, _('the second and fourth quarters'))
if self.years == 'odd':
label = '%s, %s' % (label, _('on odd years'))
elif self.years == 'even':
label = '%s, %s' % (label, _('on even years'))
return label
class Meta:
ordering = ['holiday__label', 'guardian', 'years', 'periodicity']
class SharedCustodyPeriod(models.Model):
agenda = models.ForeignKey(SharedCustodyAgenda, on_delete=models.CASCADE, related_name='periods')
guardian = models.ForeignKey(Person, on_delete=models.CASCADE, related_name='+')
holiday_rule = models.ForeignKey(SharedCustodyHolidayRule, null=True, on_delete=models.CASCADE)
date_start = models.DateField(_('Start'))
date_end = models.DateField(_('End'))

View File

@ -28,6 +28,7 @@ from django.conf import settings
from django.contrib.auth.models import Group
from django.core.exceptions import FieldDoesNotExist
from django.db import transaction
from django.db.models import DurationField, ExpressionWrapper, F
from django.forms import ValidationError
from django.utils.encoding import force_text
from django.utils.formats import date_format
@ -50,11 +51,13 @@ from chrono.agendas.models import (
MeetingType,
Person,
Resource,
SharedCustodyHolidayRule,
SharedCustodyPeriod,
SharedCustodyRule,
Subscription,
TimePeriod,
TimePeriodException,
TimePeriodExceptionGroup,
TimePeriodExceptionSource,
UnavailabilityCalendar,
VirtualMember,
@ -1339,6 +1342,49 @@ class SharedCustodyRuleForm(forms.ModelForm):
return cleaned_data
class SharedCustodyHolidayRuleForm(forms.ModelForm):
guardian = forms.ModelChoiceField(label=_('Guardian'), queryset=Person.objects.none())
class Meta:
model = SharedCustodyHolidayRule
fields = ['guardian', 'holiday', 'years', 'periodicity']
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['guardian'].empty_label = None
self.fields['guardian'].queryset = Person.objects.filter(
pk__in=[self.instance.agenda.first_guardian_id, self.instance.agenda.second_guardian_id]
)
self.fields['holiday'].queryset = TimePeriodExceptionGroup.objects.filter(
unavailability_calendar__slug='chrono-holidays',
exceptions__isnull=False,
).distinct()
def clean(self):
cleaned_data = super().clean()
holidays = cleaned_data['holiday'].exceptions.annotate(
delta=ExpressionWrapper(F('end_datetime') - F('start_datetime'), output_field=DurationField())
)
is_short_holiday = holidays.filter(delta__lt=datetime.timedelta(days=28)).exists()
if 'quarters' in cleaned_data['periodicity'] and is_short_holiday:
raise ValidationError(_('Short holidays cannot be cut into quarters.'))
if self.instance.agenda.holiday_rule_overlaps(
cleaned_data['holiday'], cleaned_data['years'], cleaned_data['periodicity'], self.instance
):
raise ValidationError(_('Rule overlaps existing rules.'))
return cleaned_data
def save(self, *args, **kwargs):
with transaction.atomic():
super().save()
self.instance.update_or_create_periods()
return self.instance
class SharedCustodyPeriodForm(forms.ModelForm):
guardian = forms.ModelChoiceField(label=_('Guardian'), queryset=Person.objects.none())

View File

@ -10,6 +10,9 @@
<h2>{% trans "Settings" %}</h2>
<span class="actions">
<a rel="popup" href="{% url 'chrono-manager-shared-custody-agenda-add-period' pk=object.id %}">{% trans 'Add custody period' %}</a>
{% if has_holidays %}
<a rel="popup" href="{% url 'chrono-manager-shared-custody-agenda-add-holiday-rule' pk=object.id %}">{% trans 'Add custody rule during holidays' %}</a>
{% endif %}
<a rel="popup" href="{% url 'chrono-manager-shared-custody-agenda-add-rule' pk=object.id %}">{% trans 'Add custody rule' %}</a>
</span>
{% endblock %}
@ -45,12 +48,39 @@
</div>
</div>
{% if has_holidays %}
<div class="section">
<h3>{% trans "Custody rules during holidays" %}</h3>
<div>
{% if agenda.holiday_rules.all %}
<ul class="objects-list single-links">
{% for rule in agenda.holiday_rules.all %}
<li><a rel="popup" href="{% url 'chrono-manager-shared-custody-agenda-edit-holiday-rule' pk=agenda.pk rule_pk=rule.pk %}">
<span class="rule-info">
{{ rule.guardian.name }}, {{ rule.label }}
</span>
</a>
<a rel="popup" class="delete" href="{% url 'chrono-manager-shared-custody-agenda-delete-holiday-rule' pk=agenda.pk rule_pk=rule.pk %}?next=settings">{% trans "remove" %}</a>
</li>
{% endfor %}
</ul>
{% else %}
<div class="big-msg-info">
{% blocktrans trimmed %}
This agenda doesn't specify any custody rules during holidays. It means normal rules will be applied.
{% endblocktrans %}
</div>
{% endif %}
</div>
</div>
{% endif %}
<div class="section">
<h3>{% trans "Exceptional custody periods" %}</h3>
<div>
{% if agenda.periods.all %}
{% if exceptional_periods %}
<ul class="objects-list single-links">
{% for period in agenda.periods.all %}
{% for period in exceptional_periods %}
<li>
<a rel="popup" href="{% url 'chrono-manager-shared-custody-agenda-edit-period' pk=agenda.pk period_pk=period.pk %}">
{{ period }}

View File

@ -411,6 +411,21 @@ urlpatterns = [
views.shared_custody_agenda_delete_rule,
name='chrono-manager-shared-custody-agenda-delete-rule',
),
url(
r'^shared-custody/(?P<pk>\d+)/add-holiday-rule$',
views.shared_custody_agenda_add_holiday_rule,
name='chrono-manager-shared-custody-agenda-add-holiday-rule',
),
url(
r'^shared-custody/(?P<pk>\d+)/holiday-rules/(?P<rule_pk>\d+)/edit$',
views.shared_custody_agenda_edit_holiday_rule,
name='chrono-manager-shared-custody-agenda-edit-holiday-rule',
),
url(
r'^shared-custody/(?P<pk>\d+)/holiday-rules/(?P<rule_pk>\d+)/delete$',
views.shared_custody_agenda_delete_holiday_rule,
name='chrono-manager-shared-custody-agenda-delete-holiday-rule',
),
url(
r'^shared-custody/(?P<pk>\d+)/add-period$',
views.shared_custody_agenda_add_period,

View File

@ -76,6 +76,7 @@ from chrono.agendas.models import (
MeetingType,
Resource,
SharedCustodyAgenda,
SharedCustodyHolidayRule,
SharedCustodyPeriod,
SharedCustodyRule,
TimePeriod,
@ -114,6 +115,7 @@ from .forms import (
NewEventForm,
NewMeetingTypeForm,
NewTimePeriodExceptionForm,
SharedCustodyHolidayRuleForm,
SharedCustodyPeriodForm,
SharedCustodyRuleForm,
SubscriptionCheckFilterSet,
@ -3458,6 +3460,12 @@ class SharedCustodyAgendaSettings(SharedCustodyAgendaMixin, DetailView):
template_name = 'chrono/manager_shared_custody_agenda_settings.html'
model = SharedCustodyAgenda
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['has_holidays'] = UnavailabilityCalendar.objects.filter(slug='chrono-holidays').exists()
context['exceptional_periods'] = SharedCustodyPeriod.objects.filter(holiday_rule__isnull=True)
return context
shared_custody_agenda_settings = SharedCustodyAgendaSettings.as_view()
@ -3492,6 +3500,36 @@ class SharedCustodyAgendaDeleteRuleView(SharedCustodyAgendaMixin, DeleteView):
shared_custody_agenda_delete_rule = SharedCustodyAgendaDeleteRuleView.as_view()
class SharedCustodyAgendaAddHolidayRuleView(SharedCustodyAgendaMixin, CreateView):
title = _('Add custody rule during holidays')
template_name = 'chrono/manager_agenda_form.html'
form_class = SharedCustodyHolidayRuleForm
model = SharedCustodyHolidayRule
shared_custody_agenda_add_holiday_rule = SharedCustodyAgendaAddHolidayRuleView.as_view()
class SharedCustodyAgendaEditHolidayRuleView(SharedCustodyAgendaMixin, UpdateView):
title = _('Edit custody rule during holidays')
template_name = 'chrono/manager_agenda_form.html'
form_class = SharedCustodyHolidayRuleForm
model = SharedCustodyHolidayRule
pk_url_kwarg = 'rule_pk'
shared_custody_agenda_edit_holiday_rule = SharedCustodyAgendaEditHolidayRuleView.as_view()
class SharedCustodyAgendaDeleteHolidayRuleView(SharedCustodyAgendaMixin, DeleteView):
template_name = 'chrono/manager_confirm_delete.html'
model = SharedCustodyHolidayRule
pk_url_kwarg = 'rule_pk'
shared_custody_agenda_delete_holiday_rule = SharedCustodyAgendaDeleteHolidayRuleView.as_view()
class SharedCustodyAgendaAddPeriodView(SharedCustodyAgendaMixin, CreateView):
title = _('Add custody period')
template_name = 'chrono/manager_agenda_form.html'

View File

@ -15,10 +15,13 @@ from chrono.agendas.models import (
Event,
Person,
SharedCustodyAgenda,
SharedCustodyHolidayRule,
SharedCustodyPeriod,
SharedCustodyRule,
Subscription,
TimePeriodException,
TimePeriodExceptionGroup,
UnavailabilityCalendar,
)
from tests.utils import login
@ -2657,3 +2660,69 @@ def test_datetimes_multiple_agendas_shared_custody_recurring_event(app):
assert [d['id'] for d in resp.json['data']] == [
'first-agenda@event-wednesday--2022-03-09-1400',
]
@pytest.mark.freeze_time('2021-12-13 14:00') # Monday of 50th week
def test_datetimes_multiple_agendas_shared_custody_holiday_rules(app):
agenda = Agenda.objects.create(label='First agenda', kind='events')
Desk.objects.create(agenda=agenda, slug='_exceptions_holder')
Event.objects.create(
slug='event-wednesday',
start_datetime=make_aware(datetime.datetime(year=2021, month=12, day=25, hour=14, minute=0)),
places=5,
agenda=agenda,
)
Subscription.objects.create(
agenda=agenda,
user_external_id='child_id',
date_start=now(),
date_end=now() + datetime.timedelta(days=14),
)
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)
SharedCustodyRule.objects.create(agenda=agenda, days=list(range(7)), weeks='even', guardian=father)
SharedCustodyRule.objects.create(agenda=agenda, days=list(range(7)), weeks='odd', guardian=mother)
resp = app.get(
'/api/agendas/datetimes/',
params={'subscribed': 'all', 'user_external_id': 'child_id', 'guardian_external_id': 'father_id'},
)
assert len(resp.json['data']) == 0
resp = app.get(
'/api/agendas/datetimes/',
params={'subscribed': 'all', 'user_external_id': 'child_id', 'guardian_external_id': 'mother_id'},
)
assert len(resp.json['data']) == 1
# add father custody during holidays
calendar = UnavailabilityCalendar.objects.create(label='Calendar')
christmas_holiday = TimePeriodExceptionGroup.objects.create(
unavailability_calendar=calendar, label='Christmas', slug='christmas'
)
TimePeriodException.objects.create(
unavailability_calendar=calendar,
start_datetime=make_aware(datetime.datetime(year=2021, month=12, day=18, hour=0, minute=0)),
end_datetime=make_aware(datetime.datetime(year=2022, month=1, day=3, hour=0, minute=0)),
group=christmas_holiday,
)
rule = SharedCustodyHolidayRule.objects.create(agenda=agenda, guardian=father, holiday=christmas_holiday)
rule.update_or_create_periods()
resp = app.get(
'/api/agendas/datetimes/',
params={'subscribed': 'all', 'user_external_id': 'child_id', 'guardian_external_id': 'father_id'},
)
assert len(resp.json['data']) == 1
resp = app.get(
'/api/agendas/datetimes/',
params={'subscribed': 'all', 'user_external_id': 'child_id', 'guardian_external_id': 'mother_id'},
)
assert len(resp.json['data']) == 0

View File

@ -1,13 +1,26 @@
import datetime
import pytest
from django.core.files.base import ContentFile
from chrono.agendas.models import Person, SharedCustodyAgenda, SharedCustodyPeriod, SharedCustodyRule
from chrono.agendas.models import (
Person,
SharedCustodyAgenda,
SharedCustodyHolidayRule,
SharedCustodyPeriod,
SharedCustodyRule,
TimePeriodExceptionGroup,
UnavailabilityCalendar,
)
from tests.utils import login
pytestmark = pytest.mark.django_db
with open('tests/data/holidays.ics') as f:
ICS_HOLIDAYS = f.read()
@pytest.mark.freeze_time('2022-02-22 14:00') # Tuesday
def test_shared_custody_agenda_settings_rules(app, admin_user):
father = Person.objects.create(user_external_id='father_id', name='John Doe')
@ -153,3 +166,78 @@ def test_shared_custody_agenda_month_view(app, admin_user):
assert resp.text == old_resp.text
app.get('/manage/shared-custody/%s/42/42/' % agenda.pk, status=404)
def test_shared_custody_agenda_holiday_rules(app, admin_user):
father = Person.objects.create(user_external_id='father_id', name='John Doe')
mother = Person.objects.create(user_external_id='mother_id', name='Jane Doe')
agenda = SharedCustodyAgenda.objects.create(first_guardian=father, second_guardian=mother)
app = login(app)
resp = app.get('/manage/shared-custody/%s/settings/' % agenda.pk)
assert 'Add custody rule during holidays' not in resp.text
assert 'Custody rules during holidays' not in resp.text
# configure holidays
unavailability_calendar = UnavailabilityCalendar.objects.create(label='Calendar', slug='chrono-holidays')
source = unavailability_calendar.timeperiodexceptionsource_set.create(
ics_filename='holidays.ics', ics_file=ContentFile(ICS_HOLIDAYS, name='holidays.ics')
)
source.refresh_timeperiod_exceptions_from_ics()
resp = app.get('/manage/shared-custody/%s/settings/' % agenda.pk)
resp = resp.click('Add custody rule during holidays')
resp.form['guardian'] = father.pk
resp.form['holiday'].select(text='Vacances de Noël')
resp.form['years'] = 'odd'
resp.form['periodicity'] = 'first-half'
resp = resp.form.submit().follow()
assert SharedCustodyHolidayRule.objects.count() == 1
assert SharedCustodyPeriod.objects.count() == 3
assert 'This agenda doesn\'t have any custody period.' in resp.text
resp = resp.click('John Doe, Vacances de Noël, the first half, on odd years')
resp.form['years'] = ''
resp = resp.form.submit().follow()
assert 'John Doe, Vacances de Noël, the first half' in resp.text
assert SharedCustodyHolidayRule.objects.count() == 1
assert SharedCustodyPeriod.objects.count() == 6
resp = resp.click('Add custody rule during holidays')
resp.form['guardian'] = mother.pk
resp.form['holiday'].select(text='Vacances de Noël')
resp.form['periodicity'] = 'first-half'
resp = resp.form.submit()
assert 'Rule overlaps existing rules.' in resp.text
resp.form['periodicity'] = 'second-half'
resp = resp.form.submit().follow()
assert 'Jane Doe, Vacances de Noël, the second half' in resp.text
assert SharedCustodyHolidayRule.objects.count() == 2
assert SharedCustodyPeriod.objects.count() == 12
resp = resp.click('remove', index=1)
resp = resp.form.submit().follow()
assert SharedCustodyHolidayRule.objects.count() == 1
assert SharedCustodyPeriod.objects.count() == 6
resp = resp.click('Add custody rule during holidays')
resp.form['guardian'] = father.pk
resp.form['holiday'].select(text='Vacances de Noël')
resp.form['periodicity'] = 'first-and-third-quarters'
resp = resp.form.submit()
assert 'Short holidays cannot be cut into quarters.' in resp.text
resp.form['holiday'].select(text='Vacances dÉté')
resp = resp.form.submit().follow()
assert 'John Doe, Vacances dÉté, the first and third quarters' in resp.text
# if dates get deleted, rules still exist but holiday is not shown anymore
summer_holidays = TimePeriodExceptionGroup.objects.get(slug='summer_holidays')
summer_holidays.exceptions.all().delete()
assert SharedCustodyHolidayRule.objects.filter(holiday=summer_holidays).exists()
resp = resp.click('Add custody rule during holidays')
assert [x[2] for x in resp.form['holiday'].options] == ['---------', 'Vacances de Noël']

View File

@ -27,6 +27,7 @@ from chrono.agendas.models import (
Person,
Resource,
SharedCustodyAgenda,
SharedCustodyHolidayRule,
SharedCustodyPeriod,
SharedCustodyRule,
TimePeriod,
@ -2913,6 +2914,64 @@ def test_shared_custody_agenda_rule_overlaps(rules, days, weeks, overlaps):
assert agenda.rule_overlaps(days, weeks) is overlaps
def test_shared_custody_agenda_holiday_rule_overlaps():
calendar = UnavailabilityCalendar.objects.create(label='Calendar')
father = Person.objects.create(user_external_id='father_id', name='John Doe')
mother = Person.objects.create(user_external_id='mother_id', name='Jane Doe')
agenda = SharedCustodyAgenda.objects.create(first_guardian=father, second_guardian=mother)
summer_holiday = TimePeriodExceptionGroup.objects.create(
unavailability_calendar=calendar, label='Summer', slug='summer'
)
winter_holiday = TimePeriodExceptionGroup.objects.create(
unavailability_calendar=calendar, label='Winter', slug='winter'
)
rule = SharedCustodyHolidayRule.objects.create(agenda=agenda, guardian=father, holiday=summer_holiday)
assert agenda.holiday_rule_overlaps(summer_holiday, years='', periodicity='') is True
assert agenda.holiday_rule_overlaps(summer_holiday, years='', periodicity='', instance=rule) is False
assert agenda.holiday_rule_overlaps(winter_holiday, years='', periodicity='') is False
assert agenda.holiday_rule_overlaps(summer_holiday, years='odd', periodicity='') is True
assert agenda.holiday_rule_overlaps(summer_holiday, years='', periodicity='first-half') is True
rule.years = 'odd'
rule.save()
assert agenda.holiday_rule_overlaps(summer_holiday, years='', periodicity='') is True
assert agenda.holiday_rule_overlaps(summer_holiday, years='odd', periodicity='') is True
assert agenda.holiday_rule_overlaps(summer_holiday, years='even', periodicity='') is False
rule.periodicity = 'first-half'
rule.save()
assert agenda.holiday_rule_overlaps(summer_holiday, years='', periodicity='') is True
assert agenda.holiday_rule_overlaps(summer_holiday, years='odd', periodicity='first-half') is True
assert agenda.holiday_rule_overlaps(summer_holiday, years='odd', periodicity='second-half') is False
assert (
agenda.holiday_rule_overlaps(summer_holiday, years='odd', periodicity='first-and-third-quarters')
is True
)
assert (
agenda.holiday_rule_overlaps(summer_holiday, years='odd', periodicity='second-and-fourth-quarters')
is True
)
assert agenda.holiday_rule_overlaps(summer_holiday, years='even', periodicity='first-half') is False
rule.periodicity = 'second-and-fourth-quarters'
rule.save()
assert agenda.holiday_rule_overlaps(summer_holiday, years='', periodicity='') is True
assert agenda.holiday_rule_overlaps(summer_holiday, years='odd', periodicity='first-half') is True
assert agenda.holiday_rule_overlaps(summer_holiday, years='odd', periodicity='second-half') is True
assert (
agenda.holiday_rule_overlaps(summer_holiday, years='odd', periodicity='first-and-third-quarters')
is False
)
assert (
agenda.holiday_rule_overlaps(summer_holiday, years='odd', periodicity='second-and-fourth-quarters')
is True
)
assert agenda.holiday_rule_overlaps(summer_holiday, years='even', periodicity='first-half') is False
@pytest.mark.parametrize(
'periods,date_start,date_end,overlaps',
(
@ -2941,6 +3000,25 @@ def test_shared_custody_agenda_period_overlaps(periods, date_start, date_end, ov
assert agenda.period_overlaps(date_start, date_end) is overlaps
def test_shared_custody_agenda_period_holiday_rule_no_overlaps():
calendar = UnavailabilityCalendar.objects.create(label='Calendar')
father = Person.objects.create(user_external_id='father_id', name='John Doe')
mother = Person.objects.create(user_external_id='mother_id', name='Jane Doe')
agenda = SharedCustodyAgenda.objects.create(first_guardian=father, second_guardian=mother)
summer_holiday = TimePeriodExceptionGroup.objects.create(
unavailability_calendar=calendar, label='Summer', slug='summer'
)
rule = SharedCustodyHolidayRule.objects.create(agenda=agenda, guardian=father, holiday=summer_holiday)
SharedCustodyPeriod.objects.create(
holiday_rule=rule, agenda=agenda, guardian=father, date_start='2022-02-03', date_end='2022-02-05'
)
assert agenda.period_overlaps('2022-02-03', '2022-02-05') is False
def test_shared_custody_agenda_rule_label():
father = Person.objects.create(user_external_id='father_id', name='John Doe')
mother = Person.objects.create(user_external_id='mother_id', name='Jane Doe')
@ -2971,6 +3049,46 @@ def test_shared_custody_agenda_rule_label():
assert rule.label == 'on Mondays, on odd weeks'
def test_shared_custody_agenda_holiday_rule_label():
calendar = UnavailabilityCalendar.objects.create(label='Calendar')
father = Person.objects.create(user_external_id='father_id', name='John Doe')
mother = Person.objects.create(user_external_id='mother_id', name='Jane Doe')
agenda = SharedCustodyAgenda.objects.create(first_guardian=father, second_guardian=mother)
summer_holiday = TimePeriodExceptionGroup.objects.create(
unavailability_calendar=calendar, label='Summer Holidays', slug='summer'
)
rule = SharedCustodyHolidayRule.objects.create(agenda=agenda, guardian=father, holiday=summer_holiday)
assert rule.label == 'Summer Holidays'
rule.years = 'even'
rule.save()
assert rule.label == 'Summer Holidays, on even years'
rule.years = 'odd'
rule.save()
assert rule.label == 'Summer Holidays, on odd years'
rule.periodicity = 'first-half'
rule.save()
assert rule.label == 'Summer Holidays, the first half, on odd years'
rule.years = ''
rule.periodicity = 'second-half'
rule.save()
assert rule.label == 'Summer Holidays, the second half'
rule.periodicity = 'first-and-third-quarters'
rule.save()
assert rule.label == 'Summer Holidays, the first and third quarters'
rule.periodicity = 'second-and-fourth-quarters'
rule.save()
assert rule.label == 'Summer Holidays, the second and fourth quarters'
def test_shared_custody_agenda_period_label(freezer):
father = Person.objects.create(user_external_id='father_id', name='John Doe')
mother = Person.objects.create(user_external_id='mother_id', name='Jane Doe')
@ -2987,3 +3105,197 @@ def test_shared_custody_agenda_period_label(freezer):
period.date_end = datetime.date(2021, 7, 13)
period.save()
assert str(period) == 'John Doe, 07/10/2021 → 07/13/2021'
def test_shared_custody_agenda_holiday_rule_create_periods():
calendar = UnavailabilityCalendar.objects.create(label='Calendar')
father = Person.objects.create(user_external_id='father_id', name='John Doe')
mother = Person.objects.create(user_external_id='mother_id', name='Jane Doe')
agenda = SharedCustodyAgenda.objects.create(first_guardian=father, second_guardian=mother)
summer_holiday = TimePeriodExceptionGroup.objects.create(
unavailability_calendar=calendar, label='Summer', slug='summer'
)
TimePeriodException.objects.create(
unavailability_calendar=calendar,
start_datetime=make_aware(datetime.datetime(year=2021, month=7, day=6, hour=0, minute=0)),
end_datetime=make_aware(datetime.datetime(year=2021, month=9, day=2, hour=0, minute=0)),
group=summer_holiday,
)
TimePeriodException.objects.create(
unavailability_calendar=calendar,
start_datetime=make_aware(datetime.datetime(year=2022, month=7, day=7, hour=0, minute=0)),
end_datetime=make_aware(datetime.datetime(year=2022, month=9, day=1, hour=0, minute=0)),
group=summer_holiday,
)
rule = SharedCustodyHolidayRule.objects.create(agenda=agenda, guardian=father, holiday=summer_holiday)
rule.update_or_create_periods()
period1, period2 = SharedCustodyPeriod.objects.all()
assert period1.holiday_rule == rule
assert period1.guardian == father
assert period1.agenda == agenda
assert period1.date_start == datetime.date(year=2021, month=7, day=6)
assert period1.date_end == datetime.date(year=2021, month=9, day=2)
assert period2.holiday_rule == rule
assert period2.guardian == father
assert period2.agenda == agenda
assert period2.date_start == datetime.date(year=2022, month=7, day=7)
assert period2.date_end == datetime.date(year=2022, month=9, day=1)
rule.years = 'odd'
rule.update_or_create_periods()
period = SharedCustodyPeriod.objects.get()
assert period.date_start == datetime.date(year=2021, month=7, day=6)
assert period.date_end == datetime.date(year=2021, month=9, day=2)
rule.years = 'even'
rule.update_or_create_periods()
period = SharedCustodyPeriod.objects.get()
assert period.date_start == datetime.date(year=2022, month=7, day=7)
assert period.date_end == datetime.date(year=2022, month=9, day=1)
rule.years = ''
rule.periodicity = 'first-half'
rule.update_or_create_periods()
period1, period2 = SharedCustodyPeriod.objects.all()
assert period1.date_start == datetime.date(year=2021, month=7, day=6)
assert period1.date_end == datetime.date(year=2021, month=8, day=1)
assert period1.date_end.weekday() == 6
assert period2.date_start == datetime.date(year=2022, month=7, day=7)
assert period2.date_end == datetime.date(year=2022, month=7, day=31)
assert period2.date_end.weekday() == 6
rule.periodicity = 'second-half'
rule.update_or_create_periods()
period1, period2 = SharedCustodyPeriod.objects.all()
assert period1.date_start == datetime.date(year=2021, month=8, day=1)
assert period1.date_start.weekday() == 6
assert period1.date_end == datetime.date(year=2021, month=9, day=2)
assert period2.date_start == datetime.date(year=2022, month=7, day=31)
assert period2.date_start.weekday() == 6
assert period2.date_end == datetime.date(year=2022, month=9, day=1)
rule.periodicity = 'first-and-third-quarters'
rule.update_or_create_periods()
period1, period2, period3, period4 = SharedCustodyPeriod.objects.all()
assert period1.date_start == datetime.date(year=2021, month=7, day=6)
assert period1.date_end == datetime.date(year=2021, month=7, day=25)
assert period1.date_end.weekday() == 6
assert period2.date_start == datetime.date(year=2021, month=8, day=8)
assert period2.date_end == datetime.date(year=2021, month=8, day=22)
assert period2.date_end.weekday() == 6
assert period3.date_start == datetime.date(year=2022, month=7, day=7)
assert period3.date_end == datetime.date(year=2022, month=7, day=24)
assert period3.date_end.weekday() == 6
assert period4.date_start == datetime.date(year=2022, month=8, day=7)
assert period4.date_end == datetime.date(year=2022, month=8, day=21)
assert period4.date_end.weekday() == 6
rule.periodicity = 'second-and-fourth-quarters'
rule.update_or_create_periods()
period1, period2, period3, period4 = SharedCustodyPeriod.objects.all()
assert period1.date_start == datetime.date(year=2021, month=7, day=25)
assert period1.date_start.weekday() == 6
assert period1.date_end == datetime.date(year=2021, month=8, day=8)
assert period2.date_start == datetime.date(year=2021, month=8, day=22)
assert period2.date_start.weekday() == 6
assert period2.date_end == datetime.date(year=2021, month=9, day=2)
assert period3.date_start == datetime.date(year=2022, month=7, day=24)
assert period3.date_start.weekday() == 6
assert period3.date_end == datetime.date(year=2022, month=8, day=7)
assert period4.date_start == datetime.date(year=2022, month=8, day=21)
assert period4.date_start.weekday() == 6
assert period4.date_end == datetime.date(year=2022, month=9, day=1)
def test_shared_custody_agenda_holiday_rule_create_periods_christmas_holidays():
calendar = UnavailabilityCalendar.objects.create(label='Calendar')
father = Person.objects.create(user_external_id='father_id', name='John Doe')
mother = Person.objects.create(user_external_id='mother_id', name='Jane Doe')
agenda = SharedCustodyAgenda.objects.create(first_guardian=father, second_guardian=mother)
christmas_holiday = TimePeriodExceptionGroup.objects.create(
unavailability_calendar=calendar, label='Christmas', slug='christmas'
)
TimePeriodException.objects.create(
unavailability_calendar=calendar,
start_datetime=make_aware(datetime.datetime(year=2021, month=12, day=18, hour=0, minute=0)),
end_datetime=make_aware(datetime.datetime(year=2022, month=1, day=3, hour=0, minute=0)),
group=christmas_holiday,
)
TimePeriodException.objects.create(
unavailability_calendar=calendar,
start_datetime=make_aware(datetime.datetime(year=2022, month=12, day=17, hour=0, minute=0)),
end_datetime=make_aware(datetime.datetime(year=2023, month=1, day=3, hour=0, minute=0)),
group=christmas_holiday,
)
rule = SharedCustodyHolidayRule.objects.create(agenda=agenda, guardian=father, holiday=christmas_holiday)
rule.update_or_create_periods()
period1, period2 = SharedCustodyPeriod.objects.all()
assert period1.date_start == datetime.date(year=2021, month=12, day=18)
assert period1.date_end == datetime.date(year=2022, month=1, day=3)
assert period2.date_start == datetime.date(year=2022, month=12, day=17)
assert period2.date_end == datetime.date(year=2023, month=1, day=3)
rule.periodicity = 'first-half'
rule.update_or_create_periods()
period1, period2 = SharedCustodyPeriod.objects.all()
assert period1.date_start == datetime.date(year=2021, month=12, day=18)
assert period1.date_end == datetime.date(year=2021, month=12, day=26)
assert period1.date_end.weekday() == 6
assert period2.date_start == datetime.date(year=2022, month=12, day=17)
assert period2.date_end == datetime.date(year=2022, month=12, day=25)
assert period2.date_end.weekday() == 6
rule.periodicity = 'second-half'
rule.update_or_create_periods()
period1, period2 = SharedCustodyPeriod.objects.all()
assert period1.date_start == datetime.date(year=2021, month=12, day=26)
assert period1.date_start.weekday() == 6
assert period1.date_end == datetime.date(year=2022, month=1, day=3)
assert period2.date_start == datetime.date(year=2022, month=12, day=25)
assert period2.date_start.weekday() == 6
assert period2.date_end == datetime.date(year=2023, month=1, day=3)
rule.periodicity = 'first-and-third-quarters'
rule.update_or_create_periods()
assert not SharedCustodyPeriod.objects.exists()
rule.periodicity = 'second-and-fourth-quarters'
rule.update_or_create_periods()
assert not SharedCustodyPeriod.objects.exists()
def test_shared_custody_agenda_holiday_rules_application():
calendar = UnavailabilityCalendar.objects.create(label='Calendar')
father = Person.objects.create(user_external_id='father_id', name='John Doe')
mother = Person.objects.create(user_external_id='mother_id', name='Jane Doe')
agenda = SharedCustodyAgenda.objects.create(first_guardian=father, second_guardian=mother)
christmas_holiday = TimePeriodExceptionGroup.objects.create(
unavailability_calendar=calendar, label='Christmas', slug='christmas'
)
TimePeriodException.objects.create(
unavailability_calendar=calendar,
start_datetime=make_aware(datetime.datetime(year=2021, month=12, day=18, hour=0, minute=0)),
end_datetime=make_aware(datetime.datetime(year=2022, month=1, day=3, hour=0, minute=0)),
group=christmas_holiday,
)
SharedCustodyRule.objects.create(agenda=agenda, days=list(range(7)), weeks='even', guardian=father)
SharedCustodyRule.objects.create(agenda=agenda, days=list(range(7)), weeks='odd', guardian=mother)
rule = SharedCustodyHolidayRule.objects.create(agenda=agenda, guardian=father, holiday=christmas_holiday)
rule.update_or_create_periods()
date_start = datetime.date(year=2021, month=12, day=13) # Monday, even week
slots = agenda.get_custody_slots(date_start, date_start + datetime.timedelta(days=30))
guardians = [x.guardian.name for x in slots]
assert all(name == 'John Doe' for name in guardians[:21])
assert all(name == 'Jane Doe' for name in guardians[21:28])
assert all(name == 'John Doe' for name in guardians[28:])