chrono/chrono/manager/forms.py

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

1418 lines
50 KiB
Python
Raw Normal View History

# 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/>.
2022-02-15 17:16:31 +01:00
import copy
import csv
import datetime
2022-02-15 17:16:31 +01:00
from collections import defaultdict
from operator import itemgetter
import django_filters
2022-02-15 17:16:31 +01:00
from dateutil.relativedelta import relativedelta
from django import forms
from django.conf import settings
from django.contrib.auth.models import Group
from django.core.exceptions import FieldDoesNotExist
from django.db import transaction
from django.db.models import DurationField, ExpressionWrapper, F
from django.forms import ValidationError
from django.utils.encoding import force_text
from django.utils.formats import date_format
from django.utils.six import StringIO
2022-02-15 17:16:31 +01:00
from django.utils.timezone import localtime, make_aware, now
from django.utils.translation import ugettext_lazy as _
from chrono.agendas.models import (
WEEK_CHOICES,
WEEKDAY_CHOICES,
WEEKDAYS_LIST,
2021-03-04 15:16:31 +01:00
AbsenceReason,
AbsenceReasonGroup,
Agenda,
AgendaNotificationsSettings,
AgendaReminderSettings,
Booking,
2021-04-08 11:25:34 +02:00
Desk,
Event,
MeetingType,
Person,
Resource,
SharedCustodyHolidayRule,
SharedCustodyPeriod,
SharedCustodyRule,
Subscription,
TimePeriod,
TimePeriodException,
TimePeriodExceptionGroup,
TimePeriodExceptionSource,
UnavailabilityCalendar,
VirtualMember,
generate_slug,
)
2016-07-22 14:57:11 +02:00
from . import widgets
from .widgets import SplitDateTimeField, WeekdaysWidget
class AbsenceReasonForm(forms.ModelForm):
class Meta:
model = AbsenceReason
fields = ['label', 'slug']
def clean_slug(self):
slug = self.cleaned_data['slug']
if self.instance.group.absence_reasons.filter(slug=slug).exclude(pk=self.instance.pk).exists():
raise ValidationError(_('Another absence reason exists with the same identifier.'))
return slug
class AgendaAddForm(forms.ModelForm):
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 Meta:
model = Agenda
fields = ['label', 'kind', 'category', 'edit_role', 'view_role']
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',
2020-07-24 15:59:49 +02:00
'category',
2020-08-13 11:48:52 +02:00
'anonymize_delay',
'default_view',
'booking_form_url',
'event_display_template',
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if kwargs['instance'].kind != 'events':
del self.fields['booking_form_url']
del self.fields['event_display_template']
self.fields['default_view'].choices = [
(k, v) for k, v in self.fields['default_view'].choices if k != 'open_events'
]
class AgendaBookingDelaysForm(forms.ModelForm):
class Meta:
model = Agenda
fields = [
'minimal_booking_delay',
'minimal_booking_delay_in_working_days',
'maximal_booking_delay',
]
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=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']
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',
'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'),
}
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
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',
'frequency',
'recurrence_days',
'recurrence_week_interval',
)
class Meta:
model = Event
widgets = {
'recurrence_end_date': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
}
fields = [
'label',
'slug',
'start_datetime',
'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:
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.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 self.instance.has_recurrences_booked(
after=self.cleaned_data['recurrence_end_date']
):
raise ValidationError(_('Bookings exist after this date.'))
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(*args, **kwargs)
return self.instance
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')),
('absence', _('Absence')),
]
if self.agenda.absence_reasons_group:
status_choices += [
('absence-%s' % r.label, _('Absence (%s)') % r.label)
for r in self.agenda.absence_reasons_group.absence_reasons.all()
]
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)
if value.startswith('absence-'):
return queryset.filter(user_was_present=False, user_absence_reason=value[8:])
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
2021-03-04 15:16:31 +01:00
class BookingAbsenceReasonForm(forms.Form):
reason = forms.ChoiceField(required=False)
def __init__(self, *args, **kwargs):
agenda = kwargs.pop('agenda')
super().__init__(*args, **kwargs)
if agenda.absence_reasons_group:
self.fields['reason'].choices = [('', '---------')] + [
(r.label, r.label) for r in agenda.absence_reasons_group.absence_reasons.all()
]
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'),
)
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.'),
)
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=_('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,
)
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')
super().__init__(*args, **kwargs)
def get_slots(self):
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-02-15 17:16:31 +01:00
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 = (
2022-02-15 17:16:31 +01:00
self.agenda.event_set.filter(
recurrence_days__isnull=True,
start_datetime__gte=min_start,
start_datetime__lt=max_start,
cancelled=False,
2022-02-15 17:16:31 +01:00
)
.select_related('primary_event')
.order_by('start_datetime', 'label')
)
dates = set()
events = []
dates_per_event_id = defaultdict(list)
for event in all_events:
date = localtime(event.start_datetime).date()
dates.add(date)
real_event = event.primary_event or event
if real_event not in events:
events.append(real_event)
dates_per_event_id[real_event.pk].append(date)
dates = sorted(dates)
date_display = self.cleaned_data['date_display']
if date_display in ['month', 'week']:
grouper = defaultdict(list)
for date in dates:
if date_display == 'month':
attr = date.month
else:
attr = date.isocalendar().week
grouper[(date.year, attr)].append(date)
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
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,
'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': '',
'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,
'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.'))
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
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 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']
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 = {
'start_time': widgets.TimeWidget(),
'end_time': widgets.TimeWidget(),
2017-09-01 15:01:07 +02:00
}
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
if self.instance.weekday_indexes:
self.fields['repeat'].initial = 'custom'
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):
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
exclude = ['agenda', 'slug']
2017-09-01 15:01:07 +02:00
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
2017-09-01 15:01:07 +02:00
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()
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_pk, **kwargs):
self.agenda_pk = agenda_pk
super().__init__(**kwargs)
def clean_events_csv_file(self):
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:
2019-09-21 22:36:08 +02:00
dialect = csv.Sniffer().sniff(content)
except csv.Error:
dialect = None
events = []
warnings = {}
events_by_slug = {x.slug: x for x in Event.objects.filter(agenda=self.agenda_pk)}
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())
for i, csvline in enumerate(csv.reader(StringIO(content), dialect=dialect)):
if not csvline:
continue
if len(csvline) < 3:
raise ValidationError(_('Invalid file format. (line %d)') % (i + 1))
if i == 0 and csvline[0].strip('#') in ('date', 'Date', _('date'), _('Date')):
continue
# 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
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 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
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)
)
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(_('Invalid file format. (date/time format, line %d)') % (i + 1))
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))
try:
event.full_clean(exclude=['desk', 'meeting_type', 'primary_event'])
except ValidationError as e:
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)
raise ValidationError(errors)
events.append(event)
self.events = events
self.warnings = warnings
@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.'),
2017-11-06 10:40:24 +01:00
)
ics_url = forms.URLField(
label=_('URL'),
required=False,
2017-11-06 10:40:24 +01:00
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.'))
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 AgendaBookingCheckSettingsForm(forms.ModelForm):
class Meta:
model = Agenda
fields = [
'absence_reasons_group',
'booking_check_filters',
'booking_user_block_template',
'mark_event_checked_auto',
'disable_check_update',
]
widgets = {'booking_user_block_template': forms.Textarea(attrs={'rows': 3})}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if not AbsenceReasonGroup.objects.exists():
del self.fields['absence_reasons_group']
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']
2020-11-04 17:26:16 +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)
unavailability_calendars = forms.BooleanField(
label=_('Unavailability calendars'), required=False, initial=True
)
absence_reason_groups = forms.BooleanField(label=_('Absence reason groups'), required=False, initial=True)
categories = forms.BooleanField(label=_('Categories'), required=False, initial=True)
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
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]
)
self.fields['holiday'].queryset = TimePeriodExceptionGroup.objects.filter(
unavailability_calendar__slug='chrono-holidays',
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