chrono/chrono/manager/forms.py

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

672 lines
22 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/>.
from __future__ import unicode_literals
import csv
import datetime
from django import forms
from django.conf import settings
from django.contrib.auth.models import Group
from django.core.exceptions import FieldDoesNotExist
from django.forms import ValidationError
from django.utils.encoding import force_text
from django.utils.six import StringIO
from django.utils.timezone import make_aware
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _
from chrono.agendas.models import (
Agenda,
Booking,
Event,
MeetingType,
TimePeriod,
Desk,
TimePeriodException,
TimePeriodExceptionSource,
VirtualMember,
Resource,
2020-07-24 15:59:49 +02:00
Category,
AgendaNotificationsSettings,
AgendaReminderSettings,
WEEKDAYS_LIST,
UnavailabilityCalendar,
generate_slug,
)
2016-07-22 14:57:11 +02:00
from . import widgets
from .widgets import SplitDateTimeField
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):
super().save()
if 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',
]
def __init__(self, *args, **kwargs):
super(AgendaEditForm, self).__init__(*args, **kwargs)
if kwargs['instance'].kind != 'events':
del self.fields['default_view']
del self.fields['booking_form_url']
class AgendaBookingDelaysForm(forms.ModelForm):
class Meta:
model = Agenda
fields = [
'minimal_booking_delay',
'maximal_booking_delay',
]
def __init__(self, *args, **kwargs):
super(AgendaBookingDelaysForm, self).__init__(*args, **kwargs)
if kwargs['instance'].kind != 'virtual':
self.fields['minimal_booking_delay'].required = True
self.fields['maximal_booking_delay'].required = True
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 ResourceAddForm(forms.ModelForm):
class Meta:
model = Resource
fields = ['label', 'description']
class ResourceEditForm(forms.ModelForm):
class Meta:
model = Resource
fields = ['label', 'slug', 'description']
2020-07-24 15:59:49 +02:00
class CategoryAddForm(forms.ModelForm):
class Meta:
model = Category
fields = ['label']
class CategoryEditForm(forms.ModelForm):
class Meta:
model = Category
fields = ['label', 'slug']
class NewEventForm(forms.ModelForm):
class Meta:
model = Event
fields = [
'label',
'start_datetime',
'duration',
'places',
]
field_classes = {
'start_datetime': SplitDateTimeField,
}
class EventForm(forms.ModelForm):
class Meta:
model = Event
widgets = {
'publication_date': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
}
fields = [
'label',
'slug',
'start_datetime',
'duration',
'publication_date',
'places',
'waiting_list_places',
'description',
'pricing',
'url',
]
field_classes = {
'start_datetime': SplitDateTimeField,
}
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(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 TimePeriodAddForm(forms.Form):
weekdays = forms.MultipleChoiceField(
label=_('Days'), widget=widgets.WeekdaysWidget(), choices=WEEKDAYS_LIST
)
start_time = forms.TimeField(label=_('Start Time'), widget=widgets.TimeWidget())
end_time = forms.TimeField(label=_('End Time'), widget=widgets.TimeWidget())
def clean_end_time(self):
if self.cleaned_data['end_time'] <= self.cleaned_data['start_time']:
raise ValidationError(_('End time must come after start time.'))
return self.cleaned_data['end_time']
class TimePeriodForm(forms.ModelForm):
class Meta:
model = TimePeriod
widgets = {
'start_time': widgets.TimeWidget(),
'end_time': widgets.TimeWidget(),
2017-09-01 15:01:07 +02:00
}
exclude = ['agenda', 'desk']
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
def clean_end_time(self):
if self.cleaned_data['end_time'] <= self.cleaned_data['start_time']:
raise ValidationError(_('End time must come after start time.'))
return self.cleaned_data['end_time']
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):
all_desks = forms.BooleanField(label=_('Apply exception on all desks of the agenda'), required=False)
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)
if self.instance.pk is not None:
del self.fields['all_desks']
elif 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']
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
class VirtualMemberForm(forms.ModelForm):
class Meta:
model = VirtualMember
fields = ['real_agenda']
def __init__(self, *args, **kwargs):
super(VirtualMemberForm, self).__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(ImportEventsForm, self).__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 = {e.slug: e for e 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 date_fmt in ('%Y-%m-%d', '%d/%m/%Y'):
try:
event.publication_date = datetime.datetime.strptime(csvline[9], date_fmt).date()
break
except ValueError:
continue
else:
raise ValidationError(_('Invalid file format. (date 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'])
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.'),
)
class Meta:
model = Desk
fields = []
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 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):
if bool(self.instance.ics_file):
self.instance.ics_file.delete()
self.instance.ics_file = self.cleaned_data['ics_newfile']
self.instance.ics_filename = self.cleaned_data['ics_newfile'].name
self.instance.save()
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 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['send_sms']
del self.fields['sms_extra_info']
def clean(self):
cleaned_data = super().clean()
if cleaned_data['days'] and not (cleaned_data['send_email'] or cleaned_data.get('send_sms')):
raise ValidationError(_('Select at least one notification medium.'))
return cleaned_data
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
)