manager: forbid multiple events on same day in partial bookings agenda (#79112)
gitea/chrono/pipeline/head This commit looks good Details

This commit is contained in:
Valentin Deniaud 2023-06-27 16:44:55 +02:00
parent 06af90608f
commit 4834743c6d
5 changed files with 189 additions and 7 deletions

View File

@ -1114,6 +1114,34 @@ class Agenda(models.Model):
return True
def event_overlaps(self, start_datetime, recurrence_days, recurrence_end_date, instance=None):
qs = self.event_set
if hasattr(instance, 'pk'):
qs = qs.exclude(pk=instance.pk)
if recurrence_days:
qs = qs.filter(
# check overlap with other recurring events
Q(recurrence_end_date__isnull=True) | Q(recurrence_end_date__gte=start_datetime),
Q(recurrence_days__overlap=recurrence_days)
# check overlap with normal events
| Q(start_datetime__gte=start_datetime, start_datetime__iso_week_day__in=recurrence_days),
)
if recurrence_end_date:
qs = qs.filter(start_datetime__lte=recurrence_end_date)
else:
qs = qs.filter(
# check overlap with other normal events
Q(start_datetime__date=start_datetime.date())
# check overlap with recurring events
| Q(
Q(recurrence_end_date__isnull=True) | Q(recurrence_end_date__gte=start_datetime),
recurrence_days__contains=[localtime(start_datetime).isoweekday()],
)
)
return qs.exists()
def get_min_datetime(self, start_datetime=None):
if self.minimal_booking_delay is None:
return start_datetime

View File

@ -221,6 +221,7 @@ class NewEventForm(forms.ModelForm):
super().__init__(*args, **kwargs)
if self.instance.agenda.partial_bookings:
del self.fields['duration']
del self.fields['recurrence_week_interval']
else:
del self.fields['end_time']
@ -234,6 +235,14 @@ class NewEventForm(forms.ModelForm):
if end_time and self.cleaned_data['start_datetime'].time() > end_time:
self.add_error('end_time', _('End time must be greater than start time.'))
if self.instance.agenda.partial_bookings and self.instance.agenda.event_overlaps(
start_datetime=self.cleaned_data['start_datetime'],
recurrence_days=self.cleaned_data['recurrence_days'],
recurrence_end_date=self.cleaned_data['recurrence_end_date'],
instance=self.instance,
):
raise ValidationError(_('There can only be one event per day.'))
def clean_start_datetime(self):
start_datetime = self.cleaned_data['start_datetime']
if start_datetime.year < 2000:

View File

@ -1497,13 +1497,13 @@ class AgendaDayView(AgendaDateView, DayArchiveView):
return context
def fill_partial_bookings_context(self, context):
events = self.agenda.event_set.filter(start_datetime__date=self.date.date())
if not events.exists():
try:
event = self.agenda.event_set.get(start_datetime__date=self.date.date())
except Event.DoesNotExist:
return
event_times = events.aggregate(Min('start_datetime'), Max('end_time'))
min_time = localtime(event_times['start_datetime__min']).time()
max_time = event_times['end_time__max']
min_time = localtime(event.start_datetime).time()
max_time = event.end_time
start_time = datetime.time(max(min_time.hour - 1, 0), 0)
end_time = datetime.time(min(max_time.hour + 1, 23), 0)
@ -1512,9 +1512,8 @@ class AgendaDayView(AgendaDateView, DayArchiveView):
def get_time_ratio(t1, t2):
return 100 * ((t1.hour - t2.hour) * 60 + t1.minute - t2.minute) // 60
bookings = Booking.objects.filter(event__in=events)
bookings_by_user = collections.defaultdict(list)
for booking in bookings:
for booking in event.booking_set.all():
booking.css_left = get_time_ratio(booking.start_time, start_time)
booking.css_width = get_time_ratio(booking.end_time, booking.start_time)
bookings_by_user[booking.user_external_id].append(booking)

View File

@ -37,6 +37,7 @@ def test_manager_partial_bookings_add_event(app, admin_user):
resp = app.get('/manage/agendas/%s/settings' % agenda.pk)
resp = resp.click('New Event')
assert 'duration' not in resp.form.fields
assert 'recurrence_week_interval' not in resp.form.fields
resp.form['start_datetime_0'] = '2023-02-15'
resp.form['start_datetime_1'] = '08:00'
@ -52,6 +53,18 @@ def test_manager_partial_bookings_add_event(app, admin_user):
assert 'duration' not in resp.form.fields
assert resp.form['end_time'].value == '18:00'
resp.form['end_time'] = '17:00'
resp = resp.form.submit().follow()
resp = app.get('/manage/agendas/%s/settings' % agenda.pk)
resp = resp.click('New Event')
resp.form['start_datetime_0'] = '2023-02-15'
resp.form['start_datetime_1'] = '10:00'
resp.form['end_time'] = '12:00'
resp.form['places'] = 10
resp = resp.form.submit()
assert 'There can only be one event per day.' in resp.text
def test_manager_partial_bookings_day_view(app, admin_user, freezer):
agenda = Agenda.objects.create(label='Foo bar', kind='events', partial_bookings=True)

View File

@ -3783,3 +3783,136 @@ virtual Agenda 30
paris('2023-04-04 14:30 30m'),
paris('2023-04-06 15:00 30m'),
]
def test_agenda_event_overlaps():
agenda = Agenda.objects.create(label='Agenda', kind='events')
start_datetime = make_aware(datetime.datetime(2023, 5, 1, 14, 00))
event_kwargs = {
'start_datetime': start_datetime,
'recurrence_days': None,
'recurrence_end_date': None,
}
assert agenda.event_overlaps(**event_kwargs) is False
event = Event.objects.create(start_datetime=start_datetime, agenda=agenda, places=1)
assert agenda.event_overlaps(**event_kwargs) is True
assert agenda.event_overlaps(instance=event, **event_kwargs) is False
event_kwargs['start_datetime'] = start_datetime + datetime.timedelta(days=1)
assert agenda.event_overlaps(**event_kwargs) is False
event_kwargs['start_datetime'] = start_datetime.replace(month=2)
assert agenda.event_overlaps(**event_kwargs) is False
# recurring event
event_kwargs = {
'start_datetime': start_datetime,
'recurrence_days': [1, 2],
'recurrence_end_date': None,
}
assert agenda.event_overlaps(**event_kwargs) is True
# same weekday, starts after
event_kwargs['start_datetime'] = start_datetime + datetime.timedelta(days=7)
assert agenda.event_overlaps(**event_kwargs) is False
# same weekday, starts before
event_kwargs['start_datetime'] = start_datetime - datetime.timedelta(days=7)
assert agenda.event_overlaps(**event_kwargs) is True
# same weekday, starts before
event_kwargs['start_datetime'] = start_datetime - datetime.timedelta(days=6)
assert agenda.event_overlaps(**event_kwargs) is True
# different weekday, starts before
event_kwargs['recurrence_days'] = [3]
assert agenda.event_overlaps(**event_kwargs) is False
# same weekday, starts before, ends before
event_kwargs = {
'start_datetime': start_datetime - datetime.timedelta(days=30),
'recurrence_days': [1, 2],
'recurrence_end_date': start_datetime - datetime.timedelta(days=15),
}
assert agenda.event_overlaps(**event_kwargs) is False
# same weekday, starts before, ends after
event_kwargs['recurrence_end_date'] = start_datetime + datetime.timedelta(days=15)
assert agenda.event_overlaps(**event_kwargs) is True
def test_agenda_event_overlaps_recurring():
agenda = Agenda.objects.create(label='Agenda', kind='events')
event_kwargs = {
'start_datetime': make_aware(datetime.datetime(2023, 5, 1, 14, 00)),
'recurrence_days': [1, 2],
'recurrence_end_date': datetime.date(2023, 6, 1),
}
assert agenda.event_overlaps(**event_kwargs) is False
event = Event.objects.create(agenda=agenda, places=1, **event_kwargs)
assert agenda.event_overlaps(**event_kwargs) is True
assert agenda.event_overlaps(instance=event, **event_kwargs) is False
# starts before, ends during existing event
event_kwargs = {
'start_datetime': make_aware(datetime.datetime(2023, 4, 1, 14, 00)),
'recurrence_days': [1],
'recurrence_end_date': datetime.date(2023, 5, 15),
}
assert agenda.event_overlaps(**event_kwargs) is True
assert agenda.event_overlaps(instance=event, **event_kwargs) is False
# starts during, ends after existing event
event_kwargs = {
'start_datetime': make_aware(datetime.datetime(2023, 5, 15, 14, 00)),
'recurrence_days': [2],
'recurrence_end_date': datetime.date(2023, 6, 15),
}
assert agenda.event_overlaps(**event_kwargs) is True
# not the same day
event_kwargs['recurrence_days'] = [3, 4]
assert agenda.event_overlaps(**event_kwargs) is False
# starts after
event_kwargs = {
'start_datetime': make_aware(datetime.datetime(2023, 6, 15, 14, 00)),
'recurrence_days': [2],
'recurrence_end_date': datetime.date(2023, 7, 15),
}
assert agenda.event_overlaps(**event_kwargs) is False
# remove recurrence_end_date from event
event.recurrence_end_date = None
event.save()
assert agenda.event_overlaps(**event_kwargs) is True
# starts before, ends before
event_kwargs = {
'start_datetime': make_aware(datetime.datetime(2023, 4, 1, 14, 00)),
'recurrence_days': [2],
'recurrence_end_date': datetime.date(2023, 4, 20),
}
assert agenda.event_overlaps(**event_kwargs) is False
# starts before, not end
event_kwargs['recurrence_end_date'] = None
assert agenda.event_overlaps(**event_kwargs) is True
# normal event, before
event_kwargs['recurrence_days'] = None
assert agenda.event_overlaps(**event_kwargs) is False
# normal event, after on a recurrence day
event_kwargs['start_datetime'] = make_aware(datetime.datetime(2023, 5, 9, 14, 00))
assert agenda.event_overlaps(**event_kwargs) is True
# normal event, after not on a recurrence day
event_kwargs['start_datetime'] = make_aware(datetime.datetime(2023, 5, 10, 14, 00))
assert agenda.event_overlaps(**event_kwargs) is False