chrono/chrono/manager/views.py

4294 lines
154 KiB
Python

# chrono - agendas system
# Copyright (C) 2016 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import collections
import copy
import csv
import datetime
import itertools
import json
import math
import uuid
from operator import attrgetter
import requests
from 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('{} <small>({})</small>', label, _('event label')),
description=format_html('{} <small>({})</small>', paragraph, _('event description, if present')),
pricing=format_html('{} <small>({})</small>', '...', _('event pricing, if present')),
url='#',
)
booking = Booking(user_display_label='{{ user_display_label }}', form_url='#')
booking.event = event
button_label = format_html('{}<br>({})', _('More information'), _('link to event url, if present'))
reminder_ctx = {
'booking': booking,
'in_x_days': _('tomorrow') if days == 1 else _('in %s days') % days,
'date': booking.event.start_datetime,
'email_extra_info': self.agenda.reminder_settings.email_extra_info,
'sms_extra_info': self.agenda.reminder_settings.sms_extra_info,
'event_url_button_label': button_label,
}
if self.type_ == 'email':
ctx['subject'] = render_to_string(
'agendas/%s_reminder_subject.txt' % kind, reminder_ctx, request=self.request
).strip()
ctx['message'] = render_to_string(
'agendas/%s_reminder_body.html' % kind, reminder_ctx, request=self.request
)
elif self.type_ == 'sms':
ctx['message'] = render_to_string(
'agendas/%s_reminder_message.txt' % kind, reminder_ctx, request=self.request
)
return ctx
agenda_reminder_preview = AgendaReminderPreviewView.as_view()
class EventsTimesheetView(ViewableAgendaMixin, DetailView):
model = Agenda
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