# 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 . import collections import copy import csv import datetime import itertools import json import math import uuid from operator import attrgetter import requests from dateutil.relativedelta import MO, relativedelta from django.conf import settings from django.contrib import messages from django.core.exceptions import PermissionDenied from django.db import IntegrityError, transaction from django.db.models import BooleanField, Count, Max, Min, Q, Value from django.db.models.deletion import ProtectedError 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_str from django.utils.formats import date_format from django.utils.html import format_html from django.utils.translation import gettext_lazy as _ from django.utils.translation import ngettext from django.views.generic import ( CreateView, DayArchiveView, DeleteView, DetailView, FormView, ListView, RedirectView, TemplateView, UpdateView, View, ) from django.views.generic.dates import MonthMixin, WeekMixin, YearMixin from weasyprint import HTML from chrono.agendas.management.commands.utils import send_reminder from chrono.agendas.models import ( Agenda, AgendaImportError, AgendaNotificationsSettings, AgendaReminderSettings, Booking, BookingColor, Category, Desk, Event, EventCancellationReport, EventsType, ICSError, MeetingType, Resource, SharedCustodyAgenda, SharedCustodyHolidayRule, SharedCustodyPeriod, SharedCustodyRule, SharedCustodySettings, Subscription, TimePeriod, TimePeriodException, TimePeriodExceptionSource, UnavailabilityCalendar, VirtualMember, ) from chrono.utils.date import get_weekday_index from chrono.utils.timezone import localtime, make_aware, make_naive, now from .forms import ( AgendaAddForm, AgendaBookingCheckSettingsForm, AgendaBookingDelaysForm, AgendaDisplaySettingsForm, AgendaDuplicateForm, AgendaEditForm, AgendaNotificationsForm, AgendaReminderForm, AgendaReminderTestForm, AgendaResourceForm, AgendaRolesForm, AgendasExportForm, AgendasImportForm, BookingCancelForm, BookingCheckAbsenceForm, BookingCheckFilterSet, BookingCheckPresenceForm, CustomFieldFormSet, DateTimePeriodForm, DeskExceptionsImportForm, DeskForm, EventCancelForm, EventDuplicateForm, EventForm, EventsTimesheetForm, ExcludedPeriodAddForm, ImportEventsForm, MeetingTypeForm, NewDeskForm, NewEventForm, NewMeetingTypeForm, NewTimePeriodExceptionForm, SharedCustodyHolidayRuleForm, SharedCustodyPeriodForm, SharedCustodyRuleForm, SharedCustodySettingsForm, SubscriptionCheckFilterSet, TimePeriodAddForm, TimePeriodExceptionForm, TimePeriodExceptionSourceReplaceForm, TimePeriodForm, UnavailabilityCalendarAddForm, UnavailabilityCalendarEditForm, UnavailabilityCalendarExceptionsImportForm, VirtualMemberForm, ) from .utils import export_site, import_site FUTURE_BOOKING_ERROR_MSG = _('This cannot be removed as there are bookings for a future date.') def is_ajax(request): return request.headers.get('x-requested-with') == 'XMLHttpRequest' 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() context['shared_custody_enabled'] = settings.SHARED_CUSTODY_ENABLED 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_weeks(self): return [(str(x), _('Week %s') % x) for x in range(1, 53)] def get_week_dates(self): dates = {} for year in self.get_years(): year = int(year) dates[year] = {} for week, week_label in self.get_weeks(): date = datetime.datetime.strptime('%s-W%s-1' % (year, week), "%Y-W%W-%w") dates[year][date] = week_label return dates 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 try: date = datetime.date(int(self.get_year()), int(self.get_month()), 1) except ValueError: raise Http404 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', 'agenda') .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.strftime('%m'), 'day': previous_day.strftime('%d'), }, ) 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.strftime('%m'), 'day': next_day.strftime('%d'), }, ) 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 ResourceWeekMonthMixin: 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() 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: raise Http404 return super().dispatch(request, *args, **kwargs) def get_queryset(self): queryset = ( self.resource.event_set.all() .select_related('meeting_type', 'agenda') .prefetch_related('booking_set') ) return queryset @property def first_day(self): first_day = self.date if self.kind == 'month': first_day -= datetime.timedelta(days=first_day.day - 1) else: first_day -= datetime.timedelta(days=first_day.weekday()) return first_day def get_dated_queryset(self, **kwargs): # adjust min and max, incorrect as DayArchiveView is used to have Y/m/d in url kwargs['start_datetime__gte'] = self._make_date_lookup_arg(self.first_day) kwargs['start_datetime__lt'] = self._make_date_lookup_arg( getattr(self, 'get_next_%s' % self.kind)(self.first_day) ) return super().get_dated_queryset(**kwargs) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['resource'] = self.resource context['kind'] = self.kind 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_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 = 0 first_week_number = self.first_day.isocalendar()[1] last_week_number = first_week_number if self.kind == 'month': first_week_offset = int( (hide_sunday and self.first_day.weekday() == 6) or (hide_weekend and self.first_day.weekday() == 5) ) if first_week_number >= 52: first_week_number = 0 last_day = self.get_next_month(self.first_day.date()) - datetime.timedelta(days=1) last_week_number = last_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.first_day + datetime.timedelta(week_index * 7) dow = date.isocalendar()[2] start_date = date - datetime.timedelta(dow) interval = datetime.timedelta(minutes=60) period = self.first_day.replace(hour=self.min_display.hour, minute=0) max_date = self.first_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) 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/week if self.kind == 'month' and timetable['other_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 class ResourceWeekView(ResourceWeekMonthMixin, DateMixin, DayArchiveView, WeekMixin): template_name = 'chrono/manager_resource_week_view.html' kind = 'week' def get_previous_week(self, date): return date - datetime.timedelta(days=7) def get_next_week(self, date): return date + datetime.timedelta(days=7) def get_previous_week_url(self): previous_week = self.get_previous_week(self.first_day.date()) return reverse( 'chrono-manager-resource-week-view', kwargs={ 'pk': self.resource.pk, 'year': previous_week.year, 'month': previous_week.strftime('%m'), 'day': previous_week.strftime('%d'), }, ) def get_next_week_url(self): next_week = self.get_next_week(self.first_day.date()) return reverse( 'chrono-manager-resource-week-view', kwargs={ 'pk': self.resource.pk, 'year': next_week.year, 'month': next_week.strftime('%m'), 'day': next_week.strftime('%d'), }, ) resource_weekly_view = ResourceWeekView.as_view() class ResourceMonthView(ResourceWeekMonthMixin, DateMixin, DayArchiveView): template_name = 'chrono/manager_resource_month_view.html' kind = 'month' def get_previous_month_url(self): previous_month = self.get_previous_month(self.first_day.date()) return reverse( 'chrono-manager-resource-month-view', kwargs={ 'pk': self.resource.pk, 'year': previous_month.year, 'month': previous_month.strftime('%m'), 'day': previous_month.strftime('%d'), }, ) def get_next_month_url(self): next_month = self.get_next_month(self.first_day.date()) return reverse( 'chrono-manager-resource-month-view', kwargs={ 'pk': self.resource.pk, 'year': next_month.year, 'month': next_month.strftime('%m'), 'day': next_month.strftime('%d'), }, ) resource_monthly_view = ResourceMonthView.as_view() class ResourceRedirectView(RedirectView): def get_redirect_url(self, *args, **kwargs): return reverse('chrono-manager-resource-view', kwargs={'pk': kwargs['pk']}) resource_redirect_view = ResourceRedirectView.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 EventsTypeListView(ListView): template_name = 'chrono/manager_events_type_list.html' model = EventsType def dispatch(self, request, *args, **kwargs): if not request.user.is_staff: raise PermissionDenied() return super().dispatch(request, *args, **kwargs) events_type_list = EventsTypeListView.as_view() class EventsTypeAddView(CreateView): template_name = 'chrono/manager_events_type_form.html' model = EventsType 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-events-type-list') events_type_add = EventsTypeAddView.as_view() class EventsTypeEditView(UpdateView): template_name = 'chrono/manager_events_type_form.html' model = EventsType 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-events-type-list') def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) data = None if self.request.method == 'POST': data = self.request.POST context['formset'] = CustomFieldFormSet( data=data, initial=self.get_object().get_custom_fields(), ) return context def form_valid(self, form): self.object = form.save(commit=False) # save object and update cache only once return HttpResponseRedirect(self.get_success_url()) def post(self, *args, **kwargs): self.object = self.get_object() form = self.get_form() formset = CustomFieldFormSet(data=self.request.POST) if form.is_valid() and formset.is_valid(): response = self.form_valid(form) self.object.custom_fields = [] for sub_data in formset.cleaned_data: if not sub_data.get('varname'): continue self.object.custom_fields.append(sub_data) self.object.save() return response else: return self.form_invalid(form) events_type_edit = EventsTypeEditView.as_view() class EventsTypeDeleteView(DeleteView): template_name = 'chrono/manager_confirm_delete.html' model = EventsType 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-events-type-list') events_type_delete = EventsTypeDeleteView.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_str(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: ngettext( 'An agenda has been created.', '%(count)d agendas have been created.', x, ), 'update_noop': _('No agenda updated.'), 'update': lambda x: ngettext( 'An agenda has been updated.', '%(count)d agendas have been updated.', x, ), }, 'unavailability_calendars': { 'create_noop': _('No unavailability calendar created.'), 'create': lambda x: ngettext( 'An unavailability calendar has been created.', '%(count)d unavailability calendars have been created.', x, ), 'update_noop': _('No unavailability calendar updated.'), 'update': lambda x: ngettext( 'An unavailability calendar has been updated.', '%(count)d unavailability calendars have been updated.', x, ), }, 'events_types': { 'create_noop': _('No events type created.'), 'create': lambda x: ngettext( 'An events type has been created.', '%(count)d events types have been created.', x, ), 'update_noop': _('No events type updated.'), 'update': lambda x: ngettext( 'An events type has been updated.', '%(count)d events types have been updated.', x, ), }, 'resources': { 'create_noop': _('No resource created.'), 'create': lambda x: ngettext( 'A resource has been created.', '%(count)d resources have been created.', x, ), 'update_noop': _('No resource updated.'), 'update': lambda x: ngettext( 'A resource has been updated.', '%(count)d resources have been updated.', x, ), }, 'categories': { 'create_noop': _('No category created.'), 'create': lambda x: ngettext( 'A category has been created.', '%(count)d categories have been created.', x, ), 'update_noop': _('No category updated.'), 'update': lambda x: ngettext( 'A category has been updated.', '%(count)d categories 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 = ( len(results['agendas']['all']), len(results['unavailability_calendars']['all']), ) if (a_count, uc_count) == (1, 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) == (0, 1): # 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 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['events_types']['messages']) messages.info(self.request, results['resources']['messages']) messages.info(self.request, results['categories']['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): tab_anchor = None 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): url = reverse('chrono-manager-agenda-settings', kwargs={'pk': self.agenda.id}) if self.tab_anchor: url += '#open:%s' % self.tab_anchor return url 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') tab_anchor = 'delays' agenda_booking_delays = AgendaBookingDelaysView.as_view() class AgendaRolesView(AgendaEditView): form_class = AgendaRolesForm title = _('Configure roles') tab_anchor = 'permissions' agenda_roles = AgendaRolesView.as_view() class AgendaDisplaySettingsView(AgendaEditView): form_class = AgendaDisplaySettingsForm title = _("Configure display options") tab_anchor = 'display-options' def set_agenda(self, **kwargs): self.agenda = get_object_or_404(Agenda.objects.exclude(kind='virtual'), pk=kwargs.get('pk')) def get_initial(self): return {'booking_user_block_template': self.agenda.get_booking_user_block_template()} agenda_display_settings = AgendaDisplaySettingsView.as_view() class AgendaBookingCheckSettingsView(AgendaEditView): form_class = AgendaBookingCheckSettingsForm title = _("Configure booking check options") tab_anchor = 'booking-check-options' def set_agenda(self, **kwargs): self.agenda = get_object_or_404(Agenda, pk=kwargs.get('pk'), kind='events') 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 set_agenda(self, **kwargs): if kwargs.get('pk'): self.agenda = get_object_or_404(Agenda, pk=kwargs['pk']) else: self.agenda = get_object_or_404(Agenda, slug=kwargs['slug']) 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 == 'week': return redirect('chrono-manager-agenda-week-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.strftime('%m'), day=day.strftime('%d'), ) agenda_month_redirect_view = AgendaMonthRedirectView.as_view() class AgendaWeekRedirectView(AgendaMonthRedirectView): def get(self, request, *args, **kwargs): day = self.get_day() return redirect( 'chrono-manager-agenda-week-view', pk=self.agenda.pk, year=day.year, month=day.strftime('%m'), day=day.strftime('%d'), ) agenda_week_redirect_view = AgendaWeekRedirectView.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.strftime('%m'), day=day.strftime('%d'), ) 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 set_agenda(self, **kwargs): super().set_agenda(**kwargs) if self.agenda.kind == 'virtual': self.agenda._excluded_timeperiods = self.agenda.excluded_timeperiods.all() 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 try: date = datetime.date(int(self.get_year()), int(self.get_month()), 1) except ValueError: raise Http404 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) queryset = Event.annotate_booking_checks(queryset) else: self.agenda.prefetch_desks_and_exceptions( min_date=getattr(self, 'first_day', self.date), max_date=self.get_max_date() ) 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', 'agenda') .prefetch_related('booking_set') ) return queryset def get_exceptions_from_excluded_periods(self, date): return [ TimePeriodException( start_datetime=make_aware(datetime.datetime.combine(date, period.start_time)), end_datetime=make_aware(datetime.datetime.combine(date, period.end_time)), ) for period in getattr(self.agenda, '_excluded_timeperiods', []) if period.weekday == date.weekday() ] 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_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.strftime('%m'), 'day': previous_day.strftime('%d'), }, ) 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.strftime('%m'), 'day': next_day.strftime('%d'), }, ) def get_max_date(self): return self.date.date() + datetime.timedelta(days=1) 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.date == self.date.date() or t.weekday == self.date.weekday() and (not t.weekday_indexes or get_weekday_index(self.date) in t.weekday_indexes) ] 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 exceptions = desk.prefetched_exceptions + self.get_exceptions_from_excluded_periods( current_date.date() ) info['exceptions'] = [] for exception in 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 AgendaWeekMonthMixin: def get_queryset(self): qs = super().get_queryset() if self.agenda.kind != 'events': return qs return qs.order_by('start_datetime', 'label') @property def first_day(self): first_day = self.date if self.kind == 'month': first_day -= datetime.timedelta(days=first_day.day - 1) else: first_day -= datetime.timedelta(days=first_day.weekday()) return first_day def get_dated_queryset(self, **kwargs): # adjust min and max, incorrect as DayArchiveView is used to have Y/m/d in url kwargs['start_datetime__gte'] = self._make_date_lookup_arg(self.first_day) kwargs['start_datetime__lt'] = self._make_date_lookup_arg( getattr(self, 'get_next_%s' % self.kind)(self.first_day) ) return super().get_dated_queryset(**kwargs) def get_dated_items(self): date_list, object_list, extra_context = super().get_dated_items() if self.agenda.kind == 'events': min_start = self.first_day max_start = getattr(self, 'get_next_%s' % self.kind)(self.first_day) 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_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) context['kind'] = self.kind return context def get_timetable_infos(self): timeperiods = list(itertools.chain(*(d.timeperiod_set.all() for d in self.agenda.prefetched_desks))) 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 or (e.date and e.date.weekday() == 6) for e in timeperiods] ) hide_weekend_timeperiod = hide_sunday_timeperiod and not any( [e.weekday == 5 or (e.date and e.date.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 = 0 first_week_number = self.first_day.isocalendar()[1] last_week_number = first_week_number if self.kind == 'month': first_week_offset = int( (hide_sunday and self.first_day.weekday() == 6) or (hide_weekend and self.first_day.weekday() == 5) ) first_week_number = self.first_day.isocalendar()[1] if first_week_number >= 52: first_week_number = 0 last_day = self.get_next_month(self.first_day.date()) - datetime.timedelta(days=1) last_week_number = last_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.first_day + datetime.timedelta(week_index * 7) dow = date.isocalendar()[2] start_date = date - datetime.timedelta(dow) interval = datetime.timedelta(minutes=60) period = self.first_day.replace(hour=self.min_display.hour, minute=0) max_date = self.first_day.replace(hour=self.max_display.hour, minute=0) if period == max_date: # add at least an interval max_date = max_date + interval 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/week if self.kind == 'month' and timetable['other_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, } ) exceptions = desk.prefetched_exceptions + self.get_exceptions_from_excluded_periods( current_date.date() ) for exception in 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 class AgendaWeekView(AgendaWeekMonthMixin, AgendaDateView, DayArchiveView, WeekMixin): kind = 'week' def get_template_names(self): if self.agenda.kind == 'virtual': return ['chrono/manager_meetings_agenda_week_view.html'] return ['chrono/manager_%s_agenda_week_view.html' % self.agenda.kind] def get_previous_week(self, date): return date - datetime.timedelta(days=7) def get_next_week(self, date): return date + datetime.timedelta(days=7) def get_previous_week_url(self): previous_week = self.get_previous_week(self.first_day.date()) return reverse( 'chrono-manager-agenda-week-view', kwargs={ 'pk': self.agenda.id, 'year': previous_week.year, 'month': previous_week.strftime('%m'), 'day': previous_week.strftime('%d'), }, ) def get_next_week_url(self): next_week = self.get_next_week(self.first_day.date()) return reverse( 'chrono-manager-agenda-week-view', kwargs={ 'pk': self.agenda.id, 'year': next_week.year, 'month': next_week.strftime('%m'), 'day': next_week.strftime('%d'), }, ) def get_max_date(self): return self.get_next_week(self.first_day.date()) agenda_weekly_view = AgendaWeekView.as_view() class AgendaMonthView(AgendaWeekMonthMixin, AgendaDateView, DayArchiveView): kind = 'month' 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_previous_month_url(self): previous_month = self.get_previous_month(self.first_day.date()) return reverse( 'chrono-manager-agenda-month-view', kwargs={ 'pk': self.agenda.id, 'year': previous_month.year, 'month': previous_month.strftime('%m'), 'day': previous_month.strftime('%d'), }, ) def get_next_month_url(self): next_month = self.get_next_month(self.first_day.date()) return reverse( 'chrono-manager-agenda-month-view', kwargs={ 'pk': self.agenda.id, 'year': next_month.year, 'month': next_month.strftime('%m'), 'day': next_month.strftime('%d'), }, ) def get_max_date(self): return self.get_next_month(self.first_day.date()) agenda_monthly_view = AgendaMonthView.as_view() class AgendaRedirectView(RedirectView): def get_redirect_url(self, *args, **kwargs): return reverse('chrono-manager-agenda-view', kwargs={'pk': kwargs['pk']}) agenda_redirect_view = AgendaRedirectView.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 tab_anchor = 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): url = reverse('chrono-manager-agenda-settings', kwargs={'pk': self.agenda.id}) if self.tab_anchor: url += '#open:%s' % self.tab_anchor return url class ManagedDeskMixin: desk = None tab_anchor = 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): url = reverse('chrono-manager-agenda-settings', kwargs={'pk': self.desk.agenda.id}) if self.tab_anchor: url += '#open:%s' % self.tab_anchor return url class ManagedTimePeriodMixin: agenda = None tab_anchor = 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): url = reverse('chrono-manager-agenda-settings', kwargs={'pk': self.agenda.id}) if self.tab_anchor: url += '#open:%s' % self.tab_anchor return url class ManagedTimePeriodExceptionMixin: desk = None unavailability_calendar = None tab_anchor = 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 context['base_template'] = 'chrono/manager_agenda_settings.html' else: context['unavailability_calendar'] = self.unavailability_calendar context['base_template'] = 'chrono/manager_unavailability_calendar_settings.html' return context def get_success_url(self): if self.desk: url = reverse('chrono-manager-agenda-settings', kwargs={'pk': self.desk.agenda.id}) elif self.unavailability_calendar: url = reverse( 'chrono-manager-unavailability-calendar-settings', kwargs={'pk': self.unavailability_calendar.pk}, ) if self.tab_anchor: url += '#open:%s' % self.tab_anchor return url class AgendaSettingsRedirectView(RedirectView): def get_redirect_url(self, *args, **kwargs): agenda = get_object_or_404(Agenda, slug=kwargs['slug']) return reverse('chrono-manager-agenda-settings', kwargs={'pk': agenda.pk}) agenda_settings_redirect_view = AgendaSettingsRedirectView.as_view() 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'), pk=kwargs.get('pk'), ) def get_object(self, *args, **kwargs): if self.agenda.kind == 'meetings': self.agenda.prefetch_desks_and_exceptions(with_sources=True, min_date=now()) 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_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 AgendaEventDuplicateView(ManagedAgendaMixin, UpdateView): model = Event queryset = Event.objects.filter(primary_event__isnull=True) pk_url_kwarg = 'event_pk' form_class = EventDuplicateForm template_name = 'chrono/manager_event_duplicate_form.html' def get_success_url(self): return reverse('chrono-manager-event-edit', kwargs={'pk': self.agenda.id, 'event_pk': self.object.id}) def form_valid(self, form): messages.success(self.request, _('Event successfully duplicated.')) return super().form_valid(form) event_duplicate = AgendaEventDuplicateView.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 tab_anchor = 'notifications' 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 tab_anchor = 'reminders' 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 AgendaReminderTestView(ManagedAgendaMixin, FormView): template_name = 'chrono/manager_send_reminder_form.html' form_class = AgendaReminderTestForm def get_form_kwargs(self): kwargs = super(FormView, self).get_form_kwargs() kwargs['agenda'] = self.agenda return kwargs def form_valid(self, form): booking = form.cleaned_data['booking'] if form.cleaned_data.get('phone_number'): booking.user_phone_number = form.cleaned_data['phone_number'] booking.extra_phone_numbers.clear() if form.cleaned_data.get('email'): booking.user_email = form.cleaned_data['email'] booking.extra_emails.clear() for msg_type in form.cleaned_data['msg_type']: send_reminder(booking, msg_type) return super().form_valid(form) agenda_reminder_test = AgendaReminderTestView.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('{} ({})', label, _('event label')), description=format_html('{} ({})', paragraph, _('event description, if present')), pricing=format_html('{} ({})', '...', _('event pricing, if present')), url='#', ) booking = Booking(user_display_label='{{ user_display_label }}', form_url='#') booking.event = event button_label = format_html('{}
({})', _('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 def set_agenda(self, **kwargs): self.agenda = get_object_or_404(Agenda, pk=kwargs.get('pk'), kind='events') self.event = None if 'event_pk' in kwargs: self.event = get_object_or_404( Event, pk=kwargs.get('event_pk'), agenda=self.agenda, recurrence_days__isnull=True, cancelled=False, ) 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, event=self.event, data=self.request.GET or None) if self.request.GET: form.is_valid() context['form'] = form context['event'] = self.event return context def get_template_names(self): if self.event is not None: return ['chrono/manager_event_timesheet.html'] return ['chrono/manager_events_timesheet.html'] 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) if 'csv' in request.GET and context['form'].is_valid(): return self.csv(request, context) return self.render_to_response(context) def get_export_filename(self, context): if self.event is not None: return 'timesheet_{}_{}'.format( self.agenda.slug, self.event.slug, ) else: return 'timesheet_{}_{}_{}'.format( self.agenda.slug, context['form'].cleaned_data['date_start'].strftime('%Y-%m-%d'), context['form'].cleaned_data['date_end'].strftime('%Y-%m-%d'), ) 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="%s.pdf"' % self.get_export_filename(context) return response def csv(self, request, context): response = HttpResponse(content_type='text/csv') response['Content-Disposition'] = 'attachment; filename="%s.csv"' % self.get_export_filename(context) writer = csv.writer(response) form = context['form'] slots = form.get_slots() events_num = len(slots['events']) activity_display = form.cleaned_data.get('activity_display') def get_booked(item, date): booked = item['dates'].get(date) if booked is True: return '☐' if booked is None: return '-' return '' def write_group(dates, grouper): # title if form.cleaned_data['group_by']: writer.writerow(['%s: %s' % (form.cleaned_data['group_by'], grouper['grouper'])]) # headers headers = [ _('First name'), _('Last name'), ] headers += slots['extra_data'] if activity_display != 'col' and events_num > 1: headers.append(_('Activity')) for date, events in dates: if activity_display == 'col': for event in events: headers.append( _('%(event)s of %(date)s') % {'event': event, 'date': date_format(date, 'd-m')} ) else: headers.append(date_format(date, 'D d/m')) writer.writerow(headers) # and data for user in grouper['users']: if activity_display == 'col': data = [ user['user_first_name'], user['user_last_name'], ] data += [user['extra_data'].get(k) or '' for k in slots['extra_data']] for date, events in dates: for event in events: for item in user['events']: if item['event'] != event: continue data.append(get_booked(item, date)) break writer.writerow(data) else: for i, item in enumerate(user['events']): if i == 0: data = [ user['user_first_name'], user['user_last_name'], ] data += [user['extra_data'].get(k) or '' for k in slots['extra_data']] else: data = ['', ''] data += ['' for k in slots['extra_data']] if events_num > 1: data.append(item['event']) for date, events in dates: data.append(get_booked(item, date)) writer.writerow(data) for dates in slots['dates']: for grouper in slots['users']: write_group(dates, grouper) 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 is_ajax(self.request): 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.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') event.present_count = len([b for b in context['booked'] if b.user_was_present is True]) event.absent_count = len([b for b in context['booked'] if b.user_was_present is False]) event.notchecked_count = len([b for b in context['booked'] if b.user_was_present is None]) return context event_view = EventDetailView.as_view() class EventDetailRedirectView(RedirectView): def get_redirect_url(self, *args, **kwargs): agenda = get_object_or_404(Agenda, slug=kwargs['slug']) event = get_object_or_404(Event, slug=kwargs['event_slug'], agenda=agenda) return reverse('chrono-manager-event-view', kwargs={'pk': agenda.pk, 'event_pk': event.pk}) event_redirect_view = EventDetailRedirectView.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, 'day': day.day}, ) 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, pk=kwargs.get('pk'), kind='events', ) def get_queryset(self): queryset = super().get_queryset() queryset = Event.annotate_booking_checks(queryset) return queryset.filter( Q(start_datetime__date__lte=now().date()) | Q(agenda__enable_check_for_future_events=True), agenda=self.agenda, 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 and not isinstance(v, (list, dict)): 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} booking_qs = event.booking_set booked_qs = booking_qs.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 = booking_qs.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( data=self.request.GET or None, queryset=booked_qs, agenda=self.agenda, filters=filters ) subscription_filterset = SubscriptionCheckFilterSet( data=self.request.GET or None, 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.absence_form = BookingCheckAbsenceForm( agenda=self.agenda, initial={'check_type': booking.user_check_type_slug}, ) booking.presence_form = BookingCheckPresenceForm( agenda=self.agenda, initial={'check_type': booking.user_check_type_slug}, ) booking.kind = 'booking' results.append(booking) for subscription in subscription_filterset.qs: subscription.absence_form = BookingCheckAbsenceForm( agenda=self.agenda, ) subscription.presence_form = BookingCheckPresenceForm( agenda=self.agenda, ) subscription.kind = 'subscription' results.append(subscription) # sort results if ( booked_filterset.form.is_valid() and booked_filterset.form.cleaned_data.get('sort') == 'firstname,lastname' ): sort_fields = ['user_first_name', 'user_last_name'] else: sort_fields = ['user_last_name', 'user_first_name'] results = sorted(results, key=attrgetter(*sort_fields, 'user_external_id')) # set context context['booked_without_status'] = booked_without_status context['absence_form'] = BookingCheckAbsenceForm(agenda=self.agenda) context['presence_form'] = BookingCheckPresenceForm(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), Q(start_datetime__date__lte=now().date()) | Q(agenda__enable_check_for_future_events=True), pk=kwargs.get('event_pk'), agenda=self.agenda, cancelled=False, ) def get_bookings(self): return self.event.booking_set.filter( event__agenda=self.agenda, event__cancelled=False, cancellation_datetime__isnull=True, in_waiting_list=False, user_was_present__isnull=True, ) def get_check_type(self, kind): form = self.get_form() if form.is_valid() and form.cleaned_data['check_type']: check_types = getattr(form, '%s_check_types' % kind) for ct in check_types: if ct.slug == form.cleaned_data['check_type']: return ct 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, FormView): form_class = BookingCheckPresenceForm def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs['agenda'] = self.agenda return kwargs def post(self, request, *args, **kwargs): qs_kwargs = {} check_type = self.get_check_type(kind='presence') qs_kwargs['user_check_type_slug'] = check_type.slug if check_type else None qs_kwargs['user_check_type_label'] = check_type.label if check_type else None bookings = self.get_bookings() bookings.update(user_was_present=True, **qs_kwargs) self.event.set_is_checked() return self.response(request) event_presence = EventPresenceView.as_view() class EventAbsenceView(EventCheckMixin, ViewableAgendaMixin, FormView): form_class = BookingCheckAbsenceForm def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs['agenda'] = self.agenda return kwargs def post(self, request, *args, **kwargs): qs_kwargs = {} check_type = self.get_check_type(kind='absence') qs_kwargs['user_check_type_slug'] = check_type.slug if check_type else None qs_kwargs['user_check_type_label'] = check_type.label if check_type else None 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 tab_anchor = 'resources' 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' tab_anchor = 'resources' 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( '%s#open:time-periods' % 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 tab_anchor = 'meeting-types' agenda_add_meeting_type = AgendaAddMeetingTypeView.as_view() class MeetingTypeEditView(ManagedAgendaSubobjectMixin, UpdateView): template_name = 'chrono/manager_meeting_type_form.html' model = MeetingType form_class = MeetingTypeForm tab_anchor = 'meeting-types' def form_valid(self, form): try: with transaction.atomic(): return super().form_valid(form) except IntegrityError as e: if 'tstzrange_constraint' in str(e): form.add_error( 'duration', _('Not possible to change duration: existing events will overlap.') ) return self.form_invalid(form) raise meeting_type_edit = MeetingTypeEditView.as_view() class MeetingTypeDeleteView(ManagedAgendaSubobjectMixin, DeleteView): template_name = 'chrono/manager_confirm_delete.html' model = MeetingType tab_anchor = 'meeting-types' 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'], weekday_indexes=form.cleaned_data.get('weekday_indexes'), ) 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 tab_anchor = 'time-periods' 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 = ExcludedPeriodAddForm tab_anchor = 'time-periods' 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): model = TimePeriod tab_anchor = 'time-periods' def get_form_class(self): if self.object.weekday is not None: return TimePeriodForm else: return DateTimePeriodForm def get_template_names(self): if self.object.weekday is not None: return ['chrono/manager_time_period_form.html'] else: return ['chrono/manager_date_time_period_form.html'] time_period_edit = TimePeriodEditView.as_view() class TimePeriodDeleteView(ManagedTimePeriodMixin, DeleteView): template_name = 'chrono/manager_confirm_delete.html' model = TimePeriod tab_anchor = 'time-periods' 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, date=time_period.date, ).first() if tp is not None: tp.delete() return response time_period_delete = TimePeriodDeleteView.as_view() class AgendaAddDateTimePeriodView(ManagedDeskMixin, FormView): template_name = 'chrono/manager_date_time_period_form.html' model = TimePeriod form_class = DateTimePeriodForm tab_anchor = 'time-periods' def form_valid(self, form): create_kwargs = { 'date': form.cleaned_data['date'], 'start_time': form.cleaned_data['start_time'], 'end_time': form.cleaned_data['end_time'], } if self.desk.agenda.desk_simple_management: for desk in self.desk.agenda.desk_set.all(): TimePeriod.objects.create(desk=desk, **create_kwargs) else: TimePeriod.objects.create(desk=self.desk, **create_kwargs) return super().form_valid(form) agenda_add_date_time_period = AgendaAddDateTimePeriodView.as_view() class AgendaDateTimePeriodListView(ManagedDeskMixin, ListView): template_name = 'chrono/manager_date_time_period_list.html' model = TimePeriod paginate_by = 20 def get_queryset(self): return self.model.objects.filter(desk=self.desk, date__isnull=False).order_by('-date') agenda_date_time_period_list = AgendaDateTimePeriodListView.as_view() class AgendaDeskMixin: def get_success_url(self): if not self.object.agenda.desk_simple_management: self.tab_anchor = 'time-periods' return super().get_success_url() class AgendaAddDesk(AgendaDeskMixin, ManagedAgendaMixin, CreateView): template_name = 'chrono/manager_desk_form.html' model = Desk form_class = NewDeskForm agenda = None tab_anchor = 'desks' 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(AgendaDeskMixin, ManagedAgendaSubobjectMixin, UpdateView): template_name = 'chrono/manager_desk_form.html' model = Desk form_class = DeskForm tab_anchor = 'desks' def get_queryset(self): return super().get_queryset().filter(agenda__kind='meetings') desk_edit = DeskEditView.as_view() class DeskDeleteView(AgendaDeskMixin, ManagedAgendaSubobjectMixin, DeleteView): template_name = 'chrono/manager_confirm_delete.html' model = Desk tab_anchor = 'desks' 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 tab_anchor = 'time-periods' 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 = ngettext( '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 tab_anchor = 'time-periods' 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() ) 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 tab_anchor = 'time-periods' def get_success_url(self): if self.desk: referer = self.request.headers.get('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 = DeskExceptionsImportForm template_name = 'chrono/manager_import_exceptions.html' tab_anchor = 'time-periods' 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()) ) ) ) context['base_template'] = 'chrono/manager_agenda_settings.html' 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_str(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_str(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(ManagedTimePeriodExceptionMixin, DeleteView): template_name = 'chrono/manager_confirm_source_delete.html' model = TimePeriodExceptionSource tab_anchor = 'time-periods' def delete(self, request, *args, **kwargs): source = self.get_object() response = super().delete(request, *args, **kwargs) if not source.desk or 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(ManagedTimePeriodExceptionMixin, UpdateView): model = TimePeriodExceptionSource form_class = TimePeriodExceptionSourceReplaceForm template_name = 'chrono/manager_replace_exceptions.html' tab_anchor = 'time-periods' def get_queryset(self): queryset = super().get_queryset() return queryset.filter(ics_filename__isnull=False) def import_file(self, obj, form): source = obj.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): try: if self.desk and self.desk.agenda.desk_simple_management: for _desk in self.desk.agenda.desk_set.all(): self.import_file(_desk, form) else: self.import_file(self.desk or self.unavailability_calendar, form) except ICSError as e: form.add_error(None, force_str(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(ManagedTimePeriodExceptionMixin, DetailView): model = TimePeriodExceptionSource tab_anchor = 'time-periods' def get_queryset(self): queryset = super().get_queryset() return queryset.filter(ics_url__isnull=False) def import_file(self, obj): source = obj.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): try: if self.desk and self.desk.agenda.desk_simple_management: for _desk in self.desk.agenda.desk_set.all(): self.import_file(_desk) else: self.import_file(self.desk or self.unavailability_calendar) except ICSError as e: messages.error(self.request, force_str(e)) messages.info(self.request, _('Exceptions will be synchronized in a few minutes.')) # redirect to settings return HttpResponseRedirect(self.get_success_url()) 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, 'day': day.day}, ) booking_cancel = BookingCancelView.as_view() class BookingCheckMixin: def get_booking(self, **kwargs): booking = get_object_or_404( Booking, Q(event__checked=False) | Q(event__agenda__disable_check_update=False), Q(event__start_datetime__date__lte=now().date()) | Q(event__agenda__enable_check_for_future_events=True), pk=kwargs['booking_pk'], event__agenda=self.agenda, event__cancelled=False, in_waiting_list=False, primary_booking__isnull=True, ) if not booking.event.agenda.subscriptions.exists() and booking.cancellation_datetime is not None: raise Http404 return booking def get_check_type(self, kind): form = self.get_form() if form.is_valid() and form.cleaned_data['check_type']: check_types = getattr(form, '%s_check_types' % kind) for ct in check_types: if ct.slug == form.cleaned_data['check_type']: return ct def response(self, request, booking): if is_ajax(request): booking.absence_form = BookingCheckAbsenceForm( agenda=self.agenda, initial={'check_type': booking.user_check_type_slug} ) booking.presence_form = BookingCheckPresenceForm( agenda=self.agenda, initial={'check_type': booking.user_check_type_slug} ) booking.kind = 'booking' 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 PresenceViewMixin: 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) check_type = self.get_check_type(kind='presence') booking.mark_user_presence( check_type_slug=check_type.slug if check_type else None, check_type_label=check_type.label if check_type else None, ) return self.response(request, booking) class AbsenceViewMixin: 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) check_type = self.get_check_type(kind='absence') booking.mark_user_absence( check_type_slug=check_type.slug if check_type else None, check_type_label=check_type.label if check_type else None, ) return self.response(request, booking) class BookingPresenceView(ViewableAgendaMixin, BookingCheckMixin, PresenceViewMixin, FormView): form_class = BookingCheckPresenceForm booking_presence = BookingPresenceView.as_view() class BookingAbsenceView(ViewableAgendaMixin, BookingCheckMixin, AbsenceViewMixin, FormView): form_class = BookingCheckAbsenceForm booking_absence = BookingAbsenceView.as_view() class BookingResetView(ViewableAgendaMixin, BookingCheckMixin, FormView): def post(self, request, *args, **kwargs): booking = self.get_booking(**kwargs) booking.reset_user_was_present() return self.response(request, booking) booking_reset = BookingResetView.as_view() class SubscriptionCheckMixin(BookingCheckMixin): def get_booking(self, **kwargs): event = get_object_or_404( Event, Q(checked=False) | Q(agenda__disable_check_update=False), Q(start_datetime__date__lte=now().date()) | Q(agenda__enable_check_for_future_events=True), agenda=self.agenda, cancelled=False, pk=kwargs['event_pk'], ) subscription = get_object_or_404( Subscription, agenda=self.agenda, pk=kwargs['subscription_pk'], date_start__lte=event.start_datetime, date_end__gt=event.start_datetime, ) try: booking = event.booking_set.get(user_external_id=subscription.user_external_id) raise Http404 except Booking.MultipleObjectsReturned: raise Http404 except Booking.DoesNotExist: pass booking = event.booking_set.create( user_external_id=subscription.user_external_id, user_last_name=subscription.user_last_name, user_first_name=subscription.user_first_name, user_email=subscription.user_email, user_phone_number=subscription.user_phone_number, extra_data=subscription.extra_data, ) # create booking return booking class SubscriptionPresenceView(ViewableAgendaMixin, SubscriptionCheckMixin, PresenceViewMixin, FormView): form_class = BookingCheckPresenceForm subscription_presence = SubscriptionPresenceView.as_view() class SubscriptionAbsenceView(ViewableAgendaMixin, SubscriptionCheckMixin, AbsenceViewMixin, FormView): form_class = BookingCheckAbsenceForm subscription_absence = SubscriptionAbsenceView.as_view() class BookingExtraUserBlock(ViewableAgendaMixin, View): def get(self, request, *args, **kwargs): booking = get_object_or_404( Booking, pk=kwargs['booking_pk'], event__agenda=self.agenda, event__cancelled=False, primary_booking__isnull=True, ) return HttpResponse(booking.get_extra_user_block(request)) booking_extra_user_block = BookingExtraUserBlock.as_view() class SubscriptionExtraUserBlock(ViewableAgendaMixin, View): def get(self, request, *args, **kwargs): subscription = get_object_or_404( Subscription, agenda=self.agenda, pk=kwargs['subscription_pk'], ) return HttpResponse(subscription.get_extra_user_block(request)) subscription_extra_user_block = SubscriptionExtraUserBlock.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, 'day': day.day}, ) 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 TimePeriodExceptionSourceToggleView(ManagedTimePeriodExceptionMixin, 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( '%s#open:time-periods' % 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') def delete(self, request, *args, **kwargs): try: return super().delete(request, *args, **kwargs) except ProtectedError: messages.warning( request, _('This calendar cannot be deleted because it is used by shared custody agendas.') ) return HttpResponseRedirect(self.get_object().get_absolute_url()) 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() class UnavailabilityCalendarImportUnavailabilitiesView(ManagedUnavailabilityCalendarMixin, UpdateView): model = UnavailabilityCalendar form_class = UnavailabilityCalendarExceptionsImportForm template_name = 'chrono/manager_import_exceptions.html' def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) unavailabilty_calendar = self.get_object() context['exception_sources'] = unavailabilty_calendar.timeperiodexceptionsource_set.all() context['base_template'] = 'chrono/manager_unavailability_calendar_settings.html' return context def import_file(self, form): unavailabilty_calendar = self.get_object() if form.cleaned_data['ics_file']: ics_file = form.cleaned_data['ics_file'] source = unavailabilty_calendar.timeperiodexceptionsource_set.create( ics_filename=ics_file.name, ics_file=ics_file ) ics_file.seek(0) elif form.cleaned_data['ics_url']: source = unavailabilty_calendar.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): try: with transaction.atomic(): source = self.import_file(form) except ICSError as e: form.add_error(None, force_str(e)) return self.form_invalid(form) try: source.refresh_timeperiod_exceptions(data=source._parsed) except ICSError as e: form.add_error(None, force_str(e)) return self.form_invalid(form) messages.info(self.request, _('Exceptions will be imported in a few minutes.')) return super().form_valid(form) unavailability_calendar_import_unavailabilities = UnavailabilityCalendarImportUnavailabilitiesView.as_view() class SharedCustodyAgendaMixin: agenda = None tab_anchor = None def set_agenda(self, **kwargs): self.agenda = get_object_or_404(SharedCustodyAgenda, 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): if user.is_staff: return True management_role = SharedCustodySettings.get_singleton().management_role return bool(management_role in user.groups.all()) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['agenda'] = self.agenda context['user_can_manage'] = True return context 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): url = reverse('chrono-manager-shared-custody-agenda-settings', kwargs={'pk': self.agenda.id}) if self.tab_anchor: url += '#open:%s' % self.tab_anchor return url class SharedCustodyAgendaView(SharedCustodyAgendaMixin, RedirectView): def get_redirect_url(self, *args, **kwargs): return reverse( 'chrono-manager-shared-custody-agenda-month-view', kwargs={'pk': self.agenda.pk, 'year': now().year, 'month': now().month}, ) shared_custody_agenda_view = SharedCustodyAgendaView.as_view() class SharedCustodyAgendaMonthView(SharedCustodyAgendaMixin, YearMixin, MonthMixin, DateMixin, DetailView): template_name = 'chrono/manager_shared_custody_agenda_month_view.html' model = SharedCustodyAgenda def dispatch(self, request, *args, **kwargs): try: self.date = datetime.date(year=int(self.get_year()), month=int(self.get_month()), day=1) except ValueError: raise Http404('invalid date') return super().dispatch(request, *args, **kwargs) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['today'] = localtime().date() first_monday_this_month = self.date - datetime.timedelta(days=self.date.weekday()) first_monday_next_month = self.date + relativedelta(months=1, day=1, weekday=MO(1)) slots = self.object.get_custody_slots(first_monday_this_month, first_monday_next_month) slots_by_week = collections.defaultdict(list) for slot in slots: slots_by_week[slot.date.isocalendar()[1]].append(slot) context['slots_by_week'] = dict(slots_by_week) return context def get_previous_month_url(self): previous_month = self.date - relativedelta(months=1) return reverse( 'chrono-manager-shared-custody-agenda-month-view', kwargs={'pk': self.object.id, 'year': previous_month.year, 'month': previous_month.month}, ) def get_next_month_url(self): next_month = self.date + relativedelta(months=1) return reverse( 'chrono-manager-shared-custody-agenda-month-view', kwargs={'pk': self.object.id, 'year': next_month.year, 'month': next_month.month}, ) shared_custody_agenda_monthly_view = SharedCustodyAgendaMonthView.as_view() class SharedCustodyAgendaSettings(SharedCustodyAgendaMixin, DetailView): template_name = 'chrono/manager_shared_custody_agenda_settings.html' model = SharedCustodyAgenda def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['has_holidays'] = bool(SharedCustodySettings.get_singleton().holidays_calendar_id) context['exceptional_periods'] = SharedCustodyPeriod.objects.filter(holiday_rule__isnull=True) return context shared_custody_agenda_settings = SharedCustodyAgendaSettings.as_view() class SharedCustodyAgendaDeleteView(SharedCustodyAgendaMixin, DeleteView): template_name = 'chrono/manager_confirm_delete.html' model = SharedCustodyAgenda pk_url_kwarg = 'pk' 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-homepage') shared_custody_agenda_delete = SharedCustodyAgendaDeleteView.as_view() class SharedCustodyAgendaAddRuleView(SharedCustodyAgendaMixin, CreateView): title = _('Add custody rule') template_name = 'chrono/manager_agenda_form.html' form_class = SharedCustodyRuleForm model = SharedCustodyRule shared_custody_agenda_add_rule = SharedCustodyAgendaAddRuleView.as_view() class SharedCustodyAgendaEditRuleView(SharedCustodyAgendaMixin, UpdateView): title = _('Edit custody rule') template_name = 'chrono/manager_agenda_form.html' form_class = SharedCustodyRuleForm model = SharedCustodyRule pk_url_kwarg = 'rule_pk' shared_custody_agenda_edit_rule = SharedCustodyAgendaEditRuleView.as_view() class SharedCustodyAgendaDeleteRuleView(SharedCustodyAgendaMixin, DeleteView): template_name = 'chrono/manager_confirm_delete.html' model = SharedCustodyRule pk_url_kwarg = 'rule_pk' shared_custody_agenda_delete_rule = SharedCustodyAgendaDeleteRuleView.as_view() class SharedCustodyAgendaAddHolidayRuleView(SharedCustodyAgendaMixin, CreateView): title = _('Add custody rule during holidays') template_name = 'chrono/manager_agenda_form.html' form_class = SharedCustodyHolidayRuleForm model = SharedCustodyHolidayRule tab_anchor = 'holidays' shared_custody_agenda_add_holiday_rule = SharedCustodyAgendaAddHolidayRuleView.as_view() class SharedCustodyAgendaEditHolidayRuleView(SharedCustodyAgendaMixin, UpdateView): title = _('Edit custody rule during holidays') template_name = 'chrono/manager_agenda_form.html' form_class = SharedCustodyHolidayRuleForm model = SharedCustodyHolidayRule pk_url_kwarg = 'rule_pk' tab_anchor = 'holidays' shared_custody_agenda_edit_holiday_rule = SharedCustodyAgendaEditHolidayRuleView.as_view() class SharedCustodyAgendaDeleteHolidayRuleView(SharedCustodyAgendaMixin, DeleteView): template_name = 'chrono/manager_confirm_delete.html' model = SharedCustodyHolidayRule pk_url_kwarg = 'rule_pk' tab_anchor = 'holidays' shared_custody_agenda_delete_holiday_rule = SharedCustodyAgendaDeleteHolidayRuleView.as_view() class SharedCustodyAgendaAddPeriodView(SharedCustodyAgendaMixin, CreateView): title = _('Add custody period') template_name = 'chrono/manager_agenda_form.html' form_class = SharedCustodyPeriodForm model = SharedCustodyPeriod tab_anchor = 'time-periods' shared_custody_agenda_add_period = SharedCustodyAgendaAddPeriodView.as_view() class SharedCustodyAgendaEditPeriodView(SharedCustodyAgendaMixin, UpdateView): title = _('Edit custody period') template_name = 'chrono/manager_agenda_form.html' form_class = SharedCustodyPeriodForm model = SharedCustodyPeriod pk_url_kwarg = 'period_pk' tab_anchor = 'time-periods' shared_custody_agenda_edit_period = SharedCustodyAgendaEditPeriodView.as_view() class SharedCustodyAgendaDeletePeriodView(SharedCustodyAgendaMixin, DeleteView): template_name = 'chrono/manager_confirm_delete.html' model = SharedCustodyPeriod pk_url_kwarg = 'period_pk' tab_anchor = 'time-periods' shared_custody_agenda_delete_period = SharedCustodyAgendaDeletePeriodView.as_view() class SharedCustodySettingsView(UpdateView): title = _('Shared custody settings') template_name = 'chrono/manager_agenda_form.html' form_class = SharedCustodySettingsForm model = SharedCustodySettings 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_object(self): return SharedCustodySettings.get_singleton() shared_custody_settings = SharedCustodySettingsView.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_str(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