manager: add more granular control over event recurrence (#50560)

This commit is contained in:
Valentin Deniaud 2021-04-21 16:21:31 +02:00
parent 2f9bf16a57
commit 6372afc4a5
19 changed files with 415 additions and 134 deletions

View File

@ -24,6 +24,6 @@ class Command(BaseCommand):
help = 'Update event recurrences to reflect exceptions'
def handle(self, **options):
agendas = Agenda.objects.filter(kind='events', event__recurrence_rule__isnull=False).distinct()
agendas = Agenda.objects.filter(kind='events', event__recurrence_days__isnull=False).distinct()
for agenda in agendas:
agenda.update_event_recurrences()

View File

@ -0,0 +1,80 @@
# Generated by Django 2.2.19 on 2021-04-21 13:56
import django.contrib.postgres.fields
from dateutil.rrule import DAILY, WEEKLY
from django.db import migrations, models
def migrate_recurrence_fields(apps, schema_editor):
Event = apps.get_model('agendas', 'Event')
for event in Event.objects.filter(recurrence_rule__isnull=False):
if event.recurrence_rule['freq'] == DAILY:
event.recurrence_days = list(range(7))
elif event.recurrence_rule['freq'] == WEEKLY:
event.recurrence_days = event.recurrence_rule['byweekday']
event.recurrence_week_interval = event.recurrence_rule.get('interval', 1)
event.save()
def reverse_migrate_recurrence_fields(apps, schema_editor):
Event = apps.get_model('agendas', 'Event')
for event in Event.objects.filter(recurrence_days__isnull=False):
rrule = {}
if event.recurrence_days == list(range(7)):
event.repeat = 'daily'
rrule['freq'] = DAILY
else:
rrule['freq'] = WEEKLY
rrule['byweekday'] = event.recurrence_days
if event.recurrence_days == list(range(5)):
event.repeat = 'weekdays'
elif event.recurrence_week_interval == 2:
event.repeat = '2-weeks'
rrule['interval'] = 2
else:
event.repeat = 'weekly'
event.recurrence_rule = rrule
event.save()
class Migration(migrations.Migration):
dependencies = [
('agendas', '0091_lease'),
]
operations = [
migrations.AddField(
model_name='event',
name='recurrence_days',
field=django.contrib.postgres.fields.ArrayField(
base_field=models.IntegerField(
choices=[(0, 'Mo'), (1, 'Tu'), (2, 'We'), (3, 'Th'), (4, 'Fr'), (5, 'Sa'), (6, 'Su')]
),
blank=True,
null=True,
size=None,
verbose_name='Recurrence days',
),
),
migrations.AddField(
model_name='event',
name='recurrence_week_interval',
field=models.IntegerField(
choices=[(1, 'Every week'), (2, 'Every two weeks'), (3, 'Every three weeks')],
default=1,
verbose_name='Repeat',
),
),
migrations.RunPython(migrate_recurrence_fields, reverse_migrate_recurrence_fields),
migrations.RemoveField(
model_name='event',
name='recurrence_rule',
),
migrations.RemoveField(
model_name='event',
name='repeat',
),
]

View File

@ -616,7 +616,7 @@ class Agenda(models.Model):
entries = self.prefetched_events
else:
# recurring events are never opened
entries = self.event_set.filter(recurrence_rule__isnull=True)
entries = self.event_set.filter(recurrence_days__isnull=True)
# exclude canceled events except for event recurrences
entries = entries.filter(Q(cancelled=False) | Q(primary_event__isnull=False))
# we never want to allow booking for past events.
@ -687,7 +687,7 @@ class Agenda(models.Model):
else:
recurring_events = self.event_set.filter(
Q(publication_date__isnull=True) | Q(publication_date__lte=localtime(now()).date()),
recurrence_rule__isnull=False,
recurrence_days__isnull=False,
)
exceptions = self.get_recurrence_exceptions(min_start, max_start)
@ -705,7 +705,7 @@ class Agenda(models.Model):
@transaction.atomic
def update_event_recurrences(self):
recurring_events = self.event_set.filter(recurrence_rule__isnull=False)
recurring_events = self.event_set.filter(recurrence_days__isnull=False)
recurrences = self.event_set.filter(primary_event__isnull=False)
# remove recurrences
@ -1178,17 +1178,31 @@ class MeetingType(models.Model):
class Event(models.Model):
REPEAT_CHOICES = [
('daily', _('Daily')),
('weekly', _('Weekly')),
('2-weeks', _('Once every two weeks')),
('weekdays', _('Every weekdays (Monday to Friday)')),
WEEKDAY_CHOICES = [
(0, _('Mo')),
(1, _('Tu')),
(2, _('We')),
(3, _('Th')),
(4, _('Fr')),
(5, _('Sa')),
(6, _('Su')),
]
INTERVAL_CHOICES = [
(1, 'Every week'),
(2, 'Every two weeks'),
(3, 'Every three weeks'),
]
agenda = models.ForeignKey(Agenda, on_delete=models.CASCADE)
start_datetime = models.DateTimeField(_('Date/time'))
repeat = models.CharField(_('Repeat'), max_length=16, blank=True, choices=REPEAT_CHOICES)
recurrence_rule = JSONField(_('Recurrence rule'), null=True, blank=True)
recurrence_days = ArrayField(
models.IntegerField(choices=WEEKDAY_CHOICES),
verbose_name=_('Recurrence days'),
blank=True,
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)
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)
@ -1243,7 +1257,6 @@ class Event(models.Model):
self.check_full()
if not self.slug:
self.slug = generate_slug(self, seen_slugs=seen_slugs, agenda=self.agenda)
self.recurrence_rule = self.get_recurrence_rule()
return super(Event, self).save(*args, **kwargs)
@property
@ -1267,7 +1280,7 @@ class Event(models.Model):
return False
if self.agenda.maximal_booking_delay and self.start_datetime > self.agenda.max_booking_datetime:
return False
if self.recurrence_rule is not None:
if self.recurrence_days is not None:
# bookable recurrences probably exist
return True
if self.agenda.minimal_booking_delay and self.start_datetime < self.agenda.min_booking_datetime:
@ -1402,7 +1415,7 @@ class Event(models.Model):
else:
event = cls(**data)
event.save()
if event.recurrence_rule and event.recurrence_end_date:
if event.recurrence_days and event.recurrence_end_date:
event.refresh_from_db()
event.recurrences.filter(start_datetime__gt=event.recurrence_end_date).delete()
update_fields = {
@ -1431,8 +1444,8 @@ class Event(models.Model):
return {
'start_datetime': make_naive(self.start_datetime).strftime('%Y-%m-%d %H:%M:%S'),
'publication_date': self.publication_date.strftime('%Y-%m-%d') if self.publication_date else None,
'repeat': self.repeat,
'recurrence_rule': self.recurrence_rule,
'recurrence_days': self.recurrence_days,
'recurrence_week_interval': self.recurrence_week_interval,
'recurrence_end_date': recurrence_end_date,
'places': self.places,
'waiting_list_places': self.waiting_list_places,
@ -1523,11 +1536,6 @@ class Event(models.Model):
url=self.url,
)
if self.recurrence_end_date:
self.recurrence_rule['until'] = datetime.datetime.combine(
self.recurrence_end_date, datetime.time(0, 0)
)
# remove pytz info because dateutil doesn't support DST changes
min_datetime = make_naive(min_datetime)
max_datetime = make_naive(max_datetime)
@ -1548,35 +1556,54 @@ class Event(models.Model):
return recurrences
def get_recurrence_display(self):
repeat = str(self.get_repeat_display())
time = date_format(localtime(self.start_datetime), 'TIME_FORMAT')
if self.repeat in ('weekly', '2-weeks'):
day = date_format(localtime(self.start_datetime), 'l')
return _('%(every_x_days)s on %(day)s at %(time)s') % {
'every_x_days': repeat,
'day': day,
'time': time,
days_count = len(self.recurrence_days)
if days_count == 7:
repeat = _('Daily')
elif days_count > 1 and (self.recurrence_days[-1] - self.recurrence_days[0]) == days_count - 1:
# days are contiguous
repeat = _('From %(weekday)s to %(last_weekday)s') % {
'weekday': str(WEEKDAYS[self.recurrence_days[0]]),
'last_weekday': str(WEEKDAYS[self.recurrence_days[-1]]),
}
else:
return _('%(every_x_days)s at %(time)s') % {'every_x_days': repeat, 'time': time}
repeat = _('On %(weekdays)s') % {
'weekdays': ', '.join([str(WEEKDAYS[i]) for i in self.recurrence_days])
}
def get_recurrence_rule(self):
rrule = {}
if self.repeat == 'daily':
rrule['freq'] = DAILY
elif self.repeat == 'weekly':
rrule['freq'] = WEEKLY
rrule['byweekday'] = [localtime(self.start_datetime).weekday()]
elif self.repeat == '2-weeks':
rrule['freq'] = WEEKLY
rrule['byweekday'] = [localtime(self.start_datetime).weekday()]
rrule['interval'] = 2
elif self.repeat == 'weekdays':
rrule['freq'] = WEEKLY
rrule['byweekday'] = [i for i in range(5)]
else:
return None
return rrule
recurrence_display = _('%(On_day_x)s at %(time)s') % {'On_day_x': repeat, 'time': time}
if self.recurrence_week_interval > 1:
if self.recurrence_week_interval == 2:
every_x_weeks = _('every two weeks')
elif self.recurrence_week_interval == 3:
every_x_weeks = _('every three weeks')
recurrence_display = _('%(Every_x_days)s, once %(every_x_weeks)s') % {
'Every_x_days': recurrence_display,
'every_x_weeks': every_x_weeks,
}
if self.recurrence_end_date:
end_date = date_format(self.recurrence_end_date, 'DATE_FORMAT')
recurrence_display = _('%(Every_x_days)s, until %(date)s') % {
'Every_x_days': recurrence_display,
'date': end_date,
}
return recurrence_display
@property
def recurrence_rule(self):
recurrence_rule = {
'freq': WEEKLY,
'byweekday': self.recurrence_days,
'interval': self.recurrence_week_interval,
}
if self.recurrence_end_date:
recurrence_rule['until'] = datetime.datetime.combine(
self.recurrence_end_date, datetime.time(0, 0)
)
return recurrence_rule
def has_recurrences_booked(self, after=None):
return Booking.objects.filter(

View File

@ -617,7 +617,7 @@ class Agendas(APIView):
).order_by()
recurring_event_queryset = Event.objects.filter(
Q(publication_date__isnull=True) | Q(publication_date__lte=localtime(now()).date()),
recurrence_rule__isnull=False,
recurrence_days__isnull=False,
)
exceptions_desk = Desk.objects.filter(slug='_exceptions_holder').prefetch_related(
'unavailability_calendars'

View File

@ -53,7 +53,7 @@ from chrono.agendas.models import (
)
from . import widgets
from .widgets import SplitDateTimeField
from .widgets import SplitDateTimeField, WeekdaysWidget
class AbsenceReasonForm(forms.ModelForm):
@ -160,22 +160,53 @@ class UnavailabilityCalendarEditForm(UnavailabilityCalendarAddForm):
class NewEventForm(forms.ModelForm):
frequency = forms.ChoiceField(
label=_('Event frequency'),
widget=forms.RadioSelect,
choices=(
('unique', _('Unique')),
('recurring', _('Recurring')),
),
initial='unique',
)
recurrence_days = forms.TypedMultipleChoiceField(
choices=Event.WEEKDAY_CHOICES, coerce=int, required=False, widget=WeekdaysWidget
)
class Meta:
model = Event
fields = [
'label',
'start_datetime',
'repeat',
'frequency',
'recurrence_days',
'recurrence_week_interval',
'recurrence_end_date',
'duration',
'places',
]
field_classes = {
'start_datetime': SplitDateTimeField,
}
widgets = {
'recurrence_end_date': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
}
def clean_recurrence_days(self):
recurrence_days = self.cleaned_data['recurrence_days']
if recurrence_days == []:
return None
return recurrence_days
class EventForm(forms.ModelForm):
protected_fields = ('repeat', 'slug', 'start_datetime')
class EventForm(NewEventForm):
protected_fields = (
'slug',
'start_datetime',
'frequency',
'recurrence_days',
'recurrence_week_interval',
)
class Meta:
model = Event
@ -187,7 +218,9 @@ class EventForm(forms.ModelForm):
'label',
'slug',
'start_datetime',
'repeat',
'frequency',
'recurrence_days',
'recurrence_week_interval',
'recurrence_end_date',
'duration',
'publication_date',
@ -203,14 +236,22 @@ class EventForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.instance.recurrence_rule and self.instance.has_recurrences_booked():
self.fields['frequency'].initial = 'recurring' if self.instance.recurrence_days else 'unique'
if self.instance.recurrence_days 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.'
)
if self.instance.primary_event:
for field in ('slug', 'repeat', 'recurrence_end_date', 'publication_date'):
for field in (
'slug',
'recurrence_end_date',
'publication_date',
'frequency',
'recurrence_days',
'recurrence_week_interval',
):
del self.fields[field]
def clean(self):
@ -219,18 +260,21 @@ class EventForm(forms.ModelForm):
after=self.cleaned_data['recurrence_end_date']
):
raise ValidationError(_('Bookings exist after this date.'))
if self.cleaned_data.get('recurrence_end_date') and not self.cleaned_data.get('repeat'):
raise ValidationError(_('Recurrence end date makes no sense without repetition.'))
if self.cleaned_data.get('frequency') == 'unique':
self.cleaned_data['recurrence_days'] = None
self.cleaned_data['recurrence_end_date'] = None
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:
elif self.instance.recurrence_days:
protected_fields = list(self.protected_fields) + ['recurrence_end_date', 'frequency']
update_fields = {
field: value
for field, value in self.cleaned_data.items()
if field != 'recurrence_end_date' and field not in self.protected_fields
if field not in protected_fields
}
self.instance.recurrences.update(**update_fields)

View File

@ -442,3 +442,7 @@ $booking-colors: (
background-color: $color;
}
}
form div.widget[id^=id_recurrence] {
padding-left: 1em;
}

View File

@ -20,7 +20,7 @@
{% else %}
{% if event.label %}{{ event.label }} / {% endif %}
{% endif %}
{% if not event.repeat %}
{% if not event.recurrence_days %}
{% if view_mode == 'day_view' %}{{ event.start_datetime|time }}{% else %}{{ event.start_datetime }}{% endif %}
{% else %}
{{ event.get_recurrence_display }}

View File

@ -1,5 +1,5 @@
{% extends "chrono/manager_agenda_view.html" %}
{% load i18n %}
{% load i18n gadjo %}
{% block extrascripts %}
{{ block.super }}
@ -29,10 +29,25 @@
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
<input type="hidden" name="next" value="{% firstof request.POST.next request.GET.next %}">
{{ form.as_p }}
{{ form|with_template }}
<div class="buttons">
<button class="submit-button">{% trans "Save" %}</button>
<a class="cancel" href="{{ view.get_success_url }}">{% trans 'Cancel' %}</a>
</div>
<script>
$(function () {
recurrence_fields = $('.widget[id^=id_recurrence]');
$('input[type=radio][name=frequency]').change(function() {
if(!this.checked)
return;
if(this.value == 'unique') {
recurrence_fields.hide();
} else {
recurrence_fields.show();
}
}).change();
});
</script>
</form>
{% endblock %}

View File

@ -0,0 +1,10 @@
{% spaceless %}
<span id="{{ widget.attrs.id }}" class="inputs-buttons-group{% if widget.attrs.class %} {{ widget.attrs.class }}{% endif %}">
{% for group, options, index in widget.optgroups %}
{% for option in options %}
{% include "django/forms/widgets/input.html" with widget=option %}
<label{% if option.attrs.id %} for="{{ option.attrs.id }}"{% endif %}>{{ option.label }}</label>
{% endfor %}
{% endfor %}
</span>
{% endspaceless %}

View File

@ -1054,7 +1054,7 @@ class AgendaDateView(DateMixin, ViewableAgendaMixin):
def get_queryset(self):
if self.agenda.kind == 'events':
queryset = self.agenda.event_set.filter(recurrence_rule__isnull=True)
queryset = self.agenda.event_set.filter(recurrence_days__isnull=True)
else:
self.agenda.prefetch_desks_and_exceptions()
if self.agenda.kind == 'meetings':
@ -1583,7 +1583,7 @@ class AgendaSettings(ManagedAgendaMixin, DetailView):
if self.agenda.kind == 'events':
context['has_absence_reasons'] = AbsenceReasonGroup.objects.exists()
context['has_recurring_events'] = self.agenda.event_set.filter(
recurrence_rule__isnull=False
recurrence_days__isnull=False
).exists()
desk = Desk.objects.get(agenda=self.agenda, slug='_exceptions_holder')
context['exceptions'] = TimePeriodException.objects.filter(
@ -1865,7 +1865,7 @@ class EventDetailView(ViewableAgendaMixin, DetailView):
pk_url_kwarg = 'event_pk'
def dispatch(self, request, *args, **kwargs):
if self.get_object().recurrence_rule:
if self.get_object().recurrence_days:
raise Http404('this view makes no sense for recurring events')
return super().dispatch(request, *args, **kwargs)
@ -1900,7 +1900,7 @@ class EventEditView(ManagedAgendaMixin, UpdateView):
if (
self.request.GET.get('next') == 'settings'
or self.request.POST.get('next') == 'settings'
or self.object.recurrence_rule
or self.object.recurrence_days
):
return reverse('chrono-manager-agenda-settings', kwargs={'pk': self.agenda.id})
return reverse('chrono-manager-event-view', kwargs={'pk': self.agenda.id, 'event_pk': self.object.id})

View File

@ -16,7 +16,7 @@
from django.forms.fields import SplitDateTimeField
from django.forms.widgets import SplitDateTimeWidget, TimeInput
from django.forms.widgets import CheckboxSelectMultiple, SplitDateTimeWidget, TimeInput
from django.utils.safestring import mark_safe
@ -59,3 +59,14 @@ class TimeWidget(TimeInput):
super(TimeWidget, self).__init__(**kwargs)
self.attrs['step'] = '300' # 5 minutes
self.attrs['pattern'] = '[0-9]{2}:[0-9]{2}'
class WeekdaysWidget(CheckboxSelectMultiple):
template_name = 'chrono/widgets/weekdays.html'
def id_for_label(self, id_, index=None):
"""Workaround CheckboxSelectMultiple id_for_label, which would return empty string when
index is None, leading to more complicated JS from our side."""
if index is None:
index = ''
return super(CheckboxSelectMultiple, self).id_for_label(id_, index)

View File

@ -224,7 +224,7 @@ def test_agendas_api(app):
start_datetime=now(),
places=10,
agenda=event_agenda,
repeat='daily',
recurrence_days=list(range(7)),
)
assert len(event_agenda.get_open_events()) == 2
resp = app.get('/api/agenda/', params={'with_open_events': '1'})
@ -232,7 +232,9 @@ def test_agendas_api(app):
for i in range(10):
event_agenda = Agenda.objects.create(label='Foo bar', category=category_a)
Event.objects.create(start_datetime=now(), places=10, agenda=event_agenda, repeat='daily')
event = Event.objects.create(
start_datetime=now(), places=10, agenda=event_agenda, recurrence_days=[now().weekday()]
)
TimePeriodException.objects.create(
desk=event_agenda.desk_set.get(),
start_datetime=now(),

View File

@ -193,10 +193,11 @@ def test_datetimes_api_exclude_slots(app):
event.delete()
# recurrent event
start_datetime = localtime().replace(hour=12, minute=0)
event = Event.objects.create(
slug='recurrent',
start_datetime=localtime().replace(hour=12, minute=0),
repeat='weekly',
start_datetime=start_datetime,
recurrence_days=[start_datetime.weekday()],
places=2,
agenda=agenda,
)
@ -479,7 +480,12 @@ def test_datetimes_api_meta(app, freezer):
# recurring event
Event.objects.all().delete()
Event.objects.create(
slug='abc', label='Test', start_datetime=localtime(), repeat='weekly', places=5, agenda=agenda
slug='abc',
label='Test',
start_datetime=localtime(),
recurrence_days=[localtime().weekday()],
places=5,
agenda=agenda,
)
resp = app.get(api_url)
assert resp.json['meta']['first_bookable_slot']['text'] == 'Test (May 27, 2017, 1:12 a.m.)'
@ -491,7 +497,12 @@ def test_recurring_events_api(app, user, freezer):
label='Foo bar', kind='events', minimal_booking_delay=1, maximal_booking_delay=30
)
base_event = Event.objects.create(
slug='abc', label='Test', start_datetime=localtime(), repeat='weekly', places=5, agenda=agenda
slug='abc',
label='Test',
start_datetime=localtime(),
recurrence_days=[localtime().weekday()],
places=5,
agenda=agenda,
)
resp = app.get('/api/agenda/%s/datetimes/' % agenda.slug)
@ -565,7 +576,11 @@ def test_recurring_events_api_various_times(app, user, mock_now):
label='Foo bar', kind='events', minimal_booking_delay=0, maximal_booking_delay=30
)
event = Event.objects.create(
slug='abc', start_datetime=localtime(), repeat='weekly', places=5, agenda=agenda
slug='abc',
start_datetime=localtime(),
recurrence_days=[localtime().weekday()],
places=5,
agenda=agenda,
)
event.refresh_from_db()
@ -606,7 +621,13 @@ def test_recurring_events_api_exceptions(app, user, freezer):
agenda = Agenda.objects.create(
label='Foo bar', kind='events', minimal_booking_delay=1, maximal_booking_delay=30
)
Event.objects.create(slug='abc', start_datetime=localtime(), repeat='weekly', places=5, agenda=agenda)
Event.objects.create(
slug='abc',
start_datetime=localtime(),
recurrence_days=[localtime().weekday()],
places=5,
agenda=agenda,
)
resp = app.get('/api/agenda/%s/datetimes/' % agenda.slug)
data = resp.json['data']

View File

@ -200,10 +200,11 @@ def test_booking_api_exclude_slots(app, user):
event.delete()
# recurrent event
start_datetime = localtime().replace(hour=12, minute=0)
event = Event.objects.create(
slug='recurrent',
start_datetime=localtime().replace(hour=12, minute=0),
repeat='weekly',
start_datetime=start_datetime,
recurrence_days=[start_datetime.weekday()],
places=2,
agenda=agenda,
)

View File

@ -2659,12 +2659,13 @@ def test_agenda_events_day_view(app, admin_user):
Event.objects.create(
label='xyz', start_datetime=localtime().replace(day=11, month=11, year=2020), places=10, agenda=agenda
)
recurring_start_datetime = localtime().replace(day=4, month=11, year=2020)
event = Event.objects.create(
label='abc',
start_datetime=localtime().replace(day=4, month=11, year=2020),
start_datetime=recurring_start_datetime,
places=10,
agenda=agenda,
repeat='weekly',
recurrence_days=[recurring_start_datetime.weekday()],
)
with CaptureQueriesContext(connection) as ctx:
@ -2687,7 +2688,11 @@ def test_agenda_events_day_view(app, admin_user):
# create another event with recurrence, the same day/time
start_datetime = localtime().replace(day=4, month=11, year=2020)
event = Event.objects.create(
label='def', start_datetime=start_datetime, places=10, agenda=agenda, repeat='weekly'
label='def',
start_datetime=start_datetime,
places=10,
agenda=agenda,
recurrence_days=[start_datetime.weekday()],
)
resp = app.get('/manage/agendas/%s/2020/11/11/' % agenda.pk)
# the event occurence in DB does not hide recurrence of the second recurrent event
@ -2727,7 +2732,11 @@ def test_agenda_events_month_view(app, admin_user):
# add recurring event on every Wednesday
start_datetime = localtime().replace(day=4, month=11, year=2020)
event = Event.objects.create(
label='abc', start_datetime=start_datetime, places=10, agenda=agenda, repeat='weekly'
label='abc',
start_datetime=start_datetime,
places=10,
agenda=agenda,
recurrence_days=[start_datetime.weekday()],
)
with CaptureQueriesContext(connection) as ctx:
@ -2767,7 +2776,11 @@ def test_agenda_events_month_view(app, admin_user):
# create another event with recurrence, the same day/time
start_datetime = localtime().replace(day=4, month=11, year=2020)
event = Event.objects.create(
label='def', start_datetime=start_datetime, places=10, agenda=agenda, repeat='weekly'
label='def',
start_datetime=start_datetime,
places=10,
agenda=agenda,
recurrence_days=[start_datetime.weekday()],
)
resp = app.get('/manage/agendas/%s/%s/%s/' % (agenda.id, 2020, 12))
# the event occurence in DB does not hide recurrence of the second recurrent event
@ -2850,12 +2863,13 @@ def test_agenda_open_events_view(app, admin_user, manager_user):
places=42,
)
# weekly recurring event, first recurrence is in the past but second is in range
start_datetime = now() - datetime.timedelta(days=3)
event = Event.objects.create(
label='event G',
start_datetime=now() - datetime.timedelta(days=3),
start_datetime=start_datetime,
places=10,
agenda=agenda,
repeat='weekly',
recurrence_days=[start_datetime.weekday()],
)
resp = app.get('/manage/agendas/%s/events/open/' % agenda.pk)
assert 'event A' not in resp.text
@ -4520,7 +4534,7 @@ def test_recurring_events_manage_exceptions(settings, app, admin_user, freezer):
resp = app.get('/manage/agendas/%s/settings' % agenda.id)
assert not 'Recurrence exceptions' in resp.text
event.repeat = 'daily'
event.recurrence_days = list(range(7))
event.save()
resp = app.get('/manage/agendas/%s/%s/%s/' % (agenda.id, 2021, 7))
@ -4564,7 +4578,7 @@ def test_recurring_events_exceptions_report(settings, app, admin_user, freezer):
event = Event.objects.create(
start_datetime=now(),
places=10,
repeat='daily',
recurrence_days=list(range(7)),
recurrence_end_date=now() + datetime.timedelta(days=30),
agenda=agenda,
)

View File

@ -83,7 +83,7 @@ def test_add_event(app, admin_user):
assert (
resp.text.count('Enter a valid date')
or resp.text.count('Enter a valid time') == 1
or resp.text.count('This field is required.') == 1
or resp.text.count('This field is required.') >= 1
)
@ -223,14 +223,15 @@ def test_edit_recurring_event(settings, app, admin_user, freezer):
app = login(app)
resp = app.get('/manage/agendas/%s/events/%s/edit' % (agenda.id, event.id))
resp.form['repeat'] = 'weekly'
resp.form['frequency'] = 'recurring'
resp.form['recurrence_days'] = [localtime().weekday()]
resp = resp.form.submit()
# detail page doesn't exist
resp = app.get('/manage/agendas/%s/events/%s/' % (agenda.id, event.id), status=404)
resp = app.get('/manage/agendas/%s/settings' % agenda.id, status=200)
assert 'Weekly on Tuesday at 1:10 p.m.' in resp.text
assert '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
@ -250,11 +251,11 @@ def test_edit_recurring_event(settings, app, admin_user, freezer):
# but some fields should not be updated
assert event_recurrence.slug != event.slug
assert not event_recurrence.repeat
assert not event_recurrence.recurrence_days
# changing recurrence attribute removes event recurrences
resp = app.get('/manage/agendas/%s/events/%s/edit' % (agenda.id, event.id))
resp.form['repeat'] = ''
resp.form['frequency'] = 'unique'
resp = resp.form.submit().follow()
assert not Event.objects.filter(primary_event=event).exists()
@ -272,7 +273,9 @@ def test_edit_recurring_event(settings, app, admin_user, freezer):
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['frequency'].attrs
assert all('disabled' in resp.form.get('recurrence_days', index=i).attrs for i in range(7))
assert 'disabled' in resp.form['recurrence_week_interval'].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
@ -287,13 +290,22 @@ def test_edit_recurring_event(settings, app, admin_user, freezer):
assert 'Delete' not in resp.text
resp = resp.click('Options')
assert {'slug', 'repeat', 'recurrence_end_date', 'publication_date'}.isdisjoint(resp.form.fields)
assert {
'slug',
'frequency',
'recurrence_days',
'recurence_weekly_interval',
'recurrence_end_date',
'publication_date',
}.isdisjoint(resp.form.fields)
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)
event = Event.objects.create(
start_datetime=now(), places=10, recurrence_days=list(range(7)), agenda=agenda
)
app = login(app)
resp = app.get('/manage/agendas/%s/events/%s/edit' % (agenda.id, event.id))
@ -301,7 +313,7 @@ def test_edit_recurring_event_with_end_date(settings, app, admin_user, freezer):
resp = resp.form.submit()
# recurrences are created automatically
event = Event.objects.get(recurrence_rule__isnull=False)
event = Event.objects.get(recurrence_days__isnull=False)
assert Event.objects.filter(primary_event=event).count() == 5
assert Event.objects.filter(primary_event=event, start_datetime=now()).exists()
@ -345,12 +357,6 @@ def test_edit_recurring_event_with_end_date(settings, app, admin_user, freezer):
assert Event.objects.filter(primary_event=event).count() == 4
assert 'Bookings exist after this date' in resp.text
Booking.objects.all().delete()
resp = app.get('/manage/agendas/%s/events/%s/edit' % (agenda.id, event.id))
resp.form['repeat'] = ''
resp = resp.form.submit()
assert 'Recurrence end date makes no sense without repetition.' in resp.text
def test_booked_places(app, admin_user):
agenda = Agenda(label=u'Foo bar')
@ -444,7 +450,9 @@ def test_delete_busy_event(app, admin_user):
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')
event = Event.objects.create(
start_datetime=start_datetime, places=10, agenda=agenda, recurrence_days=[start_datetime.weekday()]
)
app = login(app)
resp = app.get('/manage/agendas/%s/settings' % agenda.id, status=200)

View File

@ -1879,7 +1879,7 @@ def test_recurring_events(freezer):
event = Event.objects.create(
agenda=agenda,
start_datetime=now(),
repeat='weekly',
recurrence_days=[now().weekday()],
label='Event',
places=10,
waiting_list_places=10,
@ -1898,7 +1898,7 @@ def test_recurring_events(freezer):
event_json = event.export_json()
first_event_json = first_event.export_json()
different_fields = ['slug', 'repeat', 'recurrence_rule']
different_fields = ['slug', 'recurrence_days', 'recurrence_week_interval']
assert all(first_event_json[k] == event_json[k] for k in event_json if k not in different_fields)
second_event = recurrences[1]
@ -1922,7 +1922,9 @@ def test_recurring_events_dst(freezer, settings):
freezer.move_to('2020-10-24 12:00')
settings.TIME_ZONE = 'Europe/Brussels'
agenda = Agenda.objects.create(label='Agenda', kind='events')
event = Event.objects.create(agenda=agenda, start_datetime=now(), repeat='weekly', places=5)
event = Event.objects.create(
agenda=agenda, start_datetime=now(), recurrence_days=[now().weekday()], places=5
)
event.refresh_from_db()
dt = localtime()
recurrences = event.get_recurrences(dt, dt + datetime.timedelta(days=8))
@ -1940,22 +1942,13 @@ def test_recurring_events_dst(freezer, settings):
assert event_after_dst.slug == new_event_after_dst.slug
def test_recurring_events_weekday_midnight(freezer):
freezer.move_to('2021-01-06 23:30')
weekday = localtime().weekday()
agenda = Agenda.objects.create(label='Agenda', kind='events')
event = Event.objects.create(agenda=agenda, start_datetime=now(), repeat='weekly', places=5)
assert event.recurrence_rule['byweekday'][0] == weekday
def test_recurring_events_repeat(freezer):
def test_recurring_events_repetition(freezer):
freezer.move_to('2021-01-06 12:00') # Wednesday
agenda = Agenda.objects.create(label='Agenda', kind='events')
event = Event.objects.create(
agenda=agenda,
start_datetime=now(),
repeat='daily',
recurrence_days=list(range(7)), # everyday
places=5,
)
event.refresh_from_db()
@ -1971,7 +1964,7 @@ def test_recurring_events_repeat(freezer):
for i in range(len(recurrences) - 1):
assert recurrences[i].start_datetime + datetime.timedelta(days=1) == recurrences[i + 1].start_datetime
event.repeat = 'weekdays'
event.recurrence_days = list(range(5)) # from Monday to Friday
event.save()
recurrences = event.get_recurrences(
localtime() + datetime.timedelta(days=1), localtime() + datetime.timedelta(days=7)
@ -1981,7 +1974,8 @@ def test_recurring_events_repeat(freezer):
assert recurrences[1].start_datetime == start_datetime + datetime.timedelta(days=5)
assert recurrences[-1].start_datetime == start_datetime + datetime.timedelta(days=7)
event.repeat = '2-weeks'
event.recurrence_days = [localtime(event.start_datetime).weekday()] # from Monday to Friday
event.recurrence_week_interval = 2
event.save()
recurrences = event.get_recurrences(
localtime() + datetime.timedelta(days=3), localtime() + datetime.timedelta(days=45)
@ -1994,6 +1988,14 @@ def test_recurring_events_repeat(freezer):
recurrences[i].start_datetime + datetime.timedelta(days=14) == recurrences[i + 1].start_datetime
)
event.recurrence_days = [3] # Tuesday but start_datetime is a Wednesday
event.recurrence_week_interval = 1
event.save()
recurrences = event.get_recurrences(localtime(), localtime() + datetime.timedelta(days=10))
assert len(recurrences) == 2
# no recurrence exist on Wednesday
assert all(localtime(r.start_datetime).weekday() == 3 for r in recurrences)
@pytest.mark.freeze_time('2021-01-06')
def test_recurring_events_with_end_date():
@ -2001,7 +2003,7 @@ def test_recurring_events_with_end_date():
event = Event.objects.create(
agenda=agenda,
start_datetime=now(),
repeat='daily',
recurrence_days=list(range(7)),
places=5,
recurrence_end_date=(now() + datetime.timedelta(days=5)).date(),
)
@ -2019,11 +2021,21 @@ def test_recurring_events_with_end_date():
def test_recurring_events_sort(freezer):
freezer.move_to('2021-01-06 12:00') # Wednesday
agenda = Agenda.objects.create(label='Agenda', kind='events')
Event.objects.create(agenda=agenda, slug='a', start_datetime=now(), repeat='daily', places=5)
Event.objects.create(agenda=agenda, slug='b', start_datetime=now(), repeat='daily', duration=10, places=5)
Event.objects.create(agenda=agenda, slug='c', start_datetime=now(), repeat='daily', duration=5, places=5)
Event.objects.create(
agenda=agenda, slug='d', start_datetime=now() + datetime.timedelta(hours=1), repeat='daily', places=5
agenda=agenda, slug='a', start_datetime=now(), recurrence_days=list(range(7)), places=5
)
Event.objects.create(
agenda=agenda, slug='b', start_datetime=now(), recurrence_days=list(range(7)), duration=10, places=5
)
Event.objects.create(
agenda=agenda, slug='c', start_datetime=now(), recurrence_days=list(range(7)), duration=5, places=5
)
Event.objects.create(
agenda=agenda,
slug='d',
start_datetime=now() + datetime.timedelta(hours=1),
recurrence_days=list(range(7)),
places=5,
)
events = agenda.get_open_events()[:8]
@ -2043,7 +2055,7 @@ def test_recurring_events_exceptions(freezer):
event = Event.objects.create(
agenda=agenda,
start_datetime=now(),
repeat='daily',
recurrence_days=list(range(7)),
places=5,
)
event.refresh_from_db()
@ -2116,14 +2128,14 @@ def test_recurring_events_exceptions_update_recurrences(freezer):
daily_event = Event.objects.create(
agenda=agenda,
start_datetime=now(),
repeat='daily',
recurrence_days=list(range(7)),
places=5,
recurrence_end_date=datetime.date(year=2021, month=5, day=8),
)
weekly_event = Event.objects.create(
agenda=agenda,
start_datetime=now(),
repeat='weekly',
recurrence_days=[now().weekday()],
places=5,
recurrence_end_date=datetime.date(year=2021, month=6, day=1),
)
@ -2132,7 +2144,7 @@ def test_recurring_events_exceptions_update_recurrences(freezer):
daily_event_no_end_date = Event.objects.create(
agenda=agenda,
start_datetime=now() + datetime.timedelta(hours=2),
repeat='daily',
recurrence_days=list(range(7)),
places=5,
)
daily_event_no_end_date.refresh_from_db()
@ -2169,3 +2181,38 @@ def test_recurring_events_exceptions_update_recurrences(freezer):
assert Booking.objects.count() == 1
assert Event.objects.filter(primary_event=daily_event_no_end_date).count() == 1
assert agenda.recurrence_exceptions_report.events.get() == event
def test_recurring_events_display(freezer):
freezer.move_to('2021-01-06 12:30')
agenda = Agenda.objects.create(label='Agenda', kind='events')
event = Event.objects.create(
agenda=agenda, start_datetime=now(), recurrence_days=list(range(7)), places=5
)
assert event.get_recurrence_display() == 'Daily at 1:30 p.m.'
event.recurrence_days = [1, 2, 3, 4]
event.save()
assert event.get_recurrence_display() == 'From Tuesday to Friday at 1:30 p.m.'
event.recurrence_days = [4, 5, 6]
event.save()
assert event.get_recurrence_display() == 'From Friday to Sunday at 1:30 p.m.'
event.recurrence_days = [1, 4, 6]
event.save()
assert event.get_recurrence_display() == 'On Tuesday, Friday, Sunday at 1:30 p.m.'
event.recurrence_days = [0]
event.recurrence_week_interval = 2
event.save()
assert event.get_recurrence_display() == 'On Monday at 1:30 p.m., once every two weeks'
event.recurrence_week_interval = 3
event.recurrence_end_date = now() + datetime.timedelta(days=7)
event.save()
assert (
event.get_recurrence_display()
== 'On Monday at 1:30 p.m., once every three weeks, until Jan. 13, 2021'
)

View File

@ -12,7 +12,6 @@ def test_ensure_jsonb_fields():
json_fields = (
'extra_data',
'booking_errors',
'recurrence_rule',
)
with connection.cursor() as cursor:
@ -34,10 +33,6 @@ def test_ensure_jsonb_fields():
'''ALTER TABLE agendas_eventcancellationreport
ALTER COLUMN booking_errors TYPE text USING booking_errors::text'''
)
cursor.execute(
'''ALTER TABLE agendas_event
ALTER COLUMN recurrence_rule TYPE text USING recurrence_rule::text'''
)
call_command('ensure_jsonb')

View File

@ -214,7 +214,8 @@ def test_import_export_recurring_event(app, freezer):
event = Event.objects.create(
agenda=agenda,
start_datetime=now(),
repeat='daily',
recurrence_days=list(range(7)),
recurrence_week_interval=2,
places=10,
slug='test',
)
@ -235,11 +236,12 @@ def test_import_export_recurring_event(app, freezer):
assert Event.objects.count() == 1
event = Agenda.objects.get(label='Foo Bar').event_set.first()
assert event.primary_event is None
assert event.repeat == 'daily'
assert event.recurrence_rule == {'freq': DAILY}
assert event.recurrence_days == list(range(7))
assert event.recurrence_week_interval == 2
# importing event with end recurrence date creates recurrences
event.recurrence_end_date = now() + datetime.timedelta(days=7)
event.recurrence_week_interval = 1
event.save()
output = get_output_of_command('export_site')
import_site(data={}, clean=True)