chrono/chrono/manager/views.py

3346 lines
120 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/>.
import collections
import copy
import csv
import datetime
import itertools
import json
import math
import uuid
from operator import attrgetter
import requests
from django.contrib import messages
from django.core.exceptions import PermissionDenied
from django.db import transaction
from django.db.models import BooleanField, Count, Max, Min, Q, Value
from django.http import Http404, HttpResponse, HttpResponseForbidden, HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect, render
from django.template.defaultfilters import title
from django.template.loader import render_to_string
from django.urls import reverse, reverse_lazy
from django.utils import lorem_ipsum
from django.utils.dates import MONTHS
from django.utils.encoding import force_text
from django.utils.html import format_html
from django.utils.timezone import localtime, make_aware, make_naive, now
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ungettext
from django.views.generic import (
CreateView,
DayArchiveView,
DeleteView,
DetailView,
FormView,
ListView,
MonthArchiveView,
RedirectView,
TemplateView,
UpdateView,
View,
)
from weasyprint import HTML
from chrono.agendas.models import (
AbsenceReason,
AbsenceReasonGroup,
Agenda,
AgendaImportError,
AgendaNotificationsSettings,
AgendaReminderSettings,
Booking,
BookingColor,
Category,
Desk,
Event,
EventCancellationReport,
ICSError,
MeetingType,
Resource,
TimePeriod,
TimePeriodException,
TimePeriodExceptionSource,
UnavailabilityCalendar,
VirtualMember,
)
from .forms import (
AbsenceReasonForm,
AgendaAddForm,
AgendaBookingCheckSettingsForm,
AgendaBookingDelaysForm,
AgendaDuplicateForm,
AgendaEditForm,
AgendaNotificationsForm,
AgendaReminderForm,
AgendaResourceForm,
AgendaRolesForm,
AgendasExportForm,
AgendasImportForm,
BookingAbsenceReasonForm,
BookingCancelForm,
BookingCheckFilterSet,
DeskForm,
EventCancelForm,
EventForm,
EventsTimesheetForm,
ExceptionsImportForm,
ImportEventsForm,
MeetingTypeForm,
NewDeskForm,
NewEventForm,
NewMeetingTypeForm,
NewTimePeriodExceptionForm,
SubscriptionCheckFilterSet,
TimePeriodAddForm,
TimePeriodExceptionForm,
TimePeriodExceptionSourceReplaceForm,
TimePeriodForm,
UnavailabilityCalendarAddForm,
UnavailabilityCalendarEditForm,
VirtualMemberForm,
)
from .utils import export_site, import_site
FUTURE_BOOKING_ERROR_MSG = _('This cannot be removed as there are bookings for a future date.')
class HomepageView(ListView):
template_name = 'chrono/manager_home.html'
model = Agenda
def get_queryset(self):
queryset = super().get_queryset()
if not self.request.user.is_staff:
group_ids = [x.id for x in self.request.user.groups.all()]
queryset = queryset.filter(Q(view_role_id__in=group_ids) | Q(edit_role_id__in=group_ids))
return queryset.order_by('category__label', 'label')
def has_access_to_unavailability_calendars(self):
if self.request.user.is_staff:
return True
group_ids = [x.id for x in self.request.user.groups.all()]
queryset = UnavailabilityCalendar.objects.filter(
Q(view_role_id__in=group_ids) | Q(edit_role_id__in=group_ids)
)
return queryset.exists()
def has_access(self):
return self.request.user.is_staff or self.get_queryset().exists()
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['has_access_to_unavailability_calendars'] = self.has_access_to_unavailability_calendars()
return context
def get(self, request, *args, **kwargs):
if not self.has_access():
if self.has_access_to_unavailability_calendars():
return HttpResponseRedirect(reverse('chrono-manager-unavailability-calendar-list'))
self.template_name = 'chrono/manager_no_access.html'
return super().get(request, *args, **kwargs)
def render_to_response(self, context, **response_kwargs):
if self.template_name == 'chrono/manager_no_access.html':
response_kwargs['status'] = 403
return super().render_to_response(context, **response_kwargs)
homepage = HomepageView.as_view()
class AgendasExportView(FormView):
form_class = AgendasExportForm
template_name = 'chrono/agendas_export.html'
def dispatch(self, request, *args, **kwargs):
if not request.user.is_staff:
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
def form_valid(self, form):
response = HttpResponse(content_type='application/json')
today = datetime.date.today()
response['Content-Disposition'] = 'attachment; filename="export_agendas_{}.json"'.format(
today.strftime('%Y%m%d')
)
json.dump(export_site(**form.cleaned_data), response, indent=2)
return response
agendas_export = AgendasExportView.as_view()
class ResourceListView(ListView):
template_name = 'chrono/manager_resource_list.html'
model = Resource
def dispatch(self, request, *args, **kwargs):
if not request.user.is_staff:
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
resource_list = ResourceListView.as_view()
class ResourceDetailView(DetailView):
template_name = 'chrono/manager_resource_detail.html'
model = Resource
def dispatch(self, request, *args, **kwargs):
resource = self.get_object()
if not resource.can_be_viewed(request.user):
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['resource'] = self.object
return context
resource_view = ResourceDetailView.as_view()
class DateMixin:
def get_days(self):
return [str(x) for x in range(1, 32)]
def get_months(self):
return [(str(x), MONTHS[x]) for x in range(1, 13)]
def get_years(self):
year = now().year
return [str(x) for x in range(year - 1, year + 5)]
class ResourceDayView(DateMixin, DayArchiveView):
template_name = 'chrono/manager_resource_day_view.html'
model = Event
month_format = '%m'
date_field = 'start_datetime'
allow_empty = True
allow_future = True
def dispatch(self, request, *args, **kwargs):
self.resource = get_object_or_404(Resource, pk=kwargs['pk'])
if not self.resource.can_be_viewed(request.user):
raise PermissionDenied()
# specify 6am time to get the expected timezone on daylight saving time
# days.
try:
self.date = make_aware(
datetime.datetime.strptime(
'%s-%s-%s 06:00' % (self.get_year(), self.get_month(), self.get_day()), '%Y-%m-%d %H:%M'
)
)
except ValueError: # day is out of range for month
# redirect to last day of month
date = datetime.date(int(self.get_year()), int(self.get_month()), 1)
date += datetime.timedelta(days=40)
date = date.replace(day=1)
date -= datetime.timedelta(days=1)
return HttpResponseRedirect(
reverse(
'chrono-manager-resource-day-view',
kwargs={'pk': self.resource.pk, 'year': date.year, 'month': date.month, 'day': date.day},
)
)
return super().dispatch(request, *args, **kwargs)
def get_queryset(self):
queryset = (
self.resource.event_set.all().select_related('meeting_type').prefetch_related('booking_set')
)
return queryset
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['resource'] = self.resource
context['hour_span'] = 1
durations = MeetingType.objects.filter(agenda__resources=self.resource).values_list(
'duration', flat=True
)
if durations:
gcd = durations[0]
for duration in durations[1:]:
gcd = math.gcd(duration, gcd)
context['hour_span'] = max(60 // gcd, 1)
return context
def get_previous_day_url(self):
previous_day = self.date.date() - datetime.timedelta(days=1)
return reverse(
'chrono-manager-resource-day-view',
kwargs={
'pk': self.resource.pk,
'year': previous_day.year,
'month': previous_day.month,
'day': previous_day.day,
},
)
def get_next_day_url(self):
next_day = self.date.date() + datetime.timedelta(days=1)
return reverse(
'chrono-manager-resource-day-view',
kwargs={
'pk': self.resource.pk,
'year': next_day.year,
'month': next_day.month,
'day': next_day.day,
},
)
def get_timetable_infos(self):
interval = datetime.timedelta(minutes=60)
min_event = max_event = None
timeperiods = TimePeriod.objects.filter(desk__agenda__resources=self.resource).aggregate(
Min('start_time'), Max('end_time')
)
min_timeperiod = timeperiods['start_time__min']
max_timeperiod = timeperiods['end_time__max']
active_events = [
x for x in self.object_list if any([y.cancellation_datetime is None for y in x.booking_set.all()])
]
if active_events:
min_event = min(localtime(x.start_datetime).time() for x in active_events)
max_event = max(localtime(x.start_datetime + interval).time() for x in active_events)
if min_timeperiod is None and min_event is None:
return
min_display = min(min_timeperiod or datetime.time(23), min_event or datetime.time(23))
max_display = max(max_timeperiod or datetime.time(0), max_event or datetime.time(0))
current_date = self.date.replace(hour=min_display.hour, minute=0)
max_date = self.date.replace(hour=max_display.hour, minute=0)
if max_display.minute != 0:
# until the end of the last hour.
max_date += datetime.timedelta(hours=1)
while current_date < max_date:
info = {}
info['bookings'] = bookings = [] # bookings for this resource
finish_datetime = current_date + interval
for event in [
x
for x in self.object_list
if x.start_datetime >= current_date and x.start_datetime < finish_datetime
]:
# don't consider cancelled bookings
for booking in [x for x in event.booking_set.all() if not x.cancellation_datetime]:
booking.css_top = int(100 * event.start_datetime.minute / 60)
booking.css_height = int(100 * event.meeting_type.duration / 60)
bookings.append(booking)
yield current_date, info
current_date += interval
resource_day_view = ResourceDayView.as_view()
class ResourceMonthView(DateMixin, MonthArchiveView):
template_name = 'chrono/manager_resource_month_view.html'
model = Event
month_format = '%m'
date_field = 'start_datetime'
allow_empty = True
allow_future = True
def dispatch(self, request, *args, **kwargs):
self.resource = get_object_or_404(Resource, pk=kwargs['pk'])
if not self.resource.can_be_viewed(request.user):
raise PermissionDenied()
self.date = make_aware(
datetime.datetime.strptime(
'%s-%s-%s 06:00' % (self.get_year(), self.get_month(), 1), '%Y-%m-%d %H:%M'
)
)
return super().dispatch(request, *args, **kwargs)
def get_queryset(self):
queryset = (
self.resource.event_set.all().select_related('meeting_type').prefetch_related('booking_set')
)
return queryset
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['resource'] = self.resource
return context
def get_previous_month_url(self):
previous_month = self.get_previous_month(self.date.date())
return reverse(
'chrono-manager-resource-month-view',
kwargs={'pk': self.resource.pk, 'year': previous_month.year, 'month': previous_month.month},
)
def get_next_month_url(self):
next_month = self.get_next_month(self.date.date())
return reverse(
'chrono-manager-resource-month-view',
kwargs={'pk': self.resource.pk, 'year': next_month.year, 'month': next_month.month},
)
def get_timetable_infos(self):
interval = datetime.timedelta(minutes=60)
min_event = max_event = None
timeperiods = TimePeriod.objects.filter(desk__agenda__resources=self.resource).aggregate(
Min('start_time'), Max('end_time')
)
min_timeperiod = timeperiods['start_time__min']
max_timeperiod = timeperiods['end_time__max']
hide_sunday_timeperiod = hide_weekend_timeperiod = hide_sunday_event = hide_weekend_event = True
weekdays = TimePeriod.objects.filter(desk__agenda__resources=self.resource).values_list(
'weekday', flat=True
)
if weekdays:
hide_sunday_timeperiod = 6 not in weekdays
hide_weekend_timeperiod = hide_sunday_timeperiod and 5 not in weekdays
active_events = [
x for x in self.object_list if any([y.cancellation_datetime is None for y in x.booking_set.all()])
]
if active_events:
min_event = min(localtime(x.start_datetime).time() for x in active_events)
max_event = max(localtime(x.start_datetime + interval).time() for x in active_events)
hide_sunday_event = not any([x.start_datetime.weekday() == 6 for x in active_events])
hide_weekend_event = hide_sunday_event and not any(
[x.start_datetime.weekday() == 5 for x in active_events]
)
if min_timeperiod is None and min_event is None:
return
self.min_display = min(min_timeperiod or datetime.time(23), min_event or datetime.time(23))
self.max_display = max(max_timeperiod or datetime.time(0), max_event or datetime.time(0))
hide_sunday = hide_sunday_timeperiod and hide_sunday_event
hide_weekend = hide_weekend_timeperiod and hide_weekend_event
# avoid displaying empty first week
first_week_offset = int(
(hide_sunday and self.date.weekday() == 6) or (hide_weekend and self.date.weekday() == 5)
)
first_week_number = self.date.isocalendar()[1]
if first_week_number >= 52:
first_week_number = 0
last_month_day = self.get_next_month(self.date.date()) - datetime.timedelta(days=1)
last_week_number = last_month_day.isocalendar()[1]
if last_week_number < first_week_number: # new year
last_week_number = 53
for week_number in range(first_week_number + first_week_offset, last_week_number + 1):
yield self.get_week_timetable_infos(
week_number - first_week_number,
week_end_offset=int(hide_sunday) + int(hide_weekend),
)
def get_week_timetable_infos(self, week_index, week_end_offset=0):
date = self.date + datetime.timedelta(week_index * 7)
dow = date.isocalendar()[2]
start_date = date - datetime.timedelta(dow)
interval = datetime.timedelta(minutes=60)
period = self.date.replace(hour=self.min_display.hour, minute=0)
max_date = self.date.replace(hour=self.max_display.hour, minute=0)
if self.max_display.minute != 0:
# until the end of the last hour.
max_date += datetime.timedelta(hours=1)
periods = []
while period < max_date:
periods.append(period)
period = period + interval
return {
'days': [
self.get_day_timetable_infos(start_date + datetime.timedelta(i), interval)
for i in range(1, 8 - week_end_offset)
],
'periods': periods,
}
def get_day_timetable_infos(self, day, interval):
day = make_aware(make_naive(day)) # give day correct timezone
period = current_date = day.replace(hour=self.min_display.hour, minute=0)
timetable = {
'date': current_date,
'today': day.date() == datetime.date.today(),
'other_month': day.month != self.date.month,
'infos': {'booked_slots': []},
}
max_date = day.replace(hour=self.max_display.hour, minute=0)
if self.max_display.minute != 0:
# until the end of the last hour.
max_date += datetime.timedelta(hours=1)
# compute booking and opening hours only for current month
if self.date.month != day.month:
return timetable
while period <= max_date:
period_end = period + interval
for event in [
x for x in self.object_list if x.start_datetime >= period and x.start_datetime < period_end
]:
# don't consider cancelled bookings
bookings = [x for x in event.booking_set.all() if not x.cancellation_datetime]
if not bookings:
continue
booking = {
'css_top': 100 * (event.start_datetime - current_date).seconds // 3600,
'css_height': 100 * event.meeting_type.duration // 60,
'booking': bookings[0],
}
timetable['infos']['booked_slots'].append(booking)
period += interval
return timetable
resource_monthly_view = ResourceMonthView.as_view()
class ResourceAddView(CreateView):
template_name = 'chrono/manager_resource_form.html'
model = Resource
fields = ['label', 'description']
def dispatch(self, request, *args, **kwargs):
if not request.user.is_staff:
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
def get_success_url(self):
return reverse('chrono-manager-resource-view', kwargs={'pk': self.object.id})
resource_add = ResourceAddView.as_view()
class ResourceEditView(UpdateView):
template_name = 'chrono/manager_resource_form.html'
model = Resource
fields = ['label', 'slug', 'description']
def dispatch(self, request, *args, **kwargs):
if not request.user.is_staff:
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
def get_success_url(self):
return reverse('chrono-manager-resource-view', kwargs={'pk': self.object.id})
resource_edit = ResourceEditView.as_view()
class ResourceDeleteView(DeleteView):
template_name = 'chrono/manager_confirm_delete.html'
model = Resource
def dispatch(self, request, *args, **kwargs):
if not request.user.is_staff:
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
def get_success_url(self):
return reverse('chrono-manager-resource-list')
resource_delete = ResourceDeleteView.as_view()
class CategoryListView(ListView):
template_name = 'chrono/manager_category_list.html'
model = Category
def dispatch(self, request, *args, **kwargs):
if not request.user.is_staff:
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
category_list = CategoryListView.as_view()
class CategoryAddView(CreateView):
template_name = 'chrono/manager_category_form.html'
model = Category
fields = ['label']
def dispatch(self, request, *args, **kwargs):
if not request.user.is_staff:
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
def get_success_url(self):
return reverse('chrono-manager-category-list')
category_add = CategoryAddView.as_view()
class CategoryEditView(UpdateView):
template_name = 'chrono/manager_category_form.html'
model = Category
fields = ['label', 'slug']
def dispatch(self, request, *args, **kwargs):
if not request.user.is_staff:
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
def get_success_url(self):
return reverse('chrono-manager-category-list')
category_edit = CategoryEditView.as_view()
class CategoryDeleteView(DeleteView):
template_name = 'chrono/manager_confirm_delete.html'
model = Category
def dispatch(self, request, *args, **kwargs):
if not request.user.is_staff:
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
def get_success_url(self):
return reverse('chrono-manager-category-list')
category_delete = CategoryDeleteView.as_view()
class AbsenceReasonListView(ListView):
template_name = 'chrono/manager_absence_reason_list.html'
model = AbsenceReasonGroup
def dispatch(self, request, *args, **kwargs):
if not request.user.is_staff:
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
def get_queryset(self):
return AbsenceReasonGroup.objects.prefetch_related('absence_reasons')
absence_reason_list = AbsenceReasonListView.as_view()
class AbsenceReasonGroupAddView(CreateView):
template_name = 'chrono/manager_absence_reason_group_form.html'
model = AbsenceReasonGroup
fields = ['label']
def dispatch(self, request, *args, **kwargs):
if not request.user.is_staff:
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
def get_success_url(self):
return reverse('chrono-manager-absence-reason-list')
absence_reason_group_add = AbsenceReasonGroupAddView.as_view()
class AbsenceReasonGroupEditView(UpdateView):
template_name = 'chrono/manager_absence_reason_group_form.html'
model = AbsenceReasonGroup
fields = ['label', 'slug']
def dispatch(self, request, *args, **kwargs):
if not request.user.is_staff:
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
def get_success_url(self):
return reverse('chrono-manager-absence-reason-list')
absence_reason_group_edit = AbsenceReasonGroupEditView.as_view()
class AbsenceReasonGroupDeleteView(DeleteView):
template_name = 'chrono/manager_confirm_delete.html'
model = AbsenceReasonGroup
def dispatch(self, request, *args, **kwargs):
if not request.user.is_staff:
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
def get_success_url(self):
return reverse('chrono-manager-absence-reason-list')
absence_reason_group_delete = AbsenceReasonGroupDeleteView.as_view()
class AbsenceReasonGroupExport(DetailView):
model = AbsenceReasonGroup
def dispatch(self, request, *args, **kwargs):
if not request.user.is_staff:
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
def get(self, request, *args, **kwargs):
response = HttpResponse(content_type='application/json')
today = datetime.date.today()
attachment = 'attachment; filename="export_absence_reason_group_{}_{}.json"'.format(
self.get_object().slug, today.strftime('%Y%m%d')
)
response['Content-Disposition'] = attachment
json.dump({'absence_reason_groups': [self.get_object().export_json()]}, response, indent=2)
return response
absence_reason_group_export = AbsenceReasonGroupExport.as_view()
class AbsenceReasonAddView(CreateView):
template_name = 'chrono/manager_absence_reason_form.html'
model = AbsenceReason
fields = ['label']
def dispatch(self, request, *args, **kwargs):
self.group_pk = kwargs.pop('group_pk')
if not request.user.is_staff:
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
if not kwargs.get('instance'):
kwargs['instance'] = self.model()
kwargs['instance'].group_id = self.group_pk
return kwargs
def get_success_url(self):
return reverse('chrono-manager-absence-reason-list')
absence_reason_add = AbsenceReasonAddView.as_view()
class AbsenceReasonEditView(UpdateView):
template_name = 'chrono/manager_absence_reason_form.html'
model = AbsenceReason
form_class = AbsenceReasonForm
def dispatch(self, request, *args, **kwargs):
self.group_pk = kwargs.pop('group_pk')
if not request.user.is_staff:
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
def get_queryset(self):
return AbsenceReason.objects.filter(group=self.group_pk)
def get_success_url(self):
return reverse('chrono-manager-absence-reason-list')
absence_reason_edit = AbsenceReasonEditView.as_view()
class AbsenceReasonDeleteView(DeleteView):
template_name = 'chrono/manager_confirm_delete.html'
model = AbsenceReason
def dispatch(self, request, *args, **kwargs):
self.group_pk = kwargs.pop('group_pk')
if not request.user.is_staff:
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
def get_queryset(self):
return AbsenceReason.objects.filter(group=self.group_pk)
def get_success_url(self):
return reverse('chrono-manager-absence-reason-list')
absence_reason_delete = AbsenceReasonDeleteView.as_view()
class AgendaAddView(CreateView):
template_name = 'chrono/manager_agenda_add_form.html'
model = Agenda
form_class = AgendaAddForm
def dispatch(self, request, *args, **kwargs):
if not request.user.is_staff:
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
def get_success_url(self):
return reverse('chrono-manager-agenda-settings', kwargs={'pk': self.object.id})
def form_valid(self, form):
result = super().form_valid(form)
if self.object.kind == 'events':
desk = Desk.objects.create(agenda=self.object, slug='_exceptions_holder')
desk.import_timeperiod_exceptions_from_settings()
return result
agenda_add = AgendaAddView.as_view()
class AgendasImportView(FormView):
form_class = AgendasImportForm
template_name = 'chrono/agendas_import.html'
success_url = reverse_lazy('chrono-manager-homepage')
def dispatch(self, request, *args, **kwargs):
if not request.user.is_staff:
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
def form_valid(self, form):
try:
agendas_json = json.loads(force_text(self.request.FILES['agendas_json'].read()))
except ValueError:
form.add_error('agendas_json', _('File is not in the expected JSON format.'))
return self.form_invalid(form)
try:
results = import_site(agendas_json, overwrite=False)
except AgendaImportError as exc:
form.add_error('agendas_json', '%s' % exc)
return self.form_invalid(form)
except KeyError as exc:
form.add_error('agendas_json', _('Key "%s" is missing.') % exc.args[0])
return self.form_invalid(form)
import_messages = {
'agendas': {
'create_noop': _('No agenda created.'),
'create': lambda x: ungettext(
'An agenda has been created.',
'%(count)d agendas have been created.',
x,
),
'update_noop': _('No agenda updated.'),
'update': lambda x: ungettext(
'An agenda has been updated.',
'%(count)d agendas have been updated.',
x,
),
},
'unavailability_calendars': {
'create_noop': _('No unavailability calendar created.'),
'create': lambda x: ungettext(
'An unavailability calendar has been created.',
'%(count)d unavailability calendars have been created.',
x,
),
'update_noop': _('No unavailability calendar updated.'),
'update': lambda x: ungettext(
'An unavailability calendar has been updated.',
'%(count)d unavailability calendars have been updated.',
x,
),
},
'absence_reason_groups': {
'create_noop': _('No absence reason group created.'),
'create': lambda x: ungettext(
'An absence reason group has been created.',
'%(count)d absence reason groups have been created.',
x,
),
'update_noop': _('No absence reason group updated.'),
'update': lambda x: ungettext(
'An absence reason group has been updated.',
'%(count)d absence reason groups have been updated.',
x,
),
},
}
global_noop = True
for obj_name, obj_results in results.items():
if obj_results['all']:
global_noop = False
count = len(obj_results['created'])
if not count:
message1 = import_messages[obj_name]['create_noop']
else:
message1 = import_messages[obj_name]['create'](count) % {'count': count}
count = len(obj_results['updated'])
if not count:
message2 = import_messages[obj_name]['update_noop']
else:
message2 = import_messages[obj_name]['update'](count) % {'count': count}
obj_results['messages'] = "%s %s" % (message1, message2)
a_count, uc_count, arg_count = (
len(results['agendas']['all']),
len(results['unavailability_calendars']['all']),
len(results['absence_reason_groups']['all']),
)
if (a_count, uc_count, arg_count) == (1, 0, 0):
# only one agenda imported, redirect to settings page
return HttpResponseRedirect(
reverse('chrono-manager-agenda-settings', kwargs={'pk': results['agendas']['all'][0].pk})
)
if (a_count, uc_count, arg_count) == (0, 1, 0):
# only one unavailability calendar imported, redirect to settings page
return HttpResponseRedirect(
reverse(
'chrono-manager-unavailability-calendar-settings',
kwargs={'pk': results['unavailability_calendars']['all'][0].pk},
)
)
if (a_count, uc_count, arg_count) == (0, 0, 1):
# only one absence reason group imported, redirect to group page
return HttpResponseRedirect(reverse('chrono-manager-absence-reason-list'))
if global_noop:
messages.info(self.request, _('No data found.'))
else:
messages.info(self.request, results['agendas']['messages'])
messages.info(self.request, results['unavailability_calendars']['messages'])
messages.info(self.request, results['absence_reason_groups']['messages'])
return super().form_valid(form)
agendas_import = AgendasImportView.as_view()
class ViewableAgendaMixin:
agenda = None
def set_agenda(self, **kwargs):
self.agenda = get_object_or_404(Agenda, id=kwargs.get('pk'))
def dispatch(self, request, *args, **kwargs):
self.set_agenda(**kwargs)
if not self.check_permissions(request.user):
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
def check_permissions(self, user):
return self.agenda.can_be_viewed(user)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['agenda'] = self.agenda
return context
class ManagedAgendaMixin(ViewableAgendaMixin):
def check_permissions(self, user):
return self.agenda.can_be_managed(user)
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
if not kwargs.get('instance'):
kwargs['instance'] = self.model()
kwargs['instance'].agenda = self.agenda
return kwargs
def get_success_url(self):
return reverse('chrono-manager-agenda-settings', kwargs={'pk': self.agenda.id})
class AgendaEditView(ManagedAgendaMixin, UpdateView):
template_name = 'chrono/manager_agenda_form.html'
title = _('Edit Agenda')
model = Agenda
form_class = AgendaEditForm
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['title'] = self.title
return context
agenda_edit = AgendaEditView.as_view()
class AgendaBookingDelaysView(AgendaEditView):
form_class = AgendaBookingDelaysForm
title = _('Configure booking delays')
agenda_booking_delays = AgendaBookingDelaysView.as_view()
class AgendaRolesView(AgendaEditView):
form_class = AgendaRolesForm
title = _('Configure roles')
agenda_roles = AgendaRolesView.as_view()
class AgendaBookingCheckSettingsView(AgendaEditView):
form_class = AgendaBookingCheckSettingsForm
title = _("Configure booking check options")
def set_agenda(self, **kwargs):
self.agenda = get_object_or_404(Agenda, pk=kwargs.get('pk'), kind='events')
def get_initial(self):
return {'booking_user_block_template': self.agenda.get_booking_user_block_template()}
agenda_booking_check_settings = AgendaBookingCheckSettingsView.as_view()
class AgendaDeleteView(DeleteView):
template_name = 'chrono/manager_confirm_delete.html'
model = Agenda
success_url = reverse_lazy('chrono-manager-homepage')
def dispatch(self, request, *args, **kwargs):
if not request.user.is_staff:
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['cannot_delete'] = Booking.objects.filter(
event__agenda=self.get_object(),
event__start_datetime__gt=now(),
cancellation_datetime__isnull=True,
).exists()
context['cannot_delete_msg'] = FUTURE_BOOKING_ERROR_MSG
return context
def delete(self, request, *args, **kwargs):
self.object = self.get_object()
context = self.get_context_data()
if context['cannot_delete']:
raise PermissionDenied()
return super().delete(request, *args, **kwargs)
agenda_delete = AgendaDeleteView.as_view()
class AgendaView(ViewableAgendaMixin, View):
def get(self, request, *args, **kwargs):
if self.agenda.default_view == 'day':
return redirect('chrono-manager-agenda-day-redirect-view', pk=self.agenda.pk)
if self.agenda.default_view == 'month':
return redirect('chrono-manager-agenda-month-redirect-view', pk=self.agenda.pk)
return redirect('chrono-manager-agenda-open-events-view', pk=self.agenda.pk)
agenda_view = AgendaView.as_view()
class AgendaMonthRedirectView(ViewableAgendaMixin, View):
def get_day(self):
today = datetime.date.today()
if self.agenda.kind != 'events':
return today
# first day where there are events,
# otherwise latest day with events, otherwise today.
event = self.agenda.event_set.filter(start_datetime__gte=today).first()
if not event:
event = self.agenda.event_set.filter(start_datetime__lte=today).last()
if event:
return localtime(event.start_datetime)
return today
def get(self, request, *args, **kwargs):
day = self.get_day()
return redirect('chrono-manager-agenda-month-view', pk=self.agenda.pk, year=day.year, month=day.month)
agenda_month_redirect_view = AgendaMonthRedirectView.as_view()
class AgendaDayRedirectView(AgendaMonthRedirectView):
def get(self, request, *args, **kwargs):
day = self.get_day()
return redirect(
'chrono-manager-agenda-day-view', pk=self.agenda.pk, year=day.year, month=day.month, day=day.day
)
agenda_day_redirect_view = AgendaDayRedirectView.as_view()
class AgendaDateView(DateMixin, ViewableAgendaMixin):
model = Event
month_format = '%m'
date_field = 'start_datetime'
allow_empty = True
allow_future = True
def dispatch(self, request, *args, **kwargs):
# specify 6am time to get the expected timezone on daylight saving time
# days.
try:
self.date = make_aware(
datetime.datetime.strptime(
'%s-%s-%s 06:00' % (self.get_year(), self.get_month(), self.get_day()), '%Y-%m-%d %H:%M'
)
)
except ValueError: # day is out of range for month
# redirect to last day of month
date = datetime.date(int(self.get_year()), int(self.get_month()), 1)
date += datetime.timedelta(days=40)
date = date.replace(day=1)
date -= datetime.timedelta(days=1)
return HttpResponseRedirect(
reverse(
'chrono-manager-agenda-day-view',
kwargs={'pk': kwargs['pk'], 'year': date.year, 'month': date.month, 'day': date.day},
)
)
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['agenda'] = self.agenda
if self.agenda.kind != 'events':
try:
context['hour_span'] = max(60 // self.agenda.get_base_meeting_duration(), 1)
except ValueError: # no meeting types defined
context['hour_span'] = 1
context['booking_colors'] = BookingColor.objects.filter(
bookings__event__in=self.object_list
).distinct()
context['user_can_manage'] = self.agenda.can_be_managed(self.request.user)
return context
def get_queryset(self):
if self.agenda.kind == 'events':
queryset = self.agenda.event_set.filter(recurrence_days__isnull=True)
else:
self.agenda.prefetch_desks_and_exceptions()
if self.agenda.kind == 'meetings':
queryset = self.agenda.event_set.select_related('meeting_type').prefetch_related(
'booking_set'
)
else:
queryset = (
Event.objects.filter(agenda__virtual_agendas=self.agenda)
.select_related('meeting_type')
.prefetch_related('booking_set')
)
return queryset
class AgendaDayView(AgendaDateView, DayArchiveView):
def get_queryset(self):
qs = super().get_queryset()
if self.agenda.kind != 'events':
return qs
return qs.order_by('start_datetime', 'label')
def get_dated_items(self):
date_list, object_list, extra_context = super().get_dated_items()
if self.agenda.kind == 'events':
min_start = make_aware(datetime.datetime.combine(self.date.date(), datetime.time(0, 0)))
max_start = min_start + datetime.timedelta(days=1)
object_list = self.agenda.add_event_recurrences(
object_list, min_start, max_start, include_cancelled=True
)
return date_list, object_list, extra_context
def get_template_names(self):
if self.agenda.kind == 'virtual':
return ['chrono/manager_meetings_agenda_day_view.html']
return ['chrono/manager_%s_agenda_day_view.html' % self.agenda.kind]
def get_previous_day_url(self):
previous_day = self.date.date() - datetime.timedelta(days=1)
return reverse(
'chrono-manager-agenda-day-view',
kwargs={
'pk': self.agenda.id,
'year': previous_day.year,
'month': previous_day.month,
'day': previous_day.day,
},
)
def get_next_day_url(self):
next_day = self.date.date() + datetime.timedelta(days=1)
return reverse(
'chrono-manager-agenda-day-view',
kwargs={
'pk': self.agenda.id,
'year': next_day.year,
'month': next_day.month,
'day': next_day.day,
},
)
def get_timetable_infos(self):
timeperiods = itertools.chain(*(d.timeperiod_set.all() for d in self.agenda.prefetched_desks))
timeperiods = [t for t in timeperiods if t.weekday == self.date.weekday()]
timeperiods = sorted(timeperiods, key=lambda t: t.start_time)
interval = datetime.timedelta(minutes=60)
min_timeperiod = max_timeperiod = min_event = max_event = None
if timeperiods:
min_timeperiod = min(x.start_time for x in timeperiods)
max_timeperiod = max(x.end_time for x in timeperiods)
active_events = [
x for x in self.object_list if any([y.cancellation_datetime is None for y in x.booking_set.all()])
]
if active_events:
min_event = min(localtime(x.start_datetime).time() for x in active_events)
max_event = max(localtime(x.start_datetime + interval).time() for x in active_events)
if min_timeperiod is None and min_event is None:
return
min_display = min(min_timeperiod or datetime.time(23), min_event or datetime.time(23))
max_display = max(max_timeperiod or datetime.time(0), max_event or datetime.time(0))
current_date = self.date.replace(hour=min_display.hour, minute=0)
start_date = current_date
max_date = self.date.replace(hour=max_display.hour, minute=0)
if max_display.minute != 0:
# until the end of the last hour.
max_date += datetime.timedelta(hours=1)
first = True
while current_date < max_date:
# for each timeslot return the timeslot date and a list of per-desk
# bookings
infos = [] # various infos, for each desk
for desk in self.agenda.prefetched_desks:
info = {'desk': desk}
if first:
# use first row to include opening hours
info['opening_hours'] = opening_hours = []
for opening_hour in desk.get_opening_hours(current_date.date()):
opening_hours.append(
{
'css_top': 100 * (opening_hour.begin - start_date).seconds // 3600,
'css_height': 100 * (opening_hour.end - opening_hour.begin).seconds // 3600,
}
)
# and exceptions for this desk
info['exceptions'] = []
for exception in desk.prefetched_exceptions:
if exception.end_datetime < current_date:
continue
if exception.start_datetime > max_date:
continue
start_max = max(start_date, exception.start_datetime)
end_min = min(max_date, exception.end_datetime)
exception.css_top = int(100 * (start_max - start_date).seconds // 3600)
exception.css_height = int(100 * (end_min - start_max).seconds // 3600)
info['exceptions'].append(exception)
infos.append(info)
info['bookings'] = bookings = [] # bookings for this desk
finish_datetime = current_date + interval
for event in [
x
for x in self.object_list
if x.desk_id == desk.id
and x.start_datetime >= current_date
and x.start_datetime < finish_datetime
]:
# don't consider cancelled bookings
for booking in [x for x in event.booking_set.all() if not x.cancellation_datetime]:
booking.css_top = int(100 * event.start_datetime.minute / 60)
booking.css_height = int(100 * event.meeting_type.duration / 60)
bookings.append(booking)
yield current_date, infos
current_date += interval
first = False
agenda_day_view = AgendaDayView.as_view()
class AgendaMonthView(AgendaDateView, MonthArchiveView):
def get_queryset(self):
qs = super().get_queryset()
if self.agenda.kind != 'events':
return qs
return qs.order_by('start_datetime', 'label')
def get_dated_items(self):
date_list, object_list, extra_context = super().get_dated_items()
if self.agenda.kind == 'events':
min_start = make_aware(datetime.datetime.combine(extra_context['month'], datetime.time(0, 0)))
max_start = make_aware(
datetime.datetime.combine(extra_context['next_month'], datetime.time(0, 0))
)
object_list = self.agenda.add_event_recurrences(
object_list, min_start, max_start, include_cancelled=True
)
exceptions = TimePeriodException.objects.filter(
desk__agenda=self.agenda, start_datetime__gte=min_start, end_datetime__lt=max_start
).annotate(is_exception=Value(True, BooleanField()))
object_list = sorted(itertools.chain(object_list, exceptions), key=lambda x: x.start_datetime)
return date_list, object_list, extra_context
def get_template_names(self):
if self.agenda.kind == 'virtual':
return ['chrono/manager_meetings_agenda_month_view.html']
return ['chrono/manager_%s_agenda_month_view.html' % self.agenda.kind]
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
if self.agenda.kind == 'events':
context['cancellation_reports'] = EventCancellationReport.objects.filter(
event__agenda=self.agenda,
seen=False,
).all()
else:
context['single_desk'] = bool(len(self.agenda.prefetched_desks) == 1)
return context
def get_previous_month_url(self):
previous_month = self.get_previous_month(self.date.date())
return reverse(
'chrono-manager-agenda-month-view',
kwargs={'pk': self.agenda.id, 'year': previous_month.year, 'month': previous_month.month},
)
def get_next_month_url(self):
next_month = self.get_next_month(self.date.date())
return reverse(
'chrono-manager-agenda-month-view',
kwargs={'pk': self.agenda.id, 'year': next_month.year, 'month': next_month.month},
)
def get_day(self):
return '1'
def get_timetable_infos(self):
timeperiods = itertools.chain(*(d.timeperiod_set.all() for d in self.agenda.prefetched_desks))
timeperiods = sorted(timeperiods, key=lambda t: (t.weekday, t.start_time))
interval = datetime.timedelta(minutes=60)
min_timeperiod = max_timeperiod = min_event = max_event = None
hide_sunday_timeperiod = hide_weekend_timeperiod = hide_sunday_event = hide_weekend_event = True
if timeperiods:
min_timeperiod = min(x.start_time for x in timeperiods)
max_timeperiod = max(x.end_time for x in timeperiods)
hide_sunday_timeperiod = not any([e.weekday == 6 for e in timeperiods])
hide_weekend_timeperiod = hide_sunday_timeperiod and not any(
[e.weekday == 5 for e in timeperiods]
)
active_events = [
x for x in self.object_list if any([y.cancellation_datetime is None for y in x.booking_set.all()])
]
if active_events:
min_event = min(localtime(x.start_datetime).time() for x in active_events)
max_event = max(localtime(x.start_datetime + interval).time() for x in active_events)
hide_sunday_event = not any([x.start_datetime.weekday() == 6 for x in active_events])
hide_weekend_event = hide_sunday_event and not any(
[x.start_datetime.weekday() == 5 for x in active_events]
)
if min_timeperiod is None and min_event is None:
return
self.min_display = min(min_timeperiod or datetime.time(23), min_event or datetime.time(23))
self.max_display = max(max_timeperiod or datetime.time(0), max_event or datetime.time(0))
hide_sunday = hide_sunday_timeperiod and hide_sunday_event
hide_weekend = hide_weekend_timeperiod and hide_weekend_event
# avoid displaying empty first week
first_week_offset = int(
(hide_sunday and self.date.weekday() == 6) or (hide_weekend and self.date.weekday() == 5)
)
first_week_number = self.date.isocalendar()[1]
if first_week_number >= 52:
first_week_number = 0
last_month_day = self.get_next_month(self.date.date()) - datetime.timedelta(days=1)
last_week_number = last_month_day.isocalendar()[1]
if last_week_number < first_week_number: # new year
last_week_number = 53
for week_number in range(first_week_number + first_week_offset, last_week_number + 1):
yield self.get_week_timetable_infos(
week_number - first_week_number,
week_end_offset=int(hide_sunday) + int(hide_weekend),
)
def get_week_timetable_infos(self, week_index, week_end_offset=0):
date = self.date + datetime.timedelta(week_index * 7)
dow = date.isocalendar()[2]
start_date = date - datetime.timedelta(dow)
interval = datetime.timedelta(minutes=60)
period = self.date.replace(hour=self.min_display.hour, minute=0)
max_date = self.date.replace(hour=self.max_display.hour, minute=0)
periods = []
while period < max_date:
periods.append(period)
period = period + interval
return {
'days': [
self.get_day_timetable_infos(start_date + datetime.timedelta(i), interval)
for i in range(1, 8 - week_end_offset)
],
'periods': periods,
}
def get_day_timetable_infos(self, day, interval):
day = make_aware(make_naive(day)) # give day correct timezone
period = current_date = day.replace(hour=self.min_display.hour, minute=0)
timetable = {
'date': current_date,
'today': day.date() == datetime.date.today(),
'other_month': day.month != self.date.month,
'infos': {'opening_hours': [], 'exceptions': [], 'booked_slots': []},
}
desks = self.agenda.prefetched_desks
desks_len = len(desks)
max_date = day.replace(hour=self.max_display.hour, minute=0)
# compute booking and opening hours only for current month
if self.date.month != day.month:
return timetable
while period <= max_date:
left = 1
period_end = period + interval
for desk in desks:
width = (98.0 / desks_len) - 1
for event in [
x
for x in self.object_list
if x.desk_id == desk.id and x.start_datetime >= period and x.start_datetime < period_end
]:
# don't consider cancelled bookings
bookings = [x for x in event.booking_set.all() if not x.cancellation_datetime]
if not bookings:
continue
booking = {
'css_top': 100 * (event.start_datetime - current_date).seconds // 3600,
'css_height': 100 * event.meeting_type.duration // 60,
'css_width': width,
'css_left': left,
'desk': desk,
'booking': bookings[0],
}
timetable['infos']['booked_slots'].append(booking)
# get desks opening hours on last period iteration
if period == max_date:
for opening_hour in desk.get_opening_hours(current_date):
timetable['infos']['opening_hours'].append(
{
'css_top': 100 * (opening_hour.begin - current_date).seconds // 3600,
'css_height': 100 * (opening_hour.end - opening_hour.begin).seconds // 3600,
'css_width': width,
'css_left': left,
}
)
for exception in desk.prefetched_exceptions:
if exception.end_datetime < current_date:
continue
if exception.start_datetime > max_date:
continue
exception = copy.copy(exception)
start_max = max(current_date, exception.start_datetime)
end_min = min(max_date, exception.end_datetime)
exception.css_top = int(100 * (start_max - current_date).seconds // 3600)
exception.css_height = int(100 * (end_min - start_max).seconds // 3600)
exception.css_width = width
exception.css_left = left
timetable['infos']['exceptions'].append(exception)
left += width + 1
period += interval
return timetable
agenda_monthly_view = AgendaMonthView.as_view()
class AgendaOpenEventsView(ViewableAgendaMixin, DetailView):
model = Agenda
template_name = 'chrono/manager_agenda_open_events.html'
def set_agenda(self, **kwargs):
self.agenda = get_object_or_404(Agenda, pk=kwargs.get('pk'), kind='events')
def get_object(self, **kwargs):
return self.agenda
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['user_can_manage'] = self.agenda.can_be_managed(self.request.user)
context['open_events'] = self.agenda.get_open_events()
return context
agenda_open_events_view = AgendaOpenEventsView.as_view()
class ManagedAgendaSubobjectMixin:
agenda = None
def dispatch(self, request, *args, **kwargs):
self.agenda = self.get_object().agenda
if not self.agenda.can_be_managed(request.user):
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['agenda'] = self.object.agenda
return context
def get_success_url(self):
return reverse('chrono-manager-agenda-settings', kwargs={'pk': self.agenda.id})
class ManagedDeskMixin:
desk = None
def dispatch(self, request, *args, **kwargs):
try:
self.desk = Desk.objects.get(id=kwargs.get('pk'))
except Desk.DoesNotExist:
raise Http404()
if not self.desk.agenda.can_be_managed(request.user):
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
if not kwargs.get('instance'):
kwargs['instance'] = self.model()
kwargs['instance'].desk = self.desk
return kwargs
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['desk'] = self.desk
context['agenda'] = self.desk.agenda
return context
def get_success_url(self):
return reverse('chrono-manager-agenda-settings', kwargs={'pk': self.desk.agenda.id})
class ManagedDeskSubobjectMixin:
desk = None
def dispatch(self, request, *args, **kwargs):
self.desk = self.get_object().desk
if not self.desk.agenda.can_be_managed(request.user):
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['desk'] = self.object.desk
context['agenda'] = self.object.desk.agenda
return context
def get_success_url(self):
return reverse('chrono-manager-agenda-settings', kwargs={'pk': self.desk.agenda.id})
class ManagedTimePeriodMixin:
agenda = None
def dispatch(self, request, *args, **kwargs):
self.time_period = self.get_object()
self.agenda = self.time_period.agenda
if self.time_period.desk:
self.agenda = self.time_period.desk.agenda
if not self.agenda.can_be_managed(request.user):
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['agenda'] = self.agenda
return context
def get_success_url(self):
return reverse('chrono-manager-agenda-settings', kwargs={'pk': self.agenda.id})
class ManagedTimePeriodExceptionMixin:
desk = None
unavailability_calendar = None
def dispatch(self, request, *args, **kwargs):
object_ = self.get_object()
if object_.desk:
self.desk = self.get_object().desk
if not self.desk.agenda.can_be_managed(request.user):
raise PermissionDenied()
elif object_.unavailability_calendar:
self.unavailability_calendar = object_.unavailability_calendar
if not self.unavailability_calendar.can_be_managed(request.user):
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
if self.desk:
context['desk'] = self.object.desk
context['agenda'] = self.object.desk.agenda
else:
context['unavailability_calendar'] = self.unavailability_calendar
return context
def get_success_url(self):
if self.desk:
return reverse('chrono-manager-agenda-settings', kwargs={'pk': self.desk.agenda.id})
elif self.unavailability_calendar:
return reverse(
'chrono-manager-unavailability-calendar-settings',
kwargs={'pk': self.unavailability_calendar.pk},
)
class AgendaSettings(ManagedAgendaMixin, DetailView):
model = Agenda
def set_agenda(self, **kwargs):
self.agenda = get_object_or_404(
Agenda.objects.select_related('edit_role', 'view_role', 'absence_reasons_group'),
pk=kwargs.get('pk'),
)
def get_object(self, *args, **kwargs):
if self.agenda.kind == 'meetings':
self.agenda.prefetch_desks_and_exceptions(with_sources=True)
return self.agenda
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
if self.agenda.accept_meetings():
context['meeting_types'] = self.object.iter_meetingtypes()
if self.agenda.kind == 'virtual':
context['virtual_members'] = [
(virtual_member, virtual_member.real_agenda.can_be_managed(self.request.user))
for virtual_member in self.object.get_virtual_members()
]
if self.agenda.kind == 'meetings':
context['has_resources'] = Resource.objects.exists()
context['has_unavailability_calendars'] = UnavailabilityCalendar.objects.exists()
context['agenda_is_available_for_simple_management'] = (
self.object.is_available_for_simple_management()
if not self.object.desk_simple_management
else False
)
if self.agenda.kind == 'events':
context['has_absence_reasons'] = AbsenceReasonGroup.objects.exists()
context['has_recurring_events'] = self.agenda.event_set.filter(
recurrence_days__isnull=False
).exists()
desk, created = Desk.objects.get_or_create(agenda=self.agenda, slug='_exceptions_holder')
if created:
desk.import_timeperiod_exceptions_from_settings()
context['exceptions'] = TimePeriodException.objects.filter(
Q(desk=desk) | Q(unavailability_calendar__desks=desk),
end_datetime__gt=now(),
)
context['desk'] = desk
return context
def get_events(self):
return self.agenda.event_set.filter(primary_event__isnull=True)
def get_template_names(self):
return ['chrono/manager_%s_agenda_settings.html' % self.agenda.kind]
agenda_settings = AgendaSettings.as_view()
class AgendaExport(ManagedAgendaMixin, DetailView):
model = Agenda
def get(self, request, *args, **kwargs):
response = HttpResponse(content_type='application/json')
today = datetime.date.today()
response['Content-Disposition'] = 'attachment; filename="export_agenda_{}_{}.json"'.format(
self.get_object().slug, today.strftime('%Y%m%d')
)
json.dump({'agendas': [self.get_object().export_json()]}, response, indent=2)
return response
agenda_export = AgendaExport.as_view()
class AgendaDuplicate(ManagedAgendaMixin, FormView):
form_class = AgendaDuplicateForm
template_name = 'chrono/manager_agenda_duplicate_form.html'
def get_success_url(self):
return reverse('chrono-manager-agenda-settings', kwargs={'pk': self.new_agenda.pk})
def get_form_kwargs(self):
return super(FormView, self).get_form_kwargs()
def form_valid(self, form):
self.new_agenda = self.agenda.duplicate(label=form.cleaned_data['label'])
return super().form_valid(form)
agenda_duplicate = AgendaDuplicate.as_view()
class AgendaAddEventView(ManagedAgendaMixin, CreateView):
template_name = 'chrono/manager_event_form.html'
model = Event
form_class = NewEventForm
agenda_add_event = AgendaAddEventView.as_view()
class AgendaImportEventsSampleView(TemplateView):
template_name = 'chrono/manager_sample_events.txt'
content_type = 'text/csv'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
some_future_date = datetime.datetime.now() + datetime.timedelta(days=10)
some_future_date = some_future_date.replace(hour=14, minute=0, second=0)
context['some_future_date'] = some_future_date
return context
agenda_import_events_sample_csv = AgendaImportEventsSampleView.as_view()
class AgendaImportEventsView(ManagedAgendaMixin, FormView):
form_class = ImportEventsForm
template_name = 'chrono/manager_import_events.html'
agenda = None
def set_agenda(self, **kwargs):
self.agenda = get_object_or_404(Agenda, pk=kwargs.get('pk'), kind='events')
def get_form_kwargs(self):
kwargs = super(FormView, self).get_form_kwargs()
kwargs['agenda_pk'] = self.kwargs['pk']
return kwargs
def form_valid(self, form):
if form.events:
# existing event slugs for this agenda
for event in form.events:
event.agenda = self.agenda
event.save()
messages.info(self.request, _('%d events have been imported.') % len(form.events))
for event in form.warnings.values():
messages.warning(
self.request,
_('Event "%s" start date has changed. Do not forget to notify the registrants.')
% (event.label or event.slug),
)
return super().form_valid(form)
agenda_import_events = AgendaImportEventsView.as_view()
class AgendaExportEventsView(ManagedAgendaMixin, View):
template_name = 'chrono/manager_export_events.html'
agenda = None
def set_agenda(self, **kwargs):
self.agenda = get_object_or_404(Agenda, pk=kwargs.get('pk'), kind='events')
def get(self, request, *args, **kwargs):
response = HttpResponse(content_type='text/csv')
today = datetime.date.today()
response['Content-Disposition'] = 'attachment; filename="export_agenda_events_{}_{}.csv"'.format(
self.agenda.slug, today.strftime('%Y%m%d')
)
writer = csv.writer(response)
# headers
writer.writerow(
[
_('date'),
_('time'),
_('number of places'),
_('number of places in waiting list'),
_('label'),
_('identifier'),
_('description'),
_('pricing'),
_('URL'),
_('publication date/time'),
_('duration'),
]
)
for event in self.agenda.event_set.all():
start_datetime = localtime(event.start_datetime)
publication_datetime = (
localtime(event.publication_datetime) if event.publication_datetime else None
)
writer.writerow(
[
start_datetime.strftime('%Y-%m-%d'),
start_datetime.strftime('%H:%M'),
event.places,
event.waiting_list_places,
event.label,
event.slug,
event.description,
event.pricing,
event.url,
publication_datetime.strftime('%Y-%m-%d %H:%M') if publication_datetime else '',
event.duration,
]
)
return response
agenda_export_events = AgendaExportEventsView.as_view()
class AgendaDeskManagementToggleView(ManagedAgendaMixin, View):
agenda = None
def set_agenda(self, **kwargs):
self.agenda = get_object_or_404(Agenda, pk=kwargs.get('pk'), kind='meetings')
def get(self, request, *args, **kwargs):
message = None
if self.agenda.desk_simple_management:
self.agenda.desk_simple_management = False
self.agenda.save()
message = _('Desk individual management enabled.')
elif self.agenda.is_available_for_simple_management():
self.agenda.desk_simple_management = True
self.agenda.save()
message = _('Desk global management enabled.')
if message:
messages.info(self.request, message)
return HttpResponseRedirect(reverse('chrono-manager-agenda-settings', kwargs={'pk': self.agenda.pk}))
agenda_desk_management_toggle_view = AgendaDeskManagementToggleView.as_view()
class AgendaNotificationsSettingsView(ManagedAgendaMixin, UpdateView):
template_name = 'chrono/manager_agenda_notifications_form.html'
model = AgendaNotificationsSettings
form_class = AgendaNotificationsForm
def get_object(self):
try:
return self.agenda.notifications_settings
except AgendaNotificationsSettings.DoesNotExist:
# prevent old events from sending notifications
statuses = ('almost_full', 'full', 'cancelled')
timestamp = now()
for status in statuses:
filter_kwargs = {status: True}
update_kwargs = {status + '_notification_timestamp': timestamp}
self.agenda.event_set.filter(**filter_kwargs).update(**update_kwargs)
return AgendaNotificationsSettings.objects.create(agenda=self.agenda)
agenda_notifications_settings = AgendaNotificationsSettingsView.as_view()
class AgendaReminderSettingsView(AgendaEditView):
title = _('Reminder Settings')
model = AgendaReminderSettings
form_class = AgendaReminderForm
def get_object(self):
try:
return self.agenda.reminder_settings
except AgendaReminderSettings.DoesNotExist:
return AgendaReminderSettings.objects.create(agenda=self.agenda)
agenda_reminder_settings = AgendaReminderSettingsView.as_view()
class AgendaReminderPreviewView(ManagedAgendaMixin, TemplateView):
template_name = 'chrono/manager_agenda_reminder_preview.html'
def dispatch(self, request, *args, **kwargs):
self.type_ = kwargs['type']
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
kind = self.agenda.kind
days = getattr(self.agenda.reminder_settings, 'days_before_%s' % self.type_)
paragraph = lorem_ipsum.paragraphs(1)[0][:232]
label = title(lorem_ipsum.words(2))
event = Event(
start_datetime=datetime.datetime(year=2020, month=6, day=2, hour=14, minute=30),
label=format_html('{} <small>({})</small>', label, _('event label')),
description=format_html('{} <small>({})</small>', paragraph, _('event description, if present')),
pricing=format_html('{} <small>({})</small>', '...', _('event pricing, if present')),
url='#',
)
booking = Booking(user_display_label='{{ user_display_label }}', form_url='#')
booking.event = event
button_label = format_html('{}<br>({})', _('More information'), _('link to event url, if present'))
reminder_ctx = {
'booking': booking,
'in_x_days': _('tomorrow') if days == 1 else _('in %s days') % days,
'date': booking.event.start_datetime,
'email_extra_info': self.agenda.reminder_settings.email_extra_info,
'sms_extra_info': self.agenda.reminder_settings.sms_extra_info,
'event_url_button_label': button_label,
}
if self.type_ == 'email':
ctx['subject'] = render_to_string(
'agendas/%s_reminder_subject.txt' % kind, reminder_ctx, request=self.request
).strip()
ctx['message'] = render_to_string(
'agendas/%s_reminder_body.html' % kind, reminder_ctx, request=self.request
)
elif self.type_ == 'sms':
ctx['message'] = render_to_string(
'agendas/%s_reminder_message.txt' % kind, reminder_ctx, request=self.request
)
return ctx
agenda_reminder_preview = AgendaReminderPreviewView.as_view()
class EventsTimesheetView(ViewableAgendaMixin, DetailView):
model = Agenda
template_name = 'chrono/manager_events_timesheet.html'
def set_agenda(self, **kwargs):
self.agenda = get_object_or_404(Agenda, pk=kwargs.get('pk'), kind='events')
def get_object(self, **kwargs):
return self.agenda
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
form = EventsTimesheetForm(agenda=self.agenda, data=self.request.GET or None)
if self.request.GET:
form.is_valid()
context['form'] = form
return context
def get(self, request, *args, **kwargs):
self.object = self.get_object()
context = self.get_context_data(object=self.object)
if 'pdf' in request.GET and context['form'].is_valid():
return self.pdf(request, context)
return self.render_to_response(context)
def pdf(self, request, context):
context['base_uri'] = request.build_absolute_uri('/')
html = HTML(
string=render_to_string('chrono/manager_events_timesheet_pdf.html', context, request=request)
)
pdf = html.write_pdf()
response = HttpResponse(pdf, content_type='application/pdf')
response['Content-Disposition'] = 'attachment; filename="timesheet_{}_{}_{}.pdf"'.format(
self.agenda.slug,
context['form'].cleaned_data['date_start'].strftime('%Y-%m-%d'),
context['form'].cleaned_data['date_end'].strftime('%Y-%m-%d'),
)
return response
events_timesheet = EventsTimesheetView.as_view()
class EventDetailView(ViewableAgendaMixin, DetailView):
model = Event
pk_url_kwarg = 'event_pk'
def dispatch(self, request, *args, **kwargs):
if self.get_object().recurrence_days:
raise Http404('this view makes no sense for recurring events')
return super().dispatch(request, *args, **kwargs)
def get_template_names(self):
if self.request.is_ajax():
return ['chrono/manager_event_detail_fragment.html']
return ['chrono/manager_event_detail.html']
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['user_can_manage'] = self.agenda.can_be_managed(self.request.user)
event = self.get_object()
context['booked'] = event.booking_set.filter(
cancellation_datetime__isnull=True, in_waiting_list=False
).order_by('creation_datetime')
context['waiting'] = event.booking_set.filter(
cancellation_datetime__isnull=True, in_waiting_list=True
).order_by('creation_datetime')
return context
event_view = EventDetailView.as_view()
class EventEditView(ManagedAgendaMixin, UpdateView):
template_name = 'chrono/manager_event_form.html'
model = Event
form_class = EventForm
pk_url_kwarg = 'event_pk'
def get_success_url(self):
if (
self.request.GET.get('next') == 'settings'
or self.request.POST.get('next') == 'settings'
or self.object.recurrence_days
):
return reverse('chrono-manager-agenda-settings', kwargs={'pk': self.agenda.id})
return reverse('chrono-manager-event-view', kwargs={'pk': self.agenda.id, 'event_pk': self.object.id})
event_edit = EventEditView.as_view()
class EventDeleteView(ManagedAgendaMixin, DeleteView):
template_name = 'chrono/manager_confirm_event_delete.html'
model = Event
pk_url_kwarg = 'event_pk'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['cannot_delete'] = bool(
self.object.booking_set.filter(cancellation_datetime__isnull=True).exists()
and self.object.start_datetime > now()
or self.object.has_recurrences_booked()
)
return context
def delete(self, request, *args, **kwargs):
self.object = self.get_object()
context = self.get_context_data()
if context['cannot_delete']:
raise PermissionDenied()
return super().delete(request, *args, **kwargs)
def get_success_url(self):
if self.request.GET.get('next') == 'settings' or self.request.POST.get('next') == 'settings':
return reverse('chrono-manager-agenda-settings', kwargs={'pk': self.agenda.id})
day = self.object.start_datetime
return reverse(
'chrono-manager-agenda-month-view',
kwargs={'pk': self.agenda.id, 'year': day.year, 'month': day.month},
)
event_delete = EventDeleteView.as_view()
class EventCheckView(ViewableAgendaMixin, DetailView):
template_name = 'chrono/manager_event_check.html'
model = Event
pk_url_kwarg = 'event_pk'
def set_agenda(self, **kwargs):
self.agenda = get_object_or_404(
Agenda.objects.prefetch_related('absence_reasons_group__absence_reasons'),
pk=kwargs.get('pk'),
kind='events',
)
def get_queryset(self):
queryset = super().get_queryset()
return queryset.filter(agenda=self.agenda, start_datetime__date__lte=now().date(), cancelled=False)
def get_filters(self, booked_queryset, subscription_queryset):
agenda_filters = self.agenda.get_booking_check_filters()
filters = collections.defaultdict(set)
extra_data_from_booked = booked_queryset.filter(extra_data__has_any_keys=agenda_filters).values_list(
'extra_data', flat=True
)
extra_data_from_subscriptions = subscription_queryset.filter(
extra_data__has_any_keys=agenda_filters
).values_list('extra_data', flat=True)
for extra_data in list(extra_data_from_booked) + list(extra_data_from_subscriptions):
for k, v in extra_data.items():
if k in agenda_filters:
filters[k].add(v)
filters = sorted(filters.items())
filters = {k: sorted(list(v)) for k, v in filters}
return filters
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
event = self.object
# booking base queryset
booking_qs_kwargs = {}
if not self.agenda.subscriptions.exists():
booking_qs_kwargs = {'cancellation_datetime__isnull': True}
booked_qs = event.booking_set.filter(
in_waiting_list=False, primary_booking__isnull=True, **booking_qs_kwargs
)
booked_qs = booked_qs.annotate(
places_count=Value(1) + Count('secondary_booking_set', filter=Q(**booking_qs_kwargs))
)
# waiting list queryset
waiting_qs = event.booking_set.filter(
in_waiting_list=True, primary_booking__isnull=True, **booking_qs_kwargs
).order_by('user_last_name', 'user_first_name')
waiting_qs = waiting_qs.annotate(
places_count=Value(1)
+ Count('secondary_booking_set', filter=Q(cancellation_datetime__isnull=True))
)
# subscription base queryset
subscription_qs = (
self.agenda.subscriptions.filter(
date_start__lte=event.start_datetime, date_end__gt=event.start_datetime
)
# exclude user_external_id from booked_qs and waiting_qs
.exclude(user_external_id__in=booked_qs.values('user_external_id')).exclude(
user_external_id__in=waiting_qs.values('user_external_id')
)
)
# build filters from booked_qs and subscription_qs
filters = self.get_filters(booked_queryset=booked_qs, subscription_queryset=subscription_qs)
# and filter booked and subscriptions
booked_filterset = BookingCheckFilterSet(
self.request.GET, queryset=booked_qs, agenda=self.agenda, filters=filters
)
subscription_filterset = SubscriptionCheckFilterSet(
self.request.GET, queryset=subscription_qs, agenda=self.agenda, filters=filters
)
# build results from mixed booked and subscriptions
results = []
booked_without_status = False
for booking in booked_filterset.qs:
if booking.cancellation_datetime is None and booking.user_was_present is None:
booked_without_status = True
booking.form = BookingAbsenceReasonForm(
agenda=self.agenda, initial={'reason': booking.user_absence_reason}
)
booking.kind = 'booking'
results.append(booking)
for subscription in subscription_filterset.qs:
subscription.kind = 'subscription'
results.append(subscription)
# sort results
results = sorted(results, key=attrgetter('user_last_name', 'user_first_name'))
# set context
context['booked_without_status'] = booked_without_status
if context['booked_without_status']:
context['absence_form'] = BookingAbsenceReasonForm(agenda=self.agenda)
context['filterset'] = booked_filterset
context['results'] = results
context['waiting'] = waiting_qs
return context
event_check = EventCheckView.as_view()
class EventCheckMixin:
def set_agenda(self, **kwargs):
self.agenda = get_object_or_404(
Agenda,
pk=kwargs.get('pk'),
kind='events',
)
self.event = get_object_or_404(
Event,
Q(checked=False) | Q(agenda__disable_check_update=False),
pk=kwargs.get('event_pk'),
agenda=self.agenda,
start_datetime__date__lte=now().date(),
cancelled=False,
)
def get_bookings(self):
return self.event.booking_set.filter(
event__agenda=self.agenda,
event__start_datetime__date__lte=now().date(),
event__cancelled=False,
cancellation_datetime__isnull=True,
in_waiting_list=False,
user_was_present__isnull=True,
)
def response(self, request):
return HttpResponseRedirect(
reverse(
'chrono-manager-event-check',
kwargs={'pk': self.agenda.pk, 'event_pk': self.event.pk},
)
)
class EventPresenceView(EventCheckMixin, ViewableAgendaMixin, View):
def post(self, request, *args, **kwargs):
bookings = self.get_bookings()
bookings.update(user_absence_reason='', user_was_present=True)
self.event.set_is_checked()
return self.response(request)
event_presence = EventPresenceView.as_view()
class EventAbsenceView(EventCheckMixin, ViewableAgendaMixin, FormView):
form_class = BookingAbsenceReasonForm
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['agenda'] = self.agenda
return kwargs
def post(self, request, *args, **kwargs):
form = self.get_form()
qs_kwargs = {}
if form.is_valid():
qs_kwargs['user_absence_reason'] = form.cleaned_data['reason']
bookings = self.get_bookings()
bookings.update(user_was_present=False, **qs_kwargs)
self.event.set_is_checked()
return self.response(request)
event_absence = EventAbsenceView.as_view()
class EventCheckedView(EventCheckMixin, ViewableAgendaMixin, View):
def post(self, request, *args, **kwargs):
if not self.event.checked:
self.event.checked = True
self.event.save(update_fields=['checked'])
return self.response(request)
event_checked = EventCheckedView.as_view()
class AgendaAddResourceView(ManagedAgendaMixin, FormView):
template_name = 'chrono/manager_agenda_resource_form.html'
form_class = AgendaResourceForm
def set_agenda(self, **kwargs):
self.agenda = get_object_or_404(Agenda, id=kwargs.get('pk'), kind='meetings')
def get_form_kwargs(self):
kwargs = super(FormView, self).get_form_kwargs()
kwargs['agenda'] = self.agenda
return kwargs
def form_valid(self, form):
self.agenda.resources.add(form.cleaned_data['resource'])
return super().form_valid(form)
agenda_add_resource = AgendaAddResourceView.as_view()
class AgendaResourceDeleteView(ManagedAgendaMixin, DeleteView):
template_name = 'chrono/manager_confirm_delete.html'
model = Resource
pk_url_kwarg = 'resource_pk'
def set_agenda(self, **kwargs):
self.agenda = get_object_or_404(Agenda, id=kwargs.get('pk'), kind='meetings')
def delete(self, request, *args, **kwargs):
self.object = self.get_object()
self.agenda.resources.remove(self.object)
return HttpResponseRedirect(self.get_success_url())
agenda_delete_resource = AgendaResourceDeleteView.as_view()
class UnavailabilityCalendarToggleView(ManagedDeskMixin, DetailView):
model = UnavailabilityCalendar
pk_url_kwarg = 'unavailability_calendar_pk'
def get(self, request, *args, **kwargs):
unavailability_calendar = self.get_object()
enabled = False
try:
self.desk.unavailability_calendars.get(pk=unavailability_calendar.pk)
self.desk.unavailability_calendars.remove(unavailability_calendar)
if self.desk.label and not self.desk.agenda.desk_simple_management:
message = _(
'Unavailability calendar %(unavailability_calendar)s has been disabled on desk %(desk)s.'
)
else:
message = _('Unavailability calendar %(unavailability_calendar)s has been disabled.')
except UnavailabilityCalendar.DoesNotExist:
enabled = True
self.desk.unavailability_calendars.add(unavailability_calendar)
if self.desk.label and not self.desk.agenda.desk_simple_management:
message = _(
'Unavailability calendar %(unavailability_calendar)s has been enabled on desk %(desk)s.'
)
else:
message = _('Unavailability calendar %(unavailability_calendar)s has been enabled.')
if self.desk.agenda.desk_simple_management:
for desk in self.desk.agenda.desk_set.exclude(pk=self.desk.pk):
if enabled:
desk.unavailability_calendars.add(unavailability_calendar)
else:
desk.unavailability_calendars.remove(unavailability_calendar)
messages.info(
self.request, message % {'unavailability_calendar': unavailability_calendar, 'desk': self.desk}
)
if enabled:
for exception in unavailability_calendar.timeperiodexception_set.all():
desks = [self.desk]
if self.desk.agenda.desk_simple_management:
desks = self.desk.agenda.desk_set.all()
for desk in desks:
if exception.has_booking_within_time_slot(target_desk=desk):
message = _('One or several bookings overlap with exceptions.')
messages.warning(self.request, message)
break
return HttpResponseRedirect(
reverse('chrono-manager-agenda-settings', kwargs={'pk': self.desk.agenda_id})
)
unavailability_calendar_toggle_view = UnavailabilityCalendarToggleView.as_view()
class AgendaAddMeetingTypeView(ManagedAgendaMixin, CreateView):
template_name = 'chrono/manager_meeting_type_form.html'
model = MeetingType
form_class = NewMeetingTypeForm
agenda_add_meeting_type = AgendaAddMeetingTypeView.as_view()
class MeetingTypeEditView(ManagedAgendaSubobjectMixin, UpdateView):
template_name = 'chrono/manager_meeting_type_form.html'
model = MeetingType
form_class = MeetingTypeForm
meeting_type_edit = MeetingTypeEditView.as_view()
class MeetingTypeDeleteView(ManagedAgendaSubobjectMixin, DeleteView):
template_name = 'chrono/manager_confirm_delete.html'
model = MeetingType
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
meeting_type = self.get_object()
context['cannot_delete'] = False
for virtual_agenda in self.get_object().agenda.virtual_agendas.all():
if virtual_agenda.real_agendas.count() == 1:
continue
for mt in virtual_agenda.iter_meetingtypes():
if (
meeting_type.slug == mt.slug
and meeting_type.label == mt.label
and meeting_type.duration == mt.duration
):
context['cannot_delete'] = True
context['cannot_delete_msg'] = _(
'This cannot be removed as it used by a virtual agenda: %(agenda)s'
% {'agenda': virtual_agenda}
)
break
return context
def delete(self, request, *args, **kwargs):
self.object = self.get_object()
context = self.get_context_data()
if context['cannot_delete']:
raise PermissionDenied()
# rewrite django/views/generic/edit.py::DeletionMixin.delete
# to mark for deletion instead of actually delete
success_url = self.get_success_url()
self.object.deleted = True
self.object.slug += '__deleted__' + str(uuid.uuid4())
self.object.save()
return HttpResponseRedirect(success_url)
meeting_type_delete = MeetingTypeDeleteView.as_view()
def process_time_period_add_form(form, desk=None, agenda=None):
assert desk or agenda, "a time period requires a desk or a agenda"
for weekday in form.cleaned_data.get('weekdays'):
period = TimePeriod(
weekday=weekday,
start_time=form.cleaned_data['start_time'],
end_time=form.cleaned_data['end_time'],
)
if desk:
period.desk = desk
elif agenda:
period.agenda = agenda
period.save()
class AgendaAddTimePeriodView(ManagedDeskMixin, FormView):
template_name = 'chrono/manager_time_period_form.html'
form_class = TimePeriodAddForm
def get_form_kwargs(self):
return super(FormView, self).get_form_kwargs()
def form_valid(self, form):
if self.desk.agenda.desk_simple_management:
for desk in self.desk.agenda.desk_set.all():
process_time_period_add_form(form, desk=desk)
else:
process_time_period_add_form(form, desk=self.desk)
return super().form_valid(form)
agenda_add_time_period = AgendaAddTimePeriodView.as_view()
class VirtualAgendaAddTimePeriodView(ManagedAgendaMixin, FormView):
template_name = 'chrono/manager_time_period_form.html'
form_class = TimePeriodAddForm
def get_form_kwargs(self):
return super(FormView, self).get_form_kwargs()
def form_valid(self, form):
process_time_period_add_form(form, agenda=self.agenda)
return super().form_valid(form)
virtual_agenda_add_time_period = VirtualAgendaAddTimePeriodView.as_view()
class TimePeriodEditView(ManagedTimePeriodMixin, UpdateView):
template_name = 'chrono/manager_time_period_form.html'
model = TimePeriod
form_class = TimePeriodForm
time_period_edit = TimePeriodEditView.as_view()
class TimePeriodDeleteView(ManagedTimePeriodMixin, DeleteView):
template_name = 'chrono/manager_confirm_delete.html'
model = TimePeriod
def delete(self, request, *args, **kwargs):
time_period = self.get_object()
response = super().delete(request, *args, **kwargs)
if not time_period.desk:
return response
if not time_period.desk.agenda.desk_simple_management:
return response
for desk in time_period.desk.agenda.desk_set.exclude(pk=time_period.desk.pk):
tp = desk.timeperiod_set.filter(
weekday=time_period.weekday, start_time=time_period.start_time, end_time=time_period.end_time
).first()
if tp is not None:
tp.delete()
return response
time_period_delete = TimePeriodDeleteView.as_view()
class AgendaAddDesk(ManagedAgendaMixin, CreateView):
template_name = 'chrono/manager_desk_form.html'
model = Desk
form_class = NewDeskForm
agenda = None
def set_agenda(self, **kwargs):
self.agenda = get_object_or_404(Agenda, pk=kwargs.get('pk'), kind='meetings')
agenda_add_desk = AgendaAddDesk.as_view()
class DeskEditView(ManagedAgendaSubobjectMixin, UpdateView):
template_name = 'chrono/manager_desk_form.html'
model = Desk
form_class = DeskForm
def get_queryset(self):
return super().get_queryset().filter(agenda__kind='meetings')
desk_edit = DeskEditView.as_view()
class DeskDeleteView(ManagedAgendaSubobjectMixin, DeleteView):
template_name = 'chrono/manager_confirm_delete.html'
model = Desk
def dispatch(self, request, *args, **kwargs):
agenda = self.get_object().agenda
if agenda.desk_set.count() == 1:
raise Http404
return super().dispatch(request, *args, **kwargs)
def get_queryset(self):
return super().get_queryset().filter(agenda__kind='meetings')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['cannot_delete'] = Booking.objects.filter(
event__desk=self.get_object(), event__start_datetime__gt=now(), cancellation_datetime__isnull=True
).exists()
context['cannot_delete_msg'] = FUTURE_BOOKING_ERROR_MSG
return context
def delete(self, request, *args, **kwargs):
self.object = self.get_object()
context = self.get_context_data()
if context['cannot_delete']:
raise PermissionDenied()
return super().delete(request, *args, **kwargs)
desk_delete = DeskDeleteView.as_view()
class VirtualMemberAddView(ManagedAgendaMixin, CreateView):
template_name = 'chrono/manager_virtual_member_form.html'
form_class = VirtualMemberForm
model = VirtualMember
def get_form_kwargs(self):
kwargs = super(CreateView, self).get_form_kwargs()
kwargs['instance'] = VirtualMember(virtual_agenda=self.agenda)
return kwargs
agenda_add_virtual_member = VirtualMemberAddView.as_view()
class VirtualMemberDeleteView(DeleteView):
template_name = 'chrono/manager_confirm_virtual_member_delete.html'
model = VirtualMember
def dispatch(self, request, *args, **kwargs):
self.agenda = self.get_object().virtual_agenda
if not self.agenda.can_be_managed(request.user):
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['agenda'] = self.agenda
return context
def get_success_url(self):
return reverse('chrono-manager-agenda-settings', kwargs={'pk': self.agenda.pk})
virtual_member_delete = VirtualMemberDeleteView.as_view()
class AgendaAddTimePeriodExceptionView(ManagedDeskMixin, CreateView):
template_name = 'chrono/manager_time_period_exception_form.html'
model = TimePeriodException
form_class = NewTimePeriodExceptionForm
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['base_template'] = 'chrono/manager_agenda_settings.html'
context['cancel_url'] = reverse('chrono-manager-agenda-settings', kwargs={'pk': self.desk.agenda.pk})
return context
def form_valid(self, form):
result = super().form_valid(form)
all_desks = form.cleaned_data.get('all_desks')
message = ungettext(
'Exception added.',
'Exceptions added.',
len(form.exceptions) if all_desks else 1,
)
messages.info(self.request, message)
for exception in form.exceptions:
if exception.has_booking_within_time_slot():
messages.warning(self.request, _('One or several bookings exists within this time slot.'))
break
return result
agenda_add_time_period_exception = AgendaAddTimePeriodExceptionView.as_view()
class TimePeriodExceptionEditView(ManagedTimePeriodExceptionMixin, UpdateView):
template_name = 'chrono/manager_time_period_exception_form.html'
model = TimePeriodException
form_class = TimePeriodExceptionForm
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
if self.desk:
context['base_template'] = 'chrono/manager_agenda_settings.html'
context['cancel_url'] = reverse(
'chrono-manager-agenda-settings', kwargs={'pk': self.desk.agenda.pk}
)
elif self.unavailability_calendar:
context['base_template'] = 'chrono/manager_unavailability_calendar_settings.html'
context['cancel_url'] = reverse(
'chrono-manager-unavailability-calendar-settings',
kwargs={'pk': self.unavailability_calendar.pk},
)
return context
def form_valid(self, form):
result = super().form_valid(form)
messages.info(self.request, _('Exception updated.'))
for exception in form.exceptions:
if exception.has_booking_within_time_slot():
messages.warning(self.request, _('One or several bookings exists within this time slot.'))
break
return result
time_period_exception_edit = TimePeriodExceptionEditView.as_view()
class TimePeriodExceptionListView(ManagedDeskMixin, ListView):
template_name = 'chrono/manager_time_period_exception_list.html'
model = TimePeriodException
paginate_by = 20
def get_queryset(self):
return self.model.objects.filter(
Q(desk=self.desk) | Q(unavailability_calendar__desks=self.desk), end_datetime__gte=now()
)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['user_can_manage'] = self.desk.agenda.can_be_managed(self.request.user)
return context
time_period_exception_list = TimePeriodExceptionListView.as_view()
class TimePeriodExceptionExtractListView(TimePeriodExceptionListView):
paginate_by = None # no pagination
def get_queryset(self):
# but limit queryset
return super().get_queryset()[:10]
time_period_exception_extract_list = TimePeriodExceptionExtractListView.as_view()
class TimePeriodExceptionDeleteView(ManagedTimePeriodExceptionMixin, DeleteView):
template_name = 'chrono/manager_confirm_exception_delete.html'
model = TimePeriodException
def get_success_url(self):
if self.desk:
referer = self.request.META.get('HTTP_REFERER')
success_url = reverse('chrono-manager-time-period-exception-list', kwargs={'pk': self.desk.pk})
if success_url in referer:
return success_url
success_url = super().get_success_url()
if self.desk and 'from_popup' in self.request.GET:
success_url = f'{success_url}?display_exceptions={self.desk.pk}'
return success_url
def get_queryset(self):
return super().get_queryset().filter(source__settings_slug__isnull=True)
def delete(self, request, *args, **kwargs):
exception = self.get_object()
response = super().delete(request, *args, **kwargs)
if not exception.desk_id:
return response
if not exception.desk.agenda.desk_simple_management:
return response
for desk in exception.desk.agenda.desk_set.exclude(pk=exception.desk.pk):
exc = desk.timeperiodexception_set.filter(
source__isnull=True,
label=exception.label,
start_datetime=exception.start_datetime,
end_datetime=exception.end_datetime,
).first()
if exc is not None:
exc.delete()
return response
time_period_exception_delete = TimePeriodExceptionDeleteView.as_view()
class DeskImportTimePeriodExceptionsView(ManagedAgendaSubobjectMixin, UpdateView):
model = Desk
form_class = ExceptionsImportForm
template_name = 'chrono/manager_import_exceptions.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
desk = self.get_object()
context['exception_sources'] = desk.timeperiodexceptionsource_set.all()
context['desk'] = desk
context['unavailability_calendars'] = (
UnavailabilityCalendar.objects.filter(desks=desk)
.annotate(enabled=Value(True, BooleanField()))
.union(
UnavailabilityCalendar.objects.exclude(desks=desk).annotate(
enabled=Value(False, BooleanField())
)
)
)
return context
def import_file(self, desk, form):
if form.cleaned_data['ics_file']:
ics_file = form.cleaned_data['ics_file']
source = desk.timeperiodexceptionsource_set.create(ics_filename=ics_file.name, ics_file=ics_file)
ics_file.seek(0)
elif form.cleaned_data['ics_url']:
source = desk.timeperiodexceptionsource_set.create(ics_url=form.cleaned_data['ics_url'])
parsed = source._check_ics_content()
source._parsed = parsed
return source
def form_valid(self, form):
desk = self.get_object()
sources = []
all_desks = form.cleaned_data.get('all_desks')
try:
with transaction.atomic():
if all_desks or desk.agenda.desk_simple_management:
for _desk in desk.agenda.desk_set.all():
sources.append(self.import_file(_desk, form))
else:
sources.append(self.import_file(desk, form))
except ICSError as e:
form.add_error(None, force_text(e))
return self.form_invalid(form)
try:
for source in sources:
source.refresh_timeperiod_exceptions(data=source._parsed)
except ICSError as e:
form.add_error(None, force_text(e))
return self.form_invalid(form)
messages.info(self.request, _('Exceptions will be imported in a few minutes.'))
return super().form_valid(form)
desk_import_time_period_exceptions = DeskImportTimePeriodExceptionsView.as_view()
class TimePeriodExceptionSourceDeleteView(ManagedDeskSubobjectMixin, DeleteView):
template_name = 'chrono/manager_confirm_source_delete.html'
model = TimePeriodExceptionSource
def get_success_url(self):
return reverse('chrono-manager-agenda-settings', kwargs={'pk': self.desk.agenda_id})
def delete(self, request, *args, **kwargs):
source = self.get_object()
response = super().delete(request, *args, **kwargs)
if not source.desk.agenda.desk_simple_management:
return response
for desk in source.desk.agenda.desk_set.exclude(pk=source.desk_id):
if source.ics_filename:
queryset = desk.timeperiodexceptionsource_set.filter(ics_filename=source.ics_filename)
else:
queryset = desk.timeperiodexceptionsource_set.filter(ics_url=source.ics_url)
_source = queryset.first()
if _source is not None:
_source.delete()
return response
time_period_exception_source_delete = TimePeriodExceptionSourceDeleteView.as_view()
class TimePeriodExceptionSourceReplaceView(ManagedDeskSubobjectMixin, UpdateView):
model = TimePeriodExceptionSource
form_class = TimePeriodExceptionSourceReplaceForm
template_name = 'chrono/manager_replace_exceptions.html'
def get_queryset(self):
queryset = super().get_queryset()
return queryset.filter(ics_filename__isnull=False)
def import_file(self, desk, form):
source = desk.timeperiodexceptionsource_set.filter(
ics_filename=self.get_object().ics_filename
).first()
if source is not None:
source.refresh_timeperiod_exceptions()
def form_valid(self, form):
desk = self.get_object().desk
try:
if desk.agenda.desk_simple_management:
for _desk in desk.agenda.desk_set.all():
self.import_file(_desk, form)
else:
self.import_file(desk, form)
except ICSError as e:
form.add_error(None, force_text(e))
return self.form_invalid(form)
messages.info(self.request, _('Exceptions will be synchronized in a few minutes.'))
return super().form_valid(form)
time_period_exception_source_replace = TimePeriodExceptionSourceReplaceView.as_view()
class TimePeriodExceptionSourceRefreshView(ManagedDeskSubobjectMixin, DetailView):
model = TimePeriodExceptionSource
def get_queryset(self):
queryset = super().get_queryset()
return queryset.filter(ics_url__isnull=False)
def import_file(self, desk):
source = desk.timeperiodexceptionsource_set.filter(ics_url=self.get_object().ics_url).first()
if source is not None:
source.refresh_timeperiod_exceptions()
def get(self, request, *args, **kwargs):
desk = self.get_object().desk
try:
if desk.agenda.desk_simple_management:
for _desk in desk.agenda.desk_set.all():
self.import_file(_desk)
else:
self.import_file(desk)
except ICSError as e:
messages.error(self.request, force_text(e))
messages.info(self.request, _('Exceptions will be synchronized in a few minutes.'))
# redirect to settings
return HttpResponseRedirect(reverse('chrono-manager-agenda-settings', kwargs={'pk': desk.agenda_id}))
time_period_exception_source_refresh = TimePeriodExceptionSourceRefreshView.as_view()
class BookingCancelView(ViewableAgendaMixin, UpdateView):
template_name = 'chrono/manager_confirm_booking_cancellation.html'
model = Booking
pk_url_kwarg = 'booking_pk'
form_class = BookingCancelForm
def dispatch(self, request, *args, **kwargs):
self.booking = self.get_object()
return super().dispatch(request, *args, **kwargs)
def get_queryset(self):
queryset = super().get_queryset()
return queryset.filter(cancellation_datetime__isnull=True, primary_booking__isnull=True)
def form_valid(self, form):
trigger_callback = not form.cleaned_data['disable_trigger']
try:
self.booking.cancel(trigger_callback)
except requests.RequestException:
form.add_error(None, _('There has been an error sending cancellation notification to form.'))
form.add_error(None, _('Check this box if you are sure you want to proceed anyway.'))
form.show_trigger_checkbox()
return self.form_invalid(form)
return HttpResponseRedirect(self.get_success_url())
def get_success_url(self):
next_url = self.request.POST.get('next')
if next_url:
return next_url
event = self.booking.event
day = event.start_datetime
return reverse(
'chrono-manager-agenda-month-view',
kwargs={'pk': event.agenda.pk, 'year': day.year, 'month': day.month},
)
booking_cancel = BookingCancelView.as_view()
class BookingCheckMixin:
def get_booking(self, **kwargs):
return get_object_or_404(
Booking,
Q(event__checked=False) | Q(event__agenda__disable_check_update=False),
pk=kwargs['booking_pk'],
event__agenda=self.agenda,
event__start_datetime__date__lte=now().date(),
event__cancelled=False,
cancellation_datetime__isnull=True,
in_waiting_list=False,
primary_booking__isnull=True,
)
def response(self, request, booking):
if request.is_ajax():
booking.form = BookingAbsenceReasonForm(
agenda=self.agenda, initial={'reason': booking.user_absence_reason}
)
return render(
request,
'chrono/manager_event_check_booking_fragment.html',
{'booking': booking, 'agenda': self.agenda},
)
return HttpResponseRedirect(
reverse(
'chrono-manager-event-check',
kwargs={'pk': booking.event.agenda_id, 'event_pk': booking.event.pk},
)
)
class BookingPresenceView(ViewableAgendaMixin, BookingCheckMixin, View):
def post(self, request, *args, **kwargs):
booking = self.get_booking(**kwargs)
booking.mark_user_presence()
return self.response(request, booking)
booking_presence = BookingPresenceView.as_view()
class BookingAbsenceView(ViewableAgendaMixin, BookingCheckMixin, FormView):
form_class = BookingAbsenceReasonForm
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['agenda'] = self.agenda
return kwargs
def post(self, request, *args, **kwargs):
booking = self.get_booking(**kwargs)
form = self.get_form()
reason = None
if form.is_valid():
reason = form.cleaned_data['reason']
booking.mark_user_absence(reason=reason)
return self.response(request, booking)
booking_absence = BookingAbsenceView.as_view()
class EventCancelView(ViewableAgendaMixin, UpdateView):
template_name = 'chrono/manager_confirm_event_cancellation.html'
model = Event
pk_url_kwarg = 'event_pk'
form_class = EventCancelForm
def dispatch(self, request, *args, **kwargs):
self.event = self.get_object()
if self.event.cancellation_status:
raise PermissionDenied()
self.cancel_bookings = not (self.request.GET.get('force_cancellation'))
return super().dispatch(request, *args, **kwargs)
def form_valid(self, form):
self.event.cancel(self.cancel_bookings)
return HttpResponseRedirect(self.get_success_url())
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['cancel_bookings'] = self.cancel_bookings
context['bookings_count'] = self.event.booking_set.filter(cancellation_datetime__isnull=True).count()
context['cancellation_forbidden'] = (
self.event.booking_set.filter(cancellation_datetime__isnull=True, cancel_callback_url='')
.exclude(backoffice_url='')
.exists()
)
return context
def get_success_url(self):
self.event.refresh_from_db()
if self.event.cancellation_scheduled:
messages.info(self.request, _('Event "%s" will be cancelled in a few minutes.') % self.event)
next_url = self.request.POST.get('next')
if next_url:
return next_url
day = self.event.start_datetime
return reverse(
'chrono-manager-agenda-month-view',
kwargs={'pk': self.event.agenda.pk, 'year': day.year, 'month': day.month},
)
event_cancel = EventCancelView.as_view()
class EventCancellationReportView(ViewableAgendaMixin, DetailView):
model = EventCancellationReport
template_name = 'chrono/manager_event_cancellation_report.html'
context_object_name = 'report'
pk_url_kwarg = 'report_pk'
def get(self, *args, **kwargs):
self.report = self.get_object()
self.report.seen = True
self.report.save()
return super().get(*args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
bookings = self.report.bookings.all()
errors = self.report.booking_errors
context['errors'] = {
booking: errors[str(booking.pk)] for booking in bookings if str(booking.pk) in errors
}
return context
event_cancellation_report = EventCancellationReportView.as_view()
class EventCancellationReportListView(ViewableAgendaMixin, ListView):
model = EventCancellationReport
context_object_name = 'cancellation_reports'
template_name = 'chrono/manager_event_cancellation_reports.html'
event_cancellation_report_list = EventCancellationReportListView.as_view()
class EventCreateRecurrenceView(ViewableAgendaMixin, RedirectView):
def get_redirect_url(self, *args, **kwargs):
event_slug, datetime_str = kwargs['event_identifier'].split(':')
try:
start_datetime = make_aware(datetime.datetime.strptime(datetime_str, '%Y-%m-%d-%H%M'))
except ValueError:
raise Http404()
event = self.agenda.event_set.get(slug=event_slug)
try:
event_recurrence = event.get_or_create_event_recurrence(start_datetime)
except ValueError:
raise Http404()
return event_recurrence.get_absolute_view_url()
event_create_recurrence = EventCreateRecurrenceView.as_view()
class TimePeriodExceptionSourceToggleView(ManagedDeskSubobjectMixin, DetailView):
model = TimePeriodExceptionSource
def get_object(self, queryset=None):
source = super().get_object(queryset)
if source.settings_slug is None:
raise Http404('This source cannot be enabled nor disabled')
return source
def get(self, request, *args, **kwargs):
source = self.get_object()
if source.enabled:
source.disable()
was_enabled = False
if source.desk.label:
message = _('Exception source %(source)s has been disabled on desk %(desk)s.')
else:
message = _('Exception source %(source)s has been disabled.')
else:
source.enable()
was_enabled = True
if source.desk.label:
message = _('Exception source %(source)s has been enabled on desk %(desk)s.')
else:
message = _('Exception source %(source)s has been enabled.')
if self.desk.agenda.desk_simple_management:
for desk in self.desk.agenda.desk_set.exclude(pk=self.desk.pk):
_source = desk.timeperiodexceptionsource_set.filter(
settings_slug=source.settings_slug
).first()
if _source is None:
continue
if was_enabled:
_source.enable()
message = _(
'Exception source %(source)s has been enabled. Exceptions will be imported in a few minutes.'
)
else:
_source.disable()
message = _('Exception source %(source)s has been disabled.')
messages.info(self.request, message % {'source': source, 'desk': source.desk})
return HttpResponseRedirect(
reverse('chrono-manager-agenda-settings', kwargs={'pk': source.desk.agenda_id})
)
time_period_exception_source_toggle = TimePeriodExceptionSourceToggleView.as_view()
class ViewableUnavailabilityCalendarMixin:
unavailability_calendar = None
def set_unavailability_calendar(self, **kwargs):
self.unavailability_calendar = get_object_or_404(UnavailabilityCalendar, id=kwargs.get('pk'))
def dispatch(self, request, *args, **kwargs):
self.set_unavailability_calendar(**kwargs)
if not self.check_permissions(request.user):
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
def check_permissions(self, user):
return self.unavailability_calendar.can_be_viewed(user)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['unavailability_calendar'] = self.unavailability_calendar
context['user_can_manage'] = self.unavailability_calendar.can_be_managed(self.request.user)
return context
class ManagedUnavailabilityCalendarMixin(ViewableUnavailabilityCalendarMixin):
def check_permissions(self, user):
return self.unavailability_calendar.can_be_managed(user)
def get_success_url(self):
return reverse(
'chrono-manager-unavailability-calendar-settings', kwargs={'pk': self.unavailability_calendar.id}
)
class UnavailabilityCalendarListView(ListView):
template_name = 'chrono/manager_unavailability_calendar_list.html'
model = UnavailabilityCalendar
def get_queryset(self):
queryset = super().get_queryset()
if not self.request.user.is_staff:
group_ids = [x.id for x in self.request.user.groups.all()]
queryset = queryset.filter(Q(view_role_id__in=group_ids) | Q(edit_role_id__in=group_ids))
if not queryset.count():
raise PermissionDenied
return queryset.order_by('label')
unavailability_calendar_list = UnavailabilityCalendarListView.as_view()
class UnavailabilityCalendarAddView(CreateView):
template_name = 'chrono/manager_unavailability_calendar_form.html'
model = UnavailabilityCalendar
form_class = UnavailabilityCalendarAddForm
def dispatch(self, request, *args, **kwargs):
if not request.user.is_staff:
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
def get_success_url(self):
return reverse('chrono-manager-unavailability-calendar-settings', kwargs={'pk': self.object.id})
unavailability_calendar_add = UnavailabilityCalendarAddView.as_view()
class UnavailabilityCalendarDetailView(ViewableUnavailabilityCalendarMixin, DetailView):
template_name = 'chrono/manager_unavailability_calendar_detail.html'
model = UnavailabilityCalendar
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['agendas'] = Agenda.objects.filter(
desk__unavailability_calendars__pk=self.unavailability_calendar.pk
).distinct()
return context
unavailability_calendar_view = UnavailabilityCalendarDetailView.as_view()
class UnavailabilityCalendarEditView(ManagedUnavailabilityCalendarMixin, UpdateView):
template_name = 'chrono/manager_unavailability_calendar_form.html'
model = UnavailabilityCalendar
form_class = UnavailabilityCalendarEditForm
unavailability_calendar_edit = UnavailabilityCalendarEditView.as_view()
class UnavailabilityCalendarDeleteView(DeleteView):
template_name = 'chrono/manager_confirm_delete.html'
model = UnavailabilityCalendar
def dispatch(self, request, *args, **kwargs):
if not request.user.is_staff:
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
def get_success_url(self):
return reverse('chrono-manager-unavailability-calendar-list')
unavailability_calendar_delete = UnavailabilityCalendarDeleteView.as_view()
class UnavailabilityCalendarSettings(ManagedUnavailabilityCalendarMixin, DetailView):
template_name = 'chrono/manager_unavailability_calendar_settings.html'
model = UnavailabilityCalendar
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['unavailability_calendar'] = self.object
return context
unavailability_calendar_settings = UnavailabilityCalendarSettings.as_view()
class UnavailabilityCalendarExport(ManagedUnavailabilityCalendarMixin, DetailView):
model = UnavailabilityCalendar
def get(self, request, *args, **kwargs):
response = HttpResponse(content_type='application/json')
today = datetime.date.today()
response[
'Content-Disposition'
] = 'attachment; filename="export_unavailability-calendar_{}_{}.json"'.format(
self.get_object().slug, today.strftime('%Y%m%d')
)
json.dump({'unavailability_calendars': [self.get_object().export_json()]}, response, indent=2)
return response
unavailability_calendar_export = UnavailabilityCalendarExport.as_view()
class UnavailabilityCalendarAddUnavailabilityView(ManagedUnavailabilityCalendarMixin, CreateView):
template_name = 'chrono/manager_time_period_exception_form.html'
form_class = NewTimePeriodExceptionForm
model = TimePeriodException
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
if not kwargs.get('instance'):
kwargs['instance'] = self.model()
kwargs['instance'].unavailability_calendar = self.unavailability_calendar
return kwargs
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['base_template'] = 'chrono/manager_unavailability_calendar_settings.html'
context['cancel_url'] = reverse(
'chrono-manager-unavailability-calendar-settings', kwargs={'pk': self.unavailability_calendar.pk}
)
return context
def form_valid(self, form):
result = super().form_valid(form)
messages.info(self.request, _('Unavailability added.'))
if self.object.has_booking_within_time_slot():
messages.warning(self.request, _('One or several bookings exists within this time slot.'))
return result
unavailability_calendar_add_unavailability = UnavailabilityCalendarAddUnavailabilityView.as_view()
def menu_json(request):
if not request.user.is_staff:
homepage_view = HomepageView(request=request)
if not (
homepage_view.get_queryset().exists() or homepage_view.has_access_to_unavailability_calendars()
):
return HttpResponseForbidden()
label = _('Agendas')
json_str = json.dumps(
[
{
'label': force_text(label),
'slug': 'calendar',
'url': request.build_absolute_uri(reverse('chrono-manager-homepage')),
}
]
)
content_type = 'application/json'
for variable in ('jsonpCallback', 'callback'):
if variable in request.GET:
json_str = '%s(%s);' % (request.GET[variable], json_str)
content_type = 'application/javascript'
break
response = HttpResponse(content_type=content_type)
response.write(json_str)
return response