1957 lines
70 KiB
Python
1957 lines
70 KiB
Python
# 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/>.
|
|
|
|
|
|
import copy
|
|
import csv
|
|
import datetime
|
|
from collections import defaultdict
|
|
from io import StringIO
|
|
from operator import itemgetter
|
|
|
|
import django_filters
|
|
from dateutil.relativedelta import relativedelta
|
|
from django import forms
|
|
from django.conf import settings
|
|
from django.contrib.humanize.templatetags.humanize import ordinal
|
|
from django.core.exceptions import FieldDoesNotExist
|
|
from django.core.validators import URLValidator
|
|
from django.db import transaction
|
|
from django.db.models import DurationField, ExpressionWrapper, F
|
|
from django.forms import ValidationError, formset_factory
|
|
from django.template import Context, Template, TemplateSyntaxError, VariableDoesNotExist
|
|
from django.utils.encoding import force_str
|
|
from django.utils.formats import date_format
|
|
from django.utils.html import format_html, mark_safe
|
|
from django.utils.translation import gettext_lazy as _
|
|
from django.utils.translation import pgettext
|
|
from gadjo.forms.widgets import MultiSelectWidget
|
|
|
|
from chrono.agendas.models import (
|
|
WEEK_CHOICES,
|
|
WEEKDAY_CHOICES,
|
|
WEEKDAYS_LIST,
|
|
Agenda,
|
|
AgendaNotificationsSettings,
|
|
AgendaReminderSettings,
|
|
Booking,
|
|
BookingCheck,
|
|
Category,
|
|
Desk,
|
|
Event,
|
|
EventsType,
|
|
MeetingType,
|
|
Person,
|
|
Resource,
|
|
SharedCustodyHolidayRule,
|
|
SharedCustodyPeriod,
|
|
SharedCustodyRule,
|
|
SharedCustodySettings,
|
|
Subscription,
|
|
TimePeriod,
|
|
TimePeriodException,
|
|
TimePeriodExceptionGroup,
|
|
TimePeriodExceptionSource,
|
|
UnavailabilityCalendar,
|
|
VirtualMember,
|
|
generate_slug,
|
|
)
|
|
from chrono.utils.lingo import get_agenda_check_types
|
|
from chrono.utils.timezone import localtime, make_aware, now
|
|
|
|
from . import widgets
|
|
from .utils import get_role_queryset
|
|
from .widgets import SplitDateTimeField, WeekdaysWidget
|
|
|
|
|
|
class AgendaAddForm(forms.ModelForm):
|
|
edit_role = forms.ModelChoiceField(label=_('Edit Role'), required=False, queryset=get_role_queryset())
|
|
view_role = forms.ModelChoiceField(label=_('View Role'), required=False, queryset=get_role_queryset())
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
if 'kind' in self.fields and settings.PARTIAL_BOOKINGS_ENABLED:
|
|
self.fields['kind'].choices += [('partial-bookings', _('Partial bookings'))]
|
|
|
|
class Meta:
|
|
model = Agenda
|
|
fields = ['label', 'kind', 'category', 'edit_role', 'view_role']
|
|
|
|
def clean(self):
|
|
super().clean()
|
|
if self.cleaned_data.get('kind') == 'partial-bookings':
|
|
self.cleaned_data['kind'] = 'events'
|
|
self.instance.partial_bookings = True
|
|
self.instance.default_view = 'day'
|
|
self.instance.enable_check_for_future_events = True
|
|
|
|
def save(self, *args, **kwargs):
|
|
create = self.instance.pk is None
|
|
super().save()
|
|
if create and self.instance.kind == 'meetings':
|
|
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
|
|
|
|
|
|
class AgendaEditForm(forms.ModelForm):
|
|
class Meta:
|
|
model = Agenda
|
|
fields = [
|
|
'label',
|
|
'slug',
|
|
'category',
|
|
'anonymize_delay',
|
|
'default_view',
|
|
'booking_form_url',
|
|
'events_type',
|
|
]
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
if kwargs['instance'].kind != 'events':
|
|
del self.fields['booking_form_url']
|
|
del self.fields['events_type']
|
|
self.fields['default_view'].choices = [
|
|
(k, v) for k, v in self.fields['default_view'].choices if k != 'open_events'
|
|
]
|
|
else:
|
|
if not EventsType.objects.exists():
|
|
del self.fields['events_type']
|
|
if kwargs['instance'].partial_bookings:
|
|
self.fields['default_view'].choices = [
|
|
(k, v) for k, v in self.fields['default_view'].choices if k not in ('open_events', 'week')
|
|
]
|
|
|
|
|
|
class AgendaBookingDelaysForm(forms.ModelForm):
|
|
class Meta:
|
|
model = Agenda
|
|
fields = [
|
|
'minimal_booking_delay',
|
|
'minimal_booking_delay_in_working_days',
|
|
'maximal_booking_delay',
|
|
'minimal_booking_time',
|
|
]
|
|
widgets = {
|
|
'minimal_booking_time': widgets.TimeWidget,
|
|
}
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
if kwargs['instance'].kind != 'virtual':
|
|
self.fields['minimal_booking_delay'].required = True
|
|
self.fields['maximal_booking_delay'].required = True
|
|
if kwargs['instance'].kind != 'events' or settings.WORKING_DAY_CALENDAR is None:
|
|
del self.fields['minimal_booking_delay_in_working_days']
|
|
|
|
|
|
class AgendaRolesForm(AgendaAddForm):
|
|
class Meta:
|
|
model = Agenda
|
|
fields = [
|
|
'edit_role',
|
|
'view_role',
|
|
]
|
|
|
|
|
|
class UnavailabilityCalendarAddForm(forms.ModelForm):
|
|
class Meta:
|
|
model = UnavailabilityCalendar
|
|
fields = ['label', 'edit_role', 'view_role']
|
|
|
|
edit_role = forms.ModelChoiceField(label=_('Edit Role'), required=False, queryset=get_role_queryset())
|
|
view_role = forms.ModelChoiceField(label=_('View Role'), required=False, queryset=get_role_queryset())
|
|
|
|
|
|
class UnavailabilityCalendarEditForm(UnavailabilityCalendarAddForm):
|
|
class Meta:
|
|
model = UnavailabilityCalendar
|
|
fields = ['label', 'slug', 'edit_role', 'view_role']
|
|
|
|
|
|
class NewEventForm(forms.ModelForm):
|
|
frequency = forms.ChoiceField(
|
|
label=_('Event frequency'),
|
|
widget=forms.RadioSelect,
|
|
choices=(
|
|
('unique', _('Unique')),
|
|
('recurring', _('Recurring')),
|
|
),
|
|
initial='unique',
|
|
help_text=_('This field will not be editable once event has bookings.'),
|
|
)
|
|
recurrence_days = forms.TypedMultipleChoiceField(
|
|
choices=WEEKDAY_CHOICES,
|
|
coerce=int,
|
|
required=False,
|
|
widget=WeekdaysWidget,
|
|
label=_('Recurrence days'),
|
|
)
|
|
|
|
class Meta:
|
|
model = Event
|
|
fields = [
|
|
'label',
|
|
'start_datetime',
|
|
'end_time',
|
|
'frequency',
|
|
'recurrence_days',
|
|
'recurrence_week_interval',
|
|
'recurrence_end_date',
|
|
'duration',
|
|
'places',
|
|
]
|
|
field_classes = {
|
|
'start_datetime': SplitDateTimeField,
|
|
}
|
|
widgets = {
|
|
'recurrence_end_date': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
|
|
'end_time': widgets.TimeWidget,
|
|
}
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
if self.instance.agenda.partial_bookings:
|
|
del self.fields['duration']
|
|
del self.fields['recurrence_week_interval']
|
|
else:
|
|
del self.fields['end_time']
|
|
|
|
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
|
|
|
|
end_time = self.cleaned_data.get('end_time')
|
|
if end_time and self.cleaned_data['start_datetime'].time() > end_time:
|
|
self.add_error('end_time', _('End time must be greater than start time.'))
|
|
|
|
if self.instance.agenda.partial_bookings and self.instance.agenda.event_overlaps(
|
|
start_datetime=self.cleaned_data['start_datetime'],
|
|
recurrence_days=self.cleaned_data['recurrence_days'],
|
|
recurrence_end_date=self.cleaned_data['recurrence_end_date'],
|
|
instance=self.instance,
|
|
):
|
|
raise ValidationError(_('There can only be one event per day.'))
|
|
|
|
def clean_start_datetime(self):
|
|
start_datetime = self.cleaned_data['start_datetime']
|
|
if start_datetime.year < 2000:
|
|
raise ValidationError(_('Year must be after 2000.'))
|
|
return start_datetime
|
|
|
|
def clean_recurrence_days(self):
|
|
recurrence_days = self.cleaned_data['recurrence_days']
|
|
if recurrence_days == []:
|
|
return None
|
|
return recurrence_days
|
|
|
|
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
|
|
|
|
def save(self, *args, **kwargs):
|
|
with transaction.atomic():
|
|
event = super().save(*args, **kwargs)
|
|
if event.recurrence_days:
|
|
event.create_all_recurrences()
|
|
return event
|
|
|
|
|
|
class EventForm(NewEventForm):
|
|
protected_fields = (
|
|
'slug',
|
|
'start_datetime',
|
|
'end_time',
|
|
'frequency',
|
|
'recurrence_days',
|
|
'recurrence_week_interval',
|
|
)
|
|
|
|
class Meta:
|
|
model = Event
|
|
widgets = {
|
|
'recurrence_end_date': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
|
|
'end_time': widgets.TimeWidget,
|
|
}
|
|
fields = [
|
|
'label',
|
|
'slug',
|
|
'start_datetime',
|
|
'end_time',
|
|
'frequency',
|
|
'recurrence_days',
|
|
'recurrence_week_interval',
|
|
'recurrence_end_date',
|
|
'duration',
|
|
'publication_datetime',
|
|
'places',
|
|
'waiting_list_places',
|
|
'description',
|
|
'pricing',
|
|
'url',
|
|
]
|
|
field_classes = {
|
|
'start_datetime': SplitDateTimeField,
|
|
'publication_datetime': SplitDateTimeField,
|
|
}
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
self.fields['frequency'].initial = 'recurring' if self.instance.recurrence_days else 'unique'
|
|
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.'
|
|
)
|
|
if self.instance.recurrence_days and self.instance.has_recurrences_booked():
|
|
for field in self.protected_fields:
|
|
if field not in self.fields:
|
|
continue
|
|
self.fields[field].disabled = True
|
|
self.fields[field].help_text = _(
|
|
'This field cannot be modified because some recurrences have bookings attached to them.'
|
|
)
|
|
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']),
|
|
)
|
|
if self.instance.primary_event:
|
|
for field in (
|
|
'slug',
|
|
'recurrence_end_date',
|
|
'publication_datetime',
|
|
'frequency',
|
|
'recurrence_days',
|
|
'recurrence_week_interval',
|
|
):
|
|
del self.fields[field]
|
|
|
|
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
|
|
|
|
def clean(self):
|
|
super().clean()
|
|
if (
|
|
'recurrence_end_date' in self.changed_data
|
|
and 'recurrence_end_date' in self.cleaned_data
|
|
and self.instance.has_recurrences_booked(after=self.cleaned_data['recurrence_end_date'])
|
|
):
|
|
raise ValidationError(_('Bookings exist after this date.'))
|
|
|
|
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
|
|
|
|
def save(self, *args, **kwargs):
|
|
with self.instance.update_recurrences(
|
|
self.changed_data,
|
|
self.cleaned_data,
|
|
self.protected_fields,
|
|
list(self.protected_fields) + ['recurrence_end_date', 'frequency'],
|
|
):
|
|
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()
|
|
return self.instance
|
|
|
|
|
|
class EventDuplicateForm(forms.ModelForm):
|
|
class Meta:
|
|
model = Event
|
|
fields = [
|
|
'label',
|
|
'start_datetime',
|
|
]
|
|
field_classes = {
|
|
'start_datetime': SplitDateTimeField,
|
|
}
|
|
|
|
def save(self, *args, **kwargs):
|
|
with transaction.atomic():
|
|
self.instance = self.instance.duplicate(
|
|
label=self.cleaned_data['label'], start_datetime=self.cleaned_data['start_datetime']
|
|
)
|
|
if self.instance.recurrence_days:
|
|
self.instance.create_all_recurrences()
|
|
return self.instance
|
|
|
|
|
|
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_type', _('This field is required.'))
|
|
|
|
return cleaned_data
|
|
|
|
|
|
CustomFieldFormSet = formset_factory(CustomFieldForm)
|
|
|
|
|
|
class BookingCheckFilterSet(django_filters.FilterSet):
|
|
class Meta:
|
|
model = Booking
|
|
fields = []
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
self.agenda = kwargs.pop('agenda')
|
|
filters = kwargs.pop('filters')
|
|
super().__init__(*args, **kwargs)
|
|
|
|
# add filters on extra_data to filterset
|
|
for key, values in filters.items():
|
|
self.filters['extra-data-%s' % key] = django_filters.ChoiceFilter(
|
|
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,
|
|
)
|
|
|
|
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
|
|
|
|
# add filters on booking status to filterset
|
|
status_choices = [
|
|
('booked', _('With booking')),
|
|
('not-booked', _('Without booking')),
|
|
('cancelled', _('Cancelled')),
|
|
('not-checked', _('Not checked')),
|
|
('presence', _('Presence')),
|
|
]
|
|
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
|
|
]
|
|
status_choices += [('absence', _('Absence'))]
|
|
status_choices += [
|
|
('absence::%s' % ct.slug, _('Absence (%s)') % ct.label) for ct in absence_check_types
|
|
]
|
|
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
|
|
|
|
if self.agenda.partial_bookings:
|
|
self.filters['display'] = django_filters.MultipleChoiceFilter(
|
|
label=_('Display'),
|
|
choices=[
|
|
('booked', _('Booked periods')),
|
|
('checked', _('Checked periods')),
|
|
('computed', _('Computed periods')),
|
|
],
|
|
widget=forms.CheckboxSelectMultiple,
|
|
method='do_nothing',
|
|
initial=['booked', 'checked', 'computed'],
|
|
)
|
|
self.filters['display'].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_checks__isnull=True)
|
|
if value == 'presence':
|
|
return queryset.filter(user_checks__presence=True)
|
|
if value == 'absence':
|
|
return queryset.filter(user_checks__presence=False)
|
|
if value.startswith('absence::'):
|
|
return queryset.filter(user_checks__presence=False, user_checks__type_slug=value.split('::')[1])
|
|
if value.startswith('presence::'):
|
|
return queryset.filter(user_checks__presence=True, user_checks__type_slug=value.split('::')[1])
|
|
return queryset
|
|
|
|
def do_nothing(self, queryset, name, value):
|
|
return queryset
|
|
|
|
|
|
class SubscriptionCheckFilterSet(BookingCheckFilterSet):
|
|
class Meta:
|
|
model = Subscription
|
|
fields = []
|
|
|
|
def filter_booking_status(self, queryset, name, value):
|
|
if value != 'not-booked':
|
|
return queryset.none()
|
|
return queryset
|
|
|
|
|
|
class BookingCheckAbsenceForm(forms.Form):
|
|
check_type = forms.ChoiceField(required=False)
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
agenda = kwargs.pop('agenda')
|
|
super().__init__(*args, **kwargs)
|
|
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
|
|
]
|
|
|
|
|
|
class BookingCheckPresenceForm(forms.Form):
|
|
check_type = forms.ChoiceField(required=False)
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
agenda = kwargs.pop('agenda')
|
|
super().__init__(*args, **kwargs)
|
|
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
|
|
]
|
|
|
|
|
|
class PartialBookingCheckForm(forms.ModelForm):
|
|
presence = forms.NullBooleanField(
|
|
label=_('Status'),
|
|
widget=forms.RadioSelect(
|
|
choices=(
|
|
(None, _('Not checked')),
|
|
(True, _('Present')),
|
|
(False, _('Absent')),
|
|
)
|
|
),
|
|
required=False,
|
|
)
|
|
presence_check_type = forms.ChoiceField(label=_('Type'), required=False)
|
|
absence_check_type = forms.ChoiceField(label=_('Type'), required=False)
|
|
|
|
class Meta:
|
|
model = BookingCheck
|
|
fields = ['presence', 'start_time', 'end_time', 'type_label', 'type_slug']
|
|
widgets = {
|
|
'start_time': widgets.TimeWidgetWithButton(
|
|
step=60, button_label=_('Fill with booking start time')
|
|
),
|
|
'end_time': widgets.TimeWidgetWithButton(step=60, button_label=_('Fill with booking end time')),
|
|
'type_label': forms.HiddenInput(),
|
|
'type_slug': forms.HiddenInput(),
|
|
}
|
|
|
|
def __init__(self, *args, first_check_form=None, **kwargs):
|
|
agenda = kwargs.pop('agenda')
|
|
self.event = kwargs.pop('event')
|
|
self.first_check_form = first_check_form
|
|
super().__init__(*args, **kwargs)
|
|
self.check_types = get_agenda_check_types(agenda)
|
|
absence_check_types = [(ct.slug, ct.label) for ct in self.check_types if ct.kind == 'absence']
|
|
presence_check_types = [(ct.slug, ct.label) for ct in self.check_types if ct.kind == 'presence']
|
|
|
|
if presence_check_types:
|
|
self.fields['presence_check_type'].choices = [(None, '---------')] + presence_check_types
|
|
self.fields['presence_check_type'].initial = self.instance.type_slug
|
|
else:
|
|
del self.fields['presence_check_type']
|
|
|
|
if absence_check_types:
|
|
self.fields['absence_check_type'].choices = [(None, '---------')] + absence_check_types
|
|
self.fields['absence_check_type'].initial = self.instance.type_slug
|
|
else:
|
|
del self.fields['absence_check_type']
|
|
|
|
if not self.instance.booking.start_time:
|
|
self.fields['start_time'].widget = widgets.TimeWidget(step=60)
|
|
self.fields['end_time'].widget = widgets.TimeWidget(step=60)
|
|
self.fields['presence'].widget.choices = ((None, _('Not checked')), (True, _('Present')))
|
|
self.fields.pop('absence_check_type', None)
|
|
|
|
def clean(self):
|
|
if self.cleaned_data.get('presence') is None:
|
|
return
|
|
|
|
start_time = self.cleaned_data.get('start_time')
|
|
end_time = self.cleaned_data.get('end_time')
|
|
|
|
if not start_time and not end_time:
|
|
raise ValidationError(_('Both arrival and departure cannot not be empty.'))
|
|
|
|
if start_time and end_time and end_time <= start_time:
|
|
raise ValidationError(_('Arrival must be before departure.'))
|
|
|
|
if self.cleaned_data['presence'] is not None:
|
|
kind = 'presence' if self.cleaned_data['presence'] else 'absence'
|
|
if f'{kind}_check_type' in self.cleaned_data:
|
|
self.cleaned_data['type_slug'] = self.cleaned_data[f'{kind}_check_type']
|
|
self.cleaned_data['type_label'] = dict(self.fields[f'{kind}_check_type'].choices).get(
|
|
self.cleaned_data['type_slug']
|
|
)
|
|
|
|
def clean_presence(self):
|
|
if (
|
|
self.first_check_form
|
|
and self.cleaned_data['presence'] is not None
|
|
and self.cleaned_data['presence'] == self.first_check_form.cleaned_data['presence']
|
|
):
|
|
raise ValidationError(_('Both booking checks cannot have the same status.'))
|
|
|
|
return self.cleaned_data['presence']
|
|
|
|
def clean_start_time(self):
|
|
start_time = self.cleaned_data['start_time']
|
|
if start_time and start_time < localtime(self.event.start_datetime).time():
|
|
raise ValidationError(_('Arrival must be after opening time.'))
|
|
|
|
return start_time
|
|
|
|
def clean_end_time(self):
|
|
end_time = self.cleaned_data['end_time']
|
|
if end_time and end_time > self.event.end_time:
|
|
raise ValidationError(_('Departure must be before closing time.'))
|
|
|
|
return end_time
|
|
|
|
def save(self):
|
|
booking = self.instance.booking
|
|
if self.cleaned_data['presence'] is None:
|
|
if self.instance.pk:
|
|
self.instance.delete()
|
|
booking.refresh_computed_times(commit=True)
|
|
return self.instance
|
|
|
|
super().save()
|
|
booking.refresh_computed_times(commit=True)
|
|
return self.instance
|
|
|
|
|
|
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'),
|
|
)
|
|
extra_data = forms.CharField(
|
|
label=_('Extra datas'),
|
|
max_length=250,
|
|
required=False,
|
|
help_text=_('Comma separated list of keys defined in extra_data.'),
|
|
)
|
|
group_by = forms.CharField(
|
|
label=_('Group by'),
|
|
max_length=50,
|
|
required=False,
|
|
help_text=_('Key defined in extra_data.'),
|
|
)
|
|
with_page_break = forms.BooleanField(
|
|
label=_('Add a page break before each grouper'),
|
|
required=False,
|
|
)
|
|
sort = forms.ChoiceField(
|
|
label=_('Sort by'),
|
|
choices=[
|
|
('lastname,firstname', _('Last name, first name')),
|
|
('firstname,lastname', _('First name, last name')),
|
|
],
|
|
initial='lastname,firstname',
|
|
)
|
|
date_display = forms.ChoiceField(
|
|
label=_('Date display'),
|
|
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,
|
|
)
|
|
activity_display = forms.ChoiceField(
|
|
label=_('Activity display'),
|
|
choices=[
|
|
('row', _('In line')),
|
|
('col', _('In column')),
|
|
],
|
|
initial='row',
|
|
)
|
|
orientation = forms.ChoiceField(
|
|
label=_('PDF orientation'),
|
|
choices=[
|
|
('portrait', _('Portrait')),
|
|
('landscape', _('Landscape')),
|
|
],
|
|
initial='portrait',
|
|
)
|
|
booking_filter = forms.ChoiceField(
|
|
label=_('Filter by status'),
|
|
choices=[
|
|
('all', _('All')),
|
|
('with_booking', _('With booking')),
|
|
('without_booking', _('Without booking')),
|
|
],
|
|
initial='all',
|
|
)
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
self.agenda = kwargs.pop('agenda')
|
|
self.event = kwargs.pop('event', None)
|
|
super().__init__(*args, **kwargs)
|
|
self.with_subscriptions = self.agenda.subscriptions.exists()
|
|
if self.event is not None:
|
|
del self.fields['date_start']
|
|
del self.fields['date_end']
|
|
del self.fields['date_display']
|
|
del self.fields['custom_nb_dates_per_page']
|
|
del self.fields['activity_display']
|
|
if not self.with_subscriptions:
|
|
del self.fields['booking_filter']
|
|
|
|
def get_slots(self):
|
|
extra_data = self.cleaned_data['extra_data'].split(',')
|
|
extra_data = [d.strip() for d in extra_data if d.strip()]
|
|
group_by = self.cleaned_data['group_by'].strip()
|
|
all_extra_data = extra_data[:]
|
|
if group_by:
|
|
all_extra_data += [group_by]
|
|
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')
|
|
)
|
|
dates = defaultdict(list)
|
|
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
|
|
dates[date].append(real_event)
|
|
if real_event not in events:
|
|
events.append(real_event)
|
|
dates_per_event_id[real_event.pk].append(date)
|
|
dates = sorted(dates.items(), key=lambda a: a[0])
|
|
|
|
date_display = self.cleaned_data.get('date_display') or 'all'
|
|
if date_display in ['month', 'week']:
|
|
grouper = defaultdict(list)
|
|
for date, event in dates:
|
|
if date_display == 'month':
|
|
attr = date.month
|
|
else:
|
|
attr = date.isocalendar().week
|
|
grouper[(date.year, attr)].append((date, event))
|
|
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]
|
|
|
|
event_slots = []
|
|
for event in events:
|
|
event_slots.append(
|
|
{'event': event, 'dates': {date: False for date in dates_per_event_id[event.pk]}}
|
|
)
|
|
|
|
users = {}
|
|
if self.with_subscriptions:
|
|
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,
|
|
'extra_data': {k: (subscription.extra_data or {}).get(k) or '' for k in all_extra_data},
|
|
'events': copy.deepcopy(event_slots),
|
|
}
|
|
|
|
booking_qs_kwargs = {}
|
|
if not self.with_subscriptions:
|
|
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')
|
|
)
|
|
participants = 0
|
|
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,
|
|
'extra_data': {k: (booking.extra_data or {}).get(k) or '' for k in all_extra_data},
|
|
'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
|
|
participants += 1
|
|
break
|
|
|
|
if self.cleaned_data.get('booking_filter') == 'with_booking':
|
|
# remove subscribed users without booking
|
|
users = {
|
|
k: user for k, user in users.items() if any(any(e['dates'].values()) for e in user['events'])
|
|
}
|
|
elif self.cleaned_data.get('booking_filter') == 'without_booking':
|
|
# remove subscribed users with booking
|
|
users = {
|
|
k: user
|
|
for k, user in users.items()
|
|
if not any(any(e['dates'].values()) for e in user['events'])
|
|
}
|
|
|
|
if self.cleaned_data['sort'] == 'lastname,firstname':
|
|
sort_fields = ['user_last_name', 'user_first_name']
|
|
else:
|
|
sort_fields = ['user_first_name', 'user_last_name']
|
|
|
|
if group_by:
|
|
groupers = defaultdict(list)
|
|
for user in users.values():
|
|
groupers[user['extra_data'].get(group_by) or ''].append(user)
|
|
users = [
|
|
{
|
|
'grouper': g,
|
|
'users': sorted(u, key=itemgetter(*sort_fields, 'user_id')),
|
|
}
|
|
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': '',
|
|
'users': sorted(users.values(), key=itemgetter(*sort_fields, 'user_id')),
|
|
}
|
|
]
|
|
|
|
data = {
|
|
'dates': dates,
|
|
'events': events,
|
|
'users': users,
|
|
'extra_data': extra_data,
|
|
}
|
|
if self.event:
|
|
data['participants'] = participants
|
|
return data
|
|
|
|
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.'))
|
|
|
|
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.'))
|
|
|
|
return cleaned_data
|
|
|
|
|
|
class EventsReportForm(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'),
|
|
)
|
|
agendas = forms.ModelMultipleChoiceField(
|
|
label=_('Agendas'),
|
|
queryset=Agenda.objects.none(),
|
|
widget=MultiSelectWidget,
|
|
)
|
|
status = forms.MultipleChoiceField(
|
|
label=_('Status'),
|
|
choices=[
|
|
('not-checked', _('Not checked')),
|
|
('not-invoiced', _('Not invoiced')),
|
|
],
|
|
widget=forms.CheckboxSelectMultiple,
|
|
)
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
user = kwargs.pop('user')
|
|
super().__init__(*args, **kwargs)
|
|
agendas = [a for a in Agenda.objects.filter(kind='events') if a.can_be_viewed(user)]
|
|
self.fields['agendas'].queryset = Agenda.objects.filter(pk__in=[a.pk for a in agendas])
|
|
|
|
def get_events(self):
|
|
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)
|
|
event_queryset = (
|
|
Event.objects.filter(
|
|
recurrence_days__isnull=True,
|
|
start_datetime__gte=min_start,
|
|
start_datetime__lt=max_start,
|
|
cancelled=False,
|
|
agenda__in=self.cleaned_data['agendas'],
|
|
)
|
|
.select_related('agenda')
|
|
.order_by('start_datetime', 'duration', 'label')
|
|
)
|
|
|
|
result = {}
|
|
|
|
if 'not-checked' in self.cleaned_data['status']:
|
|
result['not_checked'] = event_queryset.filter(checked=False)
|
|
if 'not-invoiced' in self.cleaned_data['status']:
|
|
result['not_invoiced'] = event_queryset.filter(invoiced=False)
|
|
|
|
return result
|
|
|
|
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.'))
|
|
|
|
return cleaned_data
|
|
|
|
|
|
class AgendaResourceForm(forms.Form):
|
|
resource = forms.ModelChoiceField(label=_('Resource'), queryset=Resource.objects.none())
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
agenda = kwargs.pop('agenda')
|
|
super().__init__(*args, **kwargs)
|
|
self.fields['resource'].queryset = Resource.objects.exclude(agenda=agenda)
|
|
|
|
|
|
class NewMeetingTypeForm(forms.ModelForm):
|
|
class Meta:
|
|
model = MeetingType
|
|
exclude = ['agenda', 'slug', 'deleted']
|
|
|
|
def clean(self):
|
|
super().clean()
|
|
agenda = self.instance.agenda
|
|
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.")
|
|
)
|
|
|
|
|
|
class MeetingTypeForm(forms.ModelForm):
|
|
class Meta:
|
|
model = MeetingType
|
|
exclude = ['agenda', 'deleted']
|
|
|
|
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
|
|
|
|
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
|
|
)
|
|
|
|
|
|
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='',
|
|
widget=forms.CheckboxSelectMultiple(),
|
|
)
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
if 'date' in self.fields:
|
|
del self.fields['repeat']
|
|
del self.fields['weekday_indexes']
|
|
|
|
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.get('repeat') == 'every-week':
|
|
cleaned_data['weekday_indexes'] = None
|
|
|
|
return cleaned_data
|
|
|
|
|
|
class TimePeriodAddForm(TimePeriodFormBase):
|
|
field_order = ['weekdays', 'start_time', 'end_time', 'repeat', 'weekday_indexes']
|
|
|
|
weekdays = forms.MultipleChoiceField(
|
|
label=_('Days'), widget=forms.CheckboxSelectMultiple(), choices=WEEKDAYS_LIST
|
|
)
|
|
start_time = forms.TimeField(label=_('Start Time'), widget=widgets.TimeWidget())
|
|
end_time = forms.TimeField(label=_('End Time'), widget=widgets.TimeWidget())
|
|
|
|
|
|
class TimePeriodForm(TimePeriodFormBase, forms.ModelForm):
|
|
class Meta:
|
|
model = TimePeriod
|
|
widgets = {
|
|
'date': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
|
|
'start_time': widgets.TimeWidget(),
|
|
'end_time': widgets.TimeWidget(),
|
|
}
|
|
fields = ['weekday', 'start_time', 'end_time', 'repeat', 'weekday_indexes']
|
|
|
|
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
|
|
self.old_date = self.instance.date
|
|
|
|
if self.instance.weekday_indexes:
|
|
self.fields['repeat'].initial = 'custom'
|
|
|
|
if self.instance.agenda: # virtual agenda exclusion period
|
|
del self.fields['repeat']
|
|
del self.fields['weekday_indexes']
|
|
|
|
def clean_date(self):
|
|
data = self.cleaned_data['date']
|
|
if data.year < 2000:
|
|
raise ValidationError(_('Year must be after 2000.'))
|
|
return data
|
|
|
|
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,
|
|
date=self.old_date,
|
|
).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.date = self.instance.date
|
|
timeperiod.save()
|
|
|
|
return self.instance
|
|
|
|
|
|
class DateTimePeriodForm(TimePeriodForm):
|
|
class Meta(TimePeriodForm.Meta):
|
|
fields = ['date', 'start_time', 'end_time']
|
|
|
|
|
|
class ExcludedPeriodAddForm(TimePeriodAddForm):
|
|
repeat = None
|
|
weekday_indexes = None
|
|
|
|
|
|
class NewDeskForm(forms.ModelForm):
|
|
copy_from = forms.ModelChoiceField(
|
|
label=_('Copy settings of desk'),
|
|
required=False,
|
|
queryset=Desk.objects.none(),
|
|
)
|
|
|
|
class Meta:
|
|
model = Desk
|
|
exclude = ['agenda', 'slug']
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
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)
|
|
|
|
def save(self):
|
|
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']:
|
|
return self.cleaned_data['copy_from'].duplicate(label=self.cleaned_data['label'])
|
|
|
|
super().save()
|
|
self.instance.import_timeperiod_exceptions_from_settings(enable=True)
|
|
return self.instance
|
|
|
|
|
|
class DeskForm(forms.ModelForm):
|
|
class Meta:
|
|
model = Desk
|
|
exclude = ['agenda']
|
|
|
|
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
|
|
|
|
|
|
class TimePeriodExceptionForm(forms.ModelForm):
|
|
class Meta:
|
|
model = TimePeriodException
|
|
fields = ['start_datetime', 'end_datetime', 'label']
|
|
field_classes = {
|
|
'start_datetime': SplitDateTimeField,
|
|
'end_datetime': SplitDateTimeField,
|
|
}
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
self.old_label = self.instance.label
|
|
self.old_start_datetime = self.instance.start_datetime
|
|
self.old_end_datetime = self.instance.end_datetime
|
|
|
|
def clean(self):
|
|
cleaned_data = super().clean()
|
|
|
|
for date_field in ('start_datetime', 'end_datetime'):
|
|
if date_field in cleaned_data and cleaned_data[date_field].year < 2000:
|
|
self.add_error(date_field, _('Year must be after 2000.'))
|
|
|
|
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
|
|
|
|
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
|
|
|
|
|
|
class VirtualMemberForm(forms.ModelForm):
|
|
class Meta:
|
|
model = VirtualMember
|
|
fields = ['real_agenda']
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
self.fields['real_agenda'].queryset = Agenda.objects.filter(kind='meetings').exclude(
|
|
virtual_agendas=self.instance.virtual_agenda
|
|
)
|
|
|
|
|
|
class ImportEventsForm(forms.Form):
|
|
events_csv_file = forms.FileField(
|
|
label=_('Events File'),
|
|
required=True,
|
|
help_text=_(
|
|
'CSV file with date, time, number of places, '
|
|
'number of places in waiting list, label, and '
|
|
'optionally, identifier, description, pricing, '
|
|
'URL, and publication date as columns.'
|
|
),
|
|
)
|
|
events = None
|
|
|
|
def __init__(self, agenda, **kwargs):
|
|
self.agenda = agenda
|
|
super().__init__(**kwargs)
|
|
|
|
def clean_events_csv_file(self):
|
|
self.exclude_from_validation = ['desk', 'meeting_type', 'primary_event']
|
|
|
|
content = self.cleaned_data['events_csv_file'].read()
|
|
if b'\0' in content:
|
|
raise ValidationError(_('Invalid file format.'))
|
|
|
|
for charset in ('utf-8-sig', 'iso-8859-15'):
|
|
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.
|
|
|
|
try:
|
|
dialect = csv.Sniffer().sniff(content)
|
|
dialect.doublequote = True
|
|
except csv.Error:
|
|
dialect = None
|
|
|
|
errors = []
|
|
self.events = []
|
|
self.warnings = {}
|
|
self.events_by_slug = {x.slug: x for x in Event.objects.filter(agenda=self.agenda.pk)}
|
|
self.event_ids_with_bookings = set(
|
|
Booking.objects.filter(
|
|
event__agenda=self.agenda.pk, cancellation_datetime__isnull=True
|
|
).values_list('event_id', flat=True)
|
|
)
|
|
self.seen_slugs = set(self.events_by_slug.keys())
|
|
line_offset = 1
|
|
for i, csvline in enumerate(csv.reader(StringIO(content), dialect=dialect)):
|
|
if not csvline:
|
|
continue
|
|
|
|
if i == 0 and csvline[0].strip('#') in ('date', 'Date', _('date'), _('Date')):
|
|
line_offset = 0
|
|
continue
|
|
|
|
try:
|
|
event = self.parse_csvline(csvline)
|
|
except ValidationError as e:
|
|
for error in getattr(e, 'error_list', [e]):
|
|
errors.append(
|
|
format_html(
|
|
'{message} ({event_no} event)',
|
|
message=error.message,
|
|
event_no=mark_safe(ordinal(i + line_offset)),
|
|
)
|
|
)
|
|
else:
|
|
self.events.append(event)
|
|
|
|
if errors:
|
|
errors = [_('Invalid file format:')] + errors
|
|
raise ValidationError(errors)
|
|
|
|
def parse_csvline(self, csvline):
|
|
if len(csvline) < 3:
|
|
raise ValidationError(_('Not enough columns.'))
|
|
|
|
# label needed to generate a slug
|
|
label = None
|
|
if len(csvline) >= 5:
|
|
label = force_str(csvline[4])
|
|
|
|
# get or create event
|
|
event = None
|
|
slug = None
|
|
if len(csvline) >= 6:
|
|
slug = force_str(csvline[5]) if csvline[5] else None
|
|
# get existing event if relevant
|
|
if slug and slug in self.seen_slugs:
|
|
event = self.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=self.seen_slugs, agenda=self.agenda.pk)
|
|
# maintain caches
|
|
self.seen_slugs.add(event.slug)
|
|
self.events_by_slug[event.slug] = event
|
|
|
|
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:
|
|
event_datetime = make_aware(
|
|
datetime.datetime.strptime('%s %s' % tuple(csvline[:2]), datetime_fmt)
|
|
)
|
|
except ValueError:
|
|
continue
|
|
if (
|
|
event.pk is not None
|
|
and event.start_datetime != event_datetime
|
|
and event.start_datetime > now()
|
|
and event.pk in self.event_ids_with_bookings
|
|
and event.pk not in self.warnings
|
|
):
|
|
# event start datetime has changed, event is not past and has not cancelled bookings
|
|
# => warn the user
|
|
self.warnings[event.pk] = event
|
|
event.start_datetime = event_datetime
|
|
break
|
|
else:
|
|
raise ValidationError(_('Wrong start date/time format.'))
|
|
try:
|
|
event.places = int(csvline[2])
|
|
except ValueError:
|
|
raise ValidationError(_('Number of places must be an integer.'))
|
|
if len(csvline) >= 4:
|
|
try:
|
|
event.waiting_list_places = int(csvline[3])
|
|
except ValueError:
|
|
raise ValidationError(_('Number of places in waiting list must be an integer.'))
|
|
|
|
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
|
|
|
|
if len(csvline) >= 10 and csvline[9]: # publication date is optional
|
|
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',
|
|
):
|
|
try:
|
|
event.publication_datetime = make_aware(
|
|
datetime.datetime.strptime(csvline[9], datetime_fmt)
|
|
)
|
|
break
|
|
except ValueError:
|
|
continue
|
|
else:
|
|
raise ValidationError(_('Wrong publication date/time format.'))
|
|
|
|
if self.agenda.partial_bookings:
|
|
if len(csvline) < 11 or not csvline[10]:
|
|
raise ValidationError(_('Missing end_time.'))
|
|
event.end_time = csvline[10]
|
|
else:
|
|
self.exclude_from_validation.append('end_time')
|
|
if len(csvline) >= 11 and csvline[10]: # duration is optional
|
|
try:
|
|
event.duration = int(csvline[10])
|
|
except ValueError:
|
|
raise ValidationError(_('Duration must be an integer.'))
|
|
|
|
try:
|
|
event.full_clean(exclude=self.exclude_from_validation)
|
|
except ValidationError as e:
|
|
errors = []
|
|
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 += ', '.join(field_errors)
|
|
errors.append(msg)
|
|
raise ValidationError(errors)
|
|
|
|
return event
|
|
|
|
@staticmethod
|
|
def get_verbose_name(field_name):
|
|
try:
|
|
return Event._meta.get_field(field_name).verbose_name
|
|
except FieldDoesNotExist:
|
|
return ''
|
|
|
|
|
|
class ExceptionsImportForm(forms.ModelForm):
|
|
ics_file = forms.FileField(
|
|
label=_('ICS File'),
|
|
required=False,
|
|
help_text=_('ICS file containing events which will be considered as exceptions.'),
|
|
)
|
|
ics_url = forms.CharField(
|
|
label=_('URL'),
|
|
max_length=200,
|
|
required=False,
|
|
help_text=_('URL to remote calendar which will be synchronised hourly.'),
|
|
)
|
|
|
|
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.'))
|
|
|
|
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']
|
|
|
|
|
|
class DeskExceptionsImportForm(ExceptionsImportForm):
|
|
all_desks = forms.BooleanField(label=_('Apply exceptions on all desks of the agenda'), required=False)
|
|
|
|
class Meta:
|
|
model = Desk
|
|
fields = []
|
|
|
|
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']
|
|
|
|
|
|
class UnavailabilityCalendarExceptionsImportForm(ExceptionsImportForm):
|
|
class Meta:
|
|
model = UnavailabilityCalendar
|
|
fields = []
|
|
|
|
|
|
class TimePeriodExceptionSourceReplaceForm(forms.ModelForm):
|
|
ics_newfile = forms.FileField(
|
|
label=_('ICS File'),
|
|
required=False,
|
|
help_text=_('ICS file containing events which will be considered as exceptions.'),
|
|
)
|
|
|
|
class Meta:
|
|
model = TimePeriodExceptionSource
|
|
fields = []
|
|
|
|
def save(self, *args, **kwargs):
|
|
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)
|
|
if self.instance.desk and self.instance.desk.agenda.desk_simple_management:
|
|
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
|
|
|
|
|
|
class AgendasImportForm(forms.Form):
|
|
agendas_json = forms.FileField(label=_('Export File'))
|
|
|
|
|
|
class AgendaDuplicateForm(forms.Form):
|
|
label = forms.CharField(label=_('New label'), max_length=150, required=False)
|
|
|
|
|
|
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 = []
|
|
|
|
|
|
class EventCancelForm(forms.ModelForm):
|
|
class Meta:
|
|
model = Event
|
|
fields = []
|
|
|
|
|
|
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)
|
|
if kwargs['instance'].kind == 'events':
|
|
if self.instance.partial_bookings:
|
|
del self.fields['booking_user_block_template']
|
|
else:
|
|
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']
|
|
|
|
|
|
class AgendaBookingCheckSettingsForm(forms.ModelForm):
|
|
class Meta:
|
|
model = Agenda
|
|
fields = [
|
|
'booking_check_filters',
|
|
'mark_event_checked_auto',
|
|
'disable_check_update',
|
|
'enable_check_for_future_events',
|
|
'booking_extra_user_block_template',
|
|
]
|
|
widgets = {'booking_extra_user_block_template': forms.Textarea(attrs={'rows': 3})}
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
if self.instance.partial_bookings:
|
|
del self.fields['enable_check_for_future_events']
|
|
del self.fields['booking_extra_user_block_template']
|
|
|
|
|
|
class AgendaInvoicingSettingsForm(forms.ModelForm):
|
|
class Meta:
|
|
model = Agenda
|
|
fields = [
|
|
'invoicing_unit',
|
|
'invoicing_tolerance',
|
|
]
|
|
|
|
def save(self):
|
|
super().save()
|
|
self.instance.async_refresh_booking_computed_times()
|
|
return self.instance
|
|
|
|
|
|
class AgendaNotificationsForm(forms.ModelForm):
|
|
class Meta:
|
|
model = AgendaNotificationsSettings
|
|
exclude = ['agenda']
|
|
|
|
@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
|
|
|
|
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.')
|
|
|
|
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)
|
|
|
|
|
|
class AgendaReminderForm(forms.ModelForm):
|
|
class Meta:
|
|
model = AgendaReminderSettings
|
|
exclude = ['agenda']
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
if not settings.SMS_URL:
|
|
del self.fields['days_before_sms']
|
|
del self.fields['sms_extra_info']
|
|
|
|
|
|
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']
|
|
|
|
|
|
class AgendasExportForm(forms.Form):
|
|
agendas = forms.ChoiceField(label=_('Agendas'), required=True)
|
|
resources = forms.BooleanField(label=_('Resources'), required=False, initial=True)
|
|
unavailability_calendars = forms.BooleanField(
|
|
label=_('Unavailability calendars'), required=False, initial=True
|
|
)
|
|
categories = forms.BooleanField(label=_('Categories'), required=False, initial=True)
|
|
events_types = forms.BooleanField(label=_('Events types'), required=False, initial=True)
|
|
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()
|
|
|
|
self.fields['agendas'].choices = [('all', pgettext('agendas', 'All')), ('none', _('None'))] + [
|
|
(x.id, x.label) for x in Category.objects.all()
|
|
]
|
|
|
|
|
|
class SharedCustodyRuleForm(forms.ModelForm):
|
|
guardian = forms.ModelChoiceField(label=_('Guardian'), queryset=Person.objects.none())
|
|
days = forms.TypedMultipleChoiceField(
|
|
choices=WEEKDAY_CHOICES,
|
|
coerce=int,
|
|
required=True,
|
|
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.get('days', []), weeks=cleaned_data['weeks'], instance=self.instance
|
|
):
|
|
raise ValidationError(_('Rule overlaps existing rules.'))
|
|
|
|
return cleaned_data
|
|
|
|
|
|
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]
|
|
)
|
|
settings = SharedCustodySettings.get_singleton()
|
|
self.fields['holiday'].queryset = TimePeriodExceptionGroup.objects.filter(
|
|
unavailability_calendar=settings.holidays_calendar_id,
|
|
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
|
|
|
|
|
|
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
|
|
|
|
|
|
class SharedCustodySettingsForm(forms.ModelForm):
|
|
management_role = forms.ModelChoiceField(
|
|
label=_('Management role'), required=False, queryset=get_role_queryset()
|
|
)
|
|
|
|
class Meta:
|
|
model = SharedCustodySettings
|
|
fields = '__all__'
|