0001-manager-add-new-recurrence-fields-50560.patch
chrono/agendas/migrations/0079_auto_20210412_1747.py | ||
---|---|---|
1 |
# Generated by Django 2.2.19 on 2021-04-12 15:47 |
|
2 | ||
3 |
import django.contrib.postgres.fields |
|
4 |
from django.db import migrations, models |
|
5 | ||
6 | ||
7 |
class Migration(migrations.Migration): |
|
8 | ||
9 |
dependencies = [ |
|
10 |
('agendas', '0078_absence_reasons'), |
|
11 |
] |
|
12 | ||
13 |
operations = [ |
|
14 |
migrations.AddField( |
|
15 |
model_name='event', |
|
16 |
name='recurrence_days', |
|
17 |
field=django.contrib.postgres.fields.ArrayField( |
|
18 |
base_field=models.IntegerField( |
|
19 |
choices=[(0, 'Mo'), (1, 'Tu'), (2, 'We'), (3, 'Th'), (4, 'Fr'), (5, 'Sa'), (6, 'Su')] |
|
20 |
), |
|
21 |
blank=True, |
|
22 |
default=list, |
|
23 |
null=True, |
|
24 |
size=None, |
|
25 |
verbose_name='Recurrence days', |
|
26 |
), |
|
27 |
), |
|
28 |
migrations.AddField( |
|
29 |
model_name='event', |
|
30 |
name='recurrence_hour', |
|
31 |
field=models.TimeField(blank=True, null=True, verbose_name='Hour'), |
|
32 |
), |
|
33 |
migrations.AddField( |
|
34 |
model_name='event', |
|
35 |
name='recurrence_start_date', |
|
36 |
field=models.DateField(blank=True, null=True, verbose_name='Recurrence start date'), |
|
37 |
), |
|
38 |
migrations.AddField( |
|
39 |
model_name='event', |
|
40 |
name='recurrence_weekly_interval', |
|
41 |
field=models.IntegerField( |
|
42 |
choices=[(1, 'Every week'), (2, 'Every two weeks'), (3, 'Every three weeks')], |
|
43 |
default=1, |
|
44 |
verbose_name='Repeat', |
|
45 |
), |
|
46 |
), |
|
47 |
migrations.AlterField( |
|
48 |
model_name='event', |
|
49 |
name='start_datetime', |
|
50 |
field=models.DateTimeField(blank=True, null=True, verbose_name='Date/time'), |
|
51 |
), |
|
52 |
] |
chrono/agendas/models.py | ||
---|---|---|
1083 | 1083 |
('weekdays', _('Every weekdays (Monday to Friday)')), |
1084 | 1084 |
] |
1085 | 1085 | |
1086 |
WEEKDAY_CHOICES = [ |
|
1087 |
(0, _('Mo')), |
|
1088 |
(1, _('Tu')), |
|
1089 |
(2, _('We')), |
|
1090 |
(3, _('Th')), |
|
1091 |
(4, _('Fr')), |
|
1092 |
(5, _('Sa')), |
|
1093 |
(6, _('Su')), |
|
1094 |
] |
|
1095 | ||
1096 |
INTERVAL_CHOICES = [ |
|
1097 |
(1, 'Every week'), |
|
1098 |
(2, 'Every two weeks'), |
|
1099 |
(3, 'Every three weeks'), |
|
1100 |
] |
|
1101 | ||
1086 | 1102 |
agenda = models.ForeignKey(Agenda, on_delete=models.CASCADE) |
1087 |
start_datetime = models.DateTimeField(_('Date/time')) |
|
1103 |
start_datetime = models.DateTimeField(_('Date/time'), null=True, blank=True)
|
|
1088 | 1104 |
repeat = models.CharField(_('Repeat'), max_length=16, blank=True, choices=REPEAT_CHOICES) |
1105 |
recurrence_days = ArrayField( |
|
1106 |
models.IntegerField(choices=WEEKDAY_CHOICES), |
|
1107 |
verbose_name=_('Recurrence days'), |
|
1108 |
default=list, |
|
1109 |
blank=True, |
|
1110 |
null=True, |
|
1111 |
) |
|
1112 |
recurrence_hour = models.TimeField(_('Hour'), null=True, blank=True) |
|
1113 |
recurrence_start_date = models.DateField(_('Recurrence start date'), null=True, blank=True) |
|
1114 |
recurrence_weekly_interval = models.IntegerField(_('Repeat'), choices=INTERVAL_CHOICES, default=1) |
|
1089 | 1115 |
recurrence_rule = JSONField(_('Recurrence rule'), null=True) |
1090 | 1116 |
recurrence_end_date = models.DateField(_('Recurrence end date'), null=True, blank=True) |
1091 | 1117 |
primary_event = models.ForeignKey('self', null=True, on_delete=models.CASCADE, related_name='recurrences') |
... | ... | |
1136 | 1162 | |
1137 | 1163 |
def save(self, seen_slugs=None, *args, **kwargs): |
1138 | 1164 |
assert self.agenda.kind != 'virtual', "an event can't reference a virtual agenda" |
1165 |
assert self.start_datetime or self.recurrence_hour and self.recurrence_start_date |
|
1139 | 1166 |
assert not (self.slug and self.slug.isdigit()), 'slug cannot be a number' |
1140 | 1167 |
self.start_datetime = self.start_datetime.replace(second=0, microsecond=0) |
1141 | 1168 |
self.check_full() |
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): |
... | ... | |
146 | 146 |
fields = [ |
147 | 147 |
'label', |
148 | 148 |
'start_datetime', |
149 |
'repeat', |
|
150 | 149 |
'duration', |
151 | 150 |
'places', |
152 | 151 |
] |
... | ... | |
155 | 154 |
} |
156 | 155 | |
157 | 156 | |
157 |
FREQUENCY_CHOICES = ( |
|
158 |
('unique', _('Unique')), |
|
159 |
('recurring', _('Recurring')), |
|
160 |
) |
|
161 | ||
162 | ||
158 | 163 |
class EventForm(forms.ModelForm): |
159 |
protected_fields = ('repeat', 'slug', 'start_datetime') |
|
164 |
protected_fields = ('slug', 'start_datetime') |
|
165 | ||
166 |
frequency = forms.ChoiceField( |
|
167 |
label=_('Event frequency'), widget=forms.RadioSelect, choices=FREQUENCY_CHOICES |
|
168 |
) |
|
169 |
recurrence_days = forms.TypedMultipleChoiceField( |
|
170 |
choices=Event.WEEKDAY_CHOICES, coerce=int, required=False, widget=WeekdaysWidget |
|
171 |
) |
|
160 | 172 | |
161 | 173 |
class Meta: |
162 | 174 |
model = Event |
163 | 175 |
widgets = { |
164 | 176 |
'publication_date': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'), |
177 |
'recurrence_hour': forms.DateInput(attrs={'type': 'time'}, format='%H:%M'), |
|
178 |
'recurrence_start_date': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'), |
|
165 | 179 |
'recurrence_end_date': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'), |
166 | 180 |
} |
167 | 181 |
fields = [ |
168 | 182 |
'label', |
169 | 183 |
'slug', |
184 |
'frequency', |
|
170 | 185 |
'start_datetime', |
171 |
'repeat', |
|
186 |
'recurrence_days', |
|
187 |
'recurrence_hour', |
|
188 |
'recurrence_weekly_interval', |
|
189 |
'recurrence_start_date', |
|
172 | 190 |
'recurrence_end_date', |
173 | 191 |
'duration', |
174 | 192 |
'publication_date', |
... | ... | |
184 | 202 | |
185 | 203 |
def __init__(self, *args, **kwargs): |
186 | 204 |
super().__init__(*args, **kwargs) |
205 |
self.fields['frequency'].initial = 'recurring' if self.instance.recurrence_days else 'unique' |
|
187 | 206 |
if self.instance.recurrence_rule and self.instance.has_recurrences_booked(): |
188 | 207 |
for field in self.protected_fields: |
189 | 208 |
self.fields[field].disabled = True |
... | ... | |
191 | 210 |
'This field cannot be modified because some recurrences have bookings attached to them.' |
192 | 211 |
) |
193 | 212 |
if self.instance.primary_event: |
194 |
for field in ('slug', 'repeat', 'recurrence_end_date', 'publication_date'):
|
|
213 |
for field in ('slug', 'recurrence_end_date', 'publication_date'): |
|
195 | 214 |
del self.fields[field] |
196 | 215 | |
197 | 216 |
def clean(self): |
... | ... | |
200 | 219 |
after=self.cleaned_data['recurrence_end_date'] |
201 | 220 |
): |
202 | 221 |
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.')) |
|
205 | 222 | |
206 | 223 |
def save(self, *args, **kwargs): |
207 | 224 |
with transaction.atomic(): |
... | ... | |
211 | 228 |
update_fields = { |
212 | 229 |
field: value |
213 | 230 |
for field, value in self.cleaned_data.items() |
214 |
if field not in self.protected_fields |
|
231 |
if field != 'frequency' and field not in self.protected_fields
|
|
215 | 232 |
} |
216 | 233 |
self.instance.recurrences.update(**update_fields) |
217 | 234 |
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> |
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 |
<label{% if option.attrs.id %} for="{{ option.attrs.id }}"{% endif %}>{{ option.label }}</label> |
|
6 |
{% include "django/forms/widgets/input.html" with widget=option %} |
|
7 |
{% endfor %} |
|
8 |
{% endfor %} |
|
9 |
</span> |
|
10 |
{% endspaceless %} |
chrono/manager/widgets.py | ||
---|---|---|
16 | 16 | |
17 | 17 | |
18 | 18 |
from django.forms.fields import SplitDateTimeField |
19 |
from django.forms.widgets import TimeInput, SplitDateTimeWidget |
|
19 |
from django.forms.widgets import TimeInput, SplitDateTimeWidget, 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' |
|
62 |
- |