Projet

Général

Profil

0002-agendas-add-shared-custody-holiday-rules-62801.patch

Valentin Deniaud, 29 mars 2022 17:20

Télécharger (44,9 ko)

Voir les différences:

Subject: [PATCH 2/4] agendas: add shared custody holiday rules (#62801)

 .../migrations/0114_auto_20220324_1702.py     |  80 +++++
 chrono/agendas/models.py                      | 135 +++++++-
 chrono/manager/forms.py                       |  46 +++
 ...anager_shared_custody_agenda_settings.html |  34 +-
 chrono/manager/urls.py                        |  15 +
 chrono/manager/views.py                       |  38 +++
 tests/api/test_datetimes.py                   |  69 ++++
 tests/manager/test_shared_custody_agenda.py   |  92 +++++-
 tests/test_agendas.py                         | 312 ++++++++++++++++++
 9 files changed, 816 insertions(+), 5 deletions(-)
 create mode 100644 chrono/agendas/migrations/0114_auto_20220324_1702.py
chrono/agendas/migrations/0114_auto_20220324_1702.py
1
# Generated by Django 2.2.19 on 2022-03-24 16:02
2

  
3
import django.db.models.deletion
4
from django.db import migrations, models
5

  
6

  
7
class Migration(migrations.Migration):
8

  
9
    dependencies = [
10
        ('agendas', '0113_auto_20220323_1708'),
11
    ]
12

  
13
    operations = [
14
        migrations.CreateModel(
15
            name='SharedCustodyHolidayRule',
16
            fields=[
17
                (
18
                    'id',
19
                    models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
20
                ),
21
                (
22
                    'years',
23
                    models.CharField(
24
                        blank=True,
25
                        choices=[('', 'All'), ('even', 'Even'), ('odd', 'Odd')],
26
                        max_length=16,
27
                        verbose_name='Years',
28
                    ),
29
                ),
30
                (
31
                    'periodicity',
32
                    models.CharField(
33
                        blank=True,
34
                        choices=[
35
                            ('first-half', 'First half'),
36
                            ('second-half', 'Second half'),
37
                            ('first-and-third-quarters', 'First and third quarters'),
38
                            ('second-and-fourth-quarters', 'Second and fourth quarters'),
39
                        ],
40
                        max_length=32,
41
                        verbose_name='Periodicity',
42
                    ),
43
                ),
44
                (
45
                    'agenda',
46
                    models.ForeignKey(
47
                        on_delete=django.db.models.deletion.CASCADE,
48
                        related_name='holiday_rules',
49
                        to='agendas.SharedCustodyAgenda',
50
                    ),
51
                ),
52
                (
53
                    'guardian',
54
                    models.ForeignKey(
55
                        on_delete=django.db.models.deletion.CASCADE,
56
                        to='agendas.Person',
57
                        verbose_name='Guardian',
58
                    ),
59
                ),
60
                (
61
                    'holiday',
62
                    models.ForeignKey(
63
                        on_delete=django.db.models.deletion.CASCADE,
64
                        to='agendas.TimePeriodExceptionGroup',
65
                        verbose_name='Holiday',
66
                    ),
67
                ),
68
            ],
69
            options={
70
                'ordering': ['holiday__label', 'guardian', 'years', 'periodicity'],
71
            },
72
        ),
73
        migrations.AddField(
74
            model_name='sharedcustodyperiod',
75
            name='holiday_rule',
76
            field=models.ForeignKey(
77
                null=True, on_delete=django.db.models.deletion.CASCADE, to='agendas.SharedCustodyHolidayRule'
78
            ),
79
        ),
80
    ]
chrono/agendas/models.py
27 27

  
28 28
import requests
29 29
import vobject
30
from dateutil.relativedelta import SU, relativedelta
30 31
from dateutil.rrule import DAILY, WEEKLY, rrule, rruleset
31 32
from django.conf import settings
32 33
from django.contrib.auth.models import Group
......
3150 3151
        qs = qs.filter(days__overlap=days)
3151 3152
        return qs.exists()
3152 3153

  
3154
    def holiday_rule_overlaps(self, holiday, years, periodicity, instance=None):
3155
        qs = self.holiday_rules.filter(holiday=holiday)
3156
        if hasattr(instance, 'pk'):
3157
            qs = qs.exclude(pk=instance.pk)
3158

  
3159
        if years:
3160
            qs = qs.filter(Q(years='') | Q(years=years))
3161

  
3162
        if periodicity in ('first-half', 'second-half'):
3163
            qs = qs.filter(
3164
                periodicity__in=(periodicity, '', 'first-and-third-quarters', 'second-and-fourth-quarters')
3165
            )
3166
        elif periodicity in ('first-and-third-quarters', 'second-and-fourth-quarters'):
3167
            qs = qs.filter(periodicity__in=(periodicity, '', 'first-half', 'second-half'))
3168

  
3169
        return qs.exists()
3170

  
3153 3171
    def period_overlaps(self, date_start, date_end, instance=None):
3154
        qs = self.periods
3172
        qs = self.periods.filter(holiday_rule__isnull=True)
3155 3173
        if hasattr(instance, 'pk'):
3156 3174
            qs = qs.exclude(pk=instance.pk)
3157 3175

  
......
3219 3237
        ordering = ['days__0', 'weeks']
3220 3238

  
3221 3239

  
3240
class SharedCustodyHolidayRule(models.Model):
3241
    YEAR_CHOICES = [
3242
        ('', pgettext_lazy('years', 'All')),
3243
        ('even', pgettext_lazy('years', 'Even')),
3244
        ('odd', pgettext_lazy('years', 'Odd')),
3245
    ]
3246

  
3247
    PERIODICITY_CHOICES = [
3248
        ('first-half', _('First half')),
3249
        ('second-half', _('Second half')),
3250
        ('first-and-third-quarters', _('First and third quarters')),
3251
        ('second-and-fourth-quarters', _('Second and fourth quarters')),
3252
    ]
3253

  
3254
    agenda = models.ForeignKey(SharedCustodyAgenda, on_delete=models.CASCADE, related_name='holiday_rules')
3255
    holiday = models.ForeignKey(TimePeriodExceptionGroup, verbose_name=_('Holiday'), on_delete=models.CASCADE)
3256
    years = models.CharField(_('Years'), choices=YEAR_CHOICES, blank=True, max_length=16)
3257
    periodicity = models.CharField(_('Periodicity'), choices=PERIODICITY_CHOICES, blank=True, max_length=32)
3258
    guardian = models.ForeignKey(Person, verbose_name=_('Guardian'), on_delete=models.CASCADE)
3259

  
3260
    def update_or_create_periods(self):
3261
        shared_custody_periods = []
3262
        for exception in self.holiday.exceptions.all():
3263
            date_start = localtime(exception.start_datetime).date()
3264

  
3265
            if self.years == 'even' and date_start.year % 2:
3266
                continue
3267
            if self.years == 'odd' and not date_start.year % 2:
3268
                continue
3269

  
3270
            date_start_sunday = date_start + relativedelta(weekday=SU)
3271
            date_end = localtime(exception.end_datetime).date()
3272

  
3273
            number_of_weeks = (date_end - date_start_sunday).days // 7
3274

  
3275
            periods = []
3276
            if self.periodicity == 'first-half':
3277
                date_end = date_start_sunday + datetime.timedelta(days=7 * (number_of_weeks // 2))
3278
                periods = [(date_start, date_end)]
3279
            elif self.periodicity == 'second-half':
3280
                date_start = date_start_sunday + datetime.timedelta(days=7 * (number_of_weeks // 2))
3281
                periods = [(date_start, date_end)]
3282
            elif self.periodicity == 'first-and-third-quarters' and number_of_weeks >= 4:
3283
                weeks_in_quarters = round(number_of_weeks / 4)
3284
                first_quarters_date_end = date_start_sunday + datetime.timedelta(days=7 * weeks_in_quarters)
3285
                third_quarters_date_start = date_start_sunday + datetime.timedelta(
3286
                    days=7 * weeks_in_quarters * 2
3287
                )
3288
                third_quarters_date_end = date_start_sunday + datetime.timedelta(
3289
                    days=7 * weeks_in_quarters * 3
3290
                )
3291
                periods = [
3292
                    (date_start, first_quarters_date_end),
3293
                    (third_quarters_date_start, third_quarters_date_end),
3294
                ]
3295
            elif self.periodicity == 'second-and-fourth-quarters' and number_of_weeks >= 4:
3296
                weeks_in_quarters = round(number_of_weeks / 4)
3297
                second_quarters_date_start = date_start_sunday + datetime.timedelta(
3298
                    days=7 * weeks_in_quarters
3299
                )
3300
                second_quarters_date_end = date_start_sunday + datetime.timedelta(
3301
                    days=7 * weeks_in_quarters * 2
3302
                )
3303
                fourth_quarters_date_start = date_start_sunday + datetime.timedelta(
3304
                    days=7 * weeks_in_quarters * 3
3305
                )
3306
                periods = [
3307
                    (second_quarters_date_start, second_quarters_date_end),
3308
                    (fourth_quarters_date_start, date_end),
3309
                ]
3310
            elif not self.periodicity:
3311
                periods = [(date_start, date_end)]
3312

  
3313
            for date_start, date_end in periods:
3314
                shared_custody_periods.append(
3315
                    SharedCustodyPeriod(
3316
                        guardian=self.guardian,
3317
                        agenda=self.agenda,
3318
                        holiday_rule=self,
3319
                        date_start=date_start,
3320
                        date_end=date_end,
3321
                    )
3322
                )
3323

  
3324
        with transaction.atomic():
3325
            SharedCustodyPeriod.objects.filter(
3326
                guardian=self.guardian, agenda=self.agenda, holiday_rule=self
3327
            ).delete()
3328
            SharedCustodyPeriod.objects.bulk_create(shared_custody_periods)
3329

  
3330
    @property
3331
    def label(self):
3332
        label = self.holiday.label
3333

  
3334
        if self.periodicity == 'first-half':
3335
            label = '%s, %s' % (label, _('the first half'))
3336
        elif self.periodicity == 'second-half':
3337
            label = '%s, %s' % (label, _('the second half'))
3338
        elif self.periodicity == 'first-and-third-quarters':
3339
            label = '%s, %s' % (label, _('the first and third quarters'))
3340
        elif self.periodicity == 'second-and-fourth-quarters':
3341
            label = '%s, %s' % (label, _('the second and fourth quarters'))
3342

  
3343
        if self.years == 'odd':
3344
            label = '%s, %s' % (label, _('on odd years'))
3345
        elif self.years == 'even':
3346
            label = '%s, %s' % (label, _('on even years'))
3347

  
3348
        return label
3349

  
3350
    class Meta:
3351
        ordering = ['holiday__label', 'guardian', 'years', 'periodicity']
3352

  
3353

  
3222 3354
class SharedCustodyPeriod(models.Model):
3223 3355
    agenda = models.ForeignKey(SharedCustodyAgenda, on_delete=models.CASCADE, related_name='periods')
3224 3356
    guardian = models.ForeignKey(Person, on_delete=models.CASCADE, related_name='+')
3357
    holiday_rule = models.ForeignKey(SharedCustodyHolidayRule, null=True, on_delete=models.CASCADE)
3225 3358
    date_start = models.DateField(_('Start'))
3226 3359
    date_end = models.DateField(_('End'))
3227 3360

  
chrono/manager/forms.py
28 28
from django.contrib.auth.models import Group
29 29
from django.core.exceptions import FieldDoesNotExist
30 30
from django.db import transaction
31
from django.db.models import DurationField, ExpressionWrapper, F
31 32
from django.forms import ValidationError
32 33
from django.utils.encoding import force_text
33 34
from django.utils.formats import date_format
......
50 51
    MeetingType,
51 52
    Person,
52 53
    Resource,
54
    SharedCustodyHolidayRule,
53 55
    SharedCustodyPeriod,
54 56
    SharedCustodyRule,
55 57
    Subscription,
56 58
    TimePeriod,
57 59
    TimePeriodException,
60
    TimePeriodExceptionGroup,
58 61
    TimePeriodExceptionSource,
59 62
    UnavailabilityCalendar,
60 63
    VirtualMember,
......
1339 1342
        return cleaned_data
1340 1343

  
1341 1344

  
1345
class SharedCustodyHolidayRuleForm(forms.ModelForm):
1346
    guardian = forms.ModelChoiceField(label=_('Guardian'), queryset=Person.objects.none())
1347

  
1348
    class Meta:
1349
        model = SharedCustodyHolidayRule
1350
        fields = ['guardian', 'holiday', 'years', 'periodicity']
1351

  
1352
    def __init__(self, *args, **kwargs):
1353
        super().__init__(*args, **kwargs)
1354
        self.fields['guardian'].empty_label = None
1355
        self.fields['guardian'].queryset = Person.objects.filter(
1356
            pk__in=[self.instance.agenda.first_guardian_id, self.instance.agenda.second_guardian_id]
1357
        )
1358
        self.fields['holiday'].queryset = TimePeriodExceptionGroup.objects.filter(
1359
            unavailability_calendar__slug='chrono-holidays',
1360
            exceptions__isnull=False,
1361
        ).distinct()
1362

  
1363
    def clean(self):
1364
        cleaned_data = super().clean()
1365

  
1366
        holidays = cleaned_data['holiday'].exceptions.annotate(
1367
            delta=ExpressionWrapper(F('end_datetime') - F('start_datetime'), output_field=DurationField())
1368
        )
1369
        is_short_holiday = holidays.filter(delta__lt=datetime.timedelta(days=28)).exists()
1370
        if 'quarters' in cleaned_data['periodicity'] and is_short_holiday:
1371
            raise ValidationError(_('Short holidays cannot be cut into quarters.'))
1372

  
1373
        if self.instance.agenda.holiday_rule_overlaps(
1374
            cleaned_data['holiday'], cleaned_data['years'], cleaned_data['periodicity'], self.instance
1375
        ):
1376
            raise ValidationError(_('Rule overlaps existing rules.'))
1377

  
1378
        return cleaned_data
1379

  
1380
    def save(self, *args, **kwargs):
1381
        with transaction.atomic():
1382
            super().save()
1383
            self.instance.update_or_create_periods()
1384

  
1385
        return self.instance
1386

  
1387

  
1342 1388
class SharedCustodyPeriodForm(forms.ModelForm):
1343 1389
    guardian = forms.ModelChoiceField(label=_('Guardian'), queryset=Person.objects.none())
1344 1390

  
chrono/manager/templates/chrono/manager_shared_custody_agenda_settings.html
10 10
<h2>{% trans "Settings" %}</h2>
11 11
<span class="actions">
12 12
  <a rel="popup" href="{% url 'chrono-manager-shared-custody-agenda-add-period' pk=object.id %}">{% trans 'Add custody period' %}</a>
13
  {% if has_holidays %}
14
  <a rel="popup" href="{% url 'chrono-manager-shared-custody-agenda-add-holiday-rule' pk=object.id %}">{% trans 'Add custody rule during holidays' %}</a>
15
  {% endif %}
13 16
  <a rel="popup" href="{% url 'chrono-manager-shared-custody-agenda-add-rule' pk=object.id %}">{% trans 'Add custody rule' %}</a>
14 17
</span>
15 18
{% endblock %}
......
45 48
  </div>
46 49
</div>
47 50

  
51
{% if has_holidays %}
52
<div class="section">
53
  <h3>{% trans "Custody rules during holidays" %}</h3>
54
  <div>
55
    {% if agenda.holiday_rules.all %}
56
    <ul class="objects-list single-links">
57
      {% for rule in agenda.holiday_rules.all %}
58
      <li><a rel="popup" href="{% url 'chrono-manager-shared-custody-agenda-edit-holiday-rule' pk=agenda.pk rule_pk=rule.pk %}">
59
          <span class="rule-info">
60
            {{ rule.guardian.name }}, {{ rule.label }}
61
          </span>
62
        </a>
63
        <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>
64
      </li>
65
      {% endfor %}
66
    </ul>
67
    {% else %}
68
    <div class="big-msg-info">
69
      {% blocktrans trimmed %}
70
      This agenda doesn't specify any custody rules during holidays. It means normal rules will be applied.
71
      {% endblocktrans %}
72
    </div>
73
    {% endif %}
74
  </div>
75
</div>
76
{% endif %}
77

  
48 78
<div class="section">
49 79
  <h3>{% trans "Exceptional custody periods" %}</h3>
50 80
  <div>
51
    {% if agenda.periods.all %}
81
    {% if exceptional_periods %}
52 82
    <ul class="objects-list single-links">
53
      {% for period in agenda.periods.all %}
83
      {% for period in exceptional_periods %}
54 84
      <li>
55 85
        <a rel="popup" href="{% url 'chrono-manager-shared-custody-agenda-edit-period' pk=agenda.pk period_pk=period.pk %}">
56 86
          {{ period }}
chrono/manager/urls.py
411 411
        views.shared_custody_agenda_delete_rule,
412 412
        name='chrono-manager-shared-custody-agenda-delete-rule',
413 413
    ),
414
    url(
415
        r'^shared-custody/(?P<pk>\d+)/add-holiday-rule$',
416
        views.shared_custody_agenda_add_holiday_rule,
417
        name='chrono-manager-shared-custody-agenda-add-holiday-rule',
418
    ),
419
    url(
420
        r'^shared-custody/(?P<pk>\d+)/holiday-rules/(?P<rule_pk>\d+)/edit$',
421
        views.shared_custody_agenda_edit_holiday_rule,
422
        name='chrono-manager-shared-custody-agenda-edit-holiday-rule',
423
    ),
424
    url(
425
        r'^shared-custody/(?P<pk>\d+)/holiday-rules/(?P<rule_pk>\d+)/delete$',
426
        views.shared_custody_agenda_delete_holiday_rule,
427
        name='chrono-manager-shared-custody-agenda-delete-holiday-rule',
428
    ),
414 429
    url(
415 430
        r'^shared-custody/(?P<pk>\d+)/add-period$',
416 431
        views.shared_custody_agenda_add_period,
chrono/manager/views.py
76 76
    MeetingType,
77 77
    Resource,
78 78
    SharedCustodyAgenda,
79
    SharedCustodyHolidayRule,
79 80
    SharedCustodyPeriod,
80 81
    SharedCustodyRule,
81 82
    TimePeriod,
......
114 115
    NewEventForm,
115 116
    NewMeetingTypeForm,
116 117
    NewTimePeriodExceptionForm,
118
    SharedCustodyHolidayRuleForm,
117 119
    SharedCustodyPeriodForm,
118 120
    SharedCustodyRuleForm,
119 121
    SubscriptionCheckFilterSet,
......
3458 3460
    template_name = 'chrono/manager_shared_custody_agenda_settings.html'
3459 3461
    model = SharedCustodyAgenda
3460 3462

  
3463
    def get_context_data(self, **kwargs):
3464
        context = super().get_context_data(**kwargs)
3465
        context['has_holidays'] = UnavailabilityCalendar.objects.filter(slug='chrono-holidays').exists()
3466
        context['exceptional_periods'] = SharedCustodyPeriod.objects.filter(holiday_rule__isnull=True)
3467
        return context
3468

  
3461 3469

  
3462 3470
shared_custody_agenda_settings = SharedCustodyAgendaSettings.as_view()
3463 3471

  
......
3492 3500
shared_custody_agenda_delete_rule = SharedCustodyAgendaDeleteRuleView.as_view()
3493 3501

  
3494 3502

  
3503
class SharedCustodyAgendaAddHolidayRuleView(SharedCustodyAgendaMixin, CreateView):
3504
    title = _('Add custody rule during holidays')
3505
    template_name = 'chrono/manager_agenda_form.html'
3506
    form_class = SharedCustodyHolidayRuleForm
3507
    model = SharedCustodyHolidayRule
3508

  
3509

  
3510
shared_custody_agenda_add_holiday_rule = SharedCustodyAgendaAddHolidayRuleView.as_view()
3511

  
3512

  
3513
class SharedCustodyAgendaEditHolidayRuleView(SharedCustodyAgendaMixin, UpdateView):
3514
    title = _('Edit custody rule during holidays')
3515
    template_name = 'chrono/manager_agenda_form.html'
3516
    form_class = SharedCustodyHolidayRuleForm
3517
    model = SharedCustodyHolidayRule
3518
    pk_url_kwarg = 'rule_pk'
3519

  
3520

  
3521
shared_custody_agenda_edit_holiday_rule = SharedCustodyAgendaEditHolidayRuleView.as_view()
3522

  
3523

  
3524
class SharedCustodyAgendaDeleteHolidayRuleView(SharedCustodyAgendaMixin, DeleteView):
3525
    template_name = 'chrono/manager_confirm_delete.html'
3526
    model = SharedCustodyHolidayRule
3527
    pk_url_kwarg = 'rule_pk'
3528

  
3529

  
3530
shared_custody_agenda_delete_holiday_rule = SharedCustodyAgendaDeleteHolidayRuleView.as_view()
3531

  
3532

  
3495 3533
class SharedCustodyAgendaAddPeriodView(SharedCustodyAgendaMixin, CreateView):
3496 3534
    title = _('Add custody period')
3497 3535
    template_name = 'chrono/manager_agenda_form.html'
tests/api/test_datetimes.py
15 15
    Event,
16 16
    Person,
17 17
    SharedCustodyAgenda,
18
    SharedCustodyHolidayRule,
18 19
    SharedCustodyPeriod,
19 20
    SharedCustodyRule,
20 21
    Subscription,
21 22
    TimePeriodException,
23
    TimePeriodExceptionGroup,
24
    UnavailabilityCalendar,
22 25
)
23 26
from tests.utils import login
24 27

  
......
2662 2665
    assert [d['id'] for d in resp.json['data']] == [
2663 2666
        'first-agenda@event-wednesday--2022-03-09-1400',
2664 2667
    ]
2668

  
2669

  
2670
@pytest.mark.freeze_time('2021-12-13 14:00')  # Monday of 50th week
2671
def test_datetimes_multiple_agendas_shared_custody_holiday_rules(app):
2672
    agenda = Agenda.objects.create(label='First agenda', kind='events')
2673
    Desk.objects.create(agenda=agenda, slug='_exceptions_holder')
2674
    Event.objects.create(
2675
        slug='event-wednesday',
2676
        start_datetime=make_aware(datetime.datetime(year=2021, month=12, day=25, hour=14, minute=0)),
2677
        places=5,
2678
        agenda=agenda,
2679
    )
2680
    Subscription.objects.create(
2681
        agenda=agenda,
2682
        user_external_id='child_id',
2683
        date_start=now(),
2684
        date_end=now() + datetime.timedelta(days=14),
2685
    )
2686

  
2687
    father = Person.objects.create(user_external_id='father_id', name='John Doe')
2688
    mother = Person.objects.create(user_external_id='mother_id', name='Jane Doe')
2689
    child = Person.objects.create(user_external_id='child_id', name='James Doe')
2690
    agenda = SharedCustodyAgenda.objects.create(first_guardian=father, second_guardian=mother)
2691
    agenda.children.add(child)
2692

  
2693
    SharedCustodyRule.objects.create(agenda=agenda, days=list(range(7)), weeks='even', guardian=father)
2694
    SharedCustodyRule.objects.create(agenda=agenda, days=list(range(7)), weeks='odd', guardian=mother)
2695

  
2696
    resp = app.get(
2697
        '/api/agendas/datetimes/',
2698
        params={'subscribed': 'all', 'user_external_id': 'child_id', 'guardian_external_id': 'father_id'},
2699
    )
2700
    assert len(resp.json['data']) == 0
2701

  
2702
    resp = app.get(
2703
        '/api/agendas/datetimes/',
2704
        params={'subscribed': 'all', 'user_external_id': 'child_id', 'guardian_external_id': 'mother_id'},
2705
    )
2706
    assert len(resp.json['data']) == 1
2707

  
2708
    # add father custody during holidays
2709
    calendar = UnavailabilityCalendar.objects.create(label='Calendar')
2710
    christmas_holiday = TimePeriodExceptionGroup.objects.create(
2711
        unavailability_calendar=calendar, label='Christmas', slug='christmas'
2712
    )
2713
    TimePeriodException.objects.create(
2714
        unavailability_calendar=calendar,
2715
        start_datetime=make_aware(datetime.datetime(year=2021, month=12, day=18, hour=0, minute=0)),
2716
        end_datetime=make_aware(datetime.datetime(year=2022, month=1, day=3, hour=0, minute=0)),
2717
        group=christmas_holiday,
2718
    )
2719

  
2720
    rule = SharedCustodyHolidayRule.objects.create(agenda=agenda, guardian=father, holiday=christmas_holiday)
2721
    rule.update_or_create_periods()
2722

  
2723
    resp = app.get(
2724
        '/api/agendas/datetimes/',
2725
        params={'subscribed': 'all', 'user_external_id': 'child_id', 'guardian_external_id': 'father_id'},
2726
    )
2727
    assert len(resp.json['data']) == 1
2728

  
2729
    resp = app.get(
2730
        '/api/agendas/datetimes/',
2731
        params={'subscribed': 'all', 'user_external_id': 'child_id', 'guardian_external_id': 'mother_id'},
2732
    )
2733
    assert len(resp.json['data']) == 0
tests/manager/test_shared_custody_agenda.py
1 1
import datetime
2 2

  
3 3
import pytest
4

  
5
from chrono.agendas.models import Person, SharedCustodyAgenda, SharedCustodyPeriod, SharedCustodyRule
4
from django.core.files.base import ContentFile
5

  
6
from chrono.agendas.models import (
7
    Person,
8
    SharedCustodyAgenda,
9
    SharedCustodyHolidayRule,
10
    SharedCustodyPeriod,
11
    SharedCustodyRule,
12
    TimePeriodExceptionGroup,
13
    UnavailabilityCalendar,
14
)
6 15
from tests.utils import login
7 16

  
8 17
pytestmark = pytest.mark.django_db
9 18

  
10 19

  
20
with open('tests/data/holidays.ics') as f:
21
    ICS_HOLIDAYS = f.read()
22

  
23

  
11 24
@pytest.mark.freeze_time('2022-02-22 14:00')  # Tuesday
12 25
def test_shared_custody_agenda_settings_rules(app, admin_user):
13 26
    father = Person.objects.create(user_external_id='father_id', name='John Doe')
......
153 166
    assert resp.text == old_resp.text
154 167

  
155 168
    app.get('/manage/shared-custody/%s/42/42/' % agenda.pk, status=404)
169

  
170

  
171
def test_shared_custody_agenda_holiday_rules(app, admin_user):
172
    father = Person.objects.create(user_external_id='father_id', name='John Doe')
173
    mother = Person.objects.create(user_external_id='mother_id', name='Jane Doe')
174
    agenda = SharedCustodyAgenda.objects.create(first_guardian=father, second_guardian=mother)
175

  
176
    app = login(app)
177
    resp = app.get('/manage/shared-custody/%s/settings/' % agenda.pk)
178
    assert 'Add custody rule during holidays' not in resp.text
179
    assert 'Custody rules during holidays' not in resp.text
180

  
181
    # configure holidays
182
    unavailability_calendar = UnavailabilityCalendar.objects.create(label='Calendar', slug='chrono-holidays')
183
    source = unavailability_calendar.timeperiodexceptionsource_set.create(
184
        ics_filename='holidays.ics', ics_file=ContentFile(ICS_HOLIDAYS, name='holidays.ics')
185
    )
186
    source.refresh_timeperiod_exceptions_from_ics()
187

  
188
    resp = app.get('/manage/shared-custody/%s/settings/' % agenda.pk)
189
    resp = resp.click('Add custody rule during holidays')
190
    resp.form['guardian'] = father.pk
191
    resp.form['holiday'].select(text='Vacances de Noël')
192
    resp.form['years'] = 'odd'
193
    resp.form['periodicity'] = 'first-half'
194
    resp = resp.form.submit().follow()
195
    assert SharedCustodyHolidayRule.objects.count() == 1
196
    assert SharedCustodyPeriod.objects.count() == 3
197
    assert 'This agenda doesn\'t have any custody period.' in resp.text
198

  
199
    resp = resp.click('John Doe, Vacances de Noël, the first half, on odd years')
200
    resp.form['years'] = ''
201
    resp = resp.form.submit().follow()
202
    assert 'John Doe, Vacances de Noël, the first half' in resp.text
203
    assert SharedCustodyHolidayRule.objects.count() == 1
204
    assert SharedCustodyPeriod.objects.count() == 6
205

  
206
    resp = resp.click('Add custody rule during holidays')
207
    resp.form['guardian'] = mother.pk
208
    resp.form['holiday'].select(text='Vacances de Noël')
209
    resp.form['periodicity'] = 'first-half'
210
    resp = resp.form.submit()
211
    assert 'Rule overlaps existing rules.' in resp.text
212

  
213
    resp.form['periodicity'] = 'second-half'
214
    resp = resp.form.submit().follow()
215
    assert 'Jane Doe, Vacances de Noël, the second half' in resp.text
216

  
217
    assert SharedCustodyHolidayRule.objects.count() == 2
218
    assert SharedCustodyPeriod.objects.count() == 12
219

  
220
    resp = resp.click('remove', index=1)
221
    resp = resp.form.submit().follow()
222
    assert SharedCustodyHolidayRule.objects.count() == 1
223
    assert SharedCustodyPeriod.objects.count() == 6
224

  
225
    resp = resp.click('Add custody rule during holidays')
226
    resp.form['guardian'] = father.pk
227
    resp.form['holiday'].select(text='Vacances de Noël')
228
    resp.form['periodicity'] = 'first-and-third-quarters'
229
    resp = resp.form.submit()
230
    assert 'Short holidays cannot be cut into quarters.' in resp.text
231

  
232
    resp.form['holiday'].select(text='Vacances d’Été')
233
    resp = resp.form.submit().follow()
234
    assert 'John Doe, Vacances d’Été, the first and third quarters' in resp.text
235

  
236
    # if dates get deleted, rules still exist but holiday is not shown anymore
237
    summer_holidays = TimePeriodExceptionGroup.objects.get(slug='summer_holidays')
238
    summer_holidays.exceptions.all().delete()
239

  
240
    assert SharedCustodyHolidayRule.objects.filter(holiday=summer_holidays).exists()
241

  
242
    resp = resp.click('Add custody rule during holidays')
243
    assert [x[2] for x in resp.form['holiday'].options] == ['---------', 'Vacances de Noël']
tests/test_agendas.py
27 27
    Person,
28 28
    Resource,
29 29
    SharedCustodyAgenda,
30
    SharedCustodyHolidayRule,
30 31
    SharedCustodyPeriod,
31 32
    SharedCustodyRule,
32 33
    TimePeriod,
......
2906 2907
    assert agenda.rule_overlaps(days, weeks) is overlaps
2907 2908

  
2908 2909

  
2910
def test_shared_custody_agenda_holiday_rule_overlaps():
2911
    calendar = UnavailabilityCalendar.objects.create(label='Calendar')
2912

  
2913
    father = Person.objects.create(user_external_id='father_id', name='John Doe')
2914
    mother = Person.objects.create(user_external_id='mother_id', name='Jane Doe')
2915
    agenda = SharedCustodyAgenda.objects.create(first_guardian=father, second_guardian=mother)
2916

  
2917
    summer_holiday = TimePeriodExceptionGroup.objects.create(
2918
        unavailability_calendar=calendar, label='Summer', slug='summer'
2919
    )
2920
    winter_holiday = TimePeriodExceptionGroup.objects.create(
2921
        unavailability_calendar=calendar, label='Winter', slug='winter'
2922
    )
2923

  
2924
    rule = SharedCustodyHolidayRule.objects.create(agenda=agenda, guardian=father, holiday=summer_holiday)
2925
    assert agenda.holiday_rule_overlaps(summer_holiday, years='', periodicity='') is True
2926
    assert agenda.holiday_rule_overlaps(summer_holiday, years='', periodicity='', instance=rule) is False
2927
    assert agenda.holiday_rule_overlaps(winter_holiday, years='', periodicity='') is False
2928
    assert agenda.holiday_rule_overlaps(summer_holiday, years='odd', periodicity='') is True
2929
    assert agenda.holiday_rule_overlaps(summer_holiday, years='', periodicity='first-half') is True
2930

  
2931
    rule.years = 'odd'
2932
    rule.save()
2933
    assert agenda.holiday_rule_overlaps(summer_holiday, years='', periodicity='') is True
2934
    assert agenda.holiday_rule_overlaps(summer_holiday, years='odd', periodicity='') is True
2935
    assert agenda.holiday_rule_overlaps(summer_holiday, years='even', periodicity='') is False
2936

  
2937
    rule.periodicity = 'first-half'
2938
    rule.save()
2939
    assert agenda.holiday_rule_overlaps(summer_holiday, years='', periodicity='') is True
2940
    assert agenda.holiday_rule_overlaps(summer_holiday, years='odd', periodicity='first-half') is True
2941
    assert agenda.holiday_rule_overlaps(summer_holiday, years='odd', periodicity='second-half') is False
2942
    assert (
2943
        agenda.holiday_rule_overlaps(summer_holiday, years='odd', periodicity='first-and-third-quarters')
2944
        is True
2945
    )
2946
    assert (
2947
        agenda.holiday_rule_overlaps(summer_holiday, years='odd', periodicity='second-and-fourth-quarters')
2948
        is True
2949
    )
2950
    assert agenda.holiday_rule_overlaps(summer_holiday, years='even', periodicity='first-half') is False
2951

  
2952
    rule.periodicity = 'second-and-fourth-quarters'
2953
    rule.save()
2954
    assert agenda.holiday_rule_overlaps(summer_holiday, years='', periodicity='') is True
2955
    assert agenda.holiday_rule_overlaps(summer_holiday, years='odd', periodicity='first-half') is True
2956
    assert agenda.holiday_rule_overlaps(summer_holiday, years='odd', periodicity='second-half') is True
2957
    assert (
2958
        agenda.holiday_rule_overlaps(summer_holiday, years='odd', periodicity='first-and-third-quarters')
2959
        is False
2960
    )
2961
    assert (
2962
        agenda.holiday_rule_overlaps(summer_holiday, years='odd', periodicity='second-and-fourth-quarters')
2963
        is True
2964
    )
2965
    assert agenda.holiday_rule_overlaps(summer_holiday, years='even', periodicity='first-half') is False
2966

  
2967

  
2909 2968
@pytest.mark.parametrize(
2910 2969
    'periods,date_start,date_end,overlaps',
2911 2970
    (
......
2934 2993
    assert agenda.period_overlaps(date_start, date_end) is overlaps
2935 2994

  
2936 2995

  
2996
def test_shared_custody_agenda_period_holiday_rule_no_overlaps():
2997
    calendar = UnavailabilityCalendar.objects.create(label='Calendar')
2998

  
2999
    father = Person.objects.create(user_external_id='father_id', name='John Doe')
3000
    mother = Person.objects.create(user_external_id='mother_id', name='Jane Doe')
3001
    agenda = SharedCustodyAgenda.objects.create(first_guardian=father, second_guardian=mother)
3002

  
3003
    summer_holiday = TimePeriodExceptionGroup.objects.create(
3004
        unavailability_calendar=calendar, label='Summer', slug='summer'
3005
    )
3006

  
3007
    rule = SharedCustodyHolidayRule.objects.create(agenda=agenda, guardian=father, holiday=summer_holiday)
3008
    SharedCustodyPeriod.objects.create(
3009
        holiday_rule=rule, agenda=agenda, guardian=father, date_start='2022-02-03', date_end='2022-02-05'
3010
    )
3011

  
3012
    assert agenda.period_overlaps('2022-02-03', '2022-02-05') is False
3013

  
3014

  
2937 3015
def test_shared_custody_agenda_rule_label():
2938 3016
    father = Person.objects.create(user_external_id='father_id', name='John Doe')
2939 3017
    mother = Person.objects.create(user_external_id='mother_id', name='Jane Doe')
......
2964 3042
    assert rule.label == 'on Mondays, on odd weeks'
2965 3043

  
2966 3044

  
3045
def test_shared_custody_agenda_holiday_rule_label():
3046
    calendar = UnavailabilityCalendar.objects.create(label='Calendar')
3047

  
3048
    father = Person.objects.create(user_external_id='father_id', name='John Doe')
3049
    mother = Person.objects.create(user_external_id='mother_id', name='Jane Doe')
3050
    agenda = SharedCustodyAgenda.objects.create(first_guardian=father, second_guardian=mother)
3051

  
3052
    summer_holiday = TimePeriodExceptionGroup.objects.create(
3053
        unavailability_calendar=calendar, label='Summer Holidays', slug='summer'
3054
    )
3055

  
3056
    rule = SharedCustodyHolidayRule.objects.create(agenda=agenda, guardian=father, holiday=summer_holiday)
3057
    assert rule.label == 'Summer Holidays'
3058

  
3059
    rule.years = 'even'
3060
    rule.save()
3061
    assert rule.label == 'Summer Holidays, on even years'
3062

  
3063
    rule.years = 'odd'
3064
    rule.save()
3065
    assert rule.label == 'Summer Holidays, on odd years'
3066

  
3067
    rule.periodicity = 'first-half'
3068
    rule.save()
3069
    assert rule.label == 'Summer Holidays, the first half, on odd years'
3070

  
3071
    rule.years = ''
3072
    rule.periodicity = 'second-half'
3073
    rule.save()
3074
    assert rule.label == 'Summer Holidays, the second half'
3075

  
3076
    rule.periodicity = 'first-and-third-quarters'
3077
    rule.save()
3078
    assert rule.label == 'Summer Holidays, the first and third quarters'
3079

  
3080
    rule.periodicity = 'second-and-fourth-quarters'
3081
    rule.save()
3082
    assert rule.label == 'Summer Holidays, the second and fourth quarters'
3083

  
3084

  
2967 3085
def test_shared_custody_agenda_period_label(freezer):
2968 3086
    father = Person.objects.create(user_external_id='father_id', name='John Doe')
2969 3087
    mother = Person.objects.create(user_external_id='mother_id', name='Jane Doe')
......
2980 3098
    period.date_end = datetime.date(2021, 7, 13)
2981 3099
    period.save()
2982 3100
    assert str(period) == 'John Doe, 07/10/2021 → 07/13/2021'
3101

  
3102

  
3103
def test_shared_custody_agenda_holiday_rule_create_periods():
3104
    calendar = UnavailabilityCalendar.objects.create(label='Calendar')
3105

  
3106
    father = Person.objects.create(user_external_id='father_id', name='John Doe')
3107
    mother = Person.objects.create(user_external_id='mother_id', name='Jane Doe')
3108
    agenda = SharedCustodyAgenda.objects.create(first_guardian=father, second_guardian=mother)
3109

  
3110
    summer_holiday = TimePeriodExceptionGroup.objects.create(
3111
        unavailability_calendar=calendar, label='Summer', slug='summer'
3112
    )
3113
    TimePeriodException.objects.create(
3114
        unavailability_calendar=calendar,
3115
        start_datetime=make_aware(datetime.datetime(year=2021, month=7, day=6, hour=0, minute=0)),
3116
        end_datetime=make_aware(datetime.datetime(year=2021, month=9, day=2, hour=0, minute=0)),
3117
        group=summer_holiday,
3118
    )
3119
    TimePeriodException.objects.create(
3120
        unavailability_calendar=calendar,
3121
        start_datetime=make_aware(datetime.datetime(year=2022, month=7, day=7, hour=0, minute=0)),
3122
        end_datetime=make_aware(datetime.datetime(year=2022, month=9, day=1, hour=0, minute=0)),
3123
        group=summer_holiday,
3124
    )
3125

  
3126
    rule = SharedCustodyHolidayRule.objects.create(agenda=agenda, guardian=father, holiday=summer_holiday)
3127
    rule.update_or_create_periods()
3128
    period1, period2 = SharedCustodyPeriod.objects.all()
3129
    assert period1.holiday_rule == rule
3130
    assert period1.guardian == father
3131
    assert period1.agenda == agenda
3132
    assert period1.date_start == datetime.date(year=2021, month=7, day=6)
3133
    assert period1.date_end == datetime.date(year=2021, month=9, day=2)
3134
    assert period2.holiday_rule == rule
3135
    assert period2.guardian == father
3136
    assert period2.agenda == agenda
3137
    assert period2.date_start == datetime.date(year=2022, month=7, day=7)
3138
    assert period2.date_end == datetime.date(year=2022, month=9, day=1)
3139

  
3140
    rule.years = 'odd'
3141
    rule.update_or_create_periods()
3142
    period = SharedCustodyPeriod.objects.get()
3143
    assert period.date_start == datetime.date(year=2021, month=7, day=6)
3144
    assert period.date_end == datetime.date(year=2021, month=9, day=2)
3145

  
3146
    rule.years = 'even'
3147
    rule.update_or_create_periods()
3148
    period = SharedCustodyPeriod.objects.get()
3149
    assert period.date_start == datetime.date(year=2022, month=7, day=7)
3150
    assert period.date_end == datetime.date(year=2022, month=9, day=1)
3151

  
3152
    rule.years = ''
3153
    rule.periodicity = 'first-half'
3154
    rule.update_or_create_periods()
3155
    period1, period2 = SharedCustodyPeriod.objects.all()
3156
    assert period1.date_start == datetime.date(year=2021, month=7, day=6)
3157
    assert period1.date_end == datetime.date(year=2021, month=8, day=1)
3158
    assert period1.date_end.weekday() == 6
3159
    assert period2.date_start == datetime.date(year=2022, month=7, day=7)
3160
    assert period2.date_end == datetime.date(year=2022, month=7, day=31)
3161
    assert period2.date_end.weekday() == 6
3162

  
3163
    rule.periodicity = 'second-half'
3164
    rule.update_or_create_periods()
3165
    period1, period2 = SharedCustodyPeriod.objects.all()
3166
    assert period1.date_start == datetime.date(year=2021, month=8, day=1)
3167
    assert period1.date_start.weekday() == 6
3168
    assert period1.date_end == datetime.date(year=2021, month=9, day=2)
3169
    assert period2.date_start == datetime.date(year=2022, month=7, day=31)
3170
    assert period2.date_start.weekday() == 6
3171
    assert period2.date_end == datetime.date(year=2022, month=9, day=1)
3172

  
3173
    rule.periodicity = 'first-and-third-quarters'
3174
    rule.update_or_create_periods()
3175
    period1, period2, period3, period4 = SharedCustodyPeriod.objects.all()
3176
    assert period1.date_start == datetime.date(year=2021, month=7, day=6)
3177
    assert period1.date_end == datetime.date(year=2021, month=7, day=25)
3178
    assert period1.date_end.weekday() == 6
3179
    assert period2.date_start == datetime.date(year=2021, month=8, day=8)
3180
    assert period2.date_end == datetime.date(year=2021, month=8, day=22)
3181
    assert period2.date_end.weekday() == 6
3182
    assert period3.date_start == datetime.date(year=2022, month=7, day=7)
3183
    assert period3.date_end == datetime.date(year=2022, month=7, day=24)
3184
    assert period3.date_end.weekday() == 6
3185
    assert period4.date_start == datetime.date(year=2022, month=8, day=7)
3186
    assert period4.date_end == datetime.date(year=2022, month=8, day=21)
3187
    assert period4.date_end.weekday() == 6
3188

  
3189
    rule.periodicity = 'second-and-fourth-quarters'
3190
    rule.update_or_create_periods()
3191
    period1, period2, period3, period4 = SharedCustodyPeriod.objects.all()
3192
    assert period1.date_start == datetime.date(year=2021, month=7, day=25)
3193
    assert period1.date_start.weekday() == 6
3194
    assert period1.date_end == datetime.date(year=2021, month=8, day=8)
3195
    assert period2.date_start == datetime.date(year=2021, month=8, day=22)
3196
    assert period2.date_start.weekday() == 6
3197
    assert period2.date_end == datetime.date(year=2021, month=9, day=2)
3198
    assert period3.date_start == datetime.date(year=2022, month=7, day=24)
3199
    assert period3.date_start.weekday() == 6
3200
    assert period3.date_end == datetime.date(year=2022, month=8, day=7)
3201
    assert period4.date_start == datetime.date(year=2022, month=8, day=21)
3202
    assert period4.date_start.weekday() == 6
3203
    assert period4.date_end == datetime.date(year=2022, month=9, day=1)
3204

  
3205

  
3206
def test_shared_custody_agenda_holiday_rule_create_periods_christmas_holidays():
3207
    calendar = UnavailabilityCalendar.objects.create(label='Calendar')
3208

  
3209
    father = Person.objects.create(user_external_id='father_id', name='John Doe')
3210
    mother = Person.objects.create(user_external_id='mother_id', name='Jane Doe')
3211
    agenda = SharedCustodyAgenda.objects.create(first_guardian=father, second_guardian=mother)
3212

  
3213
    christmas_holiday = TimePeriodExceptionGroup.objects.create(
3214
        unavailability_calendar=calendar, label='Christmas', slug='christmas'
3215
    )
3216
    TimePeriodException.objects.create(
3217
        unavailability_calendar=calendar,
3218
        start_datetime=make_aware(datetime.datetime(year=2021, month=12, day=18, hour=0, minute=0)),
3219
        end_datetime=make_aware(datetime.datetime(year=2022, month=1, day=3, hour=0, minute=0)),
3220
        group=christmas_holiday,
3221
    )
3222
    TimePeriodException.objects.create(
3223
        unavailability_calendar=calendar,
3224
        start_datetime=make_aware(datetime.datetime(year=2022, month=12, day=17, hour=0, minute=0)),
3225
        end_datetime=make_aware(datetime.datetime(year=2023, month=1, day=3, hour=0, minute=0)),
3226
        group=christmas_holiday,
3227
    )
3228

  
3229
    rule = SharedCustodyHolidayRule.objects.create(agenda=agenda, guardian=father, holiday=christmas_holiday)
3230
    rule.update_or_create_periods()
3231
    period1, period2 = SharedCustodyPeriod.objects.all()
3232
    assert period1.date_start == datetime.date(year=2021, month=12, day=18)
3233
    assert period1.date_end == datetime.date(year=2022, month=1, day=3)
3234
    assert period2.date_start == datetime.date(year=2022, month=12, day=17)
3235
    assert period2.date_end == datetime.date(year=2023, month=1, day=3)
3236

  
3237
    rule.periodicity = 'first-half'
3238
    rule.update_or_create_periods()
3239
    period1, period2 = SharedCustodyPeriod.objects.all()
3240
    assert period1.date_start == datetime.date(year=2021, month=12, day=18)
3241
    assert period1.date_end == datetime.date(year=2021, month=12, day=26)
3242
    assert period1.date_end.weekday() == 6
3243
    assert period2.date_start == datetime.date(year=2022, month=12, day=17)
3244
    assert period2.date_end == datetime.date(year=2022, month=12, day=25)
3245
    assert period2.date_end.weekday() == 6
3246

  
3247
    rule.periodicity = 'second-half'
3248
    rule.update_or_create_periods()
3249
    period1, period2 = SharedCustodyPeriod.objects.all()
3250
    assert period1.date_start == datetime.date(year=2021, month=12, day=26)
3251
    assert period1.date_start.weekday() == 6
3252
    assert period1.date_end == datetime.date(year=2022, month=1, day=3)
3253
    assert period2.date_start == datetime.date(year=2022, month=12, day=25)
3254
    assert period2.date_start.weekday() == 6
3255
    assert period2.date_end == datetime.date(year=2023, month=1, day=3)
3256

  
3257
    rule.periodicity = 'first-and-third-quarters'
3258
    rule.update_or_create_periods()
3259
    assert not SharedCustodyPeriod.objects.exists()
3260

  
3261
    rule.periodicity = 'second-and-fourth-quarters'
3262
    rule.update_or_create_periods()
3263
    assert not SharedCustodyPeriod.objects.exists()
3264

  
3265

  
3266
def test_shared_custody_agenda_holiday_rules_application():
3267
    calendar = UnavailabilityCalendar.objects.create(label='Calendar')
3268

  
3269
    father = Person.objects.create(user_external_id='father_id', name='John Doe')
3270
    mother = Person.objects.create(user_external_id='mother_id', name='Jane Doe')
3271
    agenda = SharedCustodyAgenda.objects.create(first_guardian=father, second_guardian=mother)
3272

  
3273
    christmas_holiday = TimePeriodExceptionGroup.objects.create(
3274
        unavailability_calendar=calendar, label='Christmas', slug='christmas'
3275
    )
3276
    TimePeriodException.objects.create(
3277
        unavailability_calendar=calendar,
3278
        start_datetime=make_aware(datetime.datetime(year=2021, month=12, day=18, hour=0, minute=0)),
3279
        end_datetime=make_aware(datetime.datetime(year=2022, month=1, day=3, hour=0, minute=0)),
3280
        group=christmas_holiday,
3281
    )
3282

  
3283
    SharedCustodyRule.objects.create(agenda=agenda, days=list(range(7)), weeks='even', guardian=father)
3284
    SharedCustodyRule.objects.create(agenda=agenda, days=list(range(7)), weeks='odd', guardian=mother)
3285

  
3286
    rule = SharedCustodyHolidayRule.objects.create(agenda=agenda, guardian=father, holiday=christmas_holiday)
3287
    rule.update_or_create_periods()
3288

  
3289
    date_start = datetime.date(year=2021, month=12, day=13)  # Monday, even week
3290
    slots = agenda.get_custody_slots(date_start, date_start + datetime.timedelta(days=30))
3291
    guardians = [x.guardian.name for x in slots]
3292
    assert all(name == 'John Doe' for name in guardians[:21])
3293
    assert all(name == 'Jane Doe' for name in guardians[21:28])
3294
    assert all(name == 'John Doe' for name in guardians[28:])
2983
-