From 3e4a318cd931b942eddbe00edd7682d529a422b7 Mon Sep 17 00:00:00 2001 From: Valentin Deniaud Date: Wed, 23 Mar 2022 16:46:56 +0100 Subject: [PATCH 1/4] agendas: add time period exception groups (#62801) --- .../migrations/0113_auto_20220323_1708.py | 45 +++++++++ chrono/agendas/models.py | 39 +++++++- tests/data/holidays.ics | 92 +++++++++++++++++++ tests/test_agendas.py | 36 ++++++++ 4 files changed, 210 insertions(+), 2 deletions(-) create mode 100644 chrono/agendas/migrations/0113_auto_20220323_1708.py create mode 100644 tests/data/holidays.ics diff --git a/chrono/agendas/migrations/0113_auto_20220323_1708.py b/chrono/agendas/migrations/0113_auto_20220323_1708.py new file mode 100644 index 00000000..bfdca11e --- /dev/null +++ b/chrono/agendas/migrations/0113_auto_20220323_1708.py @@ -0,0 +1,45 @@ +# Generated by Django 2.2.19 on 2022-03-23 16:08 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('agendas', '0112_auto_20220323_1320'), + ] + + operations = [ + migrations.CreateModel( + name='TimePeriodExceptionGroup', + fields=[ + ( + 'id', + models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + ('slug', models.SlugField(max_length=160, verbose_name='Identifier')), + ('label', models.CharField(max_length=150, verbose_name='Label')), + ( + 'unavailability_calendar', + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to='agendas.UnavailabilityCalendar' + ), + ), + ], + options={ + 'ordering': ['label'], + 'unique_together': {('unavailability_calendar', 'slug')}, + }, + ), + migrations.AddField( + model_name='timeperiodexception', + name='group', + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name='exceptions', + to='agendas.TimePeriodExceptionGroup', + ), + ), + ] diff --git a/chrono/agendas/models.py b/chrono/agendas/models.py index de700557..e63f4cd2 100644 --- a/chrono/agendas/models.py +++ b/chrono/agendas/models.py @@ -2403,6 +2403,7 @@ class TimePeriodExceptionSource(models.Model): else: parsed = data + categories = collections.defaultdict(list) with transaction.atomic(): # delete old exceptions related to this source self.timeperiodexception_set.all().delete() @@ -2444,9 +2445,16 @@ class TimePeriodExceptionSource(models.Model): 'recurrence_id': 0, } + if 'categories' in vevent.contents and len(vevent.categories.value) > 0: + category = vevent.categories.value[0] + else: + category = None + if not vevent.rruleset: # classical event - TimePeriodException.objects.create(**event) + exception = TimePeriodException.objects.create(**event) + if category: + categories[category].append(exception) elif vevent.rruleset.count(): # recurring event until recurring_days in the future from_dt = start_dt @@ -2464,7 +2472,18 @@ class TimePeriodExceptionSource(models.Model): event['start_datetime'] = start_dt event['end_datetime'] = end_dt if end_dt >= update_datetime: - TimePeriodException.objects.create(**event) + exception = TimePeriodException.objects.create(**event) + if category: + categories[category].append(exception) + + if self.unavailability_calendar_id: + for category, exceptions in categories.items(): + exception_group, dummy = TimePeriodExceptionGroup.objects.get_or_create( + unavailability_calendar_id=self.unavailability_calendar_id, + slug=category, + defaults={'label': exceptions[0].label}, + ) + exception_group.exceptions.add(*exceptions) @classmethod def import_json(cls, data): @@ -2568,6 +2587,19 @@ class UnavailabilityCalendar(models.Model): return created, unavailability_calendar +class TimePeriodExceptionGroup(models.Model): + unavailability_calendar = models.ForeignKey(UnavailabilityCalendar, on_delete=models.CASCADE) + slug = models.SlugField(_('Identifier'), max_length=160) + label = models.CharField(_('Label'), max_length=150) + + class Meta: + ordering = ['label'] + unique_together = ['unavailability_calendar', 'slug'] + + def __str__(self): + return self.label + + class TimePeriodException(models.Model): desk = models.ForeignKey(Desk, on_delete=models.CASCADE, null=True) unavailability_calendar = models.ForeignKey(UnavailabilityCalendar, on_delete=models.CASCADE, null=True) @@ -2577,6 +2609,9 @@ class TimePeriodException(models.Model): end_datetime = models.DateTimeField(_('Exception end time')) update_datetime = models.DateTimeField(auto_now=True) recurrence_id = models.PositiveIntegerField(_('Recurrence ID'), default=0) + group = models.ForeignKey( + TimePeriodExceptionGroup, on_delete=models.CASCADE, null=True, related_name='exceptions' + ) @property def read_only(self): diff --git a/tests/data/holidays.ics b/tests/data/holidays.ics new file mode 100644 index 00000000..019b8f77 --- /dev/null +++ b/tests/data/holidays.ics @@ -0,0 +1,92 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//PYVOBJECT//NONSGML Version 1//EN +BEGIN:VEVENT +UID:christmas_holidays-2017 +DTSTART;VALUE=DATE:20171223 +DTEND;VALUE=DATE:20180108 +CATEGORIES:christmas_holidays +DTSTAMP:20220328T140507Z +SUMMARY:Vacances de Noël +END:VEVENT +BEGIN:VEVENT +UID:summer_holidays-2018 +DTSTART;VALUE=DATE:20180707 +DTEND;VALUE=DATE:20180903 +CATEGORIES:summer_holidays +DTSTAMP:20220328T140507Z +SUMMARY:Vacances d’Été +END:VEVENT +BEGIN:VEVENT +UID:christmas_holidays-2018 +DTSTART;VALUE=DATE:20181222 +DTEND;VALUE=DATE:20190107 +CATEGORIES:christmas_holidays +DTSTAMP:20220328T140507Z +SUMMARY:Vacances de Noël +END:VEVENT +BEGIN:VEVENT +UID:summer_holidays-2019 +DTSTART;VALUE=DATE:20190706 +DTEND;VALUE=DATE:20190902 +CATEGORIES:summer_holidays +DTSTAMP:20220328T140507Z +SUMMARY:Vacances d’Été +END:VEVENT +BEGIN:VEVENT +UID:christmas_holidays-2019 +DTSTART;VALUE=DATE:20191221 +DTEND;VALUE=DATE:20200106 +CATEGORIES:christmas_holidays +DTSTAMP:20220328T140507Z +SUMMARY:Vacances de Noël +END:VEVENT +BEGIN:VEVENT +UID:summer_holidays-2020 +DTSTART;VALUE=DATE:20200704 +DTEND;VALUE=DATE:20200901 +CATEGORIES:summer_holidays +DTSTAMP:20220328T140507Z +SUMMARY:Vacances d’Été +END:VEVENT +BEGIN:VEVENT +UID:christmas_holidays-2020 +DTSTART;VALUE=DATE:20201219 +DTEND;VALUE=DATE:20210104 +CATEGORIES:christmas_holidays +DTSTAMP:20220328T140507Z +SUMMARY:Vacances de Noël +END:VEVENT +BEGIN:VEVENT +UID:summer_holidays-2021 +DTSTART;VALUE=DATE:20210706 +DTEND;VALUE=DATE:20210902 +CATEGORIES:summer_holidays +DTSTAMP:20220328T140507Z +SUMMARY:Vacances d’Été +END:VEVENT +BEGIN:VEVENT +UID:christmas_holidays-2021 +DTSTART;VALUE=DATE:20211218 +DTEND;VALUE=DATE:20220103 +CATEGORIES:christmas_holidays +DTSTAMP:20220328T140507Z +SUMMARY:Vacances de Noël +END:VEVENT +BEGIN:VEVENT +UID:summer_holidays-2022 +DTSTART;VALUE=DATE:20220707 +DTEND;VALUE=DATE:20220901 +CATEGORIES:summer_holidays +DTSTAMP:20220328T140507Z +SUMMARY:Vacances d’Été +END:VEVENT +BEGIN:VEVENT +UID:christmas_holidays-2022 +DTSTART;VALUE=DATE:20221217 +DTEND;VALUE=DATE:20230103 +CATEGORIES:christmas_holidays +DTSTAMP:20220328T140507Z +SUMMARY:Vacances de Noël +END:VEVENT +END:VCALENDAR diff --git a/tests/test_agendas.py b/tests/test_agendas.py index cc2d005b..ee50bb83 100644 --- a/tests/test_agendas.py +++ b/tests/test_agendas.py @@ -31,6 +31,7 @@ from chrono.agendas.models import ( SharedCustodyRule, TimePeriod, TimePeriodException, + TimePeriodExceptionGroup, TimePeriodExceptionSource, UnavailabilityCalendar, VirtualMember, @@ -119,6 +120,10 @@ with open('tests/data/atreal.ics') as f: ICS_ATREAL = f.read() +with open('tests/data/holidays.ics') as f: + ICS_HOLIDAYS = f.read() + + def test_slug(): agenda = Agenda(label='Foo bar') agenda.save() @@ -854,6 +859,37 @@ def test_timeperiodexception_from_settings_command(): assert not TimePeriodExceptionSource.objects.exists() +def test_timeperiodexception_groups(): + unavailability_calendar = UnavailabilityCalendar.objects.create(label='Calendar') + source = unavailability_calendar.timeperiodexceptionsource_set.create( + ics_filename='holidays.ics', ics_file=ContentFile(ICS_HOLIDAYS, name='holidays.ics') + ) + source.refresh_timeperiod_exceptions_from_ics() + assert TimePeriodException.objects.count() == 11 + group1, group2 = TimePeriodExceptionGroup.objects.all() + assert group1.label == 'Vacances de Noël' + assert group1.slug == 'christmas_holidays' + assert group2.label == 'Vacances d’Été' + assert group2.slug == 'summer_holidays' + + assert group1.exceptions.count() == 6 + assert group2.exceptions.count() == 5 + + unavailability_calendar.delete() + assert not TimePeriodException.objects.exists() + assert not TimePeriodExceptionGroup.objects.exists() + + # check no groups are created for desks + agenda = Agenda.objects.create(label='Test 1 agenda') + desk = Desk.objects.create(label='Test 1 desk', agenda=agenda) + source = desk.timeperiodexceptionsource_set.create( + ics_filename='holidays.ics', ics_file=ContentFile(ICS_HOLIDAYS, name='holidays.ics') + ) + source.refresh_timeperiod_exceptions_from_ics() + assert TimePeriodException.objects.count() == 11 + assert not TimePeriodExceptionGroup.objects.exists() + + def test_base_meeting_duration(): agenda = Agenda(label='Meeting', kind='meetings') agenda.save() -- 2.30.2