From 5078767b1fa059cfc1e6c8778c2e2cf2f7ef1b3f Mon Sep 17 00:00:00 2001 From: Valentin Deniaud Date: Thu, 28 Jan 2021 12:33:43 +0100 Subject: [PATCH 5/5] manager: handle edition/deletion of recurring event (#41663) --- chrono/agendas/models.py | 12 ++- chrono/manager/forms.py | 25 +++++ .../chrono/manager_event_detail.html | 2 + chrono/manager/views.py | 1 + tests/test_manager.py | 100 ++++++++++++++++++ 5 files changed, 138 insertions(+), 2 deletions(-) diff --git a/chrono/agendas/models.py b/chrono/agendas/models.py index ecdeccd..eea1dd1 100644 --- a/chrono/agendas/models.py +++ b/chrono/agendas/models.py @@ -995,11 +995,14 @@ class Event(models.Model): today = localtime(now()).date() event_day = localtime(self.start_datetime).date() days_to_event = event_day - today - if days_to_event < datetime.timedelta(days=self.agenda.minimal_booking_delay): - return False if self.agenda.maximal_booking_delay: if days_to_event >= datetime.timedelta(days=self.agenda.maximal_booking_delay): return False + if self.recurrence_rule is not None: + # bookable recurrences probably exist + return True + if days_to_event < datetime.timedelta(days=self.agenda.minimal_booking_delay): + return False if self.start_datetime < now(): # past the event date, we may want in the future to allow for some # extra late booking but it's forbidden for now. @@ -1211,6 +1214,11 @@ class Event(models.Model): return None return rrule + def has_recurrences_booked(self): + return Booking.objects.filter( + event__primary_event=self, event__start_datetime__gt=now(), cancellation_datetime__isnull=True + ).exists() + class BookingColor(models.Model): COLOR_COUNT = 8 diff --git a/chrono/manager/forms.py b/chrono/manager/forms.py index 2d8baed..f61b824 100644 --- a/chrono/manager/forms.py +++ b/chrono/manager/forms.py @@ -23,6 +23,7 @@ from django import forms from django.conf import settings from django.contrib.auth.models import Group from django.core.exceptions import FieldDoesNotExist +from django.db import transaction from django.forms import ValidationError from django.utils.encoding import force_text from django.utils.six import StringIO @@ -176,6 +177,8 @@ class NewEventForm(forms.ModelForm): class EventForm(forms.ModelForm): + protected_fields = ('repeat', 'slug', 'start_datetime') + class Meta: model = Event widgets = { @@ -198,6 +201,28 @@ class EventForm(forms.ModelForm): 'start_datetime': SplitDateTimeField, } + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + if self.instance.recurrence_rule and self.instance.has_recurrences_booked(): + for field in self.protected_fields: + self.fields[field].disabled = True + self.fields[field].help_text = _( + 'This field cannot be modified because some recurrences have bookings attached to them.' + ) + + def save(self, *args, **kwargs): + with transaction.atomic(): + if any(field for field in self.changed_data if field in self.protected_fields): + self.instance.recurrences.all().delete() + elif self.instance.recurrence_rule: + update_fields = { + field: value + for field, value in self.cleaned_data.items() + if field not in self.protected_fields + } + self.instance.recurrences.update(**update_fields) + return super().save(*args, **kwargs) + class AgendaResourceForm(forms.Form): resource = forms.ModelChoiceField(label=_('Resource'), queryset=Resource.objects.none()) diff --git a/chrono/manager/templates/chrono/manager_event_detail.html b/chrono/manager/templates/chrono/manager_event_detail.html index 0f7a2b3..3ee4301 100644 --- a/chrono/manager/templates/chrono/manager_event_detail.html +++ b/chrono/manager/templates/chrono/manager_event_detail.html @@ -33,8 +33,10 @@ {% if not event.cancellation_status %} {% trans "Cancel" %} {% endif %} +{% if not object.primary_event %} {% trans "Options" %} {% endif %} +{% endif %} {% if object.agenda.booking_form_url %} {% trans "Booking form" %} {% endif %} diff --git a/chrono/manager/views.py b/chrono/manager/views.py index c616c79..24d3349 100644 --- a/chrono/manager/views.py +++ b/chrono/manager/views.py @@ -1690,6 +1690,7 @@ class EventDeleteView(ManagedAgendaMixin, DeleteView): context['cannot_delete'] = bool( self.object.booking_set.filter(cancellation_datetime__isnull=True).exists() and self.object.start_datetime > now() + or self.object.has_recurrences_booked() ) return context diff --git a/tests/test_manager.py b/tests/test_manager.py index 34876b4..e23e4e4 100644 --- a/tests/test_manager.py +++ b/tests/test_manager.py @@ -1356,6 +1356,72 @@ def test_edit_event_as_manager(app, manager_user): assert event.publication_date is None +def test_edit_recurring_event(settings, app, admin_user, freezer): + freezer.move_to('2021-01-12 12:10') + agenda = Agenda.objects.create( + label='Foo bar', kind='events', minimal_booking_delay=15, maximal_booking_delay=30 + ) + event = Event.objects.create(start_datetime=now(), places=10, agenda=agenda) + + app = login(app) + resp = app.get('/manage/agendas/%s/events/%s/edit' % (agenda.id, event.id)) + resp.form['repeat'] = 'weekly' + resp = resp.form.submit() + + resp = app.get('/manage/agendas/%s/settings' % agenda.id, status=200) + assert 'Weekly on Tuesday at 1:10 p.m.' in resp.text + # event is bookable regardless of minimal_booking_delay, since it has bookable recurrences + assert len(resp.pyquery.find('.bookable')) == 1 + + # maximal_booking_delay is accounted for, because no recurrences are bookable + freezer.move_to('2020-11-12') + resp = app.get('/manage/agendas/%s/settings' % agenda.id, status=200) + assert len(resp.pyquery.find('.not-bookable')) == 1 + + # editing recurring event updates event recurrences + event.refresh_from_db() + event_recurrence = event.get_or_create_event_recurrence(event.start_datetime) + resp = app.get('/manage/agendas/%s/events/%s/edit' % (agenda.id, event.id)) + resp.form['places'] = 20 + resp = resp.form.submit().follow() + event_recurrence.refresh_from_db() + assert event_recurrence.places == 20 + + # changing recurrence attribute removes event recurrences + resp = app.get('/manage/agendas/%s/events/%s/edit' % (agenda.id, event.id)) + resp.form['repeat'] = '' + resp = resp.form.submit().follow() + assert not Event.objects.filter(primary_event=event).exists() + + # same goes with changing slug + event.recurrence = 'weekly' + event.save() + event_recurrence = event.get_or_create_event_recurrence(event.start_datetime) + assert Event.objects.filter(primary_event=event).exists() + resp = app.get('/manage/agendas/%s/events/%s/edit' % (agenda.id, event.id)) + resp.form['slug'] = 'hop' + resp = resp.form.submit().follow() + assert not Event.objects.filter(primary_event=event).exists() + + # changing recurring attribute or slug is forbidden if there are bookings for future recurrences + event_recurrence = event.get_or_create_event_recurrence(event.start_datetime + datetime.timedelta(days=7)) + Booking.objects.create(event=event_recurrence) + resp = app.get('/manage/agendas/%s/events/%s/edit' % (agenda.id, event.id)) + assert 'disabled' in resp.form['repeat'].attrs + 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 + + # changing it anyway doesn't work + resp.form['slug'] = 'changed' + resp = resp.form.submit() + assert not Event.objects.filter(slug='changed').exists() + + # individually editing event recurrence is not supported + resp = app.get('/manage/agendas/%s/events/%s/' % (agenda.id, event_recurrence.id)) + assert 'Options' not in resp.text + + def test_booked_places(app, admin_user): agenda = Agenda(label=u'Foo bar') agenda.save() @@ -1445,6 +1511,40 @@ def test_delete_busy_event(app, admin_user): resp = resp.form.submit(status=403) +def test_delete_recurring_event(app, admin_user, freezer): + agenda = Agenda.objects.create(label='Foo bar', kind='events') + start_datetime = now() + datetime.timedelta(days=10) + event = Event.objects.create(start_datetime=start_datetime, places=10, agenda=agenda, repeat='weekly') + + app = login(app) + resp = app.get('/manage/agendas/%s/settings' % agenda.id, status=200) + resp = resp.click(href='/manage/agendas/%s/events/%s/edit' % (agenda.id, event.id)) + resp = resp.click('Delete') + assert 'Are you sure you want to delete this event?' in resp.text + + event_recurrence = event.get_or_create_event_recurrence(event.start_datetime) + booking = Booking.objects.create(event=event_recurrence) + resp = app.get('/manage/agendas/%s/settings' % agenda.id, status=200) + resp = resp.click(href='/manage/agendas/%s/events/%s/edit' % (agenda.id, event.id)) + resp = resp.click('Delete') + assert 'This cannot be removed' in resp.text + + booking.cancellation_datetime = now() + booking.save() + resp = app.get('/manage/agendas/%s/settings' % agenda.id, status=200) + resp = resp.click(href='/manage/agendas/%s/events/%s/edit' % (agenda.id, event.id)) + resp = resp.click('Delete') + assert 'Are you sure you want to delete this event?' in resp.text + + booking.cancellation_datetime = None + booking.save() + freezer.move_to(now() + datetime.timedelta(days=11)) + resp = app.get('/manage/agendas/%s/settings' % agenda.id, status=200) + resp = resp.click(href='/manage/agendas/%s/events/%s/edit' % (agenda.id, event.id)) + resp = resp.click('Delete') + assert 'Are you sure you want to delete this event?' in resp.text + + def test_delete_event_as_manager(app, manager_user): agenda = Agenda(label=u'Foo bar') agenda.edit_role = manager_user.groups.all()[0] -- 2.20.1