chrono/chrono/manager/forms.py

431 lines
14 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/>.
from __future__ import unicode_literals
import csv
import datetime
from django import forms
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.timezone import make_aware
from django.utils.translation import ugettext_lazy as _
from chrono.agendas.models import (
Agenda,
Event,
MeetingType,
TimePeriod,
Desk,
TimePeriodException,
TimePeriodExceptionSource,
VirtualMember,
Resource,
WEEKDAYS_LIST,
)
from . import widgets
from .widgets import DateTimeWidget
class AgendaAddForm(forms.ModelForm):
class Meta:
model = Agenda
fields = ['label', 'kind', '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 AgendaEditForm(AgendaAddForm):
class Meta:
model = Agenda
fields = ['label', 'slug', 'edit_role', 'view_role', 'minimal_booking_delay', 'maximal_booking_delay']
def __init__(self, *args, **kwargs):
super(AgendaEditForm, self).__init__(*args, **kwargs)
if kwargs['instance'].kind != 'virtual':
self.fields['minimal_booking_delay'].required = True
self.fields['maximal_booking_delay'].required = True
class ResourceAddForm(forms.ModelForm):
class Meta:
model = Resource
fields = ['label', 'description']
class ResourceEditForm(forms.ModelForm):
class Meta:
model = Resource
fields = ['label', 'slug', 'description']
class NewEventForm(forms.ModelForm):
class Meta:
model = Event
widgets = {
'agenda': forms.HiddenInput(),
'start_datetime': DateTimeWidget(),
'publication_date': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
}
exclude = ['full', 'meeting_type', 'desk', 'slug', 'resources']
class EventForm(forms.ModelForm):
class Meta:
model = Event
widgets = {
'agenda': forms.HiddenInput(),
'start_datetime': DateTimeWidget(),
'publication_date': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
}
exclude = ['full', 'meeting_type', 'desk', 'resources']
class AgendaResourceForm(forms.Form):
resource = forms.ModelChoiceField(label=_('Resource'), queryset=Resource.objects.none())
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['resource'].queryset = Resource.objects.exclude(agenda=self.initial['agenda'])
class NewMeetingTypeForm(forms.ModelForm):
class Meta:
model = MeetingType
widgets = {
'agenda': forms.HiddenInput(),
}
exclude = ['slug', 'deleted']
def clean(self):
super().clean()
agenda = self.cleaned_data['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
widgets = {
'agenda': forms.HiddenInput(),
}
exclude = ['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(),
'desk': forms.HiddenInput(),
'agenda': forms.HiddenInput(),
}
exclude = []
def __init__(self, *args, **kwargs):
has_desk = kwargs.pop('has_desk')
super(TimePeriodForm, self).__init__(*args, **kwargs)
if has_desk:
del self.fields['agenda']
else:
del self.fields['desk']
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 NewDeskForm(forms.ModelForm):
copy_from = forms.ModelChoiceField(
label=_('Copy settings of desk'), required=False, queryset=Desk.objects.none(),
)
class Meta:
model = Desk
widgets = {
'agenda': forms.HiddenInput(),
}
exclude = ['slug']
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['copy_from'].queryset = Desk.objects.filter(agenda=self.initial['agenda'])
def save(self):
if self.cleaned_data['copy_from']:
return self.cleaned_data['copy_from'].duplicate(label=self.cleaned_data['label'])
return super().save()
class DeskForm(forms.ModelForm):
class Meta:
model = Desk
widgets = {
'agenda': forms.HiddenInput(),
}
exclude = []
class TimePeriodExceptionForm(forms.ModelForm):
class Meta:
model = TimePeriodException
fields = ['desk', 'start_datetime', 'end_datetime', 'label']
widgets = {
'desk': forms.HiddenInput(),
'start_datetime': DateTimeWidget(),
'end_datetime': DateTimeWidget(),
}
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 = ['virtual_agenda', 'real_agenda']
widgets = {
'virtual_agenda': forms.HiddenInput(),
}
def __init__(self, *args, **kwargs):
super(VirtualMemberForm, self).__init__(*args, **kwargs)
self.fields['real_agenda'].queryset = Agenda.objects.filter(kind='meetings').exclude(
virtual_agendas__pk__in=[kwargs['initial']['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:
dialect = csv.Sniffer().sniff(content)
except csv.Error:
dialect = None
events = []
slugs = set()
for i, csvline in enumerate(csv.reader(content.splitlines(), 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
event = Event()
event.agenda_id = self.agenda_pk
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 = datetime.datetime.strptime('%s %s' % tuple(csvline[:2]), datetime_fmt)
except ValueError:
continue
event.start_datetime = make_aware(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)
)
if len(csvline) >= 5:
event.label = force_text(csvline[4])
exclude = ['desk', 'meeting_type']
if len(csvline) >= 6:
event.slug = force_text(csvline[5]) if csvline[5] else None
if event.slug and event.slug in slugs:
raise ValidationError(_('File contains duplicated event identifiers: %s') % event.slug)
else:
slugs.add(event.slug)
else:
exclude += ['slug']
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))
try:
event.full_clean(exclude=exclude)
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
@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.URLField(
label=_('URL'),
required=False,
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.save()
class AgendasImportForm(forms.Form):
agendas_json = forms.FileField(label=_('Agendas Export File'))
class AgendaDuplicateForm(forms.Form):
label = forms.CharField(label=_('New label'), max_length=150, required=False)