From 055091b7cb999e406127e34d2e7e89fc81f2fca1 Mon Sep 17 00:00:00 2001 From: Valentin Deniaud Date: Tue, 19 Jan 2021 15:35:31 +0100 Subject: [PATCH 2/2] manager: create event recurrences when end date is specified (#51218) --- chrono/agendas/models.py | 28 +++++++++++++++++++--------- chrono/manager/forms.py | 10 +++++++--- tests/test_agendas.py | 8 ++++---- tests/test_import_export.py | 15 +++++++++++++++ tests/test_manager.py | 27 +++++++++++++++++++++++++++ 5 files changed, 72 insertions(+), 16 deletions(-) diff --git a/chrono/agendas/models.py b/chrono/agendas/models.py index 2fd3f3c..2abb49d 100644 --- a/chrono/agendas/models.py +++ b/chrono/agendas/models.py @@ -590,7 +590,7 @@ class Agenda(models.Model): else: recurring_events = self.event_set.filter(recurrence_rule__isnull=False) for event in recurring_events: - events.extend(event.get_recurrences(min_start, max_start, excluded_datetimes)) + events.extend(event.get_recurrences(min_start, max_start, excluded_datetimes, slug_separator=':')) events.sort(key=lambda x: [getattr(x, field) for field in Event._meta.ordering]) return events @@ -1196,10 +1196,13 @@ class Event(models.Model): ) data = clean_import_data(cls, data) if data.get('slug'): - cls.objects.update_or_create(slug=data['slug'], defaults=data) - return - event = cls(**data) - event.save() + event, _ = cls.objects.update_or_create(slug=data['slug'], defaults=data) + else: + event = cls(**data) + event.save() + if event.recurrence_rule and event.recurrence_end_date: + event.refresh_from_db() + event.create_all_recurrences() def export_json(self): recurrence_end_date = ( @@ -1253,8 +1256,6 @@ class Event(models.Model): raise ValueError('Multiple events found for specified datetime.') event = events[0] - event.slug = event.slug.replace(':', '--') - with transaction.atomic(): try: return Event.objects.get(agenda=self.agenda, slug=event.slug) @@ -1262,7 +1263,7 @@ class Event(models.Model): event.save() return event - def get_recurrences(self, min_datetime, max_datetime, excluded_datetimes=None): + def get_recurrences(self, min_datetime, max_datetime, excluded_datetimes=None, slug_separator='--'): recurrences = [] rrule_set = rruleset() # do not generate recurrences for existing events @@ -1298,7 +1299,11 @@ class Event(models.Model): event = copy.copy(event_base) # add timezone back aware_start_datetime = make_aware(start_datetime) - event.slug = '%s:%s' % (event.slug, aware_start_datetime.strftime('%Y-%m-%d-%H%M')) + event.slug = '%s%s%s' % ( + event.slug, + slug_separator, + aware_start_datetime.strftime('%Y-%m-%d-%H%M'), + ) event.start_datetime = aware_start_datetime.astimezone(utc) recurrences.append(event) @@ -1340,6 +1345,11 @@ class Event(models.Model): event__primary_event=self, event__start_datetime__gt=now(), cancellation_datetime__isnull=True ).exists() + def create_all_recurrences(self): + max_datetime = datetime.datetime.combine(self.recurrence_end_date, datetime.time(0, 0)) + recurrences = self.get_recurrences(localtime(now()), make_aware(max_datetime)) + Event.objects.bulk_create(recurrences) + class BookingColor(models.Model): COLOR_COUNT = 8 diff --git a/chrono/manager/forms.py b/chrono/manager/forms.py index 042ac44..3980b73 100644 --- a/chrono/manager/forms.py +++ b/chrono/manager/forms.py @@ -28,7 +28,7 @@ from django.forms import ValidationError from django.utils.encoding import force_text from django.utils.six import StringIO from django.utils.timezone import make_aware -from django.utils.timezone import now +from django.utils.timezone import now, localtime, make_aware from django.utils.translation import ugettext_lazy as _ from chrono.agendas.models import ( @@ -178,7 +178,7 @@ class NewEventForm(forms.ModelForm): class EventForm(forms.ModelForm): - protected_fields = ('repeat', 'slug', 'start_datetime') + protected_fields = ('repeat', 'slug', 'start_datetime', 'recurrence_end_date') class Meta: model = Event @@ -224,7 +224,11 @@ class EventForm(forms.ModelForm): if field not in self.protected_fields } self.instance.recurrences.update(**update_fields) - return super().save(*args, **kwargs) + + event = super().save(*args, **kwargs) + if event.recurrence_end_date: + event.create_all_recurrences() + return event class AgendaResourceForm(forms.Form): diff --git a/tests/test_agendas.py b/tests/test_agendas.py index 0fd27c0..b850fd3 100644 --- a/tests/test_agendas.py +++ b/tests/test_agendas.py @@ -1867,7 +1867,7 @@ def test_recurring_events(freezer): assert len(recurrences) == 3 first_event = recurrences[0] - assert first_event.slug == event.slug + ':2021-01-06-1300' + assert first_event.slug == event.slug + '--2021-01-06-1300' event_json = event.export_json() first_event_json = first_event.export_json() @@ -1877,7 +1877,7 @@ def test_recurring_events(freezer): second_event = recurrences[1] assert second_event.start_datetime == first_event.start_datetime + datetime.timedelta(days=7) assert second_event.start_datetime.weekday() == first_event.start_datetime.weekday() - assert second_event.slug == 'event:2021-01-13-1300' + assert second_event.slug == 'event--2021-01-13-1300' different_fields = ['slug', 'start_datetime'] second_event_json = second_event.export_json() @@ -1901,8 +1901,8 @@ def test_recurring_events_dst(freezer, settings): recurrences = event.get_recurrences(dt, dt + datetime.timedelta(days=8)) event_before_dst, event_after_dst = recurrences assert event_before_dst.start_datetime.hour + 1 == event_after_dst.start_datetime.hour - assert event_before_dst.slug == 'agenda-event:2020-10-24-1400' - assert event_after_dst.slug == 'agenda-event:2020-10-31-1400' + assert event_before_dst.slug == 'agenda-event--2020-10-24-1400' + assert event_after_dst.slug == 'agenda-event--2020-10-31-1400' freezer.move_to('2020-11-24 12:00') new_recurrences = event.get_recurrences(dt, dt + datetime.timedelta(days=8)) diff --git a/tests/test_import_export.py b/tests/test_import_export.py index 2840929..6a3223e 100644 --- a/tests/test_import_export.py +++ b/tests/test_import_export.py @@ -200,6 +200,7 @@ def test_import_export_recurring_event(app, freezer): start_datetime=now(), repeat='daily', places=10, + slug='test', ) event.refresh_from_db() event.get_or_create_event_recurrence(event.start_datetime + datetime.timedelta(days=3)) @@ -221,6 +222,20 @@ def test_import_export_recurring_event(app, freezer): assert event.repeat == 'daily' assert event.recurrence_rule == {'freq': DAILY} + # importing event with end recurrence date creates recurrences + event.recurrence_end_date = now() + datetime.timedelta(days=7) + event.save() + output = get_output_of_command('export_site') + import_site(data={}, clean=True) + + with tempfile.NamedTemporaryFile() as f: + f.write(force_bytes(output)) + f.flush() + call_command('import_site', f.name) + + event = Event.objects.get(slug='test') + assert Event.objects.filter(primary_event=event).count() == 7 + def test_import_export_permissions(app): meetings_agenda = Agenda.objects.create(label='Foo Bar', kind='meetings') diff --git a/tests/test_manager.py b/tests/test_manager.py index 366e35c..40ec429 100644 --- a/tests/test_manager.py +++ b/tests/test_manager.py @@ -1509,6 +1509,7 @@ def test_edit_recurring_event(settings, app, admin_user, freezer): assert 'disabled' in resp.form['slug'].attrs assert 'disabled' in resp.form['start_datetime_0'].attrs assert 'disabled' in resp.form['start_datetime_1'].attrs + assert 'disabled' in resp.form['recurrence_end_date'].attrs # changing it anyway doesn't work resp.form['slug'] = 'changed' @@ -1522,6 +1523,32 @@ def test_edit_recurring_event(settings, app, admin_user, freezer): assert 'Delete' not in resp.text +def test_edit_recurring_event_with_end_date(settings, app, admin_user, freezer): + freezer.move_to('2021-01-12 12:10') + agenda = Agenda.objects.create(label='Foo bar', kind='events') + event = Event.objects.create(start_datetime=now(), places=10, repeat='daily', agenda=agenda) + + app = login(app) + resp = app.get('/manage/agendas/%s/events/%s/edit' % (agenda.id, event.id)) + resp.form['recurrence_end_date'] = (localtime() + datetime.timedelta(days=5)).strftime('%Y-%m-%d') + resp = resp.form.submit() + + # recurrences are created automatically + event = Event.objects.get(recurrence_rule__isnull=False) + assert Event.objects.filter(primary_event=event).count() == 5 + assert Event.objects.filter(primary_event=event, start_datetime=now()).exists() + + resp = app.get('/manage/agendas/%s/events/%s/edit' % (agenda.id, event.id)) + resp.form['start_datetime_1'] = (localtime() + datetime.timedelta(hours=1)).strftime('%H:%M') + resp = resp.form.submit() + assert Event.objects.filter(primary_event=event).count() == 5 + assert Event.objects.filter( + primary_event=event, start_datetime=now() + datetime.timedelta(hours=1) + ).exists() + # old recurrences were deleted + assert not Event.objects.filter(primary_event=event, start_datetime=now()).exists() + + def test_booked_places(app, admin_user): agenda = Agenda(label=u'Foo bar') agenda.save() -- 2.20.1