# 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 . 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)