0001-manager-add-more-granular-control-over-event-recurre.patch
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/0083_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', '0082_text_to_jsonb'), |
|
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 | ||
---|---|---|
592 | 592 |
entries = self.prefetched_events |
593 | 593 |
else: |
594 | 594 |
# recurring events are never opened |
595 |
entries = self.event_set.filter(recurrence_rule__isnull=True)
|
|
595 |
entries = self.event_set.filter(recurrence_days__isnull=True)
|
|
596 | 596 |
# exclude canceled events except for event recurrences |
597 | 597 |
entries = entries.filter(Q(cancelled=False) | Q(primary_event__isnull=False)) |
598 | 598 |
# we never want to allow booking for past events. |
... | ... | |
661 | 661 |
recurring_events = self.prefetched_recurring_events |
662 | 662 |
exceptions = self.prefetched_exceptions |
663 | 663 |
else: |
664 |
recurring_events = self.event_set.filter(recurrence_rule__isnull=False)
|
|
664 |
recurring_events = self.event_set.filter(recurrence_days__isnull=False)
|
|
665 | 665 |
exceptions = self.get_recurrence_exceptions(min_start, max_start) |
666 | 666 | |
667 | 667 |
for event in recurring_events: |
... | ... | |
678 | 678 | |
679 | 679 |
@transaction.atomic |
680 | 680 |
def update_event_recurrences(self): |
681 |
recurring_events = self.event_set.filter(recurrence_rule__isnull=False)
|
|
681 |
recurring_events = self.event_set.filter(recurrence_days__isnull=False)
|
|
682 | 682 |
recurrences = self.event_set.filter(primary_event__isnull=False) |
683 | 683 | |
684 | 684 |
# remove recurrences |
... | ... | |
1140 | 1140 | |
1141 | 1141 | |
1142 | 1142 |
class Event(models.Model): |
1143 |
REPEAT_CHOICES = [ |
|
1144 |
('daily', _('Daily')), |
|
1145 |
('weekly', _('Weekly')), |
|
1146 |
('2-weeks', _('Once every two weeks')), |
|
1147 |
('weekdays', _('Every weekdays (Monday to Friday)')), |
|
1143 |
WEEKDAY_CHOICES = [ |
|
1144 |
(0, _('Mo')), |
|
1145 |
(1, _('Tu')), |
|
1146 |
(2, _('We')), |
|
1147 |
(3, _('Th')), |
|
1148 |
(4, _('Fr')), |
|
1149 |
(5, _('Sa')), |
|
1150 |
(6, _('Su')), |
|
1151 |
] |
|
1152 | ||
1153 |
INTERVAL_CHOICES = [ |
|
1154 |
(1, 'Every week'), |
|
1155 |
(2, 'Every two weeks'), |
|
1156 |
(3, 'Every three weeks'), |
|
1148 | 1157 |
] |
1149 | 1158 | |
1150 | 1159 |
agenda = models.ForeignKey(Agenda, on_delete=models.CASCADE) |
1151 | 1160 |
start_datetime = models.DateTimeField(_('Date/time')) |
1152 |
repeat = models.CharField(_('Repeat'), max_length=16, blank=True, choices=REPEAT_CHOICES) |
|
1153 |
recurrence_rule = JSONField(_('Recurrence rule'), null=True, blank=True) |
|
1161 |
recurrence_days = ArrayField( |
|
1162 |
models.IntegerField(choices=WEEKDAY_CHOICES), |
|
1163 |
verbose_name=_('Recurrence days'), |
|
1164 |
blank=True, |
|
1165 |
null=True, |
|
1166 |
) |
|
1167 |
recurrence_week_interval = models.IntegerField(_('Repeat'), choices=INTERVAL_CHOICES, default=1) |
|
1154 | 1168 |
recurrence_end_date = models.DateField(_('Recurrence end date'), null=True, blank=True) |
1155 | 1169 |
primary_event = models.ForeignKey('self', null=True, on_delete=models.CASCADE, related_name='recurrences') |
1156 | 1170 |
duration = models.PositiveIntegerField(_('Duration (in minutes)'), default=None, null=True, blank=True) |
... | ... | |
1205 | 1219 |
self.check_full() |
1206 | 1220 |
if not self.slug: |
1207 | 1221 |
self.slug = generate_slug(self, seen_slugs=seen_slugs, agenda=self.agenda) |
1208 |
self.recurrence_rule = self.get_recurrence_rule() |
|
1209 | 1222 |
return super(Event, self).save(*args, **kwargs) |
1210 | 1223 | |
1211 | 1224 |
@property |
... | ... | |
1229 | 1242 |
return False |
1230 | 1243 |
if self.agenda.maximal_booking_delay and self.start_datetime > self.agenda.max_booking_datetime: |
1231 | 1244 |
return False |
1232 |
if self.recurrence_rule is not None:
|
|
1245 |
if self.recurrence_days is not None:
|
|
1233 | 1246 |
# bookable recurrences probably exist |
1234 | 1247 |
return True |
1235 | 1248 |
if self.agenda.minimal_booking_delay and self.start_datetime < self.agenda.min_booking_datetime: |
... | ... | |
1352 | 1365 |
else: |
1353 | 1366 |
event = cls(**data) |
1354 | 1367 |
event.save() |
1355 |
if event.recurrence_rule and event.recurrence_end_date:
|
|
1368 |
if event.recurrence_days and event.recurrence_end_date:
|
|
1356 | 1369 |
event.refresh_from_db() |
1357 | 1370 |
event.recurrences.filter(start_datetime__gt=event.recurrence_end_date).delete() |
1358 | 1371 |
update_fields = { |
... | ... | |
1381 | 1394 |
return { |
1382 | 1395 |
'start_datetime': make_naive(self.start_datetime).strftime('%Y-%m-%d %H:%M:%S'), |
1383 | 1396 |
'publication_date': self.publication_date.strftime('%Y-%m-%d') if self.publication_date else None, |
1384 |
'repeat': self.repeat,
|
|
1385 |
'recurrence_rule': self.recurrence_rule,
|
|
1397 |
'recurrence_days': self.recurrence_days,
|
|
1398 |
'recurrence_week_interval': self.recurrence_week_interval,
|
|
1386 | 1399 |
'recurrence_end_date': recurrence_end_date, |
1387 | 1400 |
'places': self.places, |
1388 | 1401 |
'waiting_list_places': self.waiting_list_places, |
... | ... | |
1475 | 1488 | |
1476 | 1489 |
if self.publication_date and self.publication_date > min_datetime.date(): |
1477 | 1490 |
min_datetime = make_aware(datetime.datetime.combine(self.publication_date, datetime.time(0, 0))) |
1478 |
if self.recurrence_end_date: |
|
1479 |
self.recurrence_rule['until'] = datetime.datetime.combine( |
|
1480 |
self.recurrence_end_date, datetime.time(0, 0) |
|
1481 |
) |
|
1482 | 1491 | |
1483 | 1492 |
# remove pytz info because dateutil doesn't support DST changes |
1484 | 1493 |
min_datetime = make_naive(min_datetime) |
... | ... | |
1500 | 1509 |
return recurrences |
1501 | 1510 | |
1502 | 1511 |
def get_recurrence_display(self): |
1503 |
repeat = str(self.get_repeat_display()) |
|
1504 | 1512 |
time = date_format(localtime(self.start_datetime), 'TIME_FORMAT') |
1505 |
if self.repeat in ('weekly', '2-weeks'): |
|
1506 |
day = date_format(localtime(self.start_datetime), 'l') |
|
1507 |
return _('%(every_x_days)s on %(day)s at %(time)s') % { |
|
1508 |
'every_x_days': repeat, |
|
1509 |
'day': day, |
|
1510 |
'time': time, |
|
1513 | ||
1514 |
days_count = len(self.recurrence_days) |
|
1515 |
if days_count == 7: |
|
1516 |
repeat = _('Daily') |
|
1517 |
elif days_count > 1 and (self.recurrence_days[-1] - self.recurrence_days[0]) == days_count - 1: |
|
1518 |
# days are contiguous |
|
1519 |
repeat = _('From %(weekday)s to %(last_weekday)s') % { |
|
1520 |
'weekday': str(WEEKDAYS[self.recurrence_days[0]]), |
|
1521 |
'last_weekday': str(WEEKDAYS[self.recurrence_days[-1]]), |
|
1511 | 1522 |
} |
1512 | 1523 |
else: |
1513 |
return _('%(every_x_days)s at %(time)s') % {'every_x_days': repeat, 'time': time} |
|
1514 | ||
1515 |
def get_recurrence_rule(self): |
|
1516 |
rrule = {} |
|
1517 |
if self.repeat == 'daily': |
|
1518 |
rrule['freq'] = DAILY |
|
1519 |
elif self.repeat == 'weekly': |
|
1520 |
rrule['freq'] = WEEKLY |
|
1521 |
rrule['byweekday'] = [localtime(self.start_datetime).weekday()] |
|
1522 |
elif self.repeat == '2-weeks': |
|
1523 |
rrule['freq'] = WEEKLY |
|
1524 |
rrule['byweekday'] = [localtime(self.start_datetime).weekday()] |
|
1525 |
rrule['interval'] = 2 |
|
1526 |
elif self.repeat == 'weekdays': |
|
1527 |
rrule['freq'] = WEEKLY |
|
1528 |
rrule['byweekday'] = [i for i in range(5)] |
|
1529 |
else: |
|
1530 |
return None |
|
1531 |
return rrule |
|
1524 |
repeat = _('On %(weekdays)s') % { |
|
1525 |
'weekdays': ', '.join([str(WEEKDAYS[i]) for i in self.recurrence_days]) |
|
1526 |
} |
|
1527 | ||
1528 |
recurrence_display = _('%(On_day_x)s at %(time)s') % {'On_day_x': repeat, 'time': time} |
|
1529 | ||
1530 |
if self.recurrence_week_interval > 1: |
|
1531 |
if self.recurrence_week_interval == 2: |
|
1532 |
every_x_weeks = _('every two weeks') |
|
1533 |
elif self.recurrence_week_interval == 3: |
|
1534 |
every_x_weeks = _('every three weeks') |
|
1535 |
recurrence_display = _('%(Every_x_days)s, once %(every_x_weeks)s') % { |
|
1536 |
'Every_x_days': recurrence_display, |
|
1537 |
'every_x_weeks': every_x_weeks, |
|
1538 |
} |
|
1539 | ||
1540 |
if self.recurrence_end_date: |
|
1541 |
end_date = date_format(self.recurrence_end_date, 'DATE_FORMAT') |
|
1542 |
recurrence_display = _('%(Every_x_days)s, until %(date)s') % { |
|
1543 |
'Every_x_days': recurrence_display, |
|
1544 |
'date': end_date, |
|
1545 |
} |
|
1546 |
return recurrence_display |
|
1547 | ||
1548 |
@property |
|
1549 |
def recurrence_rule(self): |
|
1550 |
recurrence_rule = { |
|
1551 |
'freq': WEEKLY, |
|
1552 |
'byweekday': self.recurrence_days, |
|
1553 |
'interval': self.recurrence_week_interval, |
|
1554 |
} |
|
1555 |
if self.recurrence_end_date: |
|
1556 |
recurrence_rule['until'] = datetime.datetime.combine( |
|
1557 |
self.recurrence_end_date, datetime.time(0, 0) |
|
1558 |
) |
|
1559 |
return recurrence_rule |
|
1532 | 1560 | |
1533 | 1561 |
def has_recurrences_booked(self, after=None): |
1534 | 1562 |
return Booking.objects.filter( |
chrono/api/views.py | ||
---|---|---|
564 | 564 |
cancelled=False, |
565 | 565 |
start_datetime__gte=localtime(now()), |
566 | 566 |
).order_by() |
567 |
recurring_event_queryset = Event.objects.filter(recurrence_rule__isnull=False)
|
|
567 |
recurring_event_queryset = Event.objects.filter(recurrence_days__isnull=False)
|
|
568 | 568 |
exceptions_desk = Desk.objects.filter(slug='_exceptions_holder').prefetch_related( |
569 | 569 |
'unavailability_calendars' |
570 | 570 |
) |
chrono/manager/forms.py | ||
---|---|---|
50 | 50 |
) |
51 | 51 | |
52 | 52 |
from . import widgets |
53 |
from .widgets import SplitDateTimeField |
|
53 |
from .widgets import SplitDateTimeField, WeekdaysWidget
|
|
54 | 54 | |
55 | 55 | |
56 | 56 |
class AgendaAddForm(forms.ModelForm): |
... | ... | |
141 | 141 | |
142 | 142 | |
143 | 143 |
class NewEventForm(forms.ModelForm): |
144 |
frequency = forms.ChoiceField( |
|
145 |
label=_('Event frequency'), |
|
146 |
widget=forms.RadioSelect, |
|
147 |
choices=( |
|
148 |
('unique', _('Unique')), |
|
149 |
('recurring', _('Recurring')), |
|
150 |
), |
|
151 |
initial='unique', |
|
152 |
) |
|
153 |
recurrence_days = forms.TypedMultipleChoiceField( |
|
154 |
choices=Event.WEEKDAY_CHOICES, coerce=int, required=False, widget=WeekdaysWidget |
|
155 |
) |
|
156 | ||
144 | 157 |
class Meta: |
145 | 158 |
model = Event |
146 | 159 |
fields = [ |
147 | 160 |
'label', |
148 | 161 |
'start_datetime', |
149 |
'repeat', |
|
162 |
'frequency', |
|
163 |
'recurrence_days', |
|
164 |
'recurrence_week_interval', |
|
165 |
'recurrence_end_date', |
|
150 | 166 |
'duration', |
151 | 167 |
'places', |
152 | 168 |
] |
153 | 169 |
field_classes = { |
154 | 170 |
'start_datetime': SplitDateTimeField, |
155 | 171 |
} |
172 |
widgets = { |
|
173 |
'recurrence_end_date': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'), |
|
174 |
} |
|
175 | ||
176 |
def clean_recurrence_days(self): |
|
177 |
recurrence_days = self.cleaned_data['recurrence_days'] |
|
178 |
if recurrence_days == []: |
|
179 |
return None |
|
180 |
return recurrence_days |
|
156 | 181 | |
157 | 182 | |
158 |
class EventForm(forms.ModelForm): |
|
159 |
protected_fields = ('repeat', 'slug', 'start_datetime') |
|
183 |
class EventForm(NewEventForm): |
|
184 |
protected_fields = ( |
|
185 |
'slug', |
|
186 |
'start_datetime', |
|
187 |
'frequency', |
|
188 |
'recurrence_days', |
|
189 |
'recurrence_week_interval', |
|
190 |
) |
|
160 | 191 | |
161 | 192 |
class Meta: |
162 | 193 |
model = Event |
... | ... | |
168 | 199 |
'label', |
169 | 200 |
'slug', |
170 | 201 |
'start_datetime', |
171 |
'repeat', |
|
202 |
'frequency', |
|
203 |
'recurrence_days', |
|
204 |
'recurrence_week_interval', |
|
172 | 205 |
'recurrence_end_date', |
173 | 206 |
'duration', |
174 | 207 |
'publication_date', |
... | ... | |
184 | 217 | |
185 | 218 |
def __init__(self, *args, **kwargs): |
186 | 219 |
super().__init__(*args, **kwargs) |
187 |
if self.instance.recurrence_rule and self.instance.has_recurrences_booked(): |
|
220 |
self.fields['frequency'].initial = 'recurring' if self.instance.recurrence_days else 'unique' |
|
221 |
if self.instance.recurrence_days and self.instance.has_recurrences_booked(): |
|
188 | 222 |
for field in self.protected_fields: |
189 | 223 |
self.fields[field].disabled = True |
190 | 224 |
self.fields[field].help_text = _( |
191 | 225 |
'This field cannot be modified because some recurrences have bookings attached to them.' |
192 | 226 |
) |
193 | 227 |
if self.instance.primary_event: |
194 |
for field in ('slug', 'repeat', 'recurrence_end_date', 'publication_date'): |
|
228 |
for field in ( |
|
229 |
'slug', |
|
230 |
'recurrence_end_date', |
|
231 |
'publication_date', |
|
232 |
'frequency', |
|
233 |
'recurrence_days', |
|
234 |
'recurrence_week_interval', |
|
235 |
): |
|
195 | 236 |
del self.fields[field] |
196 | 237 | |
197 | 238 |
def clean(self): |
... | ... | |
200 | 241 |
after=self.cleaned_data['recurrence_end_date'] |
201 | 242 |
): |
202 | 243 |
raise ValidationError(_('Bookings exist after this date.')) |
203 |
if self.cleaned_data.get('recurrence_end_date') and not self.cleaned_data.get('repeat'): |
|
204 |
raise ValidationError(_('Recurrence end date makes no sense without repetition.')) |
|
244 | ||
245 |
if self.cleaned_data.get('frequency') == 'unique': |
|
246 |
self.cleaned_data['recurrence_days'] = None |
|
247 |
self.cleaned_data['recurrence_end_date'] = None |
|
205 | 248 | |
206 | 249 |
def save(self, *args, **kwargs): |
207 | 250 |
with transaction.atomic(): |
208 | 251 |
if any(field for field in self.changed_data if field in self.protected_fields): |
209 | 252 |
self.instance.recurrences.all().delete() |
210 |
elif self.instance.recurrence_rule: |
|
253 |
elif self.instance.recurrence_days: |
|
254 |
protected_fields = list(self.protected_fields) + ['recurrence_end_date', 'frequency'] |
|
211 | 255 |
update_fields = { |
212 | 256 |
field: value |
213 | 257 |
for field, value in self.cleaned_data.items() |
214 |
if field != 'recurrence_end_date' and field not in self.protected_fields
|
|
258 |
if field not in protected_fields
|
|
215 | 259 |
} |
216 | 260 |
self.instance.recurrences.update(**update_fields) |
217 | 261 |
chrono/manager/static/css/style.scss | ||
---|---|---|
411 | 411 |
background-color: $color; |
412 | 412 |
} |
413 | 413 |
} |
414 | ||
415 |
form div.widget[id^=id_recurrence] { |
|
416 |
padding-left: 1em; |
|
417 |
} |
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 | ||
---|---|---|
1049 | 1049 | |
1050 | 1050 |
def get_queryset(self): |
1051 | 1051 |
if self.agenda.kind == 'events': |
1052 |
queryset = self.agenda.event_set.filter(recurrence_rule__isnull=True)
|
|
1052 |
queryset = self.agenda.event_set.filter(recurrence_days__isnull=True)
|
|
1053 | 1053 |
else: |
1054 | 1054 |
self.agenda.prefetch_desks_and_exceptions() |
1055 | 1055 |
if self.agenda.kind == 'meetings': |
... | ... | |
1578 | 1578 |
if self.agenda.kind == 'events': |
1579 | 1579 |
context['has_absence_reasons'] = AbsenceReasonGroup.objects.exists() |
1580 | 1580 |
context['has_recurring_events'] = self.agenda.event_set.filter( |
1581 |
recurrence_rule__isnull=False
|
|
1581 |
recurrence_days__isnull=False
|
|
1582 | 1582 |
).exists() |
1583 | 1583 |
desk = Desk.objects.get(agenda=self.agenda, slug='_exceptions_holder') |
1584 | 1584 |
context['exceptions'] = TimePeriodException.objects.filter( |
... | ... | |
1860 | 1860 |
pk_url_kwarg = 'event_pk' |
1861 | 1861 | |
1862 | 1862 |
def dispatch(self, request, *args, **kwargs): |
1863 |
if self.get_object().recurrence_rule:
|
|
1863 |
if self.get_object().recurrence_days:
|
|
1864 | 1864 |
raise Http404('this view makes no sense for recurring events') |
1865 | 1865 |
return super().dispatch(request, *args, **kwargs) |
1866 | 1866 | |
... | ... | |
1895 | 1895 |
if ( |
1896 | 1896 |
self.request.GET.get('next') == 'settings' |
1897 | 1897 |
or self.request.POST.get('next') == 'settings' |
1898 |
or self.object.recurrence_rule
|
|
1898 |
or self.object.recurrence_days
|
|
1899 | 1899 |
): |
1900 | 1900 |
return reverse('chrono-manager-agenda-settings', kwargs={'pk': self.agenda.id}) |
1901 | 1901 |
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 SplitDateTimeWidget, TimeInput, CheckboxSelectMultiple
|
|
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/manager/test_all.py | ||
---|---|---|
2607 | 2607 |
Event.objects.create( |
2608 | 2608 |
label='xyz', start_datetime=localtime().replace(day=11, month=11, year=2020), places=10, agenda=agenda |
2609 | 2609 |
) |
2610 |
recurring_start_datetime = localtime().replace(day=4, month=11, year=2020) |
|
2610 | 2611 |
event = Event.objects.create( |
2611 | 2612 |
label='abc', |
2612 |
start_datetime=localtime().replace(day=4, month=11, year=2020),
|
|
2613 |
start_datetime=recurring_start_datetime,
|
|
2613 | 2614 |
places=10, |
2614 | 2615 |
agenda=agenda, |
2615 |
repeat='weekly',
|
|
2616 |
recurrence_days=[recurring_start_datetime.weekday()],
|
|
2616 | 2617 |
) |
2617 | 2618 | |
2618 | 2619 |
with CaptureQueriesContext(connection) as ctx: |
... | ... | |
2635 | 2636 |
# create another event with recurrence, the same day/time |
2636 | 2637 |
start_datetime = localtime().replace(day=4, month=11, year=2020) |
2637 | 2638 |
event = Event.objects.create( |
2638 |
label='def', start_datetime=start_datetime, places=10, agenda=agenda, repeat='weekly' |
|
2639 |
label='def', |
|
2640 |
start_datetime=start_datetime, |
|
2641 |
places=10, |
|
2642 |
agenda=agenda, |
|
2643 |
recurrence_days=[start_datetime.weekday()], |
|
2639 | 2644 |
) |
2640 | 2645 |
resp = app.get('/manage/agendas/%s/2020/11/11/' % agenda.pk) |
2641 | 2646 |
# the event occurence in DB does not hide recurrence of the second recurrent event |
... | ... | |
2675 | 2680 |
# add recurring event on every Wednesday |
2676 | 2681 |
start_datetime = localtime().replace(day=4, month=11, year=2020) |
2677 | 2682 |
event = Event.objects.create( |
2678 |
label='abc', start_datetime=start_datetime, places=10, agenda=agenda, repeat='weekly' |
|
2683 |
label='abc', |
|
2684 |
start_datetime=start_datetime, |
|
2685 |
places=10, |
|
2686 |
agenda=agenda, |
|
2687 |
recurrence_days=[start_datetime.weekday()], |
|
2679 | 2688 |
) |
2680 | 2689 | |
2681 | 2690 |
with CaptureQueriesContext(connection) as ctx: |
... | ... | |
2715 | 2724 |
# create another event with recurrence, the same day/time |
2716 | 2725 |
start_datetime = localtime().replace(day=4, month=11, year=2020) |
2717 | 2726 |
event = Event.objects.create( |
2718 |
label='def', start_datetime=start_datetime, places=10, agenda=agenda, repeat='weekly' |
|
2727 |
label='def', |
|
2728 |
start_datetime=start_datetime, |
|
2729 |
places=10, |
|
2730 |
agenda=agenda, |
|
2731 |
recurrence_days=[start_datetime.weekday()], |
|
2719 | 2732 |
) |
2720 | 2733 |
resp = app.get('/manage/agendas/%s/%s/%s/' % (agenda.id, 2020, 12)) |
2721 | 2734 |
# the event occurence in DB does not hide recurrence of the second recurrent event |
... | ... | |
2798 | 2811 |
places=42, |
2799 | 2812 |
) |
2800 | 2813 |
# weekly recurring event, first recurrence is in the past but second is in range |
2814 |
start_datetime = now() - datetime.timedelta(days=3) |
|
2801 | 2815 |
event = Event.objects.create( |
2802 | 2816 |
label='event G', |
2803 |
start_datetime=now() - datetime.timedelta(days=3),
|
|
2817 |
start_datetime=start_datetime,
|
|
2804 | 2818 |
places=10, |
2805 | 2819 |
agenda=agenda, |
2806 |
repeat='weekly',
|
|
2820 |
recurrence_days=[start_datetime.weekday()],
|
|
2807 | 2821 |
) |
2808 | 2822 |
resp = app.get('/manage/agendas/%s/events/open/' % agenda.pk) |
2809 | 2823 |
assert 'event A' not in resp.text |
... | ... | |
4449 | 4463 |
resp = app.get('/manage/agendas/%s/settings' % agenda.id) |
4450 | 4464 |
assert not 'Recurrence exceptions' in resp.text |
4451 | 4465 | |
4452 |
event.repeat = 'daily'
|
|
4466 |
event.recurrence_days = list(range(7))
|
|
4453 | 4467 |
event.save() |
4454 | 4468 | |
4455 | 4469 |
resp = app.get('/manage/agendas/%s/%s/%s/' % (agenda.id, 2021, 7)) |
... | ... | |
4493 | 4507 |
event = Event.objects.create( |
4494 | 4508 |
start_datetime=now(), |
4495 | 4509 |
places=10, |
4496 |
repeat='daily',
|
|
4510 |
recurrence_days=list(range(7)),
|
|
4497 | 4511 |
recurrence_end_date=now() + datetime.timedelta(days=30), |
4498 | 4512 |
agenda=agenda, |
4499 | 4513 |
) |
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 |
|
255 |
assert not event_recurrence.recurrence_week_interval |
|
254 | 256 | |
255 | 257 |
# changing recurrence attribute removes event recurrences |
256 | 258 |
resp = app.get('/manage/agendas/%s/events/%s/edit' % (agenda.id, event.id)) |
257 |
resp.form['repeat'] = ''
|
|
259 |
resp.form['frequency'] = 'unique'
|
|
258 | 260 |
resp = resp.form.submit().follow() |
259 | 261 |
assert not Event.objects.filter(primary_event=event).exists() |
260 | 262 | |
... | ... | |
272 | 274 |
event_recurrence = event.get_or_create_event_recurrence(event.start_datetime + datetime.timedelta(days=7)) |
273 | 275 |
Booking.objects.create(event=event_recurrence) |
274 | 276 |
resp = app.get('/manage/agendas/%s/events/%s/edit' % (agenda.id, event.id)) |
275 |
assert 'disabled' in resp.form['repeat'].attrs |
|
277 |
assert 'disabled' in resp.form['frequency'].attrs |
|
278 |
assert all('disabled' in resp.form.get('recurrence_days', index=i).attrs for i in range(7)) |
|
279 |
assert 'disabled' in resp.form['recurrence_week_interval'].attrs |
|
276 | 280 |
assert 'disabled' in resp.form['slug'].attrs |
277 | 281 |
assert 'disabled' in resp.form['start_datetime_0'].attrs |
278 | 282 |
assert 'disabled' in resp.form['start_datetime_1'].attrs |
... | ... | |
287 | 291 |
assert 'Delete' not in resp.text |
288 | 292 | |
289 | 293 |
resp = resp.click('Options') |
290 |
assert {'slug', 'repeat', 'recurrence_end_date', 'publication_date'}.isdisjoint(resp.form.fields) |
|
294 |
assert { |
|
295 |
'slug', |
|
296 |
'frequency', |
|
297 |
'recurrence_days', |
|
298 |
'recurence_weekly_interval', |
|
299 |
'recurrence_end_date', |
|
300 |
'publication_date', |
|
301 |
}.isdisjoint(resp.form.fields) |
|
291 | 302 | |
292 | 303 | |
293 | 304 |
def test_edit_recurring_event_with_end_date(settings, app, admin_user, freezer): |
294 | 305 |
freezer.move_to('2021-01-12 12:10') |
295 | 306 |
agenda = Agenda.objects.create(label='Foo bar', kind='events') |
296 |
event = Event.objects.create(start_datetime=now(), places=10, repeat='daily', agenda=agenda) |
|
307 |
event = Event.objects.create( |
|
308 |
start_datetime=now(), places=10, recurrence_days=list(range(7)), agenda=agenda |
|
309 |
) |
|
297 | 310 | |
298 | 311 |
app = login(app) |
299 | 312 |
resp = app.get('/manage/agendas/%s/events/%s/edit' % (agenda.id, event.id)) |
... | ... | |
301 | 314 |
resp = resp.form.submit() |
302 | 315 | |
303 | 316 |
# recurrences are created automatically |
304 |
event = Event.objects.get(recurrence_rule__isnull=False)
|
|
317 |
event = Event.objects.get(recurrence_days__isnull=False)
|
|
305 | 318 |
assert Event.objects.filter(primary_event=event).count() == 5 |
306 | 319 |
assert Event.objects.filter(primary_event=event, start_datetime=now()).exists() |
307 | 320 | |
... | ... | |
345 | 358 |
assert Event.objects.filter(primary_event=event).count() == 4 |
346 | 359 |
assert 'Bookings exist after this date' in resp.text |
347 | 360 | |
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 | 361 | |
355 | 362 |
def test_booked_places(app, admin_user): |
356 | 363 |
agenda = Agenda(label=u'Foo bar') |
... | ... | |
444 | 451 |
def test_delete_recurring_event(app, admin_user, freezer): |
445 | 452 |
agenda = Agenda.objects.create(label='Foo bar', kind='events') |
446 | 453 |
start_datetime = now() + datetime.timedelta(days=10) |
447 |
event = Event.objects.create(start_datetime=start_datetime, places=10, agenda=agenda, repeat='weekly') |
|
454 |
event = Event.objects.create( |
|
455 |
start_datetime=start_datetime, places=10, agenda=agenda, recurrence_days=[start_datetime.weekday()] |
|
456 |
) |
|
448 | 457 | |
449 | 458 |
app = login(app) |
450 | 459 |
resp = app.get('/manage/agendas/%s/settings' % agenda.id, status=200) |
tests/test_agendas.py | ||
---|---|---|
1857 | 1857 |
event = Event.objects.create( |
1858 | 1858 |
agenda=agenda, |
1859 | 1859 |
start_datetime=now(), |
1860 |
repeat='weekly',
|
|
1860 |
recurrence_days=[now().weekday()],
|
|
1861 | 1861 |
label='Event', |
1862 | 1862 |
places=10, |
1863 | 1863 |
waiting_list_places=10, |
... | ... | |
1876 | 1876 | |
1877 | 1877 |
event_json = event.export_json() |
1878 | 1878 |
first_event_json = first_event.export_json() |
1879 |
different_fields = ['slug', 'repeat', 'recurrence_rule']
|
|
1879 |
different_fields = ['slug', 'recurrence_days', 'recurrence_week_interval']
|
|
1880 | 1880 |
assert all(first_event_json[k] == event_json[k] for k in event_json if k not in different_fields) |
1881 | 1881 | |
1882 | 1882 |
second_event = recurrences[1] |
... | ... | |
1900 | 1900 |
freezer.move_to('2020-10-24 12:00') |
1901 | 1901 |
settings.TIME_ZONE = 'Europe/Brussels' |
1902 | 1902 |
agenda = Agenda.objects.create(label='Agenda', kind='events') |
1903 |
event = Event.objects.create(agenda=agenda, start_datetime=now(), repeat='weekly', places=5) |
|
1903 |
event = Event.objects.create( |
|
1904 |
agenda=agenda, start_datetime=now(), recurrence_days=[now().weekday()], places=5 |
|
1905 |
) |
|
1904 | 1906 |
event.refresh_from_db() |
1905 | 1907 |
dt = localtime() |
1906 | 1908 |
recurrences = event.get_recurrences(dt, dt + datetime.timedelta(days=8)) |
... | ... | |
1918 | 1920 |
assert event_after_dst.slug == new_event_after_dst.slug |
1919 | 1921 | |
1920 | 1922 | |
1921 |
def test_recurring_events_weekday_midnight(freezer): |
|
1922 |
freezer.move_to('2021-01-06 23:30') |
|
1923 |
weekday = localtime().weekday() |
|
1924 |
agenda = Agenda.objects.create(label='Agenda', kind='events') |
|
1925 |
event = Event.objects.create(agenda=agenda, start_datetime=now(), repeat='weekly', places=5) |
|
1926 | ||
1927 |
assert event.recurrence_rule['byweekday'][0] == weekday |
|
1928 | ||
1929 | ||
1930 |
def test_recurring_events_repeat(freezer): |
|
1923 |
def test_recurring_events_repetition(freezer): |
|
1931 | 1924 |
freezer.move_to('2021-01-06 12:00') # Wednesday |
1932 | 1925 |
agenda = Agenda.objects.create(label='Agenda', kind='events') |
1933 | 1926 |
event = Event.objects.create( |
1934 | 1927 |
agenda=agenda, |
1935 | 1928 |
start_datetime=now(), |
1936 |
repeat='daily',
|
|
1929 |
recurrence_days=list(range(7)), # everyday
|
|
1937 | 1930 |
places=5, |
1938 | 1931 |
) |
1939 | 1932 |
event.refresh_from_db() |
... | ... | |
1949 | 1942 |
for i in range(len(recurrences) - 1): |
1950 | 1943 |
assert recurrences[i].start_datetime + datetime.timedelta(days=1) == recurrences[i + 1].start_datetime |
1951 | 1944 | |
1952 |
event.repeat = 'weekdays'
|
|
1945 |
event.recurrence_days = list(range(5)) # from Monday to Friday
|
|
1953 | 1946 |
event.save() |
1954 | 1947 |
recurrences = event.get_recurrences( |
1955 | 1948 |
localtime() + datetime.timedelta(days=1), localtime() + datetime.timedelta(days=7) |
... | ... | |
1959 | 1952 |
assert recurrences[1].start_datetime == start_datetime + datetime.timedelta(days=5) |
1960 | 1953 |
assert recurrences[-1].start_datetime == start_datetime + datetime.timedelta(days=7) |
1961 | 1954 | |
1962 |
event.repeat = '2-weeks' |
|
1955 |
event.recurrence_days = [localtime(event.start_datetime).weekday()] # from Monday to Friday |
|
1956 |
event.recurrence_week_interval = 2 |
|
1963 | 1957 |
event.save() |
1964 | 1958 |
recurrences = event.get_recurrences( |
1965 | 1959 |
localtime() + datetime.timedelta(days=3), localtime() + datetime.timedelta(days=45) |
... | ... | |
1972 | 1966 |
recurrences[i].start_datetime + datetime.timedelta(days=14) == recurrences[i + 1].start_datetime |
1973 | 1967 |
) |
1974 | 1968 | |
1969 |
event.recurrence_days = [3] # Tuesday but start_datetime is a Wednesday |
|
1970 |
event.recurrence_week_interval = 1 |
|
1971 |
event.save() |
|
1972 |
recurrences = event.get_recurrences(localtime(), localtime() + datetime.timedelta(days=10)) |
|
1973 |
assert len(recurrences) == 2 |
|
1974 |
# no recurrence exist on Wednesday |
|
1975 |
assert all(localtime(r.start_datetime).weekday() == 3 for r in recurrences) |
|
1976 | ||
1975 | 1977 | |
1976 | 1978 |
@pytest.mark.freeze_time('2021-01-06') |
1977 | 1979 |
def test_recurring_events_with_end_date(): |
... | ... | |
1979 | 1981 |
event = Event.objects.create( |
1980 | 1982 |
agenda=agenda, |
1981 | 1983 |
start_datetime=now(), |
1982 |
repeat='daily',
|
|
1984 |
recurrence_days=list(range(7)),
|
|
1983 | 1985 |
places=5, |
1984 | 1986 |
recurrence_end_date=(now() + datetime.timedelta(days=5)).date(), |
1985 | 1987 |
) |
... | ... | |
1997 | 1999 |
def test_recurring_events_sort(freezer): |
1998 | 2000 |
freezer.move_to('2021-01-06 12:00') # Wednesday |
1999 | 2001 |
agenda = Agenda.objects.create(label='Agenda', kind='events') |
2000 |
Event.objects.create(agenda=agenda, slug='a', start_datetime=now(), repeat='daily', places=5) |
|
2001 |
Event.objects.create(agenda=agenda, slug='b', start_datetime=now(), repeat='daily', duration=10, places=5) |
|
2002 |
Event.objects.create(agenda=agenda, slug='c', start_datetime=now(), repeat='daily', duration=5, places=5) |
|
2003 | 2002 |
Event.objects.create( |
2004 |
agenda=agenda, slug='d', start_datetime=now() + datetime.timedelta(hours=1), repeat='daily', places=5 |
|
2003 |
agenda=agenda, slug='a', start_datetime=now(), recurrence_days=list(range(7)), places=5 |
|
2004 |
) |
|
2005 |
Event.objects.create( |
|
2006 |
agenda=agenda, slug='b', start_datetime=now(), recurrence_days=list(range(7)), duration=10, places=5 |
|
2007 |
) |
|
2008 |
Event.objects.create( |
|
2009 |
agenda=agenda, slug='c', start_datetime=now(), recurrence_days=list(range(7)), duration=5, places=5 |
|
2010 |
) |
|
2011 |
Event.objects.create( |
|
2012 |
agenda=agenda, |
|
2013 |
slug='d', |
|
2014 |
start_datetime=now() + datetime.timedelta(hours=1), |
|
2015 |
recurrence_days=list(range(7)), |
|
2016 |
places=5, |
|
2005 | 2017 |
) |
2006 | 2018 | |
2007 | 2019 |
events = agenda.get_open_events()[:8] |
... | ... | |
2021 | 2033 |
event = Event.objects.create( |
2022 | 2034 |
agenda=agenda, |
2023 | 2035 |
start_datetime=now(), |
2024 |
repeat='daily',
|
|
2036 |
recurrence_days=list(range(7)),
|
|
2025 | 2037 |
places=5, |
2026 | 2038 |
) |
2027 | 2039 |
event.refresh_from_db() |
... | ... | |
2094 | 2106 |
daily_event = Event.objects.create( |
2095 | 2107 |
agenda=agenda, |
2096 | 2108 |
start_datetime=now(), |
2097 |
repeat='daily',
|
|
2109 |
recurrence_days=list(range(7)),
|
|
2098 | 2110 |
places=5, |
2099 | 2111 |
recurrence_end_date=datetime.date(year=2021, month=5, day=8), |
2100 | 2112 |
) |
2101 | 2113 |
weekly_event = Event.objects.create( |
2102 | 2114 |
agenda=agenda, |
2103 | 2115 |
start_datetime=now(), |
2104 |
repeat='weekly',
|
|
2116 |
recurrence_days=[now().weekday()],
|
|
2105 | 2117 |
places=5, |
2106 | 2118 |
recurrence_end_date=datetime.date(year=2021, month=6, day=1), |
2107 | 2119 |
) |
... | ... | |
2110 | 2122 |
daily_event_no_end_date = Event.objects.create( |
2111 | 2123 |
agenda=agenda, |
2112 | 2124 |
start_datetime=now() + datetime.timedelta(hours=2), |
2113 |
repeat='daily',
|
|
2125 |
recurrence_days=list(range(7)),
|
|
2114 | 2126 |
places=5, |
2115 | 2127 |
) |
2116 | 2128 |
daily_event_no_end_date.refresh_from_db() |
... | ... | |
2147 | 2159 |
assert Booking.objects.count() == 1 |
2148 | 2160 |
assert Event.objects.filter(primary_event=daily_event_no_end_date).count() == 1 |
2149 | 2161 |
assert agenda.recurrence_exceptions_report.events.get() == event |
2162 | ||
2163 | ||
2164 |
def test_recurring_events_display(freezer): |
|
2165 |
freezer.move_to('2021-01-06 12:30') |
|
2166 |
agenda = Agenda.objects.create(label='Agenda', kind='events') |
|
2167 |
event = Event.objects.create( |
|
2168 |
agenda=agenda, start_datetime=now(), recurrence_days=list(range(7)), places=5 |
|
2169 |
) |
|
2170 | ||
2171 |
assert event.get_recurrence_display() == 'Daily at 1:30 p.m.' |
|
2172 | ||
2173 |
event.recurrence_days = [1, 2, 3, 4] |
|
2174 |
event.save() |
|
2175 |
assert event.get_recurrence_display() == 'From Tuesday to Friday at 1:30 p.m.' |
|
2176 | ||
2177 |
event.recurrence_days = [4, 5, 6] |
|
2178 |
event.save() |
|
2179 |
assert event.get_recurrence_display() == 'From Friday to Sunday at 1:30 p.m.' |
|
2180 | ||
2181 |
event.recurrence_days = [1, 4, 6] |
|
2182 |
event.save() |
|
2183 |
assert event.get_recurrence_display() == 'On Tuesday, Friday, Sunday at 1:30 p.m.' |
|
2184 | ||
2185 |
event.recurrence_days = [0] |
|
2186 |
event.recurrence_week_interval = 2 |
|
2187 |
event.save() |
|
2188 |
assert event.get_recurrence_display() == 'On Monday at 1:30 p.m., once every two weeks' |
|
2189 | ||
2190 |
event.recurrence_week_interval = 3 |
|
2191 |
event.recurrence_end_date = now() + datetime.timedelta(days=7) |
|
2192 |
event.save() |
|
2193 |
assert ( |
|
2194 |
event.get_recurrence_display() |
|
2195 |
== 'On Monday at 1:30 p.m., once every three weeks, until Jan. 13, 2021' |
|
2196 |
) |
tests/test_api.py | ||
---|---|---|
300 | 300 |
start_datetime=now(), |
301 | 301 |
places=10, |
302 | 302 |
agenda=event_agenda, |
303 |
repeat='daily',
|
|
303 |
recurrence_days=list(range(7)),
|
|
304 | 304 |
) |
305 | 305 |
assert len(event_agenda.get_open_events()) == 2 |
306 | 306 |
resp = app.get('/api/agenda/', params={'with_open_events': '1'}) |
... | ... | |
308 | 308 | |
309 | 309 |
for i in range(10): |
310 | 310 |
event_agenda = Agenda.objects.create(label='Foo bar', category=category_a) |
311 |
event = Event.objects.create(start_datetime=now(), places=10, agenda=event_agenda, repeat='daily') |
|
311 |
event = Event.objects.create( |
|
312 |
start_datetime=now(), places=10, agenda=event_agenda, recurrence_days=[now().weekday()] |
|
313 |
) |
|
312 | 314 |
TimePeriodException.objects.create( |
313 | 315 |
desk=event_agenda.desk_set.get(), |
314 | 316 |
start_datetime=now(), |
... | ... | |
602 | 604 |
event.delete() |
603 | 605 | |
604 | 606 |
# recurrent event |
607 |
start_datetime = localtime().replace(hour=12, minute=0) |
|
605 | 608 |
event = Event.objects.create( |
606 | 609 |
slug='recurrent', |
607 |
start_datetime=localtime().replace(hour=12, minute=0),
|
|
608 |
repeat='weekly',
|
|
610 |
start_datetime=start_datetime,
|
|
611 |
recurrence_days=[start_datetime.weekday()],
|
|
609 | 612 |
places=2, |
610 | 613 |
agenda=agenda, |
611 | 614 |
) |
... | ... | |
1312 | 1315 |
event.delete() |
1313 | 1316 | |
1314 | 1317 |
# recurrent event |
1318 |
start_datetime = localtime().replace(hour=12, minute=0) |
|
1315 | 1319 |
event = Event.objects.create( |
1316 | 1320 |
slug='recurrent', |
1317 |
start_datetime=localtime().replace(hour=12, minute=0),
|
|
1318 |
repeat='weekly',
|
|
1321 |
start_datetime=start_datetime,
|
|
1322 |
recurrence_days=[start_datetime.weekday()],
|
|
1319 | 1323 |
places=2, |
1320 | 1324 |
agenda=agenda, |
1321 | 1325 |
) |
... | ... | |
5995 | 5999 |
# recurring event |
5996 | 6000 |
Event.objects.all().delete() |
5997 | 6001 |
Event.objects.create( |
5998 |
slug='abc', label='Test', start_datetime=localtime(), repeat='weekly', places=5, agenda=agenda |
|
6002 |
slug='abc', |
|
6003 |
label='Test', |
|
6004 |
start_datetime=localtime(), |
|
6005 |
recurrence_days=[localtime().weekday()], |
|
6006 |
places=5, |
|
6007 |
agenda=agenda, |
|
5999 | 6008 |
) |
6000 | 6009 |
resp = app.get(api_url) |
6001 | 6010 |
assert resp.json['meta']['first_bookable_slot']['text'] == 'Test (May 27, 2017, 1:12 a.m.)' |
... | ... | |
6188 | 6197 |
label='Foo bar', kind='events', minimal_booking_delay=1, maximal_booking_delay=30 |
6189 | 6198 |
) |
6190 | 6199 |
base_event = Event.objects.create( |
6191 |
slug='abc', label='Test', start_datetime=localtime(), repeat='weekly', places=5, agenda=agenda |
|
6200 |
slug='abc', |
|
6201 |
label='Test', |
|
6202 |
start_datetime=localtime(), |
|
6203 |
recurrence_days=[localtime().weekday()], |
|
6204 |
places=5, |
|
6205 |
agenda=agenda, |
|
6192 | 6206 |
) |
6193 | 6207 | |
6194 | 6208 |
resp = app.get('/api/agenda/%s/datetimes/' % agenda.slug) |
... | ... | |
6256 | 6270 |
label='Foo bar', kind='events', minimal_booking_delay=0, maximal_booking_delay=30 |
6257 | 6271 |
) |
6258 | 6272 |
event = Event.objects.create( |
6259 |
slug='abc', start_datetime=localtime(), repeat='weekly', places=5, agenda=agenda |
|
6273 |
slug='abc', |
|
6274 |
start_datetime=localtime(), |
|
6275 |
recurrence_days=[localtime().weekday()], |
|
6276 |
places=5, |
|
6277 |
agenda=agenda, |
|
6260 | 6278 |
) |
6261 | 6279 |
event.refresh_from_db() |
6262 | 6280 | |
... | ... | |
6339 | 6357 |
label='Foo bar', kind='events', minimal_booking_delay=1, maximal_booking_delay=30 |
6340 | 6358 |
) |
6341 | 6359 |
event = Event.objects.create( |
6342 |
slug='abc', start_datetime=localtime(), repeat='weekly', places=5, agenda=agenda |
|
6360 |
slug='abc', |
|
6361 |
start_datetime=localtime(), |
|
6362 |
recurrence_days=[localtime().weekday()], |
|
6363 |
places=5, |
|
6364 |
agenda=agenda, |
|
6343 | 6365 |
) |
6344 | 6366 | |
6345 | 6367 |
resp = app.get('/api/agenda/%s/datetimes/' % agenda.slug) |
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 | ||
---|---|---|
202 | 202 |
event = Event.objects.create( |
203 | 203 |
agenda=agenda, |
204 | 204 |
start_datetime=now(), |
205 |
repeat='daily', |
|
205 |
recurrence_days=list(range(7)), |
|
206 |
recurrence_week_interval=2, |
|
206 | 207 |
places=10, |
207 | 208 |
slug='test', |
208 | 209 |
) |
... | ... | |
223 | 224 |
assert Event.objects.count() == 1 |
224 | 225 |
event = Agenda.objects.get(label='Foo Bar').event_set.first() |
225 | 226 |
assert event.primary_event is None |
226 |
assert event.repeat == 'daily'
|
|
227 |
assert event.recurrence_rule == {'freq': DAILY}
|
|
227 |
assert event.recurrence_days == list(range(7))
|
|
228 |
assert event.recurrence_week_interval == 2
|
|
228 | 229 | |
229 | 230 |
# importing event with end recurrence date creates recurrences |
230 | 231 |
event.recurrence_end_date = now() + datetime.timedelta(days=7) |
232 |
event.recurrence_week_interval = 1 |
|
231 | 233 |
event.save() |
232 | 234 |
output = get_output_of_command('export_site') |
233 | 235 |
import_site(data={}, clean=True) |
234 |
- |