agendas: always create event recurrences (#62635)

This commit is contained in:
Valentin Deniaud 2022-03-16 15:10:34 +01:00
parent 939c1b0cff
commit ba38629af3
8 changed files with 72 additions and 46 deletions

View File

@ -13,6 +13,11 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='event',
name='recurrence_end_date',
field=models.DateField(blank=True, null=True, verbose_name='Recurrence end date'),
field=models.DateField(
blank=True,
null=True,
verbose_name='Recurrence end date',
help_text='If left blank, a one-year maximal booking delay will be applied for this event.',
),
),
]

View File

@ -900,9 +900,7 @@ class Agenda(models.Model):
# add recurrences
excluded_datetimes = [event.datetime_slug for event in recurrences]
Event.create_events_recurrences(
recurring_events.filter(recurrence_end_date__isnull=False), excluded_datetimes
)
Event.create_events_recurrences(recurring_events, excluded_datetimes)
def get_booking_form_url(self):
if not self.booking_form_url:
@ -1548,7 +1546,12 @@ class Event(models.Model):
null=True,
)
recurrence_week_interval = models.IntegerField(_('Repeat'), choices=INTERVAL_CHOICES, default=1)
recurrence_end_date = models.DateField(_('Recurrence end date'), null=True, blank=True)
recurrence_end_date = models.DateField(
_('Recurrence end date'),
null=True,
blank=True,
help_text=_('If left blank, a one-year maximal booking delay will be applied for this event.'),
)
primary_event = models.ForeignKey('self', null=True, on_delete=models.CASCADE, related_name='recurrences')
duration = models.PositiveIntegerField(_('Duration (in minutes)'), default=None, null=True, blank=True)
publication_datetime = models.DateTimeField(_('Publication date/time'), blank=True, null=True)
@ -1618,8 +1621,9 @@ class Event(models.Model):
self.recurrences.update(**update_fields)
yield
if self.recurrence_end_date:
self.recurrences.filter(start_datetime__gt=self.recurrence_end_date).delete()
if self.recurrence_days:
if self.recurrence_end_date:
self.recurrences.filter(start_datetime__gt=self.recurrence_end_date).delete()
excluded_datetimes = [evt.datetime_slug for evt in self.recurrences.all()]
self.create_all_recurrences(excluded_datetimes)
@ -1739,9 +1743,10 @@ class Event(models.Model):
else:
event = cls(**data)
event.save()
if event.recurrence_days and event.recurrence_end_date:
if event.recurrence_days:
event.refresh_from_db()
event.recurrences.filter(start_datetime__gt=event.recurrence_end_date).delete()
if event.recurrence_end_date:
event.recurrences.filter(start_datetime__gt=event.recurrence_end_date).delete()
update_fields = {
field: getattr(event, field)
for field in [
@ -1975,6 +1980,8 @@ class Event(models.Model):
recurrence_rule['until'] = datetime.datetime.combine(
self.recurrence_end_date, datetime.time(0, 0)
)
else:
recurrence_rule['until'] = make_naive(now() + datetime.timedelta(days=365))
return recurrence_rule
def has_recurrences_booked(self, after=None):
@ -1991,7 +1998,10 @@ class Event(models.Model):
def create_events_recurrences(cls, events, excluded_datetimes=None):
recurrences = []
for event in events:
max_datetime = datetime.datetime.combine(event.recurrence_end_date, datetime.time(0, 0))
if event.recurrence_end_date:
max_datetime = datetime.datetime.combine(event.recurrence_end_date, datetime.time(0, 0))
else:
max_datetime = make_naive(now() + datetime.timedelta(days=365))
recurrences.extend(
event.get_recurrences(
localtime(event.start_datetime), make_aware(max_datetime), excluded_datetimes

View File

@ -2470,7 +2470,7 @@ class EventsAPI(APIView):
raise APIErrorBadRequest(N_('invalid payload'), errors=serializer.errors)
payload = serializer.validated_data
event = Event.objects.create(agenda=agenda, **payload)
if event.recurrence_days and event.recurrence_end_date:
if event.recurrence_days:
event.create_all_recurrences()
return Response({'err': 0, 'data': get_event_detail(request, event)})

View File

@ -235,7 +235,7 @@ class NewEventForm(forms.ModelForm):
def save(self, *args, **kwargs):
with transaction.atomic():
event = super().save(*args, **kwargs)
if event.recurrence_end_date:
if event.recurrence_days:
event.create_all_recurrences()
return event

View File

@ -185,11 +185,12 @@ def test_string_or_list_serialiser(app, user, days_in, days_out, err_msg):
'start_datetime': '2022-02-03 16:00',
'places': '1',
'recurrence_days': days_in,
'recurrence_end_date': '2022-02-13',
}
if not err_msg:
resp = app.post_json(api_url, params=params)
assert not resp.json['err']
assert Event.objects.get().recurrence_days == days_out
assert Event.objects.get(primary_event__isnull=True).recurrence_days == days_out
else:
resp = app.post_json(api_url, params=params, status=400)
assert resp.json['err_desc'] == 'invalid payload'
@ -316,13 +317,13 @@ def test_add_event(app, user):
params = {
'start_datetime': '2021-11-15 15:38',
'places': 12,
'recurrence_days': '0,3,5',
'recurrence_days': '3',
'recurrence_week_interval': '2',
'description': 'A recurrent event',
}
assert Event.objects.filter(agenda=agenda).count() == 2
resp = app.post_json(api_url, params=params)
assert Event.objects.filter(agenda=agenda).count() == 3
assert Event.objects.filter(agenda=agenda).count() == 28
assert not resp.json['err']
assert resp.json['data']['id'] == 'foo-bar-event-1'
assert {'api', 'disabled', 'places'}.isdisjoint(resp.json['data'].keys())
@ -331,7 +332,7 @@ def test_add_event(app, user):
)
event = Event.objects.filter(agenda=agenda).get(slug='foo-bar-event-1')
assert event.description == 'A recurrent event'
assert event.recurrence_days == [0, 3, 5]
assert event.recurrence_days == [3]
assert event.recurrence_week_interval == 2
assert event.recurrence_end_date is None
@ -352,7 +353,7 @@ def test_add_event(app, user):
resp.json['data'].keys()
)
event = Event.objects.filter(agenda=agenda).get(slug='foo-bar-event-2')
assert Event.objects.filter(agenda=agenda).count() == 13
assert Event.objects.filter(agenda=agenda).count() == 38
assert event.description == 'A recurrent event having recurrences'
assert event.recurrence_days == [0, 3, 5]
assert event.recurrence_week_interval == 2

View File

@ -93,8 +93,9 @@ def test_add_recurring_event(app, admin_user):
resp.form['frequency'] = 'recurring'
resp.form.submit().follow()
event = Event.objects.get()
event = Event.objects.get(primary_event__isnull=True)
assert event.recurrence_days == [1]
assert Event.objects.filter(primary_event=event).count() == 49
event.delete()
# add recurring event with end date
@ -284,6 +285,17 @@ def test_edit_recurring_event(settings, app, admin_user, freezer):
resp.form['recurrence_days'] = [localtime().weekday()]
resp = resp.form.submit()
# no end date, events are created for the year to come
assert Event.objects.count() == 54
assert Event.objects.last().start_datetime.strftime('%Y-%m-%d') == '2022-01-11'
# specifying end date removes events
resp = app.get('/manage/agendas/%s/events/%s/edit' % (agenda.id, event.id))
resp.form['recurrence_end_date'] = '2021-06-01'
resp = resp.form.submit()
assert Event.objects.count() == 21
assert Event.objects.last().start_datetime.strftime('%Y-%m-%d') == '2021-05-25'
# detail page doesn't exist
resp = app.get('/manage/agendas/%s/events/%s/' % (agenda.id, event.id), status=404)
@ -317,14 +329,14 @@ def test_edit_recurring_event(settings, app, admin_user, freezer):
assert not Event.objects.filter(primary_event=event).exists()
# same goes with changing slug
event.recurrence = 'weekly'
event.recurrence_days = [1]
event.save()
event_recurrence = event.get_or_create_event_recurrence(event.start_datetime)
assert Event.objects.filter(primary_event=event).exists()
assert Event.objects.filter(primary_event=event, slug__startswith='foo-bar-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()
assert not Event.objects.filter(primary_event=event, slug__startswith='foo-bar-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))

View File

@ -2400,21 +2400,17 @@ def test_recurring_events_exceptions_update_recurrences(freezer):
places=5,
recurrence_end_date=datetime.date(year=2021, month=6, day=1),
)
Event.create_events_recurrences([daily_event, weekly_event])
daily_event_no_end_date = Event.objects.create(
weekly_event_no_end_date = Event.objects.create(
agenda=agenda,
start_datetime=now() + datetime.timedelta(hours=2),
recurrence_days=list(range(7)),
recurrence_days=[now().weekday()],
places=5,
)
daily_event_no_end_date.refresh_from_db()
# create one recurrence on 07/05
daily_event_no_end_date.get_or_create_event_recurrence(now() + datetime.timedelta(days=6, hours=2))
Event.create_events_recurrences([daily_event, weekly_event, weekly_event_no_end_date])
assert Event.objects.filter(primary_event=daily_event).count() == 7
assert Event.objects.filter(primary_event=weekly_event).count() == 5
assert Event.objects.filter(primary_event=daily_event_no_end_date).count() == 1
assert Event.objects.filter(primary_event=weekly_event_no_end_date).count() == 53
time_period_exception = TimePeriodException.objects.create(
desk=desk,
@ -2424,25 +2420,30 @@ def test_recurring_events_exceptions_update_recurrences(freezer):
agenda.update_event_recurrences()
assert Event.objects.filter(primary_event=daily_event).count() == 4
assert Event.objects.filter(primary_event=weekly_event).count() == 4
assert Event.objects.filter(primary_event=daily_event_no_end_date).count() == 0
assert Event.objects.filter(primary_event=weekly_event_no_end_date).count() == 52
time_period_exception.delete()
agenda.update_event_recurrences()
assert Event.objects.filter(primary_event=daily_event).count() == 7
assert Event.objects.filter(primary_event=weekly_event).count() == 5
assert Event.objects.filter(primary_event=daily_event_no_end_date).count() == 0
assert Event.objects.filter(primary_event=weekly_event_no_end_date).count() == 53
event = daily_event_no_end_date.get_or_create_event_recurrence(
now() + datetime.timedelta(days=6, hours=2)
event = weekly_event_no_end_date.get_or_create_event_recurrence(
now() + datetime.timedelta(days=7, hours=2)
)
Booking.objects.create(event=event)
time_period_exception.save()
agenda.update_event_recurrences()
assert Booking.objects.count() == 1
assert Event.objects.filter(primary_event=daily_event_no_end_date).count() == 1
assert Event.objects.filter(primary_event=weekly_event_no_end_date).count() == 53
assert agenda.recurrence_exceptions_report.events.get() == event
# no recurrence end date means new events are created as time moves on
freezer.move_to('2021-06-01 12:00')
agenda.update_event_recurrences()
assert Event.objects.filter(primary_event=weekly_event_no_end_date).count() == 57
def test_recurring_events_exceptions_update_recurrences_start_datetime_modified(freezer):
freezer.move_to('2021-09-06 12:00') # Monday

View File

@ -227,14 +227,12 @@ def test_import_export_recurring_event(app, freezer):
event = Event.objects.create(
agenda=agenda,
start_datetime=now(),
recurrence_days=list(range(7)),
recurrence_days=[now().weekday()],
recurrence_week_interval=2,
places=10,
slug='test',
)
event.refresh_from_db()
event.get_or_create_event_recurrence(event.start_datetime + datetime.timedelta(days=3))
assert Event.objects.count() == 2
assert Event.objects.count() == 1
output = get_output_of_command('export_site')
assert len(json.loads(output)['agendas']) == 1
@ -246,13 +244,12 @@ def test_import_export_recurring_event(app, freezer):
call_command('import_site', f.name)
assert Agenda.objects.count() == 1
assert Event.objects.count() == 1
event = Agenda.objects.get(label='Foo Bar').event_set.first()
assert event.primary_event is None
assert event.recurrence_days == list(range(7))
assert Event.objects.count() == 28
event = Agenda.objects.get(label='Foo Bar').event_set.filter(primary_event__isnull=True).get()
assert event.recurrence_days == [now().weekday()]
assert event.recurrence_week_interval == 2
# importing event with end recurrence date creates recurrences
# importing event with end recurrence date removes recurrences
event.recurrence_end_date = now() + datetime.timedelta(days=7)
event.recurrence_week_interval = 1
event.save()
@ -265,7 +262,7 @@ def test_import_export_recurring_event(app, freezer):
call_command('import_site', f.name)
event = Event.objects.get(slug='test')
assert Event.objects.filter(primary_event=event).count() == 7
assert Event.objects.filter(primary_event=event).count() == 1
# import again
with tempfile.NamedTemporaryFile() as f:
@ -274,7 +271,7 @@ def test_import_export_recurring_event(app, freezer):
call_command('import_site', f.name)
event = Event.objects.get(slug='test')
assert Event.objects.filter(primary_event=event).count() == 7
assert Event.objects.filter(primary_event=event).count() == 1
# import again but change places
payload = json.loads(output)
@ -286,7 +283,7 @@ def test_import_export_recurring_event(app, freezer):
event = Event.objects.get(slug='test')
assert event.places == 42
assert Event.objects.filter(primary_event=event, places=42).count() == 7
assert Event.objects.filter(primary_event=event, places=42).count() == 1
def test_import_export_meetings_agenda_options(app):