2016-02-13 15:31:28 +01:00
|
|
|
# chrono - agendas system
|
|
|
|
# Copyright (C) 2016 Entr'ouvert
|
|
|
|
#
|
|
|
|
# This program is free software: you can redistribute it and/or modify it
|
|
|
|
# under the terms of the GNU Affero General Public License as published
|
|
|
|
# by the Free Software Foundation, either version 3 of the License, or
|
|
|
|
# (at your option) any later version.
|
|
|
|
#
|
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
# GNU Affero General Public License for more details.
|
|
|
|
#
|
|
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
2018-03-25 11:26:47 +02:00
|
|
|
|
2022-02-15 17:16:31 +01:00
|
|
|
import copy
|
2016-09-16 15:04:02 +02:00
|
|
|
import csv
|
|
|
|
import datetime
|
2022-02-15 17:16:31 +01:00
|
|
|
from collections import defaultdict
|
2022-04-15 16:44:42 +02:00
|
|
|
from io import StringIO
|
2022-02-15 17:16:31 +01:00
|
|
|
from operator import itemgetter
|
2016-09-16 15:04:02 +02:00
|
|
|
|
2021-04-22 11:39:42 +02:00
|
|
|
import django_filters
|
2022-02-15 17:16:31 +01:00
|
|
|
from dateutil.relativedelta import relativedelta
|
2016-02-13 15:31:28 +01:00
|
|
|
from django import forms
|
2020-09-15 14:05:38 +02:00
|
|
|
from django.conf import settings
|
2018-03-03 10:47:08 +01:00
|
|
|
from django.contrib.auth.models import Group
|
2020-06-11 17:04:58 +02:00
|
|
|
from django.core.exceptions import FieldDoesNotExist
|
2022-06-27 15:20:09 +02:00
|
|
|
from django.core.validators import URLValidator
|
2022-01-11 16:11:41 +01:00
|
|
|
from django.db import transaction
|
2022-03-24 17:05:17 +01:00
|
|
|
from django.db.models import DurationField, ExpressionWrapper, F
|
2022-04-01 08:30:24 +02:00
|
|
|
from django.forms import ValidationError, formset_factory
|
2022-06-27 15:20:09 +02:00
|
|
|
from django.template import Context, Template, TemplateSyntaxError, VariableDoesNotExist
|
2018-03-25 11:26:47 +02:00
|
|
|
from django.utils.encoding import force_text
|
2022-02-15 10:52:24 +01:00
|
|
|
from django.utils.formats import date_format
|
2022-02-15 17:16:31 +01:00
|
|
|
from django.utils.timezone import localtime, make_aware, now
|
2016-09-16 15:04:02 +02:00
|
|
|
from django.utils.translation import ugettext_lazy as _
|
2016-02-13 15:31:28 +01:00
|
|
|
|
2018-03-03 10:47:08 +01:00
|
|
|
from chrono.agendas.models import (
|
2022-03-10 15:03:54 +01:00
|
|
|
WEEK_CHOICES,
|
2022-02-22 15:59:27 +01:00
|
|
|
WEEKDAY_CHOICES,
|
2018-09-22 18:12:22 +02:00
|
|
|
WEEKDAYS_LIST,
|
2018-03-03 10:47:08 +01:00
|
|
|
Agenda,
|
2020-07-16 15:12:47 +02:00
|
|
|
AgendaNotificationsSettings,
|
2020-09-15 14:05:38 +02:00
|
|
|
AgendaReminderSettings,
|
2020-07-08 16:10:53 +02:00
|
|
|
Booking,
|
2021-04-08 11:25:34 +02:00
|
|
|
Desk,
|
2018-03-03 10:47:08 +01:00
|
|
|
Event,
|
2022-04-01 08:30:24 +02:00
|
|
|
EventsType,
|
2018-03-03 10:47:08 +01:00
|
|
|
MeetingType,
|
2022-02-22 15:59:27 +01:00
|
|
|
Person,
|
2020-05-15 16:52:58 +02:00
|
|
|
Resource,
|
2022-03-24 17:05:17 +01:00
|
|
|
SharedCustodyHolidayRule,
|
2022-02-22 15:59:27 +01:00
|
|
|
SharedCustodyPeriod,
|
|
|
|
SharedCustodyRule,
|
2022-06-28 14:49:26 +02:00
|
|
|
SharedCustodySettings,
|
2022-01-27 16:31:02 +01:00
|
|
|
Subscription,
|
2018-03-03 10:47:08 +01:00
|
|
|
TimePeriod,
|
2018-09-22 18:12:22 +02:00
|
|
|
TimePeriodException,
|
2022-03-24 17:05:17 +01:00
|
|
|
TimePeriodExceptionGroup,
|
2019-12-12 10:56:14 +01:00
|
|
|
TimePeriodExceptionSource,
|
2020-09-30 17:53:42 +02:00
|
|
|
UnavailabilityCalendar,
|
2020-02-18 12:14:36 +01:00
|
|
|
VirtualMember,
|
2020-09-11 09:29:16 +02:00
|
|
|
generate_slug,
|
2018-09-22 18:12:22 +02:00
|
|
|
)
|
2022-06-09 17:01:20 +02:00
|
|
|
from chrono.utils.lingo import get_agenda_check_types
|
2016-02-13 15:31:28 +01:00
|
|
|
|
2016-07-22 14:57:11 +02:00
|
|
|
from . import widgets
|
2021-04-21 16:21:31 +02:00
|
|
|
from .widgets import SplitDateTimeField, WeekdaysWidget
|
2016-02-13 15:31:28 +01:00
|
|
|
|
|
|
|
|
2018-03-03 10:47:08 +01:00
|
|
|
class AgendaAddForm(forms.ModelForm):
|
|
|
|
edit_role = forms.ModelChoiceField(
|
|
|
|
label=_('Edit Role'), required=False, queryset=Group.objects.all().order_by('name')
|
2019-12-16 16:21:24 +01:00
|
|
|
)
|
2018-03-03 10:47:08 +01:00
|
|
|
view_role = forms.ModelChoiceField(
|
|
|
|
label=_('View Role'), required=False, queryset=Group.objects.all().order_by('name')
|
2019-12-16 16:21:24 +01:00
|
|
|
)
|
2018-03-03 10:47:08 +01:00
|
|
|
|
2021-01-25 14:23:39 +01:00
|
|
|
class Meta:
|
|
|
|
model = Agenda
|
|
|
|
fields = ['label', 'kind', 'category', 'edit_role', 'view_role']
|
|
|
|
|
|
|
|
def save(self, *args, **kwargs):
|
2021-04-26 22:15:04 +02:00
|
|
|
create = self.instance.pk is None
|
2021-01-25 14:23:39 +01:00
|
|
|
super().save()
|
2021-04-26 22:15:04 +02:00
|
|
|
if create and self.instance.kind == 'meetings':
|
2021-01-25 14:23:39 +01:00
|
|
|
default_desk = self.instance.desk_set.create(label=_('Desk 1'))
|
|
|
|
default_desk.import_timeperiod_exceptions_from_settings(enable=True)
|
|
|
|
self.instance.desk_simple_management = True
|
|
|
|
self.instance.save()
|
|
|
|
return self.instance
|
|
|
|
|
2018-03-03 10:47:08 +01:00
|
|
|
|
2020-10-20 17:29:51 +02:00
|
|
|
class AgendaEditForm(forms.ModelForm):
|
2018-03-03 10:47:08 +01:00
|
|
|
class Meta:
|
|
|
|
model = Agenda
|
2020-07-09 14:50:36 +02:00
|
|
|
fields = [
|
|
|
|
'label',
|
|
|
|
'slug',
|
2020-07-24 15:59:49 +02:00
|
|
|
'category',
|
2020-08-13 11:48:52 +02:00
|
|
|
'anonymize_delay',
|
2020-07-09 14:50:36 +02:00
|
|
|
'default_view',
|
2020-09-25 11:00:31 +02:00
|
|
|
'booking_form_url',
|
2022-04-04 15:55:55 +02:00
|
|
|
'events_type',
|
2020-07-09 14:50:36 +02:00
|
|
|
]
|
2018-03-03 10:47:08 +01:00
|
|
|
|
2020-02-25 08:38:25 +01:00
|
|
|
def __init__(self, *args, **kwargs):
|
2021-07-09 16:01:42 +02:00
|
|
|
super().__init__(*args, **kwargs)
|
2020-07-09 14:50:36 +02:00
|
|
|
if kwargs['instance'].kind != 'events':
|
2020-09-25 11:00:31 +02:00
|
|
|
del self.fields['booking_form_url']
|
2022-04-04 15:55:55 +02:00
|
|
|
del self.fields['events_type']
|
2021-03-11 16:21:49 +01:00
|
|
|
self.fields['default_view'].choices = [
|
|
|
|
(k, v) for k, v in self.fields['default_view'].choices if k != 'open_events'
|
|
|
|
]
|
2022-04-04 15:55:55 +02:00
|
|
|
else:
|
|
|
|
if not EventsType.objects.exists():
|
|
|
|
del self.fields['events_type']
|
2020-02-25 08:38:25 +01:00
|
|
|
|
2018-03-03 10:47:08 +01:00
|
|
|
|
2020-10-20 17:29:51 +02:00
|
|
|
class AgendaBookingDelaysForm(forms.ModelForm):
|
|
|
|
class Meta:
|
|
|
|
model = Agenda
|
|
|
|
fields = [
|
|
|
|
'minimal_booking_delay',
|
2021-07-05 16:04:30 +02:00
|
|
|
'minimal_booking_delay_in_working_days',
|
2020-10-20 17:29:51 +02:00
|
|
|
'maximal_booking_delay',
|
|
|
|
]
|
|
|
|
|
|
|
|
def __init__(self, *args, **kwargs):
|
2021-07-09 16:01:42 +02:00
|
|
|
super().__init__(*args, **kwargs)
|
2020-10-20 17:29:51 +02:00
|
|
|
if kwargs['instance'].kind != 'virtual':
|
|
|
|
self.fields['minimal_booking_delay'].required = True
|
|
|
|
self.fields['maximal_booking_delay'].required = True
|
2021-07-05 16:04:30 +02:00
|
|
|
if kwargs['instance'].kind != 'events' or settings.WORKING_DAY_CALENDAR is None:
|
|
|
|
del self.fields['minimal_booking_delay_in_working_days']
|
2020-10-20 17:29:51 +02:00
|
|
|
|
|
|
|
|
|
|
|
class AgendaRolesForm(AgendaAddForm):
|
|
|
|
class Meta:
|
|
|
|
model = Agenda
|
|
|
|
fields = [
|
|
|
|
'edit_role',
|
|
|
|
'view_role',
|
|
|
|
]
|
|
|
|
|
|
|
|
|
2020-09-30 17:53:42 +02:00
|
|
|
class UnavailabilityCalendarAddForm(forms.ModelForm):
|
|
|
|
class Meta:
|
|
|
|
model = UnavailabilityCalendar
|
|
|
|
fields = ['label', 'edit_role', 'view_role']
|
|
|
|
|
|
|
|
edit_role = forms.ModelChoiceField(
|
|
|
|
label=_('Edit Role'), required=False, queryset=Group.objects.all().order_by('name')
|
|
|
|
)
|
|
|
|
view_role = forms.ModelChoiceField(
|
|
|
|
label=_('View Role'), required=False, queryset=Group.objects.all().order_by('name')
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
class UnavailabilityCalendarEditForm(UnavailabilityCalendarAddForm):
|
|
|
|
class Meta:
|
|
|
|
model = UnavailabilityCalendar
|
|
|
|
fields = ['label', 'slug', 'edit_role', 'view_role']
|
|
|
|
|
|
|
|
|
2019-11-06 15:20:07 +01:00
|
|
|
class NewEventForm(forms.ModelForm):
|
2021-04-21 16:21:31 +02:00
|
|
|
frequency = forms.ChoiceField(
|
|
|
|
label=_('Event frequency'),
|
|
|
|
widget=forms.RadioSelect,
|
|
|
|
choices=(
|
|
|
|
('unique', _('Unique')),
|
|
|
|
('recurring', _('Recurring')),
|
|
|
|
),
|
|
|
|
initial='unique',
|
2021-10-05 15:45:25 +02:00
|
|
|
help_text=_('This field will not be editable once event has bookings.'),
|
2021-04-21 16:21:31 +02:00
|
|
|
)
|
|
|
|
recurrence_days = forms.TypedMultipleChoiceField(
|
2022-02-22 15:59:27 +01:00
|
|
|
choices=WEEKDAY_CHOICES,
|
2021-06-18 08:22:45 +02:00
|
|
|
coerce=int,
|
|
|
|
required=False,
|
|
|
|
widget=WeekdaysWidget,
|
|
|
|
label=_('Recurrence days'),
|
2021-04-21 16:21:31 +02:00
|
|
|
)
|
|
|
|
|
2019-11-06 15:20:07 +01:00
|
|
|
class Meta:
|
|
|
|
model = Event
|
2020-07-09 12:46:13 +02:00
|
|
|
fields = [
|
2021-01-22 09:13:35 +01:00
|
|
|
'label',
|
2020-07-09 12:46:13 +02:00
|
|
|
'start_datetime',
|
2021-04-21 16:21:31 +02:00
|
|
|
'frequency',
|
|
|
|
'recurrence_days',
|
|
|
|
'recurrence_week_interval',
|
|
|
|
'recurrence_end_date',
|
2020-07-09 12:46:13 +02:00
|
|
|
'duration',
|
|
|
|
'places',
|
|
|
|
]
|
2020-07-16 14:34:19 +02:00
|
|
|
field_classes = {
|
|
|
|
'start_datetime': SplitDateTimeField,
|
|
|
|
}
|
2021-04-21 16:21:31 +02:00
|
|
|
widgets = {
|
|
|
|
'recurrence_end_date': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
|
|
|
|
}
|
2019-11-06 15:20:07 +01:00
|
|
|
|
2022-01-11 16:11:41 +01:00
|
|
|
def clean(self):
|
|
|
|
super().clean()
|
|
|
|
if self.cleaned_data.get('frequency') == 'unique':
|
|
|
|
self.cleaned_data['recurrence_days'] = None
|
|
|
|
self.cleaned_data['recurrence_end_date'] = None
|
|
|
|
|
2021-04-21 16:21:31 +02:00
|
|
|
def clean_recurrence_days(self):
|
|
|
|
recurrence_days = self.cleaned_data['recurrence_days']
|
|
|
|
if recurrence_days == []:
|
|
|
|
return None
|
|
|
|
return recurrence_days
|
2019-11-06 15:20:07 +01:00
|
|
|
|
2022-01-19 11:17:06 +01:00
|
|
|
def clean_recurrence_end_date(self):
|
|
|
|
recurrence_end_date = self.cleaned_data['recurrence_end_date']
|
|
|
|
if recurrence_end_date and recurrence_end_date > now().date() + datetime.timedelta(days=3 * 365):
|
|
|
|
raise ValidationError(
|
|
|
|
_(
|
|
|
|
'Recurrence end date cannot be more than 3 years from now. '
|
|
|
|
'If the end date is not known, this field can simply be left blank.'
|
|
|
|
)
|
|
|
|
)
|
|
|
|
return recurrence_end_date
|
|
|
|
|
2022-01-11 16:11:41 +01:00
|
|
|
def save(self, *args, **kwargs):
|
|
|
|
with transaction.atomic():
|
|
|
|
event = super().save(*args, **kwargs)
|
2022-03-16 15:10:34 +01:00
|
|
|
if event.recurrence_days:
|
2022-01-11 16:11:41 +01:00
|
|
|
event.create_all_recurrences()
|
|
|
|
return event
|
|
|
|
|
2021-04-21 16:21:31 +02:00
|
|
|
|
|
|
|
class EventForm(NewEventForm):
|
|
|
|
protected_fields = (
|
|
|
|
'slug',
|
|
|
|
'start_datetime',
|
|
|
|
'frequency',
|
|
|
|
'recurrence_days',
|
|
|
|
'recurrence_week_interval',
|
|
|
|
)
|
2021-01-28 12:33:43 +01:00
|
|
|
|
2016-02-13 15:31:28 +01:00
|
|
|
class Meta:
|
|
|
|
model = Event
|
|
|
|
widgets = {
|
2021-01-13 15:08:40 +01:00
|
|
|
'recurrence_end_date': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
|
2016-02-13 15:31:28 +01:00
|
|
|
}
|
2020-07-09 12:46:13 +02:00
|
|
|
fields = [
|
2021-01-22 09:13:35 +01:00
|
|
|
'label',
|
|
|
|
'slug',
|
2020-07-09 12:46:13 +02:00
|
|
|
'start_datetime',
|
2021-04-21 16:21:31 +02:00
|
|
|
'frequency',
|
|
|
|
'recurrence_days',
|
|
|
|
'recurrence_week_interval',
|
2021-01-13 15:08:40 +01:00
|
|
|
'recurrence_end_date',
|
2020-07-09 12:46:13 +02:00
|
|
|
'duration',
|
2021-10-08 15:48:14 +02:00
|
|
|
'publication_datetime',
|
2020-07-09 12:46:13 +02:00
|
|
|
'places',
|
|
|
|
'waiting_list_places',
|
|
|
|
'description',
|
|
|
|
'pricing',
|
|
|
|
'url',
|
|
|
|
]
|
2020-07-16 14:34:19 +02:00
|
|
|
field_classes = {
|
|
|
|
'start_datetime': SplitDateTimeField,
|
2021-10-08 15:48:14 +02:00
|
|
|
'publication_datetime': SplitDateTimeField,
|
2020-07-16 14:34:19 +02:00
|
|
|
}
|
2016-09-11 11:31:29 +02:00
|
|
|
|
2021-01-28 12:33:43 +01:00
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
super().__init__(*args, **kwargs)
|
2021-04-21 16:21:31 +02:00
|
|
|
self.fields['frequency'].initial = 'recurring' if self.instance.recurrence_days else 'unique'
|
2021-10-05 15:45:25 +02:00
|
|
|
if not self.instance.recurrence_days and self.instance.booking_set.exists():
|
|
|
|
self.fields['frequency'].disabled = True
|
|
|
|
self.fields['frequency'].help_text = ''
|
|
|
|
self.fields['frequency'].widget.attrs['title'] = _(
|
|
|
|
'This field cannot be modified because event has bookings.'
|
|
|
|
)
|
2021-04-21 16:21:31 +02:00
|
|
|
if self.instance.recurrence_days and self.instance.has_recurrences_booked():
|
2021-01-28 12:33:43 +01:00
|
|
|
for field in self.protected_fields:
|
|
|
|
self.fields[field].disabled = True
|
|
|
|
self.fields[field].help_text = _(
|
|
|
|
'This field cannot be modified because some recurrences have bookings attached to them.'
|
|
|
|
)
|
2022-04-05 12:04:09 +02:00
|
|
|
if self.instance.agenda.events_type and not self.instance.primary_event:
|
|
|
|
field_classes = {
|
|
|
|
'text': forms.CharField,
|
|
|
|
'textarea': forms.CharField,
|
|
|
|
'bool': forms.NullBooleanField,
|
|
|
|
}
|
|
|
|
widget_classes = {
|
|
|
|
'textarea': forms.widgets.Textarea,
|
|
|
|
}
|
|
|
|
for custom_field in self.instance.agenda.events_type.get_custom_fields():
|
|
|
|
field_class = field_classes[custom_field['field_type']]
|
|
|
|
field_name = 'custom_field_%s' % custom_field['varname']
|
|
|
|
self.fields[field_name] = field_class(
|
|
|
|
label=custom_field['label'],
|
|
|
|
required=False,
|
|
|
|
initial=self.instance.custom_fields.get(custom_field['varname']),
|
|
|
|
widget=widget_classes.get(custom_field['field_type']),
|
|
|
|
)
|
2021-03-11 12:27:58 +01:00
|
|
|
if self.instance.primary_event:
|
2021-04-21 16:21:31 +02:00
|
|
|
for field in (
|
|
|
|
'slug',
|
|
|
|
'recurrence_end_date',
|
2021-10-08 15:48:14 +02:00
|
|
|
'publication_datetime',
|
2021-04-21 16:21:31 +02:00
|
|
|
'frequency',
|
|
|
|
'recurrence_days',
|
|
|
|
'recurrence_week_interval',
|
|
|
|
):
|
2021-03-11 12:27:58 +01:00
|
|
|
del self.fields[field]
|
2021-01-28 12:33:43 +01:00
|
|
|
|
2021-09-10 14:25:45 +02:00
|
|
|
def clean_slug(self):
|
|
|
|
slug = self.cleaned_data['slug']
|
|
|
|
|
|
|
|
if self.instance.agenda.event_set.filter(slug=slug).exclude(pk=self.instance.pk).exists():
|
|
|
|
raise ValidationError(_('Another event exists with the same identifier.'))
|
|
|
|
|
|
|
|
return slug
|
|
|
|
|
2021-02-17 16:54:25 +01:00
|
|
|
def clean(self):
|
|
|
|
super().clean()
|
|
|
|
if 'recurrence_end_date' in self.changed_data and self.instance.has_recurrences_booked(
|
|
|
|
after=self.cleaned_data['recurrence_end_date']
|
|
|
|
):
|
|
|
|
raise ValidationError(_('Bookings exist after this date.'))
|
2021-04-21 16:21:31 +02:00
|
|
|
|
2022-04-05 12:04:09 +02:00
|
|
|
if self.instance.agenda.events_type and self.instance.primary_event is None:
|
|
|
|
custom_fields = {}
|
|
|
|
for custom_field in self.instance.agenda.events_type.get_custom_fields():
|
|
|
|
field_name = 'custom_field_%s' % custom_field['varname']
|
|
|
|
custom_fields[custom_field['varname']] = self.cleaned_data.get(field_name)
|
|
|
|
if field_name in self.cleaned_data:
|
|
|
|
del self.cleaned_data[field_name]
|
|
|
|
self.cleaned_data['custom_fields'] = custom_fields
|
|
|
|
|
2021-01-28 12:33:43 +01:00
|
|
|
def save(self, *args, **kwargs):
|
2021-10-05 14:36:29 +02:00
|
|
|
with self.instance.update_recurrences(
|
|
|
|
self.changed_data,
|
|
|
|
self.cleaned_data,
|
|
|
|
self.protected_fields,
|
|
|
|
list(self.protected_fields) + ['recurrence_end_date', 'frequency'],
|
|
|
|
):
|
2022-04-05 12:04:09 +02:00
|
|
|
super(NewEventForm, self).save(commit=False, *args, **kwargs)
|
|
|
|
if 'custom_fields' in self.cleaned_data:
|
|
|
|
self.instance.custom_fields = self.cleaned_data['custom_fields']
|
|
|
|
self.instance.save()
|
2021-03-12 09:03:39 +01:00
|
|
|
return self.instance
|
2021-01-28 12:33:43 +01:00
|
|
|
|
2016-09-11 11:31:29 +02:00
|
|
|
|
2022-04-01 08:30:24 +02:00
|
|
|
class EventsTypeForm(forms.ModelForm):
|
|
|
|
class Meta:
|
|
|
|
model = EventsType
|
|
|
|
fields = ['label', 'slug']
|
|
|
|
|
|
|
|
|
|
|
|
class CustomFieldForm(forms.Form):
|
|
|
|
varname = forms.SlugField(label=_('Field slug'), required=False)
|
|
|
|
label = forms.CharField(label=_('Field label'), required=False)
|
|
|
|
field_type = forms.ChoiceField(
|
|
|
|
label=_('Field type'),
|
|
|
|
choices=[
|
|
|
|
('', '-------'),
|
|
|
|
('text', _('Text')),
|
|
|
|
('textarea', _('Textarea')),
|
|
|
|
('bool', _('Boolean')),
|
|
|
|
],
|
|
|
|
required=False,
|
|
|
|
)
|
|
|
|
|
|
|
|
def clean(self):
|
|
|
|
cleaned_data = super().clean()
|
|
|
|
|
|
|
|
if cleaned_data.get('varname') and not cleaned_data.get('label'):
|
|
|
|
self.add_error('label', _('This field is required.'))
|
|
|
|
if cleaned_data.get('varname') and not cleaned_data.get('field_type'):
|
|
|
|
self.add_error('field', _('This field is required.'))
|
|
|
|
|
|
|
|
return cleaned_data
|
|
|
|
|
|
|
|
|
|
|
|
CustomFieldFormSet = formset_factory(CustomFieldForm)
|
|
|
|
|
|
|
|
|
2021-04-22 11:39:42 +02:00
|
|
|
class BookingCheckFilterSet(django_filters.FilterSet):
|
|
|
|
class Meta:
|
|
|
|
model = Booking
|
|
|
|
fields = []
|
|
|
|
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
self.agenda = kwargs.pop('agenda')
|
2022-01-27 16:31:02 +01:00
|
|
|
filters = kwargs.pop('filters')
|
2021-04-22 11:39:42 +02:00
|
|
|
super().__init__(*args, **kwargs)
|
|
|
|
|
2022-02-21 17:12:43 +01:00
|
|
|
# add filters on extra_data to filterset
|
2021-04-22 11:39:42 +02:00
|
|
|
for key, values in filters.items():
|
2022-02-21 17:12:43 +01:00
|
|
|
self.filters['extra-data-%s' % key] = django_filters.ChoiceFilter(
|
2021-04-22 11:39:42 +02:00
|
|
|
label=_('Filter by %s') % key,
|
|
|
|
field_name='extra_data__%s' % key,
|
|
|
|
lookup_expr='iexact',
|
|
|
|
choices=[(v, v) for v in values],
|
|
|
|
empty_label=_('all'),
|
|
|
|
widget=forms.RadioSelect,
|
|
|
|
)
|
|
|
|
|
2022-02-22 16:01:10 +01:00
|
|
|
self.filters['sort'] = django_filters.ChoiceFilter(
|
|
|
|
label=_('Sort by'),
|
|
|
|
choices=[
|
|
|
|
('lastname,firstname', _('Last name, first name')),
|
|
|
|
('firstname,lastname', _('First name, last name')),
|
|
|
|
],
|
|
|
|
empty_label=None,
|
|
|
|
required=True,
|
|
|
|
initial='lastname,firstname',
|
|
|
|
widget=forms.RadioSelect,
|
|
|
|
method='do_nothing',
|
|
|
|
)
|
|
|
|
self.filters['sort'].parent = self
|
|
|
|
|
2022-02-21 17:12:43 +01:00
|
|
|
# add filters on booking status to filterset
|
|
|
|
status_choices = [
|
|
|
|
('booked', _('With booking')),
|
|
|
|
('not-booked', _('Without booking')),
|
|
|
|
('cancelled', _('Cancelled')),
|
|
|
|
('not-checked', _('Not checked')),
|
|
|
|
('presence', _('Presence')),
|
|
|
|
]
|
2022-06-09 17:01:20 +02:00
|
|
|
check_types = get_agenda_check_types(self.agenda)
|
|
|
|
absence_check_types = [ct for ct in check_types if ct.kind == 'absence']
|
|
|
|
presence_check_types = [ct for ct in check_types if ct.kind == 'presence']
|
|
|
|
status_choices += [
|
|
|
|
('presence::%s' % ct.slug, _('Presence (%s)') % ct.label) for ct in presence_check_types
|
|
|
|
]
|
2022-04-14 16:24:34 +02:00
|
|
|
status_choices += [('absence', _('Absence'))]
|
2022-06-09 17:01:20 +02:00
|
|
|
status_choices += [
|
|
|
|
('absence::%s' % ct.slug, _('Absence (%s)') % ct.label) for ct in absence_check_types
|
|
|
|
]
|
2022-02-21 17:12:43 +01:00
|
|
|
self.filters['booking-status'] = django_filters.ChoiceFilter(
|
|
|
|
label=_('Filter by status'),
|
|
|
|
choices=status_choices,
|
|
|
|
empty_label=_('all'),
|
|
|
|
widget=forms.RadioSelect,
|
|
|
|
method='filter_booking_status',
|
|
|
|
)
|
|
|
|
self.filters['booking-status'].parent = self
|
|
|
|
|
|
|
|
def filter_booking_status(self, queryset, name, value):
|
|
|
|
if value == 'not-booked':
|
|
|
|
return queryset.none()
|
|
|
|
if value == 'cancelled':
|
|
|
|
return queryset.filter(cancellation_datetime__isnull=False)
|
|
|
|
queryset = queryset.filter(cancellation_datetime__isnull=True)
|
|
|
|
if value == 'booked':
|
|
|
|
return queryset
|
|
|
|
if value == 'not-checked':
|
|
|
|
return queryset.filter(user_was_present__isnull=True)
|
|
|
|
if value == 'presence':
|
|
|
|
return queryset.filter(user_was_present=True)
|
|
|
|
if value == 'absence':
|
|
|
|
return queryset.filter(user_was_present=False)
|
2022-06-09 14:53:13 +02:00
|
|
|
if value.startswith('absence::'):
|
|
|
|
return queryset.filter(user_was_present=False, user_check_type_slug=value.split('::')[1])
|
|
|
|
if value.startswith('presence::'):
|
|
|
|
return queryset.filter(user_was_present=True, user_check_type_slug=value.split('::')[1])
|
2022-02-21 17:12:43 +01:00
|
|
|
return queryset
|
|
|
|
|
2022-02-22 16:01:10 +01:00
|
|
|
def do_nothing(self, queryset, name, value):
|
|
|
|
return queryset
|
|
|
|
|
2021-04-22 11:39:42 +02:00
|
|
|
|
2022-01-27 16:31:02 +01:00
|
|
|
class SubscriptionCheckFilterSet(BookingCheckFilterSet):
|
|
|
|
class Meta:
|
|
|
|
model = Subscription
|
|
|
|
fields = []
|
|
|
|
|
2022-02-21 17:12:43 +01:00
|
|
|
def filter_booking_status(self, queryset, name, value):
|
|
|
|
if value != 'not-booked':
|
|
|
|
return queryset.none()
|
|
|
|
return queryset
|
|
|
|
|
2022-01-27 16:31:02 +01:00
|
|
|
|
2022-04-12 15:31:42 +02:00
|
|
|
class BookingCheckAbsenceForm(forms.Form):
|
2022-06-09 17:01:20 +02:00
|
|
|
check_type = forms.ChoiceField(required=False)
|
2021-03-04 15:16:31 +01:00
|
|
|
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
agenda = kwargs.pop('agenda')
|
|
|
|
super().__init__(*args, **kwargs)
|
2022-06-09 17:01:20 +02:00
|
|
|
check_types = get_agenda_check_types(agenda)
|
|
|
|
self.absence_check_types = [ct for ct in check_types if ct.kind == 'absence']
|
|
|
|
self.fields['check_type'].choices = [('', '---------')] + [
|
|
|
|
(ct.slug, ct.label) for ct in self.absence_check_types
|
|
|
|
]
|
2021-03-04 15:16:31 +01:00
|
|
|
|
|
|
|
|
2022-04-14 16:24:34 +02:00
|
|
|
class BookingCheckPresenceForm(forms.Form):
|
2022-06-09 17:01:20 +02:00
|
|
|
check_type = forms.ChoiceField(required=False)
|
2022-04-14 16:24:34 +02:00
|
|
|
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
agenda = kwargs.pop('agenda')
|
|
|
|
super().__init__(*args, **kwargs)
|
2022-06-09 17:01:20 +02:00
|
|
|
check_types = get_agenda_check_types(agenda)
|
|
|
|
self.presence_check_types = [ct for ct in check_types if ct.kind == 'presence']
|
|
|
|
self.fields['check_type'].choices = [('', '---------')] + [
|
|
|
|
(ct.slug, ct.label) for ct in self.presence_check_types
|
|
|
|
]
|
2022-04-14 16:24:34 +02:00
|
|
|
|
|
|
|
|
2022-02-15 17:16:31 +01:00
|
|
|
class EventsTimesheetForm(forms.Form):
|
|
|
|
date_start = forms.DateField(
|
|
|
|
label=_('Start date'),
|
|
|
|
widget=forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
|
|
|
|
)
|
|
|
|
date_end = forms.DateField(
|
|
|
|
label=_('End date'),
|
|
|
|
widget=forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
|
|
|
|
)
|
2022-02-17 15:22:43 +01:00
|
|
|
extra_data = forms.CharField(
|
|
|
|
label=_('Extra datas'),
|
|
|
|
max_length=250,
|
|
|
|
required=False,
|
|
|
|
help_text=_('Comma separated list of keys defined in extra_data.'),
|
|
|
|
)
|
2022-02-18 16:54:41 +01:00
|
|
|
group_by = forms.CharField(
|
|
|
|
label=_('Group by'),
|
|
|
|
max_length=50,
|
|
|
|
required=False,
|
|
|
|
help_text=_('Key defined in extra_data.'),
|
|
|
|
)
|
2022-03-03 12:03:47 +01:00
|
|
|
with_page_break = forms.BooleanField(
|
|
|
|
label=_('Add a page break before each grouper'),
|
|
|
|
required=False,
|
|
|
|
)
|
2022-02-22 16:01:10 +01:00
|
|
|
sort = forms.ChoiceField(
|
|
|
|
label=_('Sort by'),
|
|
|
|
choices=[
|
|
|
|
('lastname,firstname', _('Last name, first name')),
|
|
|
|
('firstname,lastname', _('First name, last name')),
|
|
|
|
],
|
|
|
|
initial='lastname,firstname',
|
|
|
|
)
|
2022-02-18 15:30:10 +01:00
|
|
|
date_display = forms.ChoiceField(
|
2022-06-28 14:43:28 +02:00
|
|
|
label=_('Date display'),
|
2022-02-18 15:30:10 +01:00
|
|
|
choices=[
|
|
|
|
('all', _('All on the same page')),
|
|
|
|
('month', _('1 month per page')),
|
|
|
|
('week', _('1 week per page')),
|
|
|
|
('custom', _('Custom')),
|
|
|
|
],
|
|
|
|
initial='all',
|
|
|
|
)
|
|
|
|
custom_nb_dates_per_page = forms.IntegerField(
|
|
|
|
label=_('Number of dates per page'),
|
|
|
|
required=False,
|
|
|
|
)
|
2022-06-28 14:43:28 +02:00
|
|
|
activity_display = forms.ChoiceField(
|
|
|
|
label=_('Activity display'),
|
|
|
|
choices=[
|
|
|
|
('row', _('In line')),
|
|
|
|
('col', _('In column')),
|
|
|
|
],
|
|
|
|
initial='row',
|
|
|
|
)
|
2022-02-18 11:39:17 +01:00
|
|
|
orientation = forms.ChoiceField(
|
|
|
|
label=_('PDF orientation'),
|
|
|
|
choices=[
|
|
|
|
('portrait', _('Portrait')),
|
|
|
|
('landscape', _('Landscape')),
|
|
|
|
],
|
|
|
|
initial='portrait',
|
|
|
|
)
|
2022-02-15 17:16:31 +01:00
|
|
|
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
self.agenda = kwargs.pop('agenda')
|
2022-06-17 15:19:20 +02:00
|
|
|
self.event = kwargs.pop('event', None)
|
2022-02-15 17:16:31 +01:00
|
|
|
super().__init__(*args, **kwargs)
|
2022-06-17 15:19:20 +02:00
|
|
|
if self.event is not None:
|
|
|
|
del self.fields['date_start']
|
|
|
|
del self.fields['date_end']
|
2022-06-28 14:43:28 +02:00
|
|
|
del self.fields['date_display']
|
|
|
|
del self.fields['custom_nb_dates_per_page']
|
|
|
|
del self.fields['activity_display']
|
2022-02-15 17:16:31 +01:00
|
|
|
|
|
|
|
def get_slots(self):
|
2022-02-17 15:22:43 +01:00
|
|
|
extra_data = self.cleaned_data['extra_data'].split(',')
|
|
|
|
extra_data = [d.strip() for d in extra_data if d.strip()]
|
2022-02-18 16:54:41 +01:00
|
|
|
group_by = self.cleaned_data['group_by'].strip()
|
|
|
|
all_extra_data = extra_data[:]
|
|
|
|
if group_by:
|
|
|
|
all_extra_data += [group_by]
|
2022-06-17 15:19:20 +02:00
|
|
|
if self.event is not None:
|
|
|
|
all_events = [self.event]
|
|
|
|
min_start = self.event.start_datetime
|
|
|
|
max_start = min_start + datetime.timedelta(days=1)
|
|
|
|
else:
|
|
|
|
min_start = make_aware(
|
|
|
|
datetime.datetime.combine(self.cleaned_data['date_start'], datetime.time(0, 0))
|
|
|
|
)
|
|
|
|
max_start = make_aware(
|
|
|
|
datetime.datetime.combine(self.cleaned_data['date_end'], datetime.time(0, 0))
|
|
|
|
)
|
|
|
|
max_start = max_start + datetime.timedelta(days=1)
|
|
|
|
|
|
|
|
# fetch all events in this range
|
|
|
|
all_events = (
|
|
|
|
self.agenda.event_set.filter(
|
|
|
|
recurrence_days__isnull=True,
|
|
|
|
start_datetime__gte=min_start,
|
|
|
|
start_datetime__lt=max_start,
|
|
|
|
cancelled=False,
|
|
|
|
)
|
|
|
|
.select_related('primary_event')
|
|
|
|
.order_by('start_datetime', 'label')
|
2022-02-15 17:16:31 +01:00
|
|
|
)
|
2022-06-28 14:43:28 +02:00
|
|
|
dates = defaultdict(list)
|
2022-02-15 17:16:31 +01:00
|
|
|
events = []
|
|
|
|
dates_per_event_id = defaultdict(list)
|
|
|
|
for event in all_events:
|
|
|
|
date = localtime(event.start_datetime).date()
|
|
|
|
real_event = event.primary_event or event
|
2022-06-28 14:43:28 +02:00
|
|
|
dates[date].append(real_event)
|
2022-02-15 17:16:31 +01:00
|
|
|
if real_event not in events:
|
|
|
|
events.append(real_event)
|
|
|
|
dates_per_event_id[real_event.pk].append(date)
|
2022-06-28 14:43:28 +02:00
|
|
|
dates = sorted(dates.items(), key=lambda a: a[0])
|
2022-02-15 17:16:31 +01:00
|
|
|
|
2022-06-28 14:43:28 +02:00
|
|
|
date_display = self.cleaned_data.get('date_display') or 'all'
|
2022-02-18 15:30:10 +01:00
|
|
|
if date_display in ['month', 'week']:
|
|
|
|
grouper = defaultdict(list)
|
2022-06-28 14:43:28 +02:00
|
|
|
for date, event in dates:
|
2022-02-18 15:30:10 +01:00
|
|
|
if date_display == 'month':
|
|
|
|
attr = date.month
|
|
|
|
else:
|
|
|
|
attr = date.isocalendar().week
|
2022-06-28 14:43:28 +02:00
|
|
|
grouper[(date.year, attr)].append((date, event))
|
2022-02-18 15:30:10 +01:00
|
|
|
dates = [grouper[g] for g in sorted(grouper.keys())]
|
|
|
|
elif date_display == 'custom':
|
|
|
|
n = self.cleaned_data['custom_nb_dates_per_page']
|
|
|
|
dates = [dates[i : i + n] for i in range(0, len(dates), n)]
|
|
|
|
else:
|
|
|
|
dates = [dates]
|
|
|
|
|
2022-02-15 17:16:31 +01:00
|
|
|
event_slots = []
|
|
|
|
for event in events:
|
|
|
|
event_slots.append(
|
|
|
|
{'event': event, 'dates': {date: False for date in dates_per_event_id[event.pk]}}
|
|
|
|
)
|
|
|
|
|
|
|
|
users = {}
|
|
|
|
subscriptions = self.agenda.subscriptions.filter(date_start__lt=max_start, date_end__gt=min_start)
|
|
|
|
for subscription in subscriptions:
|
|
|
|
if subscription.user_external_id in users:
|
|
|
|
continue
|
|
|
|
users[subscription.user_external_id] = {
|
|
|
|
'user_id': subscription.user_external_id,
|
|
|
|
'user_first_name': subscription.user_first_name,
|
|
|
|
'user_last_name': subscription.user_last_name,
|
2022-02-18 16:54:41 +01:00
|
|
|
'extra_data': {k: (subscription.extra_data or {}).get(k) or '' for k in all_extra_data},
|
2022-02-15 17:16:31 +01:00
|
|
|
'events': copy.deepcopy(event_slots),
|
|
|
|
}
|
|
|
|
|
|
|
|
booking_qs_kwargs = {}
|
|
|
|
if not self.agenda.subscriptions.exists():
|
|
|
|
booking_qs_kwargs = {'cancellation_datetime__isnull': True}
|
|
|
|
booked_qs = (
|
|
|
|
Booking.objects.filter(
|
|
|
|
event__in=all_events,
|
|
|
|
in_waiting_list=False,
|
|
|
|
primary_booking__isnull=True,
|
|
|
|
**booking_qs_kwargs,
|
|
|
|
)
|
|
|
|
.exclude(user_external_id='')
|
|
|
|
.select_related('event')
|
|
|
|
.order_by('event__start_datetime')
|
|
|
|
)
|
|
|
|
for booking in booked_qs:
|
|
|
|
user_id = booking.user_external_id
|
|
|
|
if user_id not in users:
|
|
|
|
users[user_id] = {
|
|
|
|
'user_id': user_id,
|
|
|
|
'user_first_name': booking.user_first_name,
|
|
|
|
'user_last_name': booking.user_last_name,
|
2022-02-18 16:54:41 +01:00
|
|
|
'extra_data': {k: (booking.extra_data or {}).get(k) or '' for k in all_extra_data},
|
2022-02-15 17:16:31 +01:00
|
|
|
'events': copy.deepcopy(event_slots),
|
|
|
|
}
|
|
|
|
if booking.cancellation_datetime is not None:
|
|
|
|
continue
|
|
|
|
# mark the slot as booked
|
|
|
|
date = localtime(booking.event.start_datetime).date()
|
|
|
|
for event in users[user_id]['events']:
|
|
|
|
if event['event'].pk != (booking.event.primary_event_id or booking.event_id):
|
|
|
|
continue
|
|
|
|
if date in event['dates']:
|
|
|
|
event['dates'][date] = True
|
|
|
|
break
|
|
|
|
|
2022-02-22 16:01:10 +01:00
|
|
|
if self.cleaned_data['sort'] == 'lastname,firstname':
|
|
|
|
sort_fields = ['user_last_name', 'user_first_name']
|
|
|
|
else:
|
|
|
|
sort_fields = ['user_first_name', 'user_last_name']
|
|
|
|
|
2022-02-18 16:54:41 +01:00
|
|
|
if group_by:
|
|
|
|
groupers = defaultdict(list)
|
|
|
|
for user in users.values():
|
|
|
|
groupers[user['extra_data'].get(group_by) or ''].append(user)
|
|
|
|
users = [
|
|
|
|
{
|
|
|
|
'grouper': g,
|
2022-02-22 16:01:10 +01:00
|
|
|
'users': sorted(u, key=itemgetter(*sort_fields, 'user_id')),
|
2022-02-18 16:54:41 +01:00
|
|
|
}
|
|
|
|
for g, u in groupers.items()
|
|
|
|
]
|
|
|
|
users = sorted(users, key=itemgetter('grouper'))
|
|
|
|
if users and users[0]['grouper'] == '':
|
|
|
|
users = users[1:] + users[:1]
|
|
|
|
else:
|
|
|
|
users = [
|
|
|
|
{
|
|
|
|
'grouper': '',
|
2022-02-22 16:01:10 +01:00
|
|
|
'users': sorted(users.values(), key=itemgetter(*sort_fields, 'user_id')),
|
2022-02-18 16:54:41 +01:00
|
|
|
}
|
|
|
|
]
|
2022-02-15 17:16:31 +01:00
|
|
|
|
|
|
|
return {
|
|
|
|
'dates': dates,
|
|
|
|
'events': events,
|
|
|
|
'users': users,
|
2022-02-17 15:22:43 +01:00
|
|
|
'extra_data': extra_data,
|
2022-02-15 17:16:31 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
def clean(self):
|
|
|
|
cleaned_data = super().clean()
|
|
|
|
|
|
|
|
if 'date_start' in cleaned_data and 'date_end' in cleaned_data:
|
|
|
|
if cleaned_data['date_end'] < cleaned_data['date_start']:
|
|
|
|
self.add_error('date_end', _('End date must be greater than start date.'))
|
|
|
|
elif (cleaned_data['date_start'] + relativedelta(months=3)) < cleaned_data['date_end']:
|
|
|
|
self.add_error('date_end', _('Please select an interval of no more than 3 months.'))
|
|
|
|
|
2022-02-18 15:30:10 +01:00
|
|
|
if cleaned_data.get('date_display') == 'custom':
|
|
|
|
if not cleaned_data.get('custom_nb_dates_per_page'):
|
|
|
|
self.add_error('custom_nb_dates_per_page', _('This field is required.'))
|
|
|
|
|
2022-02-15 17:16:31 +01:00
|
|
|
return cleaned_data
|
|
|
|
|
|
|
|
|
2020-05-18 16:45:35 +02:00
|
|
|
class AgendaResourceForm(forms.Form):
|
|
|
|
resource = forms.ModelChoiceField(label=_('Resource'), queryset=Resource.objects.none())
|
|
|
|
|
|
|
|
def __init__(self, *args, **kwargs):
|
2020-10-02 14:40:08 +02:00
|
|
|
agenda = kwargs.pop('agenda')
|
2020-05-18 16:45:35 +02:00
|
|
|
super().__init__(*args, **kwargs)
|
2020-10-02 14:40:08 +02:00
|
|
|
self.fields['resource'].queryset = Resource.objects.exclude(agenda=agenda)
|
2020-05-18 16:45:35 +02:00
|
|
|
|
|
|
|
|
2016-10-28 19:06:51 +02:00
|
|
|
class NewMeetingTypeForm(forms.ModelForm):
|
|
|
|
class Meta:
|
|
|
|
model = MeetingType
|
2020-10-02 14:40:08 +02:00
|
|
|
exclude = ['agenda', 'slug', 'deleted']
|
2016-10-28 19:06:51 +02:00
|
|
|
|
2020-02-18 12:14:36 +01:00
|
|
|
def clean(self):
|
|
|
|
super().clean()
|
2020-10-02 14:40:08 +02:00
|
|
|
agenda = self.instance.agenda
|
2020-02-18 12:14:36 +01:00
|
|
|
for virtual_agenda in agenda.virtual_agendas.all():
|
|
|
|
for real_agenda in virtual_agenda.real_agendas.all():
|
|
|
|
if real_agenda != agenda:
|
|
|
|
raise ValidationError(
|
|
|
|
_("Can't add a meetingtype to an agenda that is included in a virtual agenda.")
|
|
|
|
)
|
|
|
|
|
2016-10-28 19:06:51 +02:00
|
|
|
|
2016-09-11 11:31:29 +02:00
|
|
|
class MeetingTypeForm(forms.ModelForm):
|
|
|
|
class Meta:
|
|
|
|
model = MeetingType
|
2020-10-02 14:40:08 +02:00
|
|
|
exclude = ['agenda', 'deleted']
|
2016-09-11 11:31:29 +02:00
|
|
|
|
2021-02-12 15:48:06 +01:00
|
|
|
def clean_slug(self):
|
|
|
|
slug = self.cleaned_data['slug']
|
|
|
|
|
|
|
|
if self.instance.agenda.meetingtype_set.filter(slug=slug).exclude(pk=self.instance.pk).exists():
|
|
|
|
raise ValidationError(_('Another meeting type exists with the same identifier.'))
|
|
|
|
|
|
|
|
return slug
|
|
|
|
|
2020-02-18 12:14:36 +01:00
|
|
|
def clean(self):
|
|
|
|
super().clean()
|
|
|
|
for virtual_agenda in self.instance.agenda.virtual_agendas.all():
|
|
|
|
if virtual_agenda.real_agendas.count() == 1:
|
|
|
|
continue
|
|
|
|
for mt in virtual_agenda.iter_meetingtypes():
|
|
|
|
if (
|
|
|
|
mt.label == self.instance.label
|
|
|
|
and mt.slug == self.instance.slug
|
|
|
|
and mt.duration == self.instance.duration
|
|
|
|
):
|
|
|
|
raise ValidationError(
|
|
|
|
_('This meetingtype is used by a virtual agenda: %s' % virtual_agenda)
|
|
|
|
)
|
|
|
|
|
2016-09-11 11:31:29 +02:00
|
|
|
|
2022-03-10 15:03:54 +01:00
|
|
|
class TimePeriodFormBase(forms.Form):
|
|
|
|
repeat = forms.ChoiceField(
|
|
|
|
label=_('Repeat'),
|
|
|
|
widget=forms.RadioSelect,
|
|
|
|
choices=(
|
|
|
|
('every-week', _('Every week')),
|
|
|
|
('custom', _('Custom')),
|
|
|
|
),
|
|
|
|
initial='every-week',
|
|
|
|
)
|
|
|
|
weekday_indexes = forms.TypedMultipleChoiceField(
|
|
|
|
choices=WEEK_CHOICES,
|
|
|
|
coerce=int,
|
|
|
|
required=False,
|
|
|
|
label='',
|
2022-03-15 16:54:50 +01:00
|
|
|
widget=forms.CheckboxSelectMultiple(),
|
2022-03-10 15:03:54 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
def clean(self):
|
|
|
|
cleaned_data = super().clean()
|
|
|
|
|
|
|
|
if cleaned_data['end_time'] <= cleaned_data['start_time']:
|
|
|
|
raise ValidationError(_('End time must come after start time.'))
|
|
|
|
|
|
|
|
if cleaned_data['repeat'] == 'every-week':
|
|
|
|
cleaned_data['weekday_indexes'] = None
|
|
|
|
|
|
|
|
return cleaned_data
|
|
|
|
|
|
|
|
|
|
|
|
class TimePeriodAddForm(TimePeriodFormBase):
|
|
|
|
field_order = ['weekdays', 'start_time', 'end_time', 'repeat', 'weekday_indexes']
|
|
|
|
|
2018-09-22 18:12:22 +02:00
|
|
|
weekdays = forms.MultipleChoiceField(
|
2021-04-08 17:34:22 +02:00
|
|
|
label=_('Days'), widget=forms.CheckboxSelectMultiple(), choices=WEEKDAYS_LIST
|
2018-09-22 18:12:22 +02:00
|
|
|
)
|
|
|
|
start_time = forms.TimeField(label=_('Start Time'), widget=widgets.TimeWidget())
|
|
|
|
end_time = forms.TimeField(label=_('End Time'), widget=widgets.TimeWidget())
|
|
|
|
|
2019-03-14 14:31:48 +01:00
|
|
|
|
2022-03-10 15:03:54 +01:00
|
|
|
class TimePeriodForm(TimePeriodFormBase, forms.ModelForm):
|
2016-09-11 11:31:29 +02:00
|
|
|
class Meta:
|
|
|
|
model = TimePeriod
|
|
|
|
widgets = {
|
|
|
|
'start_time': widgets.TimeWidget(),
|
|
|
|
'end_time': widgets.TimeWidget(),
|
2017-09-01 15:01:07 +02:00
|
|
|
}
|
2022-03-10 15:03:54 +01:00
|
|
|
fields = ['weekday', 'start_time', 'end_time', 'repeat', 'weekday_indexes']
|
2020-02-26 11:52:05 +01:00
|
|
|
|
2021-01-28 14:38:26 +01:00
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
super().__init__(*args, **kwargs)
|
|
|
|
self.old_weekday = self.instance.weekday
|
|
|
|
self.old_start_time = self.instance.start_time
|
|
|
|
self.old_end_time = self.instance.end_time
|
|
|
|
|
2022-03-10 15:03:54 +01:00
|
|
|
if self.instance.weekday_indexes:
|
|
|
|
self.fields['repeat'].initial = 'custom'
|
2019-03-14 14:31:48 +01:00
|
|
|
|
2021-01-28 14:38:26 +01:00
|
|
|
def save(self):
|
|
|
|
super().save()
|
|
|
|
|
|
|
|
if not self.instance.desk:
|
|
|
|
return self.instance
|
|
|
|
|
|
|
|
if not self.instance.desk.agenda.desk_simple_management:
|
|
|
|
return self.instance
|
|
|
|
|
|
|
|
for desk in self.instance.desk.agenda.desk_set.exclude(pk=self.instance.desk.pk):
|
|
|
|
timeperiod = desk.timeperiod_set.filter(
|
|
|
|
weekday=self.old_weekday, start_time=self.old_start_time, end_time=self.old_end_time
|
|
|
|
).first()
|
|
|
|
if timeperiod is not None:
|
|
|
|
timeperiod.weekday = self.instance.weekday
|
|
|
|
timeperiod.start_time = self.instance.start_time
|
|
|
|
timeperiod.end_time = self.instance.end_time
|
|
|
|
timeperiod.save()
|
|
|
|
|
|
|
|
return self.instance
|
|
|
|
|
2017-09-01 15:01:07 +02:00
|
|
|
|
|
|
|
class NewDeskForm(forms.ModelForm):
|
2020-05-07 17:45:01 +02:00
|
|
|
copy_from = forms.ModelChoiceField(
|
|
|
|
label=_('Copy settings of desk'),
|
|
|
|
required=False,
|
|
|
|
queryset=Desk.objects.none(),
|
|
|
|
)
|
|
|
|
|
2017-09-01 15:01:07 +02:00
|
|
|
class Meta:
|
|
|
|
model = Desk
|
2020-10-02 14:40:08 +02:00
|
|
|
exclude = ['agenda', 'slug']
|
2017-09-01 15:01:07 +02:00
|
|
|
|
2020-05-07 17:45:01 +02:00
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
super().__init__(*args, **kwargs)
|
2021-01-26 15:50:34 +01:00
|
|
|
if self.instance.agenda.desk_simple_management:
|
|
|
|
del self.fields['copy_from']
|
|
|
|
else:
|
|
|
|
self.fields['copy_from'].queryset = Desk.objects.filter(agenda=self.instance.agenda)
|
2020-05-07 17:45:01 +02:00
|
|
|
|
|
|
|
def save(self):
|
2021-01-26 15:50:34 +01:00
|
|
|
if self.instance.agenda.desk_simple_management:
|
|
|
|
desk = self.instance.agenda.desk_set.first()
|
|
|
|
if desk is not None:
|
|
|
|
return desk.duplicate(label=self.cleaned_data['label'])
|
|
|
|
elif self.cleaned_data['copy_from']:
|
2020-05-07 17:45:01 +02:00
|
|
|
return self.cleaned_data['copy_from'].duplicate(label=self.cleaned_data['label'])
|
2021-01-26 15:50:34 +01:00
|
|
|
|
|
|
|
super().save()
|
|
|
|
self.instance.import_timeperiod_exceptions_from_settings(enable=True)
|
|
|
|
return self.instance
|
2020-05-07 17:45:01 +02:00
|
|
|
|
2017-09-01 15:01:07 +02:00
|
|
|
|
|
|
|
class DeskForm(forms.ModelForm):
|
|
|
|
class Meta:
|
|
|
|
model = Desk
|
2020-10-02 14:40:08 +02:00
|
|
|
exclude = ['agenda']
|
2016-09-16 15:04:02 +02:00
|
|
|
|
2021-01-28 10:31:19 +01:00
|
|
|
def clean_slug(self):
|
|
|
|
slug = self.cleaned_data['slug']
|
|
|
|
|
|
|
|
if self.instance.agenda.desk_set.filter(slug=slug).exclude(pk=self.instance.pk).exists():
|
|
|
|
raise ValidationError(_('Another desk exists with the same identifier.'))
|
|
|
|
|
|
|
|
return slug
|
|
|
|
|
2016-09-16 15:04:02 +02:00
|
|
|
|
2017-08-24 14:15:18 +02:00
|
|
|
class TimePeriodExceptionForm(forms.ModelForm):
|
|
|
|
class Meta:
|
|
|
|
model = TimePeriodException
|
2020-10-02 14:40:08 +02:00
|
|
|
fields = ['start_datetime', 'end_datetime', 'label']
|
2020-07-16 14:34:19 +02:00
|
|
|
field_classes = {
|
|
|
|
'start_datetime': SplitDateTimeField,
|
|
|
|
'end_datetime': SplitDateTimeField,
|
2017-08-24 14:15:18 +02:00
|
|
|
}
|
|
|
|
|
2020-08-28 17:35:36 +02:00
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
super().__init__(*args, **kwargs)
|
2021-01-28 16:41:58 +01:00
|
|
|
self.old_label = self.instance.label
|
|
|
|
self.old_start_datetime = self.instance.start_datetime
|
|
|
|
self.old_end_datetime = self.instance.end_datetime
|
2020-08-28 17:35:36 +02:00
|
|
|
|
2020-05-05 14:59:24 +02:00
|
|
|
def clean(self):
|
|
|
|
cleaned_data = super().clean()
|
|
|
|
|
|
|
|
if 'start_datetime' in cleaned_data and 'end_datetime' in cleaned_data:
|
|
|
|
if cleaned_data['end_datetime'] <= cleaned_data['start_datetime']:
|
|
|
|
self.add_error('end_datetime', _('End datetime must be greater than start datetime.'))
|
|
|
|
|
|
|
|
return cleaned_data
|
2017-08-24 14:15:18 +02:00
|
|
|
|
2021-01-28 16:41:58 +01:00
|
|
|
def save(self):
|
|
|
|
super().save()
|
|
|
|
|
|
|
|
self.exceptions = [self.instance]
|
|
|
|
desk_simple_management = False
|
|
|
|
if self.instance.desk_id:
|
|
|
|
desk_simple_management = self.instance.desk.agenda.desk_simple_management
|
|
|
|
if desk_simple_management:
|
|
|
|
for desk in self.instance.desk.agenda.desk_set.exclude(pk=self.instance.desk_id):
|
|
|
|
exception = desk.timeperiodexception_set.filter(
|
|
|
|
source__isnull=True,
|
|
|
|
label=self.old_label,
|
|
|
|
start_datetime=self.old_start_datetime,
|
|
|
|
end_datetime=self.old_end_datetime,
|
|
|
|
).first()
|
|
|
|
if exception is not None:
|
|
|
|
exception.label = self.instance.label
|
|
|
|
exception.start_datetime = self.instance.start_datetime
|
|
|
|
exception.end_datetime = self.instance.end_datetime
|
|
|
|
exception.save()
|
|
|
|
self.exceptions.append(exception)
|
|
|
|
|
|
|
|
return self.instance
|
|
|
|
|
|
|
|
|
|
|
|
class NewTimePeriodExceptionForm(TimePeriodExceptionForm):
|
|
|
|
all_desks = forms.BooleanField(label=_('Apply exception on all desks of the agenda'), required=False)
|
|
|
|
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
super().__init__(*args, **kwargs)
|
|
|
|
if self.instance.unavailability_calendar:
|
|
|
|
del self.fields['all_desks']
|
|
|
|
elif self.instance.desk_id and self.instance.desk.agenda.desk_set.count() == 1:
|
|
|
|
del self.fields['all_desks']
|
|
|
|
elif self.instance.desk_id and self.instance.desk.agenda.desk_simple_management:
|
|
|
|
del self.fields['all_desks']
|
|
|
|
|
|
|
|
def save(self):
|
|
|
|
super().save()
|
|
|
|
|
|
|
|
self.exceptions = [self.instance]
|
|
|
|
all_desks = self.cleaned_data.get('all_desks')
|
|
|
|
desk_simple_management = False
|
|
|
|
if self.instance.desk_id:
|
|
|
|
desk_simple_management = self.instance.desk.agenda.desk_simple_management
|
|
|
|
if all_desks or desk_simple_management:
|
|
|
|
for desk in self.instance.desk.agenda.desk_set.exclude(pk=self.instance.desk_id):
|
|
|
|
self.exceptions.append(
|
|
|
|
TimePeriodException.objects.create(
|
|
|
|
desk=desk,
|
|
|
|
label=self.instance.label,
|
|
|
|
start_datetime=self.instance.start_datetime,
|
|
|
|
end_datetime=self.instance.end_datetime,
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
return self.instance
|
|
|
|
|
2017-08-24 14:15:18 +02:00
|
|
|
|
2020-02-18 12:14:36 +01:00
|
|
|
class VirtualMemberForm(forms.ModelForm):
|
|
|
|
class Meta:
|
|
|
|
model = VirtualMember
|
2020-10-02 14:40:08 +02:00
|
|
|
fields = ['real_agenda']
|
2020-02-18 12:14:36 +01:00
|
|
|
|
|
|
|
def __init__(self, *args, **kwargs):
|
2021-07-09 16:01:42 +02:00
|
|
|
super().__init__(*args, **kwargs)
|
2020-02-18 12:14:36 +01:00
|
|
|
self.fields['real_agenda'].queryset = Agenda.objects.filter(kind='meetings').exclude(
|
2020-10-02 14:40:08 +02:00
|
|
|
virtual_agendas=self.instance.virtual_agenda
|
2020-02-18 12:14:36 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
2016-09-16 15:04:02 +02:00
|
|
|
class ImportEventsForm(forms.Form):
|
2017-05-05 15:56:33 +02:00
|
|
|
events_csv_file = forms.FileField(
|
|
|
|
label=_('Events File'),
|
|
|
|
required=True,
|
2016-09-16 15:04:02 +02:00
|
|
|
help_text=_(
|
|
|
|
'CSV file with date, time, number of places, '
|
2020-01-20 20:03:51 +01:00
|
|
|
'number of places in waiting list, label, and '
|
2020-07-03 09:45:15 +02:00
|
|
|
'optionally, identifier, description, pricing, '
|
|
|
|
'URL, and publication date as columns.'
|
2019-12-16 16:21:24 +01:00
|
|
|
),
|
2016-09-16 15:04:02 +02:00
|
|
|
)
|
2016-09-16 15:22:27 +02:00
|
|
|
events = None
|
2016-09-16 15:04:02 +02:00
|
|
|
|
2019-11-27 09:27:44 +01:00
|
|
|
def __init__(self, agenda_pk, **kwargs):
|
|
|
|
self.agenda_pk = agenda_pk
|
2021-07-09 16:01:42 +02:00
|
|
|
super().__init__(**kwargs)
|
2019-11-27 09:27:44 +01:00
|
|
|
|
2016-09-16 15:04:02 +02:00
|
|
|
def clean_events_csv_file(self):
|
|
|
|
content = self.cleaned_data['events_csv_file'].read()
|
2018-03-25 11:26:47 +02:00
|
|
|
if b'\0' in content:
|
2016-09-16 15:04:02 +02:00
|
|
|
raise ValidationError(_('Invalid file format.'))
|
|
|
|
|
2020-06-17 14:29:33 +02:00
|
|
|
for charset in ('utf-8-sig', 'iso-8859-15'):
|
2019-03-25 18:17:06 +01:00
|
|
|
try:
|
|
|
|
content = content.decode(charset)
|
|
|
|
break
|
|
|
|
except UnicodeDecodeError:
|
|
|
|
continue
|
|
|
|
# all byte-sequences are ok for iso-8859-15 so we will always reach
|
|
|
|
# this line with content being a unicode string.
|
|
|
|
|
2016-09-16 15:04:02 +02:00
|
|
|
try:
|
2019-09-21 22:36:08 +02:00
|
|
|
dialect = csv.Sniffer().sniff(content)
|
2022-07-05 11:25:43 +02:00
|
|
|
dialect.doublequote = True
|
2016-09-16 15:22:27 +02:00
|
|
|
except csv.Error:
|
2016-09-16 15:04:02 +02:00
|
|
|
dialect = None
|
|
|
|
|
|
|
|
events = []
|
2020-09-11 09:29:16 +02:00
|
|
|
warnings = {}
|
2021-08-21 09:55:47 +02:00
|
|
|
events_by_slug = {x.slug: x for x in Event.objects.filter(agenda=self.agenda_pk)}
|
2020-09-11 09:29:16 +02:00
|
|
|
event_ids_with_bookings = set(
|
|
|
|
Booking.objects.filter(
|
|
|
|
event__agenda=self.agenda_pk, cancellation_datetime__isnull=True
|
|
|
|
).values_list('event_id', flat=True)
|
|
|
|
)
|
|
|
|
seen_slugs = set(events_by_slug.keys())
|
2020-07-10 09:16:01 +02:00
|
|
|
for i, csvline in enumerate(csv.reader(StringIO(content), dialect=dialect)):
|
2016-09-16 15:04:02 +02:00
|
|
|
if not csvline:
|
|
|
|
continue
|
2016-09-16 15:27:03 +02:00
|
|
|
if len(csvline) < 3:
|
2016-09-16 15:04:02 +02:00
|
|
|
raise ValidationError(_('Invalid file format. (line %d)') % (i + 1))
|
2016-09-16 15:28:09 +02:00
|
|
|
if i == 0 and csvline[0].strip('#') in ('date', 'Date', _('date'), _('Date')):
|
2016-09-16 15:04:02 +02:00
|
|
|
continue
|
2020-09-11 09:29:16 +02:00
|
|
|
|
|
|
|
# label needed to generate a slug
|
|
|
|
label = None
|
|
|
|
if len(csvline) >= 5:
|
|
|
|
label = force_text(csvline[4])
|
|
|
|
|
|
|
|
# get or create event
|
|
|
|
event = None
|
|
|
|
slug = None
|
|
|
|
if len(csvline) >= 6:
|
|
|
|
slug = force_text(csvline[5]) if csvline[5] else None
|
|
|
|
# get existing event if relevant
|
|
|
|
if slug and slug in seen_slugs:
|
|
|
|
event = events_by_slug[slug]
|
|
|
|
# update label
|
|
|
|
event.label = label
|
|
|
|
if event is None:
|
|
|
|
# new event
|
|
|
|
event = Event(agenda_id=self.agenda_pk, label=label)
|
|
|
|
# generate a slug if not provided
|
|
|
|
event.slug = slug or generate_slug(event, seen_slugs=seen_slugs, agenda=self.agenda_pk)
|
|
|
|
# maintain caches
|
|
|
|
seen_slugs.add(event.slug)
|
|
|
|
events_by_slug[event.slug] = event
|
|
|
|
|
2016-09-16 15:04:02 +02:00
|
|
|
for datetime_fmt in (
|
|
|
|
'%Y-%m-%d %H:%M',
|
|
|
|
'%d/%m/%Y %H:%M',
|
|
|
|
'%d/%m/%Y %Hh%M',
|
|
|
|
'%Y-%m-%d %H:%M:%S',
|
|
|
|
'%d/%m/%Y %H:%M:%S',
|
|
|
|
):
|
|
|
|
try:
|
2020-09-11 09:29:16 +02:00
|
|
|
event_datetime = make_aware(
|
|
|
|
datetime.datetime.strptime('%s %s' % tuple(csvline[:2]), datetime_fmt)
|
|
|
|
)
|
2016-09-16 15:04:02 +02:00
|
|
|
except ValueError:
|
|
|
|
continue
|
2020-09-11 09:29:16 +02:00
|
|
|
if (
|
|
|
|
event.pk is not None
|
|
|
|
and event.start_datetime != event_datetime
|
|
|
|
and event.start_datetime > now()
|
|
|
|
and event.pk in event_ids_with_bookings
|
|
|
|
and event.pk not in warnings
|
|
|
|
):
|
|
|
|
# event start datetime has changed, event is not past and has not cancelled bookings
|
|
|
|
# => warn the user
|
|
|
|
warnings[event.pk] = event
|
|
|
|
event.start_datetime = event_datetime
|
2016-09-16 15:04:02 +02:00
|
|
|
break
|
|
|
|
else:
|
|
|
|
raise ValidationError(_('Invalid file format. (date/time format, line %d)') % (i + 1))
|
|
|
|
try:
|
|
|
|
event.places = int(csvline[2])
|
|
|
|
except ValueError:
|
|
|
|
raise ValidationError(_('Invalid file format. (number of places, line %d)') % (i + 1))
|
|
|
|
if len(csvline) >= 4:
|
|
|
|
try:
|
|
|
|
event.waiting_list_places = int(csvline[3])
|
|
|
|
except ValueError:
|
|
|
|
raise ValidationError(
|
|
|
|
_('Invalid file format. (number of places in waiting list, line %d)') % (i + 1)
|
2019-12-16 16:21:24 +01:00
|
|
|
)
|
2020-09-11 09:29:16 +02:00
|
|
|
|
2020-01-20 20:03:51 +01:00
|
|
|
column_index = 7
|
|
|
|
for more_attr in ('description', 'pricing', 'url'):
|
|
|
|
if len(csvline) >= column_index:
|
|
|
|
setattr(event, more_attr, csvline[column_index - 1])
|
|
|
|
column_index += 1
|
|
|
|
|
2020-06-18 10:39:04 +02:00
|
|
|
if len(csvline) >= 10 and csvline[9]: # publication date is optional
|
2021-10-08 15:48:14 +02:00
|
|
|
for datetime_fmt in (
|
|
|
|
'%Y-%m-%d',
|
|
|
|
'%d/%m/%Y',
|
|
|
|
'%Y-%m-%d %H:%M',
|
|
|
|
'%d/%m/%Y %H:%M',
|
|
|
|
'%d/%m/%Y %Hh%M',
|
|
|
|
'%Y-%m-%d %H:%M:%S',
|
|
|
|
'%d/%m/%Y %H:%M:%S',
|
|
|
|
):
|
2020-06-18 10:39:04 +02:00
|
|
|
try:
|
2021-10-08 15:48:14 +02:00
|
|
|
event.publication_datetime = make_aware(
|
|
|
|
datetime.datetime.strptime(csvline[9], datetime_fmt)
|
|
|
|
)
|
2020-06-18 10:39:04 +02:00
|
|
|
break
|
|
|
|
except ValueError:
|
|
|
|
continue
|
|
|
|
else:
|
2021-10-08 15:48:14 +02:00
|
|
|
raise ValidationError(_('Invalid file format. (date/time format, line %d)') % (i + 1))
|
2020-06-18 10:39:04 +02:00
|
|
|
|
2020-07-09 16:10:37 +02:00
|
|
|
if len(csvline) >= 11 and csvline[10]: # duration is optional
|
|
|
|
try:
|
|
|
|
event.duration = int(csvline[10])
|
|
|
|
except ValueError:
|
|
|
|
raise ValidationError(_('Invalid file format. (duration, line %d)') % (i + 1))
|
|
|
|
|
2019-07-23 16:30:30 +02:00
|
|
|
try:
|
2020-12-22 17:26:29 +01:00
|
|
|
event.full_clean(exclude=['desk', 'meeting_type', 'primary_event'])
|
2019-07-23 16:30:30 +02:00
|
|
|
except ValidationError as e:
|
2020-06-11 17:04:58 +02:00
|
|
|
errors = [_('Invalid file format:\n')]
|
|
|
|
for label, field_errors in e.message_dict.items():
|
|
|
|
label_name = self.get_verbose_name(label)
|
|
|
|
msg = _('%s: ') % label_name if label_name else ''
|
|
|
|
msg += _('%(errors)s (line %(line)d)') % {
|
|
|
|
'errors': ', '.join(field_errors),
|
|
|
|
'line': i + 1,
|
|
|
|
}
|
|
|
|
errors.append(msg)
|
2019-07-23 16:30:30 +02:00
|
|
|
raise ValidationError(errors)
|
2016-09-16 15:04:02 +02:00
|
|
|
events.append(event)
|
|
|
|
self.events = events
|
2020-09-11 09:29:16 +02:00
|
|
|
self.warnings = warnings
|
2017-09-03 13:28:50 +02:00
|
|
|
|
2020-06-11 17:04:58 +02:00
|
|
|
@staticmethod
|
|
|
|
def get_verbose_name(field_name):
|
|
|
|
try:
|
|
|
|
return Event._meta.get_field(field_name).verbose_name
|
|
|
|
except FieldDoesNotExist:
|
|
|
|
return ''
|
|
|
|
|
2017-09-03 13:28:50 +02:00
|
|
|
|
|
|
|
class ExceptionsImportForm(forms.ModelForm):
|
2017-10-03 23:15:00 +02:00
|
|
|
ics_file = forms.FileField(
|
|
|
|
label=_('ICS File'),
|
|
|
|
required=False,
|
2020-06-04 09:54:32 +02:00
|
|
|
help_text=_('ICS file containing events which will be considered as exceptions.'),
|
2017-11-06 10:40:24 +01:00
|
|
|
)
|
2022-06-27 15:20:09 +02:00
|
|
|
ics_url = forms.CharField(
|
2017-10-03 23:15:00 +02:00
|
|
|
label=_('URL'),
|
2022-06-27 15:20:09 +02:00
|
|
|
max_length=200,
|
2017-10-03 23:15:00 +02:00
|
|
|
required=False,
|
2017-11-06 10:40:24 +01:00
|
|
|
help_text=_('URL to remote calendar which will be synchronised hourly.'),
|
|
|
|
)
|
2022-03-23 14:22:53 +01:00
|
|
|
|
|
|
|
def clean(self, *args, **kwargs):
|
|
|
|
cleaned_data = super().clean(*args, **kwargs)
|
|
|
|
if not cleaned_data.get('ics_file') and not cleaned_data.get('ics_url'):
|
|
|
|
raise forms.ValidationError(_('Please provide an ICS File or an URL.'))
|
|
|
|
|
2022-06-27 15:20:09 +02:00
|
|
|
def clean_ics_url(self):
|
|
|
|
url = self.cleaned_data['ics_url']
|
|
|
|
if not url:
|
|
|
|
return url
|
|
|
|
|
|
|
|
try:
|
|
|
|
url = Template(url).render(Context(settings.TEMPLATE_VARS))
|
|
|
|
except (TemplateSyntaxError, VariableDoesNotExist) as e:
|
|
|
|
raise ValidationError(_('syntax error: %s') % e)
|
|
|
|
|
|
|
|
URLValidator()(url)
|
|
|
|
|
|
|
|
return self.cleaned_data['ics_url']
|
|
|
|
|
2022-03-23 14:22:53 +01:00
|
|
|
|
|
|
|
class DeskExceptionsImportForm(ExceptionsImportForm):
|
2021-08-10 14:58:31 +02:00
|
|
|
all_desks = forms.BooleanField(label=_('Apply exceptions on all desks of the agenda'), required=False)
|
2018-09-22 16:25:46 +02:00
|
|
|
|
2019-12-10 15:28:39 +01:00
|
|
|
class Meta:
|
|
|
|
model = Desk
|
|
|
|
fields = []
|
|
|
|
|
2021-08-10 14:58:31 +02:00
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
super().__init__(*args, **kwargs)
|
|
|
|
if self.instance.agenda.desk_set.count() == 1:
|
|
|
|
del self.fields['all_desks']
|
|
|
|
elif self.instance.agenda.desk_simple_management:
|
|
|
|
del self.fields['all_desks']
|
|
|
|
|
2022-03-23 14:22:53 +01:00
|
|
|
|
|
|
|
class UnavailabilityCalendarExceptionsImportForm(ExceptionsImportForm):
|
|
|
|
class Meta:
|
|
|
|
model = UnavailabilityCalendar
|
|
|
|
fields = []
|
2019-12-10 15:28:39 +01:00
|
|
|
|
2018-09-22 16:25:46 +02:00
|
|
|
|
2019-12-12 10:56:14 +01:00
|
|
|
class TimePeriodExceptionSourceReplaceForm(forms.ModelForm):
|
2020-01-28 15:08:24 +01:00
|
|
|
ics_newfile = forms.FileField(
|
2019-12-12 10:56:14 +01:00
|
|
|
label=_('ICS File'),
|
|
|
|
required=False,
|
|
|
|
help_text=_('ICS file containing events which will be considered as exceptions.'),
|
|
|
|
)
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
model = TimePeriodExceptionSource
|
|
|
|
fields = []
|
|
|
|
|
2020-01-28 15:08:24 +01:00
|
|
|
def save(self, *args, **kwargs):
|
2021-02-01 15:40:03 +01:00
|
|
|
def store_source(source):
|
|
|
|
if bool(source.ics_file):
|
|
|
|
source.ics_file.delete()
|
|
|
|
source.ics_file = self.cleaned_data['ics_newfile']
|
|
|
|
source.ics_filename = self.cleaned_data['ics_newfile'].name
|
|
|
|
source.save()
|
|
|
|
self.cleaned_data['ics_newfile'].seek(0)
|
|
|
|
|
|
|
|
old_filename = self.instance.ics_filename
|
|
|
|
store_source(self.instance)
|
2022-03-23 14:22:53 +01:00
|
|
|
if self.instance.desk and self.instance.desk.agenda.desk_simple_management:
|
2021-02-01 15:40:03 +01:00
|
|
|
for desk in self.instance.desk.agenda.desk_set.exclude(pk=self.instance.desk_id):
|
|
|
|
source = desk.timeperiodexceptionsource_set.filter(ics_filename=old_filename).first()
|
|
|
|
if source is not None:
|
|
|
|
store_source(source)
|
|
|
|
|
|
|
|
return self.instance
|
2020-01-28 15:08:24 +01:00
|
|
|
|
2019-12-12 10:56:14 +01:00
|
|
|
|
2018-09-22 16:25:46 +02:00
|
|
|
class AgendasImportForm(forms.Form):
|
2020-10-26 17:15:47 +01:00
|
|
|
agendas_json = forms.FileField(label=_('Export File'))
|
2020-06-17 10:52:25 +02:00
|
|
|
|
|
|
|
|
|
|
|
class AgendaDuplicateForm(forms.Form):
|
|
|
|
label = forms.CharField(label=_('New label'), max_length=150, required=False)
|
2020-07-08 16:10:53 +02:00
|
|
|
|
|
|
|
|
|
|
|
class BookingCancelForm(forms.ModelForm):
|
|
|
|
disable_trigger = forms.BooleanField(
|
|
|
|
label=_('Do not send cancel trigger to form'), initial=False, required=False, widget=forms.HiddenInput
|
|
|
|
)
|
|
|
|
|
|
|
|
def show_trigger_checkbox(self):
|
|
|
|
self.fields['disable_trigger'].widget = forms.CheckboxInput()
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
model = Booking
|
|
|
|
fields = []
|
2020-07-09 12:46:13 +02:00
|
|
|
|
|
|
|
|
|
|
|
class EventCancelForm(forms.ModelForm):
|
|
|
|
class Meta:
|
|
|
|
model = Event
|
|
|
|
fields = []
|
2020-07-16 15:12:47 +02:00
|
|
|
|
|
|
|
|
2022-05-24 10:18:13 +02:00
|
|
|
class AgendaDisplaySettingsForm(forms.ModelForm):
|
|
|
|
class Meta:
|
|
|
|
model = Agenda
|
|
|
|
fields = [
|
|
|
|
'event_display_template',
|
|
|
|
'booking_user_block_template',
|
|
|
|
]
|
|
|
|
widgets = {'booking_user_block_template': forms.Textarea(attrs={'rows': 3})}
|
|
|
|
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
super().__init__(*args, **kwargs)
|
2022-05-24 14:38:29 +02:00
|
|
|
if kwargs['instance'].kind == 'events':
|
|
|
|
self.fields['booking_user_block_template'].help_text = (
|
|
|
|
_('Displayed for each booking in event page and check page'),
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
self.fields['booking_user_block_template'].help_text = (
|
|
|
|
_('Displayed for each booking in agenda view pages'),
|
|
|
|
)
|
|
|
|
del self.fields['event_display_template']
|
2022-05-24 10:18:13 +02:00
|
|
|
|
|
|
|
|
2021-04-19 15:47:38 +02:00
|
|
|
class AgendaBookingCheckSettingsForm(forms.ModelForm):
|
|
|
|
class Meta:
|
|
|
|
model = Agenda
|
2021-05-21 10:29:20 +02:00
|
|
|
fields = [
|
|
|
|
'booking_check_filters',
|
|
|
|
'mark_event_checked_auto',
|
2021-06-29 16:43:26 +02:00
|
|
|
'disable_check_update',
|
2021-05-21 10:29:20 +02:00
|
|
|
]
|
2021-04-19 15:47:38 +02:00
|
|
|
|
|
|
|
|
2020-07-16 15:12:47 +02:00
|
|
|
class AgendaNotificationsForm(forms.ModelForm):
|
|
|
|
class Meta:
|
|
|
|
model = AgendaNotificationsSettings
|
2020-10-02 14:40:08 +02:00
|
|
|
exclude = ['agenda']
|
2020-07-16 15:12:47 +02:00
|
|
|
|
2020-09-08 16:55:56 +02:00
|
|
|
@staticmethod
|
|
|
|
def update_choices(choices, settings):
|
|
|
|
new_choices = []
|
|
|
|
for choice in choices:
|
|
|
|
if choice[0] in (settings.EDIT_ROLE, settings.VIEW_ROLE):
|
|
|
|
role = settings.get_role_from_choice(choice[0]) or _('undefined')
|
|
|
|
choice = (choice[0], '%s (%s)' % (choice[1], role))
|
|
|
|
new_choices.append(choice)
|
|
|
|
return new_choices
|
|
|
|
|
2020-07-16 15:12:47 +02:00
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
super().__init__(*args, **kwargs)
|
|
|
|
|
|
|
|
for email_field in AgendaNotificationsSettings.get_email_field_names():
|
|
|
|
self.fields[email_field].widget.attrs['size'] = 80
|
|
|
|
self.fields[email_field].label = ''
|
|
|
|
self.fields[email_field].help_text = _('Enter a comma separated list of email addresses.')
|
2020-09-08 16:55:56 +02:00
|
|
|
|
|
|
|
settings = kwargs['instance']
|
|
|
|
for role_field in AgendaNotificationsSettings.get_role_field_names():
|
|
|
|
field = self.fields[role_field]
|
|
|
|
field.choices = self.update_choices(field.choices, settings)
|
2020-09-15 14:05:38 +02:00
|
|
|
|
|
|
|
|
|
|
|
class AgendaReminderForm(forms.ModelForm):
|
|
|
|
class Meta:
|
|
|
|
model = AgendaReminderSettings
|
|
|
|
exclude = ['agenda']
|
|
|
|
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
super().__init__(*args, **kwargs)
|
2020-09-17 12:13:15 +02:00
|
|
|
if not settings.SMS_URL:
|
2022-02-02 17:45:15 +01:00
|
|
|
del self.fields['days_before_sms']
|
2020-09-15 14:05:38 +02:00
|
|
|
del self.fields['sms_extra_info']
|
|
|
|
|
2020-11-04 17:26:16 +01:00
|
|
|
|
2022-02-15 10:52:24 +01:00
|
|
|
class BookingChoiceField(forms.ModelChoiceField):
|
|
|
|
def label_from_instance(self, obj):
|
|
|
|
name = obj.user_name or obj.label or _('Anonymous')
|
|
|
|
date = date_format(localtime(obj.creation_datetime), 'SHORT_DATETIME_FORMAT')
|
|
|
|
emails = ', '.join(sorted(obj.emails)) or _('no email')
|
|
|
|
phone_numbers = ', '.join(sorted(obj.phone_numbers)) or _('no phone number')
|
|
|
|
|
|
|
|
if settings.SMS_URL:
|
|
|
|
return '%s, %s, %s (%s)' % (name, emails, phone_numbers, date)
|
|
|
|
else:
|
|
|
|
return '%s, %s (%s)' % (name, emails, date)
|
|
|
|
|
|
|
|
|
|
|
|
class AgendaReminderTestForm(forms.Form):
|
|
|
|
booking = BookingChoiceField(
|
|
|
|
label=_('Booking'),
|
|
|
|
queryset=Booking.objects.none(),
|
|
|
|
help_text=_('Only the last ten bookings are displayed.'),
|
|
|
|
)
|
|
|
|
msg_type = forms.MultipleChoiceField(
|
|
|
|
label=_('Send via'),
|
|
|
|
choices=(('email', _('Email')), ('sms', _('SMS'))),
|
|
|
|
widget=forms.CheckboxSelectMultiple(),
|
|
|
|
)
|
|
|
|
email = forms.EmailField(
|
|
|
|
label=_('Email'),
|
|
|
|
help_text=_('This will override email specified on booking creation, if any.'),
|
|
|
|
required=False,
|
|
|
|
)
|
|
|
|
phone_number = forms.CharField(
|
|
|
|
label=_('Phone number'),
|
|
|
|
max_length=16,
|
|
|
|
help_text=_('This will override phone number specified on booking creation, if any.'),
|
|
|
|
required=False,
|
|
|
|
)
|
|
|
|
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
agenda = kwargs.pop('agenda')
|
|
|
|
super().__init__(*args, **kwargs)
|
|
|
|
self.fields['booking'].queryset = Booking.objects.filter(
|
|
|
|
pk__in=Booking.objects.filter(event__agenda=agenda).order_by('-creation_datetime')[:10]
|
|
|
|
).order_by('-creation_datetime')
|
|
|
|
|
|
|
|
if not settings.SMS_URL:
|
|
|
|
self.fields['msg_type'].initial = ['email']
|
|
|
|
self.fields['msg_type'].widget = forms.MultipleHiddenInput()
|
|
|
|
del self.fields['phone_number']
|
|
|
|
|
|
|
|
|
2020-11-04 17:26:16 +01:00
|
|
|
class AgendasExportForm(forms.Form):
|
|
|
|
agendas = forms.BooleanField(label=_('Agendas'), required=False, initial=True)
|
2022-04-01 14:25:41 +02:00
|
|
|
resources = forms.BooleanField(label=_('Resources'), required=False, initial=True)
|
2020-11-04 17:26:16 +01:00
|
|
|
unavailability_calendars = forms.BooleanField(
|
|
|
|
label=_('Unavailability calendars'), required=False, initial=True
|
|
|
|
)
|
2021-12-08 17:58:20 +01:00
|
|
|
categories = forms.BooleanField(label=_('Categories'), required=False, initial=True)
|
2022-04-01 10:13:52 +02:00
|
|
|
events_types = forms.BooleanField(label=_('Events types'), required=False, initial=True)
|
2022-06-28 14:50:00 +02:00
|
|
|
shared_custody = forms.BooleanField(label=_('Shared custody'), required=False, initial=True)
|
|
|
|
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
super().__init__(*args, **kwargs)
|
|
|
|
if not SharedCustodySettings.objects.exists():
|
|
|
|
self.fields['shared_custody'].initial = False
|
|
|
|
self.fields['shared_custody'].widget = forms.HiddenInput()
|
2022-02-22 15:59:27 +01:00
|
|
|
|
|
|
|
|
|
|
|
class SharedCustodyRuleForm(forms.ModelForm):
|
|
|
|
guardian = forms.ModelChoiceField(label=_('Guardian'), queryset=Person.objects.none())
|
|
|
|
days = forms.TypedMultipleChoiceField(
|
|
|
|
choices=WEEKDAY_CHOICES,
|
|
|
|
coerce=int,
|
|
|
|
required=False,
|
|
|
|
widget=WeekdaysWidget,
|
|
|
|
label=_('Days'),
|
|
|
|
)
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
model = SharedCustodyRule
|
|
|
|
fields = ['guardian', 'days', 'weeks']
|
|
|
|
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
super().__init__(*args, **kwargs)
|
|
|
|
self.fields['guardian'].empty_label = None
|
|
|
|
self.fields['guardian'].queryset = Person.objects.filter(
|
|
|
|
pk__in=[self.instance.agenda.first_guardian_id, self.instance.agenda.second_guardian_id]
|
|
|
|
)
|
|
|
|
|
|
|
|
def clean(self):
|
|
|
|
cleaned_data = super().clean()
|
|
|
|
|
|
|
|
if self.instance.agenda.rule_overlaps(
|
|
|
|
days=cleaned_data['days'], weeks=cleaned_data['weeks'], instance=self.instance
|
|
|
|
):
|
|
|
|
raise ValidationError(_('Rule overlaps existing rules.'))
|
|
|
|
|
|
|
|
return cleaned_data
|
|
|
|
|
|
|
|
|
2022-03-24 17:05:17 +01:00
|
|
|
class SharedCustodyHolidayRuleForm(forms.ModelForm):
|
|
|
|
guardian = forms.ModelChoiceField(label=_('Guardian'), queryset=Person.objects.none())
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
model = SharedCustodyHolidayRule
|
|
|
|
fields = ['guardian', 'holiday', 'years', 'periodicity']
|
|
|
|
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
super().__init__(*args, **kwargs)
|
|
|
|
self.fields['guardian'].empty_label = None
|
|
|
|
self.fields['guardian'].queryset = Person.objects.filter(
|
|
|
|
pk__in=[self.instance.agenda.first_guardian_id, self.instance.agenda.second_guardian_id]
|
|
|
|
)
|
2022-06-28 15:17:57 +02:00
|
|
|
settings = SharedCustodySettings.get_singleton()
|
2022-03-24 17:05:17 +01:00
|
|
|
self.fields['holiday'].queryset = TimePeriodExceptionGroup.objects.filter(
|
2022-06-28 15:17:57 +02:00
|
|
|
unavailability_calendar=settings.holidays_calendar_id,
|
2022-03-24 17:05:17 +01:00
|
|
|
exceptions__isnull=False,
|
|
|
|
).distinct()
|
|
|
|
|
|
|
|
def clean(self):
|
|
|
|
cleaned_data = super().clean()
|
|
|
|
|
|
|
|
holidays = cleaned_data['holiday'].exceptions.annotate(
|
|
|
|
delta=ExpressionWrapper(F('end_datetime') - F('start_datetime'), output_field=DurationField())
|
|
|
|
)
|
|
|
|
is_short_holiday = holidays.filter(delta__lt=datetime.timedelta(days=28)).exists()
|
|
|
|
if 'quarters' in cleaned_data['periodicity'] and is_short_holiday:
|
|
|
|
raise ValidationError(_('Short holidays cannot be cut into quarters.'))
|
|
|
|
|
|
|
|
if self.instance.agenda.holiday_rule_overlaps(
|
|
|
|
cleaned_data['holiday'], cleaned_data['years'], cleaned_data['periodicity'], self.instance
|
|
|
|
):
|
|
|
|
raise ValidationError(_('Rule overlaps existing rules.'))
|
|
|
|
|
|
|
|
return cleaned_data
|
|
|
|
|
|
|
|
def save(self, *args, **kwargs):
|
|
|
|
with transaction.atomic():
|
|
|
|
super().save()
|
|
|
|
self.instance.update_or_create_periods()
|
|
|
|
|
|
|
|
return self.instance
|
|
|
|
|
|
|
|
|
2022-02-22 15:59:27 +01:00
|
|
|
class SharedCustodyPeriodForm(forms.ModelForm):
|
|
|
|
guardian = forms.ModelChoiceField(label=_('Guardian'), queryset=Person.objects.none())
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
model = SharedCustodyPeriod
|
|
|
|
fields = ['guardian', 'date_start', 'date_end']
|
|
|
|
widgets = {
|
|
|
|
'date_start': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
|
|
|
|
'date_end': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
|
|
|
|
}
|
|
|
|
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
super().__init__(*args, **kwargs)
|
|
|
|
self.fields['guardian'].empty_label = None
|
|
|
|
self.fields['guardian'].queryset = Person.objects.filter(
|
|
|
|
pk__in=[self.instance.agenda.first_guardian_id, self.instance.agenda.second_guardian_id]
|
|
|
|
)
|
|
|
|
|
|
|
|
def clean(self):
|
|
|
|
cleaned_data = super().clean()
|
|
|
|
|
|
|
|
if self.instance.agenda.period_overlaps(
|
|
|
|
date_start=cleaned_data['date_start'], date_end=cleaned_data['date_end'], instance=self.instance
|
|
|
|
):
|
|
|
|
raise ValidationError(_('Period overlaps existing periods.'))
|
|
|
|
|
|
|
|
if cleaned_data['date_end'] <= cleaned_data['date_start']:
|
|
|
|
self.add_error('date_end', _('End date must be greater than start date.'))
|
|
|
|
|
|
|
|
return cleaned_data
|
2022-06-28 14:49:26 +02:00
|
|
|
|
|
|
|
|
|
|
|
class SharedCustodySettingsForm(forms.ModelForm):
|
|
|
|
management_role = forms.ModelChoiceField(
|
|
|
|
label=_('Management role'), required=False, queryset=Group.objects.all().order_by('name')
|
|
|
|
)
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
model = SharedCustodySettings
|
|
|
|
fields = '__all__'
|