manager: handle edition/deletion of recurring event (#41663)

This commit is contained in:
Valentin Deniaud 2021-01-28 12:33:43 +01:00
parent e09281624b
commit b0d89df301
5 changed files with 142 additions and 2 deletions

View File

@ -1103,11 +1103,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.
@ -1323,6 +1326,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

View File

@ -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
@ -177,6 +178,8 @@ class NewEventForm(forms.ModelForm):
class EventForm(forms.ModelForm):
protected_fields = ('repeat', 'slug', 'start_datetime')
class Meta:
model = Event
widgets = {
@ -199,6 +202,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())

View File

@ -29,12 +29,16 @@
</h2>
<span class="actions">
{% if user_can_manage %}
{% if not object.primary_event %}
<a rel="popup" href="{% url 'chrono-manager-event-delete' pk=object.agenda.id event_pk=object.id %}">{% trans 'Delete' %}</a>
{% endif %}
{% if not event.cancellation_status %}
<a rel="popup" href="{% url 'chrono-manager-event-cancel' pk=agenda.pk event_pk=event.pk %}?next={{ request.path }}">{% trans "Cancel" %}</a>
{% endif %}
{% if not object.primary_event %}
<a href="{% url 'chrono-manager-event-edit' pk=agenda.id event_pk=object.id %}">{% trans "Options" %}</a>
{% endif %}
{% endif %}
{% if object.agenda.booking_form_url %}
<a href="{{ object.agenda.get_booking_form_url }}?agenda={{ object.agenda.slug }}&event={{ event.slug }}">{% trans "Booking form" %}</a>
{% endif %}

View File

@ -1713,6 +1713,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

View File

@ -1454,6 +1454,74 @@ 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, except for cancellation
resp = app.get('/manage/agendas/%s/events/%s/' % (agenda.id, event_recurrence.id))
assert 'Cancel' in resp.text
assert 'Options' not in resp.text
assert 'Delete' not in resp.text
def test_booked_places(app, admin_user):
agenda = Agenda(label=u'Foo bar')
agenda.save()
@ -1543,6 +1611,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]