0001-agendas-add-time-period-exception-groups-62801.patch
chrono/agendas/migrations/0113_auto_20220323_1708.py | ||
---|---|---|
1 |
# Generated by Django 2.2.19 on 2022-03-23 16:08 |
|
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', '0112_auto_20220323_1320'), |
|
11 |
] |
|
12 | ||
13 |
operations = [ |
|
14 |
migrations.CreateModel( |
|
15 |
name='TimePeriodExceptionGroup', |
|
16 |
fields=[ |
|
17 |
( |
|
18 |
'id', |
|
19 |
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), |
|
20 |
), |
|
21 |
('slug', models.SlugField(max_length=160, verbose_name='Identifier')), |
|
22 |
('label', models.CharField(max_length=150, verbose_name='Label')), |
|
23 |
( |
|
24 |
'unavailability_calendar', |
|
25 |
models.ForeignKey( |
|
26 |
on_delete=django.db.models.deletion.CASCADE, to='agendas.UnavailabilityCalendar' |
|
27 |
), |
|
28 |
), |
|
29 |
], |
|
30 |
options={ |
|
31 |
'ordering': ['label'], |
|
32 |
'unique_together': {('unavailability_calendar', 'slug')}, |
|
33 |
}, |
|
34 |
), |
|
35 |
migrations.AddField( |
|
36 |
model_name='timeperiodexception', |
|
37 |
name='group', |
|
38 |
field=models.ForeignKey( |
|
39 |
null=True, |
|
40 |
on_delete=django.db.models.deletion.CASCADE, |
|
41 |
related_name='exceptions', |
|
42 |
to='agendas.TimePeriodExceptionGroup', |
|
43 |
), |
|
44 |
), |
|
45 |
] |
chrono/agendas/models.py | ||
---|---|---|
2403 | 2403 |
else: |
2404 | 2404 |
parsed = data |
2405 | 2405 | |
2406 |
categories = collections.defaultdict(list) |
|
2406 | 2407 |
with transaction.atomic(): |
2407 | 2408 |
# delete old exceptions related to this source |
2408 | 2409 |
self.timeperiodexception_set.all().delete() |
... | ... | |
2444 | 2445 |
'recurrence_id': 0, |
2445 | 2446 |
} |
2446 | 2447 | |
2448 |
if 'categories' in vevent.contents and len(vevent.categories.value) > 0: |
|
2449 |
category = vevent.categories.value[0] |
|
2450 |
else: |
|
2451 |
category = None |
|
2452 | ||
2447 | 2453 |
if not vevent.rruleset: |
2448 | 2454 |
# classical event |
2449 |
TimePeriodException.objects.create(**event) |
|
2455 |
exception = TimePeriodException.objects.create(**event) |
|
2456 |
if category: |
|
2457 |
categories[category].append(exception) |
|
2450 | 2458 |
elif vevent.rruleset.count(): |
2451 | 2459 |
# recurring event until recurring_days in the future |
2452 | 2460 |
from_dt = start_dt |
... | ... | |
2464 | 2472 |
event['start_datetime'] = start_dt |
2465 | 2473 |
event['end_datetime'] = end_dt |
2466 | 2474 |
if end_dt >= update_datetime: |
2467 |
TimePeriodException.objects.create(**event) |
|
2475 |
exception = TimePeriodException.objects.create(**event) |
|
2476 |
if category: |
|
2477 |
categories[category].append(exception) |
|
2478 | ||
2479 |
if self.unavailability_calendar_id: |
|
2480 |
for category, exceptions in categories.items(): |
|
2481 |
exception_group, dummy = TimePeriodExceptionGroup.objects.get_or_create( |
|
2482 |
unavailability_calendar_id=self.unavailability_calendar_id, |
|
2483 |
slug=category, |
|
2484 |
defaults={'label': exceptions[0].label}, |
|
2485 |
) |
|
2486 |
exception_group.exceptions.add(*exceptions) |
|
2468 | 2487 | |
2469 | 2488 |
@classmethod |
2470 | 2489 |
def import_json(cls, data): |
... | ... | |
2568 | 2587 |
return created, unavailability_calendar |
2569 | 2588 | |
2570 | 2589 | |
2590 |
class TimePeriodExceptionGroup(models.Model): |
|
2591 |
unavailability_calendar = models.ForeignKey(UnavailabilityCalendar, on_delete=models.CASCADE) |
|
2592 |
slug = models.SlugField(_('Identifier'), max_length=160) |
|
2593 |
label = models.CharField(_('Label'), max_length=150) |
|
2594 | ||
2595 |
class Meta: |
|
2596 |
ordering = ['label'] |
|
2597 |
unique_together = ['unavailability_calendar', 'slug'] |
|
2598 | ||
2599 |
def __str__(self): |
|
2600 |
return self.label |
|
2601 | ||
2602 | ||
2571 | 2603 |
class TimePeriodException(models.Model): |
2572 | 2604 |
desk = models.ForeignKey(Desk, on_delete=models.CASCADE, null=True) |
2573 | 2605 |
unavailability_calendar = models.ForeignKey(UnavailabilityCalendar, on_delete=models.CASCADE, null=True) |
... | ... | |
2577 | 2609 |
end_datetime = models.DateTimeField(_('Exception end time')) |
2578 | 2610 |
update_datetime = models.DateTimeField(auto_now=True) |
2579 | 2611 |
recurrence_id = models.PositiveIntegerField(_('Recurrence ID'), default=0) |
2612 |
group = models.ForeignKey( |
|
2613 |
TimePeriodExceptionGroup, on_delete=models.CASCADE, null=True, related_name='exceptions' |
|
2614 |
) |
|
2580 | 2615 | |
2581 | 2616 |
@property |
2582 | 2617 |
def read_only(self): |
tests/data/holidays.ics | ||
---|---|---|
1 |
BEGIN:VCALENDAR |
|
2 |
VERSION:2.0 |
|
3 |
PRODID:-//PYVOBJECT//NONSGML Version 1//EN |
|
4 |
BEGIN:VEVENT |
|
5 |
UID:christmas_holidays-2017 |
|
6 |
DTSTART;VALUE=DATE:20171223 |
|
7 |
DTEND;VALUE=DATE:20180108 |
|
8 |
CATEGORIES:christmas_holidays |
|
9 |
DTSTAMP:20220328T140507Z |
|
10 |
SUMMARY:Vacances de Noël |
|
11 |
END:VEVENT |
|
12 |
BEGIN:VEVENT |
|
13 |
UID:summer_holidays-2018 |
|
14 |
DTSTART;VALUE=DATE:20180707 |
|
15 |
DTEND;VALUE=DATE:20180903 |
|
16 |
CATEGORIES:summer_holidays |
|
17 |
DTSTAMP:20220328T140507Z |
|
18 |
SUMMARY:Vacances d’Été |
|
19 |
END:VEVENT |
|
20 |
BEGIN:VEVENT |
|
21 |
UID:christmas_holidays-2018 |
|
22 |
DTSTART;VALUE=DATE:20181222 |
|
23 |
DTEND;VALUE=DATE:20190107 |
|
24 |
CATEGORIES:christmas_holidays |
|
25 |
DTSTAMP:20220328T140507Z |
|
26 |
SUMMARY:Vacances de Noël |
|
27 |
END:VEVENT |
|
28 |
BEGIN:VEVENT |
|
29 |
UID:summer_holidays-2019 |
|
30 |
DTSTART;VALUE=DATE:20190706 |
|
31 |
DTEND;VALUE=DATE:20190902 |
|
32 |
CATEGORIES:summer_holidays |
|
33 |
DTSTAMP:20220328T140507Z |
|
34 |
SUMMARY:Vacances d’Été |
|
35 |
END:VEVENT |
|
36 |
BEGIN:VEVENT |
|
37 |
UID:christmas_holidays-2019 |
|
38 |
DTSTART;VALUE=DATE:20191221 |
|
39 |
DTEND;VALUE=DATE:20200106 |
|
40 |
CATEGORIES:christmas_holidays |
|
41 |
DTSTAMP:20220328T140507Z |
|
42 |
SUMMARY:Vacances de Noël |
|
43 |
END:VEVENT |
|
44 |
BEGIN:VEVENT |
|
45 |
UID:summer_holidays-2020 |
|
46 |
DTSTART;VALUE=DATE:20200704 |
|
47 |
DTEND;VALUE=DATE:20200901 |
|
48 |
CATEGORIES:summer_holidays |
|
49 |
DTSTAMP:20220328T140507Z |
|
50 |
SUMMARY:Vacances d’Été |
|
51 |
END:VEVENT |
|
52 |
BEGIN:VEVENT |
|
53 |
UID:christmas_holidays-2020 |
|
54 |
DTSTART;VALUE=DATE:20201219 |
|
55 |
DTEND;VALUE=DATE:20210104 |
|
56 |
CATEGORIES:christmas_holidays |
|
57 |
DTSTAMP:20220328T140507Z |
|
58 |
SUMMARY:Vacances de Noël |
|
59 |
END:VEVENT |
|
60 |
BEGIN:VEVENT |
|
61 |
UID:summer_holidays-2021 |
|
62 |
DTSTART;VALUE=DATE:20210706 |
|
63 |
DTEND;VALUE=DATE:20210902 |
|
64 |
CATEGORIES:summer_holidays |
|
65 |
DTSTAMP:20220328T140507Z |
|
66 |
SUMMARY:Vacances d’Été |
|
67 |
END:VEVENT |
|
68 |
BEGIN:VEVENT |
|
69 |
UID:christmas_holidays-2021 |
|
70 |
DTSTART;VALUE=DATE:20211218 |
|
71 |
DTEND;VALUE=DATE:20220103 |
|
72 |
CATEGORIES:christmas_holidays |
|
73 |
DTSTAMP:20220328T140507Z |
|
74 |
SUMMARY:Vacances de Noël |
|
75 |
END:VEVENT |
|
76 |
BEGIN:VEVENT |
|
77 |
UID:summer_holidays-2022 |
|
78 |
DTSTART;VALUE=DATE:20220707 |
|
79 |
DTEND;VALUE=DATE:20220901 |
|
80 |
CATEGORIES:summer_holidays |
|
81 |
DTSTAMP:20220328T140507Z |
|
82 |
SUMMARY:Vacances d’Été |
|
83 |
END:VEVENT |
|
84 |
BEGIN:VEVENT |
|
85 |
UID:christmas_holidays-2022 |
|
86 |
DTSTART;VALUE=DATE:20221217 |
|
87 |
DTEND;VALUE=DATE:20230103 |
|
88 |
CATEGORIES:christmas_holidays |
|
89 |
DTSTAMP:20220328T140507Z |
|
90 |
SUMMARY:Vacances de Noël |
|
91 |
END:VEVENT |
|
92 |
END:VCALENDAR |
tests/test_agendas.py | ||
---|---|---|
31 | 31 |
SharedCustodyRule, |
32 | 32 |
TimePeriod, |
33 | 33 |
TimePeriodException, |
34 |
TimePeriodExceptionGroup, |
|
34 | 35 |
TimePeriodExceptionSource, |
35 | 36 |
UnavailabilityCalendar, |
36 | 37 |
VirtualMember, |
... | ... | |
119 | 120 |
ICS_ATREAL = f.read() |
120 | 121 | |
121 | 122 | |
123 |
with open('tests/data/holidays.ics') as f: |
|
124 |
ICS_HOLIDAYS = f.read() |
|
125 | ||
126 | ||
122 | 127 |
def test_slug(): |
123 | 128 |
agenda = Agenda(label='Foo bar') |
124 | 129 |
agenda.save() |
... | ... | |
854 | 859 |
assert not TimePeriodExceptionSource.objects.exists() |
855 | 860 | |
856 | 861 | |
862 |
def test_timeperiodexception_groups(): |
|
863 |
unavailability_calendar = UnavailabilityCalendar.objects.create(label='Calendar') |
|
864 |
source = unavailability_calendar.timeperiodexceptionsource_set.create( |
|
865 |
ics_filename='holidays.ics', ics_file=ContentFile(ICS_HOLIDAYS, name='holidays.ics') |
|
866 |
) |
|
867 |
source.refresh_timeperiod_exceptions_from_ics() |
|
868 |
assert TimePeriodException.objects.count() == 11 |
|
869 |
group1, group2 = TimePeriodExceptionGroup.objects.all() |
|
870 |
assert group1.label == 'Vacances de Noël' |
|
871 |
assert group1.slug == 'christmas_holidays' |
|
872 |
assert group2.label == 'Vacances d’Été' |
|
873 |
assert group2.slug == 'summer_holidays' |
|
874 | ||
875 |
assert group1.exceptions.count() == 6 |
|
876 |
assert group2.exceptions.count() == 5 |
|
877 | ||
878 |
unavailability_calendar.delete() |
|
879 |
assert not TimePeriodException.objects.exists() |
|
880 |
assert not TimePeriodExceptionGroup.objects.exists() |
|
881 | ||
882 |
# check no groups are created for desks |
|
883 |
agenda = Agenda.objects.create(label='Test 1 agenda') |
|
884 |
desk = Desk.objects.create(label='Test 1 desk', agenda=agenda) |
|
885 |
source = desk.timeperiodexceptionsource_set.create( |
|
886 |
ics_filename='holidays.ics', ics_file=ContentFile(ICS_HOLIDAYS, name='holidays.ics') |
|
887 |
) |
|
888 |
source.refresh_timeperiod_exceptions_from_ics() |
|
889 |
assert TimePeriodException.objects.count() == 11 |
|
890 |
assert not TimePeriodExceptionGroup.objects.exists() |
|
891 | ||
892 | ||
857 | 893 |
def test_base_meeting_duration(): |
858 | 894 |
agenda = Agenda(label='Meeting', kind='meetings') |
859 | 895 |
agenda.save() |
860 |
- |