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/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 |
- |