agendas: add time period exception groups (#62801)

This commit is contained in:
Valentin Deniaud 2022-03-23 16:46:56 +01:00
parent 89289ad665
commit dcec0f2d3f
4 changed files with 210 additions and 2 deletions

View File

@ -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',
),
),
]

View File

@ -2399,6 +2399,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()
@ -2440,9 +2441,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
@ -2460,7 +2468,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):
@ -2564,6 +2583,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)
@ -2573,6 +2605,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):

92
tests/data/holidays.ics Normal file
View File

@ -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

View File

@ -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()