2016-02-13 15:31:28 +01:00
|
|
|
# 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/>.
|
|
|
|
|
2018-03-25 11:26:47 +02:00
|
|
|
from __future__ import unicode_literals
|
|
|
|
|
2016-09-16 15:04:02 +02:00
|
|
|
import csv
|
|
|
|
import datetime
|
|
|
|
|
2016-02-13 15:31:28 +01:00
|
|
|
from django import forms
|
2020-09-15 14:05:38 +02:00
|
|
|
from django.conf import settings
|
2018-03-03 10:47:08 +01:00
|
|
|
from django.contrib.auth.models import Group
|
2020-06-11 17:04:58 +02:00
|
|
|
from django.core.exceptions import FieldDoesNotExist
|
2021-01-28 12:33:43 +01:00
|
|
|
from django.db import transaction
|
2016-09-16 15:04:02 +02:00
|
|
|
from django.forms import ValidationError
|
2018-03-25 11:26:47 +02:00
|
|
|
from django.utils.encoding import force_text
|
2020-07-10 09:16:01 +02:00
|
|
|
from django.utils.six import StringIO
|
2021-03-12 09:03:39 +01:00
|
|
|
from django.utils.timezone import now, make_aware, make_naive
|
2016-09-16 15:04:02 +02:00
|
|
|
from django.utils.translation import ugettext_lazy as _
|
2016-02-13 15:31:28 +01:00
|
|
|
|
2018-03-03 10:47:08 +01:00
|
|
|
from chrono.agendas.models import (
|
|
|
|
Agenda,
|
2020-07-08 16:10:53 +02:00
|
|
|
Booking,
|
2018-03-03 10:47:08 +01:00
|
|
|
Event,
|
|
|
|
MeetingType,
|
|
|
|
TimePeriod,
|
|
|
|
Desk,
|
2018-09-22 18:12:22 +02:00
|
|
|
TimePeriodException,
|
2019-12-12 10:56:14 +01:00
|
|
|
TimePeriodExceptionSource,
|
2020-02-18 12:14:36 +01:00
|
|
|
VirtualMember,
|
2020-05-15 16:52:58 +02:00
|
|
|
Resource,
|
2020-07-16 15:12:47 +02:00
|
|
|
AgendaNotificationsSettings,
|
2020-09-15 14:05:38 +02:00
|
|
|
AgendaReminderSettings,
|
2018-09-22 18:12:22 +02:00
|
|
|
WEEKDAYS_LIST,
|
2020-09-30 17:53:42 +02:00
|
|
|
UnavailabilityCalendar,
|
2020-09-11 09:29:16 +02:00
|
|
|
generate_slug,
|
2018-09-22 18:12:22 +02:00
|
|
|
)
|
2016-02-13 15:31:28 +01:00
|
|
|
|
2016-07-22 14:57:11 +02:00
|
|
|
from . import widgets
|
2020-07-16 14:34:19 +02:00
|
|
|
from .widgets import SplitDateTimeField
|
2016-02-13 15:31:28 +01:00
|
|
|
|
|
|
|
|
2018-03-03 10:47:08 +01:00
|
|
|
class AgendaAddForm(forms.ModelForm):
|
|
|
|
edit_role = forms.ModelChoiceField(
|
|
|
|
label=_('Edit Role'), required=False, queryset=Group.objects.all().order_by('name')
|
2019-12-16 16:21:24 +01:00
|
|
|
)
|
2018-03-03 10:47:08 +01:00
|
|
|
view_role = forms.ModelChoiceField(
|
|
|
|
label=_('View Role'), required=False, queryset=Group.objects.all().order_by('name')
|
2019-12-16 16:21:24 +01:00
|
|
|
)
|
2018-03-03 10:47:08 +01:00
|
|
|
|
2021-01-25 14:23:39 +01:00
|
|
|
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
|
|
|
|
|
2018-03-03 10:47:08 +01:00
|
|
|
|
2020-10-20 17:29:51 +02:00
|
|
|
class AgendaEditForm(forms.ModelForm):
|
2018-03-03 10:47:08 +01:00
|
|
|
class Meta:
|
|
|
|
model = Agenda
|
2020-07-09 14:50:36 +02:00
|
|
|
fields = [
|
|
|
|
'label',
|
|
|
|
'slug',
|
2020-07-24 15:59:49 +02:00
|
|
|
'category',
|
2020-08-13 11:48:52 +02:00
|
|
|
'anonymize_delay',
|
2020-07-09 14:50:36 +02:00
|
|
|
'default_view',
|
2020-09-25 11:00:31 +02:00
|
|
|
'booking_form_url',
|
2020-07-09 14:50:36 +02:00
|
|
|
]
|
2018-03-03 10:47:08 +01:00
|
|
|
|
2020-02-25 08:38:25 +01:00
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
super(AgendaEditForm, self).__init__(*args, **kwargs)
|
2020-07-09 14:50:36 +02:00
|
|
|
if kwargs['instance'].kind != 'events':
|
2020-09-25 11:00:31 +02:00
|
|
|
del self.fields['booking_form_url']
|
2021-03-11 16:21:49 +01:00
|
|
|
self.initial['default_view'] = 'day'
|
|
|
|
self.fields['default_view'].choices = [
|
|
|
|
(k, v) for k, v in self.fields['default_view'].choices if k != 'open_events'
|
|
|
|
]
|
2020-02-25 08:38:25 +01:00
|
|
|
|
2018-03-03 10:47:08 +01:00
|
|
|
|
2020-10-20 17:29:51 +02:00
|
|
|
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',
|
|
|
|
]
|
|
|
|
|
|
|
|
|
2020-09-30 17:53:42 +02:00
|
|
|
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']
|
|
|
|
|
|
|
|
|
2019-11-06 15:20:07 +01:00
|
|
|
class NewEventForm(forms.ModelForm):
|
|
|
|
class Meta:
|
|
|
|
model = Event
|
2020-07-09 12:46:13 +02:00
|
|
|
fields = [
|
2021-01-22 09:13:35 +01:00
|
|
|
'label',
|
2020-07-09 12:46:13 +02:00
|
|
|
'start_datetime',
|
2020-12-22 17:26:29 +01:00
|
|
|
'repeat',
|
2020-07-09 12:46:13 +02:00
|
|
|
'duration',
|
|
|
|
'places',
|
|
|
|
]
|
2020-07-16 14:34:19 +02:00
|
|
|
field_classes = {
|
|
|
|
'start_datetime': SplitDateTimeField,
|
|
|
|
}
|
2019-11-06 15:20:07 +01:00
|
|
|
|
|
|
|
|
2016-02-13 15:31:28 +01:00
|
|
|
class EventForm(forms.ModelForm):
|
2021-01-28 12:33:43 +01:00
|
|
|
protected_fields = ('repeat', 'slug', 'start_datetime')
|
|
|
|
|
2016-02-13 15:31:28 +01:00
|
|
|
class Meta:
|
|
|
|
model = Event
|
|
|
|
widgets = {
|
2020-05-12 14:22:52 +02:00
|
|
|
'publication_date': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
|
2021-01-13 15:08:40 +01:00
|
|
|
'recurrence_end_date': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
|
2016-02-13 15:31:28 +01:00
|
|
|
}
|
2020-07-09 12:46:13 +02:00
|
|
|
fields = [
|
2021-01-22 09:13:35 +01:00
|
|
|
'label',
|
|
|
|
'slug',
|
2020-07-09 12:46:13 +02:00
|
|
|
'start_datetime',
|
2020-12-22 17:26:29 +01:00
|
|
|
'repeat',
|
2021-01-13 15:08:40 +01:00
|
|
|
'recurrence_end_date',
|
2020-07-09 12:46:13 +02:00
|
|
|
'duration',
|
|
|
|
'publication_date',
|
|
|
|
'places',
|
|
|
|
'waiting_list_places',
|
|
|
|
'description',
|
|
|
|
'pricing',
|
|
|
|
'url',
|
|
|
|
]
|
2020-07-16 14:34:19 +02:00
|
|
|
field_classes = {
|
|
|
|
'start_datetime': SplitDateTimeField,
|
|
|
|
}
|
2016-09-11 11:31:29 +02:00
|
|
|
|
2021-01-28 12:33:43 +01:00
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
super().__init__(*args, **kwargs)
|
|
|
|
if self.instance.recurrence_rule 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.'
|
|
|
|
)
|
2021-03-11 12:27:58 +01:00
|
|
|
if self.instance.primary_event:
|
|
|
|
for field in ('slug', 'repeat', 'recurrence_end_date', 'publication_date'):
|
|
|
|
del self.fields[field]
|
2021-01-28 12:33:43 +01:00
|
|
|
|
2021-02-17 16:54:25 +01:00
|
|
|
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.'))
|
2021-03-11 12:27:58 +01:00
|
|
|
if self.cleaned_data.get('recurrence_end_date') and not self.cleaned_data.get('repeat'):
|
2021-03-03 14:30:00 +01:00
|
|
|
raise ValidationError(_('Recurrence end date makes no sense without repetition.'))
|
2021-02-17 16:54:25 +01:00
|
|
|
|
2021-01-28 12:33:43 +01:00
|
|
|
def save(self, *args, **kwargs):
|
|
|
|
with transaction.atomic():
|
|
|
|
if any(field for field in self.changed_data if field in self.protected_fields):
|
|
|
|
self.instance.recurrences.all().delete()
|
|
|
|
elif self.instance.recurrence_rule:
|
|
|
|
update_fields = {
|
|
|
|
field: value
|
|
|
|
for field, value in self.cleaned_data.items()
|
|
|
|
if field not in self.protected_fields
|
|
|
|
}
|
|
|
|
self.instance.recurrences.update(**update_fields)
|
2021-01-19 15:35:31 +01:00
|
|
|
|
2021-03-12 09:03:39 +01:00
|
|
|
super().save(*args, **kwargs)
|
|
|
|
if self.instance.recurrence_end_date:
|
|
|
|
self.instance.recurrences.filter(
|
|
|
|
start_datetime__gt=self.instance.recurrence_end_date
|
|
|
|
).delete()
|
2021-03-11 12:00:43 +01:00
|
|
|
excluded_datetimes = [event.datetime_slug for event in self.instance.recurrences.all()]
|
2021-03-12 09:03:39 +01:00
|
|
|
self.instance.create_all_recurrences(excluded_datetimes)
|
|
|
|
return self.instance
|
2021-01-28 12:33:43 +01:00
|
|
|
|
2016-09-11 11:31:29 +02:00
|
|
|
|
2020-05-18 16:45:35 +02:00
|
|
|
class AgendaResourceForm(forms.Form):
|
|
|
|
resource = forms.ModelChoiceField(label=_('Resource'), queryset=Resource.objects.none())
|
|
|
|
|
|
|
|
def __init__(self, *args, **kwargs):
|
2020-10-02 14:40:08 +02:00
|
|
|
agenda = kwargs.pop('agenda')
|
2020-05-18 16:45:35 +02:00
|
|
|
super().__init__(*args, **kwargs)
|
2020-10-02 14:40:08 +02:00
|
|
|
self.fields['resource'].queryset = Resource.objects.exclude(agenda=agenda)
|
2020-05-18 16:45:35 +02:00
|
|
|
|
|
|
|
|
2016-10-28 19:06:51 +02:00
|
|
|
class NewMeetingTypeForm(forms.ModelForm):
|
|
|
|
class Meta:
|
|
|
|
model = MeetingType
|
2020-10-02 14:40:08 +02:00
|
|
|
exclude = ['agenda', 'slug', 'deleted']
|
2016-10-28 19:06:51 +02:00
|
|
|
|
2020-02-18 12:14:36 +01:00
|
|
|
def clean(self):
|
|
|
|
super().clean()
|
2020-10-02 14:40:08 +02:00
|
|
|
agenda = self.instance.agenda
|
2020-02-18 12:14:36 +01:00
|
|
|
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.")
|
|
|
|
)
|
|
|
|
|
2016-10-28 19:06:51 +02:00
|
|
|
|
2016-09-11 11:31:29 +02:00
|
|
|
class MeetingTypeForm(forms.ModelForm):
|
|
|
|
class Meta:
|
|
|
|
model = MeetingType
|
2020-10-02 14:40:08 +02:00
|
|
|
exclude = ['agenda', 'deleted']
|
2016-09-11 11:31:29 +02:00
|
|
|
|
2021-02-12 15:48:06 +01:00
|
|
|
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
|
|
|
|
|
2020-02-18 12:14:36 +01:00
|
|
|
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)
|
|
|
|
)
|
|
|
|
|
2016-09-11 11:31:29 +02:00
|
|
|
|
2018-09-22 18:12:22 +02:00
|
|
|
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())
|
|
|
|
|
2019-03-14 14:31:48 +01:00
|
|
|
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']
|
|
|
|
|
2018-09-22 18:12:22 +02:00
|
|
|
|
2016-09-11 11:31:29 +02:00
|
|
|
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
|
|
|
}
|
2020-10-02 14:40:08 +02:00
|
|
|
exclude = ['agenda', 'desk']
|
2020-02-26 11:52:05 +01:00
|
|
|
|
2021-01-28 14:38:26 +01:00
|
|
|
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
|
|
|
|
|
2019-03-14 14:31:48 +01:00
|
|
|
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']
|
|
|
|
|
2021-01-28 14:38:26 +01:00
|
|
|
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):
|
2020-05-07 17:45:01 +02:00
|
|
|
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
|
2020-10-02 14:40:08 +02:00
|
|
|
exclude = ['agenda', 'slug']
|
2017-09-01 15:01:07 +02:00
|
|
|
|
2020-05-07 17:45:01 +02:00
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
super().__init__(*args, **kwargs)
|
2021-01-26 15:50:34 +01:00
|
|
|
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)
|
2020-05-07 17:45:01 +02:00
|
|
|
|
|
|
|
def save(self):
|
2021-01-26 15:50:34 +01:00
|
|
|
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']:
|
2020-05-07 17:45:01 +02:00
|
|
|
return self.cleaned_data['copy_from'].duplicate(label=self.cleaned_data['label'])
|
2021-01-26 15:50:34 +01:00
|
|
|
|
|
|
|
super().save()
|
|
|
|
self.instance.import_timeperiod_exceptions_from_settings(enable=True)
|
|
|
|
return self.instance
|
2020-05-07 17:45:01 +02:00
|
|
|
|
2017-09-01 15:01:07 +02:00
|
|
|
|
|
|
|
class DeskForm(forms.ModelForm):
|
|
|
|
class Meta:
|
|
|
|
model = Desk
|
2020-10-02 14:40:08 +02:00
|
|
|
exclude = ['agenda']
|
2016-09-16 15:04:02 +02:00
|
|
|
|
2021-01-28 10:31:19 +01:00
|
|
|
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
|
|
|
|
|
2016-09-16 15:04:02 +02:00
|
|
|
|
2017-08-24 14:15:18 +02:00
|
|
|
class TimePeriodExceptionForm(forms.ModelForm):
|
|
|
|
class Meta:
|
|
|
|
model = TimePeriodException
|
2020-10-02 14:40:08 +02:00
|
|
|
fields = ['start_datetime', 'end_datetime', 'label']
|
2020-07-16 14:34:19 +02:00
|
|
|
field_classes = {
|
|
|
|
'start_datetime': SplitDateTimeField,
|
|
|
|
'end_datetime': SplitDateTimeField,
|
2017-08-24 14:15:18 +02:00
|
|
|
}
|
|
|
|
|
2020-08-28 17:35:36 +02:00
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
super().__init__(*args, **kwargs)
|
2021-01-28 16:41:58 +01:00
|
|
|
self.old_label = self.instance.label
|
|
|
|
self.old_start_datetime = self.instance.start_datetime
|
|
|
|
self.old_end_datetime = self.instance.end_datetime
|
2020-08-28 17:35:36 +02:00
|
|
|
|
2020-05-05 14:59:24 +02:00
|
|
|
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
|
2017-08-24 14:15:18 +02:00
|
|
|
|
2021-01-28 16:41:58 +01:00
|
|
|
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
|
|
|
|
|
2017-08-24 14:15:18 +02:00
|
|
|
|
2020-02-18 12:14:36 +01:00
|
|
|
class VirtualMemberForm(forms.ModelForm):
|
|
|
|
class Meta:
|
|
|
|
model = VirtualMember
|
2020-10-02 14:40:08 +02:00
|
|
|
fields = ['real_agenda']
|
2020-02-18 12:14:36 +01:00
|
|
|
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
super(VirtualMemberForm, self).__init__(*args, **kwargs)
|
|
|
|
self.fields['real_agenda'].queryset = Agenda.objects.filter(kind='meetings').exclude(
|
2020-10-02 14:40:08 +02:00
|
|
|
virtual_agendas=self.instance.virtual_agenda
|
2020-02-18 12:14:36 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
2016-09-16 15:04:02 +02:00
|
|
|
class ImportEventsForm(forms.Form):
|
2017-05-05 15:56:33 +02:00
|
|
|
events_csv_file = forms.FileField(
|
|
|
|
label=_('Events File'),
|
|
|
|
required=True,
|
2016-09-16 15:04:02 +02:00
|
|
|
help_text=_(
|
|
|
|
'CSV file with date, time, number of places, '
|
2020-01-20 20:03:51 +01:00
|
|
|
'number of places in waiting list, label, and '
|
2020-07-03 09:45:15 +02:00
|
|
|
'optionally, identifier, description, pricing, '
|
|
|
|
'URL, and publication date as columns.'
|
2019-12-16 16:21:24 +01:00
|
|
|
),
|
2016-09-16 15:04:02 +02:00
|
|
|
)
|
2016-09-16 15:22:27 +02:00
|
|
|
events = None
|
2016-09-16 15:04:02 +02:00
|
|
|
|
2019-11-27 09:27:44 +01:00
|
|
|
def __init__(self, agenda_pk, **kwargs):
|
|
|
|
self.agenda_pk = agenda_pk
|
|
|
|
super(ImportEventsForm, self).__init__(**kwargs)
|
|
|
|
|
2016-09-16 15:04:02 +02:00
|
|
|
def clean_events_csv_file(self):
|
|
|
|
content = self.cleaned_data['events_csv_file'].read()
|
2018-03-25 11:26:47 +02:00
|
|
|
if b'\0' in content:
|
2016-09-16 15:04:02 +02:00
|
|
|
raise ValidationError(_('Invalid file format.'))
|
|
|
|
|
2020-06-17 14:29:33 +02:00
|
|
|
for charset in ('utf-8-sig', 'iso-8859-15'):
|
2019-03-25 18:17:06 +01:00
|
|
|
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.
|
|
|
|
|
2016-09-16 15:04:02 +02:00
|
|
|
try:
|
2019-09-21 22:36:08 +02:00
|
|
|
dialect = csv.Sniffer().sniff(content)
|
2016-09-16 15:22:27 +02:00
|
|
|
except csv.Error:
|
2016-09-16 15:04:02 +02:00
|
|
|
dialect = None
|
|
|
|
|
|
|
|
events = []
|
2020-09-11 09:29:16 +02:00
|
|
|
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())
|
2020-07-10 09:16:01 +02:00
|
|
|
for i, csvline in enumerate(csv.reader(StringIO(content), dialect=dialect)):
|
2016-09-16 15:04:02 +02:00
|
|
|
if not csvline:
|
|
|
|
continue
|
2016-09-16 15:27:03 +02:00
|
|
|
if len(csvline) < 3:
|
2016-09-16 15:04:02 +02:00
|
|
|
raise ValidationError(_('Invalid file format. (line %d)') % (i + 1))
|
2016-09-16 15:28:09 +02:00
|
|
|
if i == 0 and csvline[0].strip('#') in ('date', 'Date', _('date'), _('Date')):
|
2016-09-16 15:04:02 +02:00
|
|
|
continue
|
2020-09-11 09:29:16 +02:00
|
|
|
|
|
|
|
# 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
|
|
|
|
|
2016-09-16 15:04:02 +02:00
|
|
|
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:
|
2020-09-11 09:29:16 +02:00
|
|
|
event_datetime = make_aware(
|
|
|
|
datetime.datetime.strptime('%s %s' % tuple(csvline[:2]), datetime_fmt)
|
|
|
|
)
|
2016-09-16 15:04:02 +02:00
|
|
|
except ValueError:
|
|
|
|
continue
|
2020-09-11 09:29:16 +02:00
|
|
|
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
|
2016-09-16 15:04:02 +02:00
|
|
|
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)
|
2019-12-16 16:21:24 +01:00
|
|
|
)
|
2020-09-11 09:29:16 +02:00
|
|
|
|
2020-01-20 20:03:51 +01:00
|
|
|
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
|
|
|
|
|
2020-06-18 10:39:04 +02:00
|
|
|
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))
|
|
|
|
|
2020-07-09 16:10:37 +02:00
|
|
|
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))
|
|
|
|
|
2019-07-23 16:30:30 +02:00
|
|
|
try:
|
2020-12-22 17:26:29 +01:00
|
|
|
event.full_clean(exclude=['desk', 'meeting_type', 'primary_event'])
|
2019-07-23 16:30:30 +02:00
|
|
|
except ValidationError as e:
|
2020-06-11 17:04:58 +02:00
|
|
|
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)
|
2019-07-23 16:30:30 +02:00
|
|
|
raise ValidationError(errors)
|
2016-09-16 15:04:02 +02:00
|
|
|
events.append(event)
|
|
|
|
self.events = events
|
2020-09-11 09:29:16 +02:00
|
|
|
self.warnings = warnings
|
2017-09-03 13:28:50 +02:00
|
|
|
|
2020-06-11 17:04:58 +02:00
|
|
|
@staticmethod
|
|
|
|
def get_verbose_name(field_name):
|
|
|
|
try:
|
|
|
|
return Event._meta.get_field(field_name).verbose_name
|
|
|
|
except FieldDoesNotExist:
|
|
|
|
return ''
|
|
|
|
|
2017-09-03 13:28:50 +02:00
|
|
|
|
|
|
|
class ExceptionsImportForm(forms.ModelForm):
|
2017-10-03 23:15:00 +02:00
|
|
|
ics_file = forms.FileField(
|
|
|
|
label=_('ICS File'),
|
|
|
|
required=False,
|
2020-06-04 09:54:32 +02:00
|
|
|
help_text=_('ICS file containing events which will be considered as exceptions.'),
|
2017-11-06 10:40:24 +01:00
|
|
|
)
|
2017-10-03 23:15:00 +02: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.'),
|
|
|
|
)
|
2018-09-22 16:25:46 +02:00
|
|
|
|
2019-12-10 15:28:39 +01:00
|
|
|
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.'))
|
|
|
|
|
2018-09-22 16:25:46 +02:00
|
|
|
|
2019-12-12 10:56:14 +01:00
|
|
|
class TimePeriodExceptionSourceReplaceForm(forms.ModelForm):
|
2020-01-28 15:08:24 +01:00
|
|
|
ics_newfile = forms.FileField(
|
2019-12-12 10:56:14 +01:00
|
|
|
label=_('ICS File'),
|
|
|
|
required=False,
|
|
|
|
help_text=_('ICS file containing events which will be considered as exceptions.'),
|
|
|
|
)
|
|
|
|
|
|
|
|
class Meta:
|
|
|
|
model = TimePeriodExceptionSource
|
|
|
|
fields = []
|
|
|
|
|
2020-01-28 15:08:24 +01:00
|
|
|
def save(self, *args, **kwargs):
|
2021-02-01 15:40:03 +01:00
|
|
|
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.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
|
2020-01-28 15:08:24 +01:00
|
|
|
|
2019-12-12 10:56:14 +01:00
|
|
|
|
2018-09-22 16:25:46 +02:00
|
|
|
class AgendasImportForm(forms.Form):
|
2020-10-26 17:15:47 +01:00
|
|
|
agendas_json = forms.FileField(label=_('Export File'))
|
2020-06-17 10:52:25 +02:00
|
|
|
|
|
|
|
|
|
|
|
class AgendaDuplicateForm(forms.Form):
|
|
|
|
label = forms.CharField(label=_('New label'), max_length=150, required=False)
|
2020-07-08 16:10:53 +02:00
|
|
|
|
|
|
|
|
|
|
|
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 = []
|
2020-07-09 12:46:13 +02:00
|
|
|
|
|
|
|
|
|
|
|
class EventCancelForm(forms.ModelForm):
|
|
|
|
class Meta:
|
|
|
|
model = Event
|
|
|
|
fields = []
|
2020-07-16 15:12:47 +02:00
|
|
|
|
|
|
|
|
|
|
|
class AgendaNotificationsForm(forms.ModelForm):
|
|
|
|
class Meta:
|
|
|
|
model = AgendaNotificationsSettings
|
2020-10-02 14:40:08 +02:00
|
|
|
exclude = ['agenda']
|
2020-07-16 15:12:47 +02:00
|
|
|
|
2020-09-08 16:55:56 +02:00
|
|
|
@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
|
|
|
|
|
2020-07-16 15:12:47 +02:00
|
|
|
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.')
|
2020-09-08 16:55:56 +02:00
|
|
|
|
|
|
|
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)
|
2020-09-15 14:05:38 +02:00
|
|
|
|
|
|
|
|
|
|
|
class AgendaReminderForm(forms.ModelForm):
|
|
|
|
class Meta:
|
|
|
|
model = AgendaReminderSettings
|
|
|
|
exclude = ['agenda']
|
|
|
|
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
super().__init__(*args, **kwargs)
|
2020-09-17 12:13:15 +02:00
|
|
|
if not settings.SMS_URL:
|
2020-09-15 14:05:38 +02:00
|
|
|
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
|
|
|
|
)
|