Projet

Général

Profil

0001-manager-add-more-granular-control-over-event-recurre.patch

Valentin Deniaud, 08 juin 2021 14:04

Télécharger (48,9 ko)

Voir les différences:

Subject: [PATCH 1/2] manager: add more granular control over event recurrence
 (#50560)

 .../commands/update_event_recurrences.py      |   2 +-
 .../migrations/0091_auto_20210421_1556.py     |  80 ++++++++++++
 chrono/agendas/models.py                      | 119 +++++++++++-------
 chrono/api/views.py                           |   2 +-
 chrono/manager/forms.py                       |  66 ++++++++--
 chrono/manager/static/css/style.scss          |   4 +
 .../chrono/manager_agenda_event_fragment.html |   2 +-
 .../templates/chrono/manager_event_form.html  |  19 ++-
 .../templates/chrono/widgets/weekdays.html    |  10 ++
 chrono/manager/views.py                       |   8 +-
 chrono/manager/widgets.py                     |  13 +-
 tests/api/test_all.py                         |   6 +-
 tests/api/test_datetimes.py                   |  33 ++++-
 tests/api/test_fillslot.py                    |   5 +-
 tests/manager/test_all.py                     |  32 +++--
 tests/manager/test_event.py                   |  40 +++---
 tests/test_agendas.py                         |  97 ++++++++++----
 tests/test_ensure_jsonbfields.py              |   5 -
 tests/test_import_export.py                   |   8 +-
 19 files changed, 416 insertions(+), 135 deletions(-)
 create mode 100644 chrono/agendas/migrations/0091_auto_20210421_1556.py
 create mode 100644 chrono/manager/templates/chrono/widgets/weekdays.html
chrono/agendas/management/commands/update_event_recurrences.py
24 24
    help = 'Update event recurrences to reflect exceptions'
25 25

  
26 26
    def handle(self, **options):
27
        agendas = Agenda.objects.filter(kind='events', event__recurrence_rule__isnull=False).distinct()
27
        agendas = Agenda.objects.filter(kind='events', event__recurrence_days__isnull=False).distinct()
28 28
        for agenda in agendas:
29 29
            agenda.update_event_recurrences()
chrono/agendas/migrations/0091_auto_20210421_1556.py
1
# Generated by Django 2.2.19 on 2021-04-21 13:56
2

  
3
import django.contrib.postgres.fields
4
from dateutil.rrule import DAILY, WEEKLY
5
from django.db import migrations, models
6

  
7

  
8
def migrate_recurrence_fields(apps, schema_editor):
9
    Event = apps.get_model('agendas', 'Event')
10

  
11
    for event in Event.objects.filter(recurrence_days__isnull=False):
12
        if event.recurrence_rule['freq'] == DAILY:
13
            event.recurrence_days = list(range(7))
14
        elif event.recurrence_rule['freq'] == WEEKLY:
15
            event.recurrence_days = event.recurrence_rule['byweekday']
16
        event.recurrence_week_interval = event.recurrence_rule.get('interval', 1)
17
        event.save()
18

  
19

  
20
def reverse_migrate_recurrence_fields(apps, schema_editor):
21
    Event = apps.get_model('agendas', 'Event')
22

  
23
    for event in Event.objects.filter(recurrence_days__isnull=False):
24
        rrule = {}
25
        if event.recurrence_days == list(range(7)):
26
            event.repeat = 'daily'
27
            rrule['freq'] = DAILY
28
        else:
29
            rrule['freq'] = WEEKLY
30
            rrule['byweekday'] = event.recurrence_days
31
            if event.recurrence_days == list(range(5)):
32
                event.repeat = 'weekdays'
33
            elif event.recurrence_week_interval == 2:
34
                event.repeat = '2-weeks'
35
                rrule['interval'] = 2
36
            else:
37
                event.repeat = 'weekly'
38
        event.recurrence_rule = rrule
39
        event.save()
40

  
41

  
42
class Migration(migrations.Migration):
43

  
44
    dependencies = [
45
        ('agendas', '0090_default_view'),
46
    ]
47

  
48
    operations = [
49
        migrations.AddField(
50
            model_name='event',
51
            name='recurrence_days',
52
            field=django.contrib.postgres.fields.ArrayField(
53
                base_field=models.IntegerField(
54
                    choices=[(0, 'Mo'), (1, 'Tu'), (2, 'We'), (3, 'Th'), (4, 'Fr'), (5, 'Sa'), (6, 'Su')]
55
                ),
56
                blank=True,
57
                null=True,
58
                size=None,
59
                verbose_name='Recurrence days',
60
            ),
61
        ),
62
        migrations.AddField(
63
            model_name='event',
64
            name='recurrence_week_interval',
65
            field=models.IntegerField(
66
                choices=[(1, 'Every week'), (2, 'Every two weeks'), (3, 'Every three weeks')],
67
                default=1,
68
                verbose_name='Repeat',
69
            ),
70
        ),
71
        migrations.RunPython(migrate_recurrence_fields, reverse_migrate_recurrence_fields),
72
        migrations.RemoveField(
73
            model_name='event',
74
            name='recurrence_rule',
75
        ),
76
        migrations.RemoveField(
77
            model_name='event',
78
            name='repeat',
79
        ),
80
    ]
chrono/agendas/models.py
616 616
            entries = self.prefetched_events
617 617
        else:
618 618
            # recurring events are never opened
619
            entries = self.event_set.filter(recurrence_rule__isnull=True)
619
            entries = self.event_set.filter(recurrence_days__isnull=True)
620 620
            # exclude canceled events except for event recurrences
621 621
            entries = entries.filter(Q(cancelled=False) | Q(primary_event__isnull=False))
622 622
            # we never want to allow booking for past events.
......
687 687
        else:
688 688
            recurring_events = self.event_set.filter(
689 689
                Q(publication_date__isnull=True) | Q(publication_date__lte=localtime(now()).date()),
690
                recurrence_rule__isnull=False,
690
                recurrence_days__isnull=False,
691 691
            )
692 692
            exceptions = self.get_recurrence_exceptions(min_start, max_start)
693 693

  
......
705 705

  
706 706
    @transaction.atomic
707 707
    def update_event_recurrences(self):
708
        recurring_events = self.event_set.filter(recurrence_rule__isnull=False)
708
        recurring_events = self.event_set.filter(recurrence_days__isnull=False)
709 709
        recurrences = self.event_set.filter(primary_event__isnull=False)
710 710

  
711 711
        # remove recurrences
......
1178 1178

  
1179 1179

  
1180 1180
class Event(models.Model):
1181
    REPEAT_CHOICES = [
1182
        ('daily', _('Daily')),
1183
        ('weekly', _('Weekly')),
1184
        ('2-weeks', _('Once every two weeks')),
1185
        ('weekdays', _('Every weekdays (Monday to Friday)')),
1181
    WEEKDAY_CHOICES = [
1182
        (0, _('Mo')),
1183
        (1, _('Tu')),
1184
        (2, _('We')),
1185
        (3, _('Th')),
1186
        (4, _('Fr')),
1187
        (5, _('Sa')),
1188
        (6, _('Su')),
1189
    ]
1190

  
1191
    INTERVAL_CHOICES = [
1192
        (1, 'Every week'),
1193
        (2, 'Every two weeks'),
1194
        (3, 'Every three weeks'),
1186 1195
    ]
1187 1196

  
1188 1197
    agenda = models.ForeignKey(Agenda, on_delete=models.CASCADE)
1189 1198
    start_datetime = models.DateTimeField(_('Date/time'))
1190
    repeat = models.CharField(_('Repeat'), max_length=16, blank=True, choices=REPEAT_CHOICES)
1191
    recurrence_rule = JSONField(_('Recurrence rule'), null=True, blank=True)
1199
    recurrence_days = ArrayField(
1200
        models.IntegerField(choices=WEEKDAY_CHOICES),
1201
        verbose_name=_('Recurrence days'),
1202
        blank=True,
1203
        null=True,
1204
    )
1205
    recurrence_week_interval = models.IntegerField(_('Repeat'), choices=INTERVAL_CHOICES, default=1)
1192 1206
    recurrence_end_date = models.DateField(_('Recurrence end date'), null=True, blank=True)
1193 1207
    primary_event = models.ForeignKey('self', null=True, on_delete=models.CASCADE, related_name='recurrences')
1194 1208
    duration = models.PositiveIntegerField(_('Duration (in minutes)'), default=None, null=True, blank=True)
......
1243 1257
        self.check_full()
1244 1258
        if not self.slug:
1245 1259
            self.slug = generate_slug(self, seen_slugs=seen_slugs, agenda=self.agenda)
1246
        self.recurrence_rule = self.get_recurrence_rule()
1247 1260
        return super(Event, self).save(*args, **kwargs)
1248 1261

  
1249 1262
    @property
......
1267 1280
            return False
1268 1281
        if self.agenda.maximal_booking_delay and self.start_datetime > self.agenda.max_booking_datetime:
1269 1282
            return False
1270
        if self.recurrence_rule is not None:
1283
        if self.recurrence_days is not None:
1271 1284
            # bookable recurrences probably exist
1272 1285
            return True
1273 1286
        if self.agenda.minimal_booking_delay and self.start_datetime < self.agenda.min_booking_datetime:
......
1402 1415
        else:
1403 1416
            event = cls(**data)
1404 1417
            event.save()
1405
        if event.recurrence_rule and event.recurrence_end_date:
1418
        if event.recurrence_days and event.recurrence_end_date:
1406 1419
            event.refresh_from_db()
1407 1420
            event.recurrences.filter(start_datetime__gt=event.recurrence_end_date).delete()
1408 1421
            update_fields = {
......
1431 1444
        return {
1432 1445
            'start_datetime': make_naive(self.start_datetime).strftime('%Y-%m-%d %H:%M:%S'),
1433 1446
            'publication_date': self.publication_date.strftime('%Y-%m-%d') if self.publication_date else None,
1434
            'repeat': self.repeat,
1435
            'recurrence_rule': self.recurrence_rule,
1447
            'recurrence_days': self.recurrence_days,
1448
            'recurrence_week_interval': self.recurrence_week_interval,
1436 1449
            'recurrence_end_date': recurrence_end_date,
1437 1450
            'places': self.places,
1438 1451
            'waiting_list_places': self.waiting_list_places,
......
1523 1536
            url=self.url,
1524 1537
        )
1525 1538

  
1526
        if self.recurrence_end_date:
1527
            self.recurrence_rule['until'] = datetime.datetime.combine(
1528
                self.recurrence_end_date, datetime.time(0, 0)
1529
            )
1530

  
1531 1539
        # remove pytz info because dateutil doesn't support DST changes
1532 1540
        min_datetime = make_naive(min_datetime)
1533 1541
        max_datetime = make_naive(max_datetime)
......
1548 1556
        return recurrences
1549 1557

  
1550 1558
    def get_recurrence_display(self):
1551
        repeat = str(self.get_repeat_display())
1552 1559
        time = date_format(localtime(self.start_datetime), 'TIME_FORMAT')
1553
        if self.repeat in ('weekly', '2-weeks'):
1554
            day = date_format(localtime(self.start_datetime), 'l')
1555
            return _('%(every_x_days)s on %(day)s at %(time)s') % {
1556
                'every_x_days': repeat,
1557
                'day': day,
1558
                'time': time,
1560

  
1561
        days_count = len(self.recurrence_days)
1562
        if days_count == 7:
1563
            repeat = _('Daily')
1564
        elif days_count > 1 and (self.recurrence_days[-1] - self.recurrence_days[0]) == days_count - 1:
1565
            # days are contiguous
1566
            repeat = _('From %(weekday)s to %(last_weekday)s') % {
1567
                'weekday': str(WEEKDAYS[self.recurrence_days[0]]),
1568
                'last_weekday': str(WEEKDAYS[self.recurrence_days[-1]]),
1559 1569
            }
1560 1570
        else:
1561
            return _('%(every_x_days)s at %(time)s') % {'every_x_days': repeat, 'time': time}
1562

  
1563
    def get_recurrence_rule(self):
1564
        rrule = {}
1565
        if self.repeat == 'daily':
1566
            rrule['freq'] = DAILY
1567
        elif self.repeat == 'weekly':
1568
            rrule['freq'] = WEEKLY
1569
            rrule['byweekday'] = [localtime(self.start_datetime).weekday()]
1570
        elif self.repeat == '2-weeks':
1571
            rrule['freq'] = WEEKLY
1572
            rrule['byweekday'] = [localtime(self.start_datetime).weekday()]
1573
            rrule['interval'] = 2
1574
        elif self.repeat == 'weekdays':
1575
            rrule['freq'] = WEEKLY
1576
            rrule['byweekday'] = [i for i in range(5)]
1577
        else:
1578
            return None
1579
        return rrule
1571
            repeat = _('On %(weekdays)s') % {
1572
                'weekdays': ', '.join([str(WEEKDAYS[i]) for i in self.recurrence_days])
1573
            }
1574

  
1575
        recurrence_display = _('%(On_day_x)s at %(time)s') % {'On_day_x': repeat, 'time': time}
1576

  
1577
        if self.recurrence_week_interval > 1:
1578
            if self.recurrence_week_interval == 2:
1579
                every_x_weeks = _('every two weeks')
1580
            elif self.recurrence_week_interval == 3:
1581
                every_x_weeks = _('every three weeks')
1582
            recurrence_display = _('%(Every_x_days)s, once %(every_x_weeks)s') % {
1583
                'Every_x_days': recurrence_display,
1584
                'every_x_weeks': every_x_weeks,
1585
            }
1586

  
1587
        if self.recurrence_end_date:
1588
            end_date = date_format(self.recurrence_end_date, 'DATE_FORMAT')
1589
            recurrence_display = _('%(Every_x_days)s, until %(date)s') % {
1590
                'Every_x_days': recurrence_display,
1591
                'date': end_date,
1592
            }
1593
        return recurrence_display
1594

  
1595
    @property
1596
    def recurrence_rule(self):
1597
        recurrence_rule = {
1598
            'freq': WEEKLY,
1599
            'byweekday': self.recurrence_days,
1600
            'interval': self.recurrence_week_interval,
1601
        }
1602
        if self.recurrence_end_date:
1603
            recurrence_rule['until'] = datetime.datetime.combine(
1604
                self.recurrence_end_date, datetime.time(0, 0)
1605
            )
1606
        return recurrence_rule
1580 1607

  
1581 1608
    def has_recurrences_booked(self, after=None):
1582 1609
        return Booking.objects.filter(
chrono/api/views.py
589 589
            ).order_by()
590 590
            recurring_event_queryset = Event.objects.filter(
591 591
                Q(publication_date__isnull=True) | Q(publication_date__lte=localtime(now()).date()),
592
                recurrence_rule__isnull=False,
592
                recurrence_days__isnull=False,
593 593
            )
594 594
            exceptions_desk = Desk.objects.filter(slug='_exceptions_holder').prefetch_related(
595 595
                'unavailability_calendars'
chrono/manager/forms.py
53 53
)
54 54

  
55 55
from . import widgets
56
from .widgets import SplitDateTimeField
56
from .widgets import SplitDateTimeField, WeekdaysWidget
57 57

  
58 58

  
59 59
class AbsenceReasonForm(forms.ModelForm):
......
160 160

  
161 161

  
162 162
class NewEventForm(forms.ModelForm):
163
    frequency = forms.ChoiceField(
164
        label=_('Event frequency'),
165
        widget=forms.RadioSelect,
166
        choices=(
167
            ('unique', _('Unique')),
168
            ('recurring', _('Recurring')),
169
        ),
170
        initial='unique',
171
    )
172
    recurrence_days = forms.TypedMultipleChoiceField(
173
        choices=Event.WEEKDAY_CHOICES, coerce=int, required=False, widget=WeekdaysWidget
174
    )
175

  
163 176
    class Meta:
164 177
        model = Event
165 178
        fields = [
166 179
            'label',
167 180
            'start_datetime',
168
            'repeat',
181
            'frequency',
182
            'recurrence_days',
183
            'recurrence_week_interval',
184
            'recurrence_end_date',
169 185
            'duration',
170 186
            'places',
171 187
        ]
172 188
        field_classes = {
173 189
            'start_datetime': SplitDateTimeField,
174 190
        }
191
        widgets = {
192
            'recurrence_end_date': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
193
        }
194

  
195
    def clean_recurrence_days(self):
196
        recurrence_days = self.cleaned_data['recurrence_days']
197
        if recurrence_days == []:
198
            return None
199
        return recurrence_days
175 200

  
176 201

  
177
class EventForm(forms.ModelForm):
178
    protected_fields = ('repeat', 'slug', 'start_datetime')
202
class EventForm(NewEventForm):
203
    protected_fields = (
204
        'slug',
205
        'start_datetime',
206
        'frequency',
207
        'recurrence_days',
208
        'recurrence_week_interval',
209
    )
179 210

  
180 211
    class Meta:
181 212
        model = Event
......
187 218
            'label',
188 219
            'slug',
189 220
            'start_datetime',
190
            'repeat',
221
            'frequency',
222
            'recurrence_days',
223
            'recurrence_week_interval',
191 224
            'recurrence_end_date',
192 225
            'duration',
193 226
            'publication_date',
......
203 236

  
204 237
    def __init__(self, *args, **kwargs):
205 238
        super().__init__(*args, **kwargs)
206
        if self.instance.recurrence_rule and self.instance.has_recurrences_booked():
239
        self.fields['frequency'].initial = 'recurring' if self.instance.recurrence_days else 'unique'
240
        if self.instance.recurrence_days and self.instance.has_recurrences_booked():
207 241
            for field in self.protected_fields:
208 242
                self.fields[field].disabled = True
209 243
                self.fields[field].help_text = _(
210 244
                    'This field cannot be modified because some recurrences have bookings attached to them.'
211 245
                )
212 246
        if self.instance.primary_event:
213
            for field in ('slug', 'repeat', 'recurrence_end_date', 'publication_date'):
247
            for field in (
248
                'slug',
249
                'recurrence_end_date',
250
                'publication_date',
251
                'frequency',
252
                'recurrence_days',
253
                'recurrence_week_interval',
254
            ):
214 255
                del self.fields[field]
215 256

  
216 257
    def clean(self):
......
219 260
            after=self.cleaned_data['recurrence_end_date']
220 261
        ):
221 262
            raise ValidationError(_('Bookings exist after this date.'))
222
        if self.cleaned_data.get('recurrence_end_date') and not self.cleaned_data.get('repeat'):
223
            raise ValidationError(_('Recurrence end date makes no sense without repetition.'))
263

  
264
        if self.cleaned_data.get('frequency') == 'unique':
265
            self.cleaned_data['recurrence_days'] = None
266
            self.cleaned_data['recurrence_end_date'] = None
224 267

  
225 268
    def save(self, *args, **kwargs):
226 269
        with transaction.atomic():
227 270
            if any(field for field in self.changed_data if field in self.protected_fields):
228 271
                self.instance.recurrences.all().delete()
229
            elif self.instance.recurrence_rule:
272
            elif self.instance.recurrence_days:
273
                protected_fields = list(self.protected_fields) + ['recurrence_end_date', 'frequency']
230 274
                update_fields = {
231 275
                    field: value
232 276
                    for field, value in self.cleaned_data.items()
233
                    if field != 'recurrence_end_date' and field not in self.protected_fields
277
                    if field not in protected_fields
234 278
                }
235 279
                self.instance.recurrences.update(**update_fields)
236 280

  
chrono/manager/static/css/style.scss
442 442
		background-color: $color;
443 443
	}
444 444
}
445

  
446
form div.widget[id^=id_recurrence] {
447
      padding-left: 1em;
448
}
chrono/manager/templates/chrono/manager_agenda_event_fragment.html
20 20
    {% else %}
21 21
      {% if event.label %}{{ event.label }} / {% endif %}
22 22
    {% endif %}
23
    {% if not event.repeat %}
23
    {% if not event.recurrence_days %}
24 24
      {% if view_mode == 'day_view' %}{{ event.start_datetime|time }}{% else %}{{ event.start_datetime }}{% endif %}
25 25
    {% else %}
26 26
      {{ event.get_recurrence_display }}
chrono/manager/templates/chrono/manager_event_form.html
1 1
{% extends "chrono/manager_agenda_view.html" %}
2
{% load i18n %}
2
{% load i18n gadjo %}
3 3

  
4 4
{% block extrascripts %}
5 5
{{ block.super }}
......
29 29
<form method="post" enctype="multipart/form-data">
30 30
  {% csrf_token %}
31 31
  <input type="hidden" name="next" value="{% firstof request.POST.next request.GET.next %}">
32
  {{ form.as_p }}
32
  {{ form|with_template }}
33 33
  <div class="buttons">
34 34
    <button class="submit-button">{% trans "Save" %}</button>
35 35
    <a class="cancel" href="{{ view.get_success_url }}">{% trans 'Cancel' %}</a>
36 36
  </div>
37

  
38
  <script>
39
    $(function () {
40
      recurrence_fields = $('.widget[id^=id_recurrence]');
41
      $('input[type=radio][name=frequency]').change(function() {
42
        if(!this.checked)
43
          return;
44
        if(this.value == 'unique') {
45
          recurrence_fields.hide();
46
        } else {
47
          recurrence_fields.show();
48
        }
49
      }).change();
50
    });
51
  </script>
37 52
</form>
38 53
{% endblock %}
chrono/manager/templates/chrono/widgets/weekdays.html
1
{% spaceless %}
2
<span id="{{ widget.attrs.id }}" class="inputs-buttons-group{% if widget.attrs.class %} {{ widget.attrs.class }}{% endif %}">
3
	{% for group, options, index in widget.optgroups %}
4
	{% for option in options %}
5
	{% include "django/forms/widgets/input.html" with widget=option %}
6
	<label{% if option.attrs.id %} for="{{ option.attrs.id }}"{% endif %}>{{ option.label }}</label>
7
	{% endfor %}
8
	{% endfor %}
9
</span>
10
{% endspaceless %}
chrono/manager/views.py
1054 1054

  
1055 1055
    def get_queryset(self):
1056 1056
        if self.agenda.kind == 'events':
1057
            queryset = self.agenda.event_set.filter(recurrence_rule__isnull=True)
1057
            queryset = self.agenda.event_set.filter(recurrence_days__isnull=True)
1058 1058
        else:
1059 1059
            self.agenda.prefetch_desks_and_exceptions()
1060 1060
            if self.agenda.kind == 'meetings':
......
1583 1583
        if self.agenda.kind == 'events':
1584 1584
            context['has_absence_reasons'] = AbsenceReasonGroup.objects.exists()
1585 1585
            context['has_recurring_events'] = self.agenda.event_set.filter(
1586
                recurrence_rule__isnull=False
1586
                recurrence_days__isnull=False
1587 1587
            ).exists()
1588 1588
            desk = Desk.objects.get(agenda=self.agenda, slug='_exceptions_holder')
1589 1589
            context['exceptions'] = TimePeriodException.objects.filter(
......
1865 1865
    pk_url_kwarg = 'event_pk'
1866 1866

  
1867 1867
    def dispatch(self, request, *args, **kwargs):
1868
        if self.get_object().recurrence_rule:
1868
        if self.get_object().recurrence_days:
1869 1869
            raise Http404('this view makes no sense for recurring events')
1870 1870
        return super().dispatch(request, *args, **kwargs)
1871 1871

  
......
1900 1900
        if (
1901 1901
            self.request.GET.get('next') == 'settings'
1902 1902
            or self.request.POST.get('next') == 'settings'
1903
            or self.object.recurrence_rule
1903
            or self.object.recurrence_days
1904 1904
        ):
1905 1905
            return reverse('chrono-manager-agenda-settings', kwargs={'pk': self.agenda.id})
1906 1906
        return reverse('chrono-manager-event-view', kwargs={'pk': self.agenda.id, 'event_pk': self.object.id})
chrono/manager/widgets.py
16 16

  
17 17

  
18 18
from django.forms.fields import SplitDateTimeField
19
from django.forms.widgets import SplitDateTimeWidget, TimeInput
19
from django.forms.widgets import CheckboxSelectMultiple, SplitDateTimeWidget, TimeInput
20 20
from django.utils.safestring import mark_safe
21 21

  
22 22

  
......
59 59
        super(TimeWidget, self).__init__(**kwargs)
60 60
        self.attrs['step'] = '300'  # 5 minutes
61 61
        self.attrs['pattern'] = '[0-9]{2}:[0-9]{2}'
62

  
63

  
64
class WeekdaysWidget(CheckboxSelectMultiple):
65
    template_name = 'chrono/widgets/weekdays.html'
66

  
67
    def id_for_label(self, id_, index=None):
68
        """Workaround CheckboxSelectMultiple id_for_label, which would return empty string when
69
        index is None, leading to more complicated JS from our side."""
70
        if index is None:
71
            index = ''
72
        return super(CheckboxSelectMultiple, self).id_for_label(id_, index)
tests/api/test_all.py
224 224
        start_datetime=now(),
225 225
        places=10,
226 226
        agenda=event_agenda,
227
        repeat='daily',
227
        recurrence_days=list(range(7)),
228 228
    )
229 229
    assert len(event_agenda.get_open_events()) == 2
230 230
    resp = app.get('/api/agenda/', params={'with_open_events': '1'})
......
232 232

  
233 233
    for i in range(10):
234 234
        event_agenda = Agenda.objects.create(label='Foo bar', category=category_a)
235
        Event.objects.create(start_datetime=now(), places=10, agenda=event_agenda, repeat='daily')
235
        event = Event.objects.create(
236
            start_datetime=now(), places=10, agenda=event_agenda, recurrence_days=[now().weekday()]
237
        )
236 238
        TimePeriodException.objects.create(
237 239
            desk=event_agenda.desk_set.get(),
238 240
            start_datetime=now(),
tests/api/test_datetimes.py
193 193
    event.delete()
194 194

  
195 195
    # recurrent event
196
    start_datetime = localtime().replace(hour=12, minute=0)
196 197
    event = Event.objects.create(
197 198
        slug='recurrent',
198
        start_datetime=localtime().replace(hour=12, minute=0),
199
        repeat='weekly',
199
        start_datetime=start_datetime,
200
        recurrence_days=[start_datetime.weekday()],
200 201
        places=2,
201 202
        agenda=agenda,
202 203
    )
......
479 480
    # recurring event
480 481
    Event.objects.all().delete()
481 482
    Event.objects.create(
482
        slug='abc', label='Test', start_datetime=localtime(), repeat='weekly', places=5, agenda=agenda
483
        slug='abc',
484
        label='Test',
485
        start_datetime=localtime(),
486
        recurrence_days=[localtime().weekday()],
487
        places=5,
488
        agenda=agenda,
483 489
    )
484 490
    resp = app.get(api_url)
485 491
    assert resp.json['meta']['first_bookable_slot']['text'] == 'Test (May 27, 2017, 1:12 a.m.)'
......
491 497
        label='Foo bar', kind='events', minimal_booking_delay=1, maximal_booking_delay=30
492 498
    )
493 499
    base_event = Event.objects.create(
494
        slug='abc', label='Test', start_datetime=localtime(), repeat='weekly', places=5, agenda=agenda
500
        slug='abc',
501
        label='Test',
502
        start_datetime=localtime(),
503
        recurrence_days=[localtime().weekday()],
504
        places=5,
505
        agenda=agenda,
495 506
    )
496 507

  
497 508
    resp = app.get('/api/agenda/%s/datetimes/' % agenda.slug)
......
565 576
        label='Foo bar', kind='events', minimal_booking_delay=0, maximal_booking_delay=30
566 577
    )
567 578
    event = Event.objects.create(
568
        slug='abc', start_datetime=localtime(), repeat='weekly', places=5, agenda=agenda
579
        slug='abc',
580
        start_datetime=localtime(),
581
        recurrence_days=[localtime().weekday()],
582
        places=5,
583
        agenda=agenda,
569 584
    )
570 585
    event.refresh_from_db()
571 586

  
......
606 621
    agenda = Agenda.objects.create(
607 622
        label='Foo bar', kind='events', minimal_booking_delay=1, maximal_booking_delay=30
608 623
    )
609
    Event.objects.create(slug='abc', start_datetime=localtime(), repeat='weekly', places=5, agenda=agenda)
624
    Event.objects.create(
625
        slug='abc',
626
        start_datetime=localtime(),
627
        recurrence_days=[localtime().weekday()],
628
        places=5,
629
        agenda=agenda,
630
    )
610 631

  
611 632
    resp = app.get('/api/agenda/%s/datetimes/' % agenda.slug)
612 633
    data = resp.json['data']
tests/api/test_fillslot.py
200 200
    event.delete()
201 201

  
202 202
    # recurrent event
203
    start_datetime = localtime().replace(hour=12, minute=0)
203 204
    event = Event.objects.create(
204 205
        slug='recurrent',
205
        start_datetime=localtime().replace(hour=12, minute=0),
206
        repeat='weekly',
206
        start_datetime=start_datetime,
207
        recurrence_days=[start_datetime.weekday()],
207 208
        places=2,
208 209
        agenda=agenda,
209 210
    )
tests/manager/test_all.py
2659 2659
    Event.objects.create(
2660 2660
        label='xyz', start_datetime=localtime().replace(day=11, month=11, year=2020), places=10, agenda=agenda
2661 2661
    )
2662
    recurring_start_datetime = localtime().replace(day=4, month=11, year=2020)
2662 2663
    event = Event.objects.create(
2663 2664
        label='abc',
2664
        start_datetime=localtime().replace(day=4, month=11, year=2020),
2665
        start_datetime=recurring_start_datetime,
2665 2666
        places=10,
2666 2667
        agenda=agenda,
2667
        repeat='weekly',
2668
        recurrence_days=[recurring_start_datetime.weekday()],
2668 2669
    )
2669 2670

  
2670 2671
    with CaptureQueriesContext(connection) as ctx:
......
2687 2688
    # create another event with recurrence, the same day/time
2688 2689
    start_datetime = localtime().replace(day=4, month=11, year=2020)
2689 2690
    event = Event.objects.create(
2690
        label='def', start_datetime=start_datetime, places=10, agenda=agenda, repeat='weekly'
2691
        label='def',
2692
        start_datetime=start_datetime,
2693
        places=10,
2694
        agenda=agenda,
2695
        recurrence_days=[start_datetime.weekday()],
2691 2696
    )
2692 2697
    resp = app.get('/manage/agendas/%s/2020/11/11/' % agenda.pk)
2693 2698
    # the event occurence in DB does not hide recurrence of the second recurrent event
......
2727 2732
    # add recurring event on every Wednesday
2728 2733
    start_datetime = localtime().replace(day=4, month=11, year=2020)
2729 2734
    event = Event.objects.create(
2730
        label='abc', start_datetime=start_datetime, places=10, agenda=agenda, repeat='weekly'
2735
        label='abc',
2736
        start_datetime=start_datetime,
2737
        places=10,
2738
        agenda=agenda,
2739
        recurrence_days=[start_datetime.weekday()],
2731 2740
    )
2732 2741

  
2733 2742
    with CaptureQueriesContext(connection) as ctx:
......
2767 2776
    # create another event with recurrence, the same day/time
2768 2777
    start_datetime = localtime().replace(day=4, month=11, year=2020)
2769 2778
    event = Event.objects.create(
2770
        label='def', start_datetime=start_datetime, places=10, agenda=agenda, repeat='weekly'
2779
        label='def',
2780
        start_datetime=start_datetime,
2781
        places=10,
2782
        agenda=agenda,
2783
        recurrence_days=[start_datetime.weekday()],
2771 2784
    )
2772 2785
    resp = app.get('/manage/agendas/%s/%s/%s/' % (agenda.id, 2020, 12))
2773 2786
    # the event occurence in DB does not hide recurrence of the second recurrent event
......
2850 2863
        places=42,
2851 2864
    )
2852 2865
    # weekly recurring event, first recurrence is in the past but second is in range
2866
    start_datetime = now() - datetime.timedelta(days=3)
2853 2867
    event = Event.objects.create(
2854 2868
        label='event G',
2855
        start_datetime=now() - datetime.timedelta(days=3),
2869
        start_datetime=start_datetime,
2856 2870
        places=10,
2857 2871
        agenda=agenda,
2858
        repeat='weekly',
2872
        recurrence_days=[start_datetime.weekday()],
2859 2873
    )
2860 2874
    resp = app.get('/manage/agendas/%s/events/open/' % agenda.pk)
2861 2875
    assert 'event A' not in resp.text
......
4520 4534
    resp = app.get('/manage/agendas/%s/settings' % agenda.id)
4521 4535
    assert not 'Recurrence exceptions' in resp.text
4522 4536

  
4523
    event.repeat = 'daily'
4537
    event.recurrence_days = list(range(7))
4524 4538
    event.save()
4525 4539

  
4526 4540
    resp = app.get('/manage/agendas/%s/%s/%s/' % (agenda.id, 2021, 7))
......
4564 4578
    event = Event.objects.create(
4565 4579
        start_datetime=now(),
4566 4580
        places=10,
4567
        repeat='daily',
4581
        recurrence_days=list(range(7)),
4568 4582
        recurrence_end_date=now() + datetime.timedelta(days=30),
4569 4583
        agenda=agenda,
4570 4584
    )
tests/manager/test_event.py
83 83
        assert (
84 84
            resp.text.count('Enter a valid date')
85 85
            or resp.text.count('Enter a valid time') == 1
86
            or resp.text.count('This field is required.') == 1
86
            or resp.text.count('This field is required.') >= 1
87 87
        )
88 88

  
89 89

  
......
223 223

  
224 224
    app = login(app)
225 225
    resp = app.get('/manage/agendas/%s/events/%s/edit' % (agenda.id, event.id))
226
    resp.form['repeat'] = 'weekly'
226
    resp.form['frequency'] = 'recurring'
227
    resp.form['recurrence_days'] = [localtime().weekday()]
227 228
    resp = resp.form.submit()
228 229

  
229 230
    # detail page doesn't exist
230 231
    resp = app.get('/manage/agendas/%s/events/%s/' % (agenda.id, event.id), status=404)
231 232

  
232 233
    resp = app.get('/manage/agendas/%s/settings' % agenda.id, status=200)
233
    assert 'Weekly on Tuesday at 1:10 p.m.' in resp.text
234
    assert 'On Tuesday at 1:10 p.m.' in resp.text
234 235
    # event is bookable regardless of minimal_booking_delay, since it has bookable recurrences
235 236
    assert len(resp.pyquery.find('.bookable')) == 1
236 237

  
......
250 251

  
251 252
    # but some fields should not be updated
252 253
    assert event_recurrence.slug != event.slug
253
    assert not event_recurrence.repeat
254
    assert not event_recurrence.recurrence_days
254 255

  
255 256
    # changing recurrence attribute removes event recurrences
256 257
    resp = app.get('/manage/agendas/%s/events/%s/edit' % (agenda.id, event.id))
257
    resp.form['repeat'] = ''
258
    resp.form['frequency'] = 'unique'
258 259
    resp = resp.form.submit().follow()
259 260
    assert not Event.objects.filter(primary_event=event).exists()
260 261

  
......
272 273
    event_recurrence = event.get_or_create_event_recurrence(event.start_datetime + datetime.timedelta(days=7))
273 274
    Booking.objects.create(event=event_recurrence)
274 275
    resp = app.get('/manage/agendas/%s/events/%s/edit' % (agenda.id, event.id))
275
    assert 'disabled' in resp.form['repeat'].attrs
276
    assert 'disabled' in resp.form['frequency'].attrs
277
    assert all('disabled' in resp.form.get('recurrence_days', index=i).attrs for i in range(7))
278
    assert 'disabled' in resp.form['recurrence_week_interval'].attrs
276 279
    assert 'disabled' in resp.form['slug'].attrs
277 280
    assert 'disabled' in resp.form['start_datetime_0'].attrs
278 281
    assert 'disabled' in resp.form['start_datetime_1'].attrs
......
287 290
    assert 'Delete' not in resp.text
288 291

  
289 292
    resp = resp.click('Options')
290
    assert {'slug', 'repeat', 'recurrence_end_date', 'publication_date'}.isdisjoint(resp.form.fields)
293
    assert {
294
        'slug',
295
        'frequency',
296
        'recurrence_days',
297
        'recurence_weekly_interval',
298
        'recurrence_end_date',
299
        'publication_date',
300
    }.isdisjoint(resp.form.fields)
291 301

  
292 302

  
293 303
def test_edit_recurring_event_with_end_date(settings, app, admin_user, freezer):
294 304
    freezer.move_to('2021-01-12 12:10')
295 305
    agenda = Agenda.objects.create(label='Foo bar', kind='events')
296
    event = Event.objects.create(start_datetime=now(), places=10, repeat='daily', agenda=agenda)
306
    event = Event.objects.create(
307
        start_datetime=now(), places=10, recurrence_days=list(range(7)), agenda=agenda
308
    )
297 309

  
298 310
    app = login(app)
299 311
    resp = app.get('/manage/agendas/%s/events/%s/edit' % (agenda.id, event.id))
......
301 313
    resp = resp.form.submit()
302 314

  
303 315
    # recurrences are created automatically
304
    event = Event.objects.get(recurrence_rule__isnull=False)
316
    event = Event.objects.get(recurrence_days__isnull=False)
305 317
    assert Event.objects.filter(primary_event=event).count() == 5
306 318
    assert Event.objects.filter(primary_event=event, start_datetime=now()).exists()
307 319

  
......
345 357
    assert Event.objects.filter(primary_event=event).count() == 4
346 358
    assert 'Bookings exist after this date' in resp.text
347 359

  
348
    Booking.objects.all().delete()
349
    resp = app.get('/manage/agendas/%s/events/%s/edit' % (agenda.id, event.id))
350
    resp.form['repeat'] = ''
351
    resp = resp.form.submit()
352
    assert 'Recurrence end date makes no sense without repetition.' in resp.text
353

  
354 360

  
355 361
def test_booked_places(app, admin_user):
356 362
    agenda = Agenda(label=u'Foo bar')
......
444 450
def test_delete_recurring_event(app, admin_user, freezer):
445 451
    agenda = Agenda.objects.create(label='Foo bar', kind='events')
446 452
    start_datetime = now() + datetime.timedelta(days=10)
447
    event = Event.objects.create(start_datetime=start_datetime, places=10, agenda=agenda, repeat='weekly')
453
    event = Event.objects.create(
454
        start_datetime=start_datetime, places=10, agenda=agenda, recurrence_days=[start_datetime.weekday()]
455
    )
448 456

  
449 457
    app = login(app)
450 458
    resp = app.get('/manage/agendas/%s/settings' % agenda.id, status=200)
tests/test_agendas.py
1879 1879
    event = Event.objects.create(
1880 1880
        agenda=agenda,
1881 1881
        start_datetime=now(),
1882
        repeat='weekly',
1882
        recurrence_days=[now().weekday()],
1883 1883
        label='Event',
1884 1884
        places=10,
1885 1885
        waiting_list_places=10,
......
1898 1898

  
1899 1899
    event_json = event.export_json()
1900 1900
    first_event_json = first_event.export_json()
1901
    different_fields = ['slug', 'repeat', 'recurrence_rule']
1901
    different_fields = ['slug', 'recurrence_days', 'recurrence_week_interval']
1902 1902
    assert all(first_event_json[k] == event_json[k] for k in event_json if k not in different_fields)
1903 1903

  
1904 1904
    second_event = recurrences[1]
......
1922 1922
    freezer.move_to('2020-10-24 12:00')
1923 1923
    settings.TIME_ZONE = 'Europe/Brussels'
1924 1924
    agenda = Agenda.objects.create(label='Agenda', kind='events')
1925
    event = Event.objects.create(agenda=agenda, start_datetime=now(), repeat='weekly', places=5)
1925
    event = Event.objects.create(
1926
        agenda=agenda, start_datetime=now(), recurrence_days=[now().weekday()], places=5
1927
    )
1926 1928
    event.refresh_from_db()
1927 1929
    dt = localtime()
1928 1930
    recurrences = event.get_recurrences(dt, dt + datetime.timedelta(days=8))
......
1940 1942
    assert event_after_dst.slug == new_event_after_dst.slug
1941 1943

  
1942 1944

  
1943
def test_recurring_events_weekday_midnight(freezer):
1944
    freezer.move_to('2021-01-06 23:30')
1945
    weekday = localtime().weekday()
1946
    agenda = Agenda.objects.create(label='Agenda', kind='events')
1947
    event = Event.objects.create(agenda=agenda, start_datetime=now(), repeat='weekly', places=5)
1948

  
1949
    assert event.recurrence_rule['byweekday'][0] == weekday
1950

  
1951

  
1952
def test_recurring_events_repeat(freezer):
1945
def test_recurring_events_repetition(freezer):
1953 1946
    freezer.move_to('2021-01-06 12:00')  # Wednesday
1954 1947
    agenda = Agenda.objects.create(label='Agenda', kind='events')
1955 1948
    event = Event.objects.create(
1956 1949
        agenda=agenda,
1957 1950
        start_datetime=now(),
1958
        repeat='daily',
1951
        recurrence_days=list(range(7)),  # everyday
1959 1952
        places=5,
1960 1953
    )
1961 1954
    event.refresh_from_db()
......
1971 1964
    for i in range(len(recurrences) - 1):
1972 1965
        assert recurrences[i].start_datetime + datetime.timedelta(days=1) == recurrences[i + 1].start_datetime
1973 1966

  
1974
    event.repeat = 'weekdays'
1967
    event.recurrence_days = list(range(5))  # from Monday to Friday
1975 1968
    event.save()
1976 1969
    recurrences = event.get_recurrences(
1977 1970
        localtime() + datetime.timedelta(days=1), localtime() + datetime.timedelta(days=7)
......
1981 1974
    assert recurrences[1].start_datetime == start_datetime + datetime.timedelta(days=5)
1982 1975
    assert recurrences[-1].start_datetime == start_datetime + datetime.timedelta(days=7)
1983 1976

  
1984
    event.repeat = '2-weeks'
1977
    event.recurrence_days = [localtime(event.start_datetime).weekday()]  # from Monday to Friday
1978
    event.recurrence_week_interval = 2
1985 1979
    event.save()
1986 1980
    recurrences = event.get_recurrences(
1987 1981
        localtime() + datetime.timedelta(days=3), localtime() + datetime.timedelta(days=45)
......
1994 1988
            recurrences[i].start_datetime + datetime.timedelta(days=14) == recurrences[i + 1].start_datetime
1995 1989
        )
1996 1990

  
1991
    event.recurrence_days = [3]  # Tuesday but start_datetime is a Wednesday
1992
    event.recurrence_week_interval = 1
1993
    event.save()
1994
    recurrences = event.get_recurrences(localtime(), localtime() + datetime.timedelta(days=10))
1995
    assert len(recurrences) == 2
1996
    # no recurrence exist on Wednesday
1997
    assert all(localtime(r.start_datetime).weekday() == 3 for r in recurrences)
1998

  
1997 1999

  
1998 2000
@pytest.mark.freeze_time('2021-01-06')
1999 2001
def test_recurring_events_with_end_date():
......
2001 2003
    event = Event.objects.create(
2002 2004
        agenda=agenda,
2003 2005
        start_datetime=now(),
2004
        repeat='daily',
2006
        recurrence_days=list(range(7)),
2005 2007
        places=5,
2006 2008
        recurrence_end_date=(now() + datetime.timedelta(days=5)).date(),
2007 2009
    )
......
2019 2021
def test_recurring_events_sort(freezer):
2020 2022
    freezer.move_to('2021-01-06 12:00')  # Wednesday
2021 2023
    agenda = Agenda.objects.create(label='Agenda', kind='events')
2022
    Event.objects.create(agenda=agenda, slug='a', start_datetime=now(), repeat='daily', places=5)
2023
    Event.objects.create(agenda=agenda, slug='b', start_datetime=now(), repeat='daily', duration=10, places=5)
2024
    Event.objects.create(agenda=agenda, slug='c', start_datetime=now(), repeat='daily', duration=5, places=5)
2025 2024
    Event.objects.create(
2026
        agenda=agenda, slug='d', start_datetime=now() + datetime.timedelta(hours=1), repeat='daily', places=5
2025
        agenda=agenda, slug='a', start_datetime=now(), recurrence_days=list(range(7)), places=5
2026
    )
2027
    Event.objects.create(
2028
        agenda=agenda, slug='b', start_datetime=now(), recurrence_days=list(range(7)), duration=10, places=5
2029
    )
2030
    Event.objects.create(
2031
        agenda=agenda, slug='c', start_datetime=now(), recurrence_days=list(range(7)), duration=5, places=5
2032
    )
2033
    Event.objects.create(
2034
        agenda=agenda,
2035
        slug='d',
2036
        start_datetime=now() + datetime.timedelta(hours=1),
2037
        recurrence_days=list(range(7)),
2038
        places=5,
2027 2039
    )
2028 2040

  
2029 2041
    events = agenda.get_open_events()[:8]
......
2043 2055
    event = Event.objects.create(
2044 2056
        agenda=agenda,
2045 2057
        start_datetime=now(),
2046
        repeat='daily',
2058
        recurrence_days=list(range(7)),
2047 2059
        places=5,
2048 2060
    )
2049 2061
    event.refresh_from_db()
......
2116 2128
    daily_event = Event.objects.create(
2117 2129
        agenda=agenda,
2118 2130
        start_datetime=now(),
2119
        repeat='daily',
2131
        recurrence_days=list(range(7)),
2120 2132
        places=5,
2121 2133
        recurrence_end_date=datetime.date(year=2021, month=5, day=8),
2122 2134
    )
2123 2135
    weekly_event = Event.objects.create(
2124 2136
        agenda=agenda,
2125 2137
        start_datetime=now(),
2126
        repeat='weekly',
2138
        recurrence_days=[now().weekday()],
2127 2139
        places=5,
2128 2140
        recurrence_end_date=datetime.date(year=2021, month=6, day=1),
2129 2141
    )
......
2132 2144
    daily_event_no_end_date = Event.objects.create(
2133 2145
        agenda=agenda,
2134 2146
        start_datetime=now() + datetime.timedelta(hours=2),
2135
        repeat='daily',
2147
        recurrence_days=list(range(7)),
2136 2148
        places=5,
2137 2149
    )
2138 2150
    daily_event_no_end_date.refresh_from_db()
......
2169 2181
    assert Booking.objects.count() == 1
2170 2182
    assert Event.objects.filter(primary_event=daily_event_no_end_date).count() == 1
2171 2183
    assert agenda.recurrence_exceptions_report.events.get() == event
2184

  
2185

  
2186
def test_recurring_events_display(freezer):
2187
    freezer.move_to('2021-01-06 12:30')
2188
    agenda = Agenda.objects.create(label='Agenda', kind='events')
2189
    event = Event.objects.create(
2190
        agenda=agenda, start_datetime=now(), recurrence_days=list(range(7)), places=5
2191
    )
2192

  
2193
    assert event.get_recurrence_display() == 'Daily at 1:30 p.m.'
2194

  
2195
    event.recurrence_days = [1, 2, 3, 4]
2196
    event.save()
2197
    assert event.get_recurrence_display() == 'From Tuesday to Friday at 1:30 p.m.'
2198

  
2199
    event.recurrence_days = [4, 5, 6]
2200
    event.save()
2201
    assert event.get_recurrence_display() == 'From Friday to Sunday at 1:30 p.m.'
2202

  
2203
    event.recurrence_days = [1, 4, 6]
2204
    event.save()
2205
    assert event.get_recurrence_display() == 'On Tuesday, Friday, Sunday at 1:30 p.m.'
2206

  
2207
    event.recurrence_days = [0]
2208
    event.recurrence_week_interval = 2
2209
    event.save()
2210
    assert event.get_recurrence_display() == 'On Monday at 1:30 p.m., once every two weeks'
2211

  
2212
    event.recurrence_week_interval = 3
2213
    event.recurrence_end_date = now() + datetime.timedelta(days=7)
2214
    event.save()
2215
    assert (
2216
        event.get_recurrence_display()
2217
        == 'On Monday at 1:30 p.m., once every three weeks, until Jan. 13, 2021'
2218
    )
tests/test_ensure_jsonbfields.py
12 12
    json_fields = (
13 13
        'extra_data',
14 14
        'booking_errors',
15
        'recurrence_rule',
16 15
    )
17 16

  
18 17
    with connection.cursor() as cursor:
......
34 33
            '''ALTER TABLE agendas_eventcancellationreport
35 34
              ALTER COLUMN booking_errors TYPE text USING booking_errors::text'''
36 35
        )
37
        cursor.execute(
38
            '''ALTER TABLE agendas_event
39
              ALTER COLUMN recurrence_rule TYPE text USING recurrence_rule::text'''
40
        )
41 36

  
42 37
    call_command('ensure_jsonb')
43 38

  
tests/test_import_export.py
214 214
    event = Event.objects.create(
215 215
        agenda=agenda,
216 216
        start_datetime=now(),
217
        repeat='daily',
217
        recurrence_days=list(range(7)),
218
        recurrence_week_interval=2,
218 219
        places=10,
219 220
        slug='test',
220 221
    )
......
235 236
    assert Event.objects.count() == 1
236 237
    event = Agenda.objects.get(label='Foo Bar').event_set.first()
237 238
    assert event.primary_event is None
238
    assert event.repeat == 'daily'
239
    assert event.recurrence_rule == {'freq': DAILY}
239
    assert event.recurrence_days == list(range(7))
240
    assert event.recurrence_week_interval == 2
240 241

  
241 242
    # importing event with end recurrence date creates recurrences
242 243
    event.recurrence_end_date = now() + datetime.timedelta(days=7)
244
    event.recurrence_week_interval = 1
243 245
    event.save()
244 246
    output = get_output_of_command('export_site')
245 247
    import_site(data={}, clean=True)
246
-