From 86018e908ebedd9c8f1bea3e93d48008ecb7d34b Mon Sep 17 00:00:00 2001 From: Valentin Deniaud Date: Tue, 19 Jan 2021 15:35:31 +0100 Subject: [PATCH 2/3] manager: create event recurrences when end date is specified (#51218) --- chrono/agendas/models.py | 28 +++++++++++++++++++--------- chrono/manager/forms.py | 14 +++++++++++--- tests/test_agendas.py | 8 ++++---- tests/test_import_export.py | 15 +++++++++++++++ tests/test_manager.py | 36 ++++++++++++++++++++++++++++++++++++ 5 files changed, 85 insertions(+), 16 deletions(-) diff --git a/chrono/agendas/models.py b/chrono/agendas/models.py index 2fd3f3c..727696e 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, excluded_datetimes=None): + max_datetime = datetime.datetime.combine(self.recurrence_end_date, datetime.time(0, 0)) + recurrences = self.get_recurrences(localtime(now()), make_aware(max_datetime), excluded_datetimes) + 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..2772911 100644 --- a/chrono/manager/forms.py +++ b/chrono/manager/forms.py @@ -27,8 +27,7 @@ from django.db import transaction 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, make_naive from django.utils.translation import ugettext_lazy as _ from chrono.agendas.models import ( @@ -224,7 +223,16 @@ 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: + self.instance.recurrences.filter(start_datetime__gt=event.recurrence_end_date).delete() + excluded_datetimes = [ + make_naive(dt) + for dt in self.instance.recurrences.values_list('start_datetime', flat=True) + ] + event.create_all_recurrences(excluded_datetimes) + 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..d029842 100644 --- a/tests/test_manager.py +++ b/tests/test_manager.py @@ -1522,6 +1522,42 @@ 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() + + resp = app.get('/manage/agendas/%s/events/%s/edit' % (agenda.id, event.id)) + resp.form['recurrence_end_date'] = (localtime() + datetime.timedelta(days=6)).strftime('%Y-%m-%d') + resp = resp.form.submit() + assert Event.objects.filter(primary_event=event).count() == 6 + + resp = app.get('/manage/agendas/%s/events/%s/edit' % (agenda.id, event.id)) + resp.form['recurrence_end_date'] = (localtime() + datetime.timedelta(days=4)).strftime('%Y-%m-%d') + resp = resp.form.submit() + assert Event.objects.filter(primary_event=event).count() == 4 + + def test_booked_places(app, admin_user): agenda = Agenda(label=u'Foo bar') agenda.save() -- 2.20.1