4080 lines
146 KiB
Python
4080 lines
146 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.timezone import localtime, make_aware, make_naive, now
|
|
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,
|
|
MonthArchiveView,
|
|
RedirectView,
|
|
TemplateView,
|
|
UpdateView,
|
|
View,
|
|
WeekArchiveView,
|
|
)
|
|
from django.views.generic.dates import MonthMixin, 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,
|
|
TimePeriod,
|
|
TimePeriodException,
|
|
TimePeriodExceptionSource,
|
|
UnavailabilityCalendar,
|
|
VirtualMember,
|
|
)
|
|
from chrono.utils.date import get_weekday_index
|
|
|
|
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,
|
|
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_years(self):
|
|
year = now().year
|
|
return [str(x) for x in range(year - 1, year + 5)]
|
|
|
|
|
|
class ResourceDayView(DateMixin, DayArchiveView):
|
|
template_name = 'chrono/manager_resource_day_view.html'
|
|
model = Event
|
|
month_format = '%m'
|
|
date_field = 'start_datetime'
|
|
allow_empty = True
|
|
allow_future = True
|
|
|
|
def dispatch(self, request, *args, **kwargs):
|
|
self.resource = get_object_or_404(Resource, pk=kwargs['pk'])
|
|
if not self.resource.can_be_viewed(request.user):
|
|
raise PermissionDenied()
|
|
# specify 6am time to get the expected timezone on daylight saving time
|
|
# days.
|
|
try:
|
|
self.date = make_aware(
|
|
datetime.datetime.strptime(
|
|
'%s-%s-%s 06:00' % (self.get_year(), self.get_month(), self.get_day()), '%Y-%m-%d %H:%M'
|
|
)
|
|
)
|
|
except ValueError: # day is out of range for month
|
|
# redirect to last day of month
|
|
date = datetime.date(int(self.get_year()), int(self.get_month()), 1)
|
|
date += datetime.timedelta(days=40)
|
|
date = date.replace(day=1)
|
|
date -= datetime.timedelta(days=1)
|
|
return HttpResponseRedirect(
|
|
reverse(
|
|
'chrono-manager-resource-day-view',
|
|
kwargs={'pk': self.resource.pk, 'year': date.year, 'month': date.month, 'day': date.day},
|
|
)
|
|
)
|
|
return super().dispatch(request, *args, **kwargs)
|
|
|
|
def get_queryset(self):
|
|
queryset = (
|
|
self.resource.event_set.all()
|
|
.select_related('meeting_type', '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.month,
|
|
'day': previous_day.day,
|
|
},
|
|
)
|
|
|
|
def get_next_day_url(self):
|
|
next_day = self.date.date() + datetime.timedelta(days=1)
|
|
return reverse(
|
|
'chrono-manager-resource-day-view',
|
|
kwargs={
|
|
'pk': self.resource.pk,
|
|
'year': next_day.year,
|
|
'month': next_day.month,
|
|
'day': next_day.day,
|
|
},
|
|
)
|
|
|
|
def get_timetable_infos(self):
|
|
interval = datetime.timedelta(minutes=60)
|
|
|
|
min_event = max_event = None
|
|
timeperiods = TimePeriod.objects.filter(desk__agenda__resources=self.resource).aggregate(
|
|
Min('start_time'), Max('end_time')
|
|
)
|
|
min_timeperiod = timeperiods['start_time__min']
|
|
max_timeperiod = timeperiods['end_time__max']
|
|
active_events = [
|
|
x for x in self.object_list if any([y.cancellation_datetime is None for y in x.booking_set.all()])
|
|
]
|
|
if active_events:
|
|
min_event = min(localtime(x.start_datetime).time() for x in active_events)
|
|
max_event = max(localtime(x.start_datetime + interval).time() for x in active_events)
|
|
if min_timeperiod is None and min_event is None:
|
|
return
|
|
min_display = min(min_timeperiod or datetime.time(23), min_event or datetime.time(23))
|
|
max_display = max(max_timeperiod or datetime.time(0), max_event or datetime.time(0))
|
|
|
|
current_date = self.date.replace(hour=min_display.hour, minute=0)
|
|
max_date = self.date.replace(hour=max_display.hour, minute=0)
|
|
if max_display.minute != 0:
|
|
# until the end of the last hour.
|
|
max_date += datetime.timedelta(hours=1)
|
|
|
|
while current_date < max_date:
|
|
info = {}
|
|
info['bookings'] = bookings = [] # bookings for this resource
|
|
finish_datetime = current_date + interval
|
|
for event in [
|
|
x
|
|
for x in self.object_list
|
|
if x.start_datetime >= current_date and x.start_datetime < finish_datetime
|
|
]:
|
|
# don't consider cancelled bookings
|
|
for booking in [x for x in event.booking_set.all() if not x.cancellation_datetime]:
|
|
booking.css_top = int(100 * event.start_datetime.minute / 60)
|
|
booking.css_height = int(100 * event.meeting_type.duration / 60)
|
|
bookings.append(booking)
|
|
yield current_date, info
|
|
current_date += interval
|
|
|
|
|
|
resource_day_view = ResourceDayView.as_view()
|
|
|
|
|
|
class ResourceWeekMonthMixin:
|
|
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['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.date.isocalendar()[1]
|
|
last_week_number = first_week_number
|
|
if self.kind == 'month':
|
|
first_week_offset = int(
|
|
(hide_sunday and self.date.weekday() == 6) or (hide_weekend and self.date.weekday() == 5)
|
|
)
|
|
if first_week_number >= 52:
|
|
first_week_number = 0
|
|
last_day = self.get_next(self.date.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.date + datetime.timedelta(week_index * 7)
|
|
dow = date.isocalendar()[2]
|
|
start_date = date - datetime.timedelta(dow)
|
|
|
|
interval = datetime.timedelta(minutes=60)
|
|
period = self.date.replace(hour=self.min_display.hour, minute=0)
|
|
max_date = self.date.replace(hour=self.max_display.hour, minute=0)
|
|
if self.max_display.minute != 0:
|
|
# until the end of the last hour.
|
|
max_date += datetime.timedelta(hours=1)
|
|
|
|
periods = []
|
|
while period < max_date:
|
|
periods.append(period)
|
|
period = period + interval
|
|
|
|
return {
|
|
'days': [
|
|
self.get_day_timetable_infos(start_date + datetime.timedelta(i), interval)
|
|
for i in range(1, 8 - week_end_offset)
|
|
],
|
|
'periods': periods,
|
|
}
|
|
|
|
def get_day_timetable_infos(self, day, interval):
|
|
day = make_aware(make_naive(day)) # give day correct timezone
|
|
period = current_date = day.replace(hour=self.min_display.hour, minute=0)
|
|
timetable = {
|
|
'date': current_date,
|
|
'today': day.date() == datetime.date.today(),
|
|
'other_month': day.month != self.date.month,
|
|
'infos': {'booked_slots': []},
|
|
}
|
|
|
|
max_date = day.replace(hour=self.max_display.hour, minute=0)
|
|
if self.max_display.minute != 0:
|
|
# until the end of the last hour.
|
|
max_date += datetime.timedelta(hours=1)
|
|
|
|
# compute booking and opening hours only for current month/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, WeekArchiveView):
|
|
template_name = 'chrono/manager_resource_week_view.html'
|
|
model = Event
|
|
week_format = '%W'
|
|
date_field = 'start_datetime'
|
|
allow_empty = True
|
|
allow_future = True
|
|
kind = 'week'
|
|
|
|
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:
|
|
date = datetime.datetime.strptime('%s-W%s-1' % (self.get_year(), self.get_week()), "%Y-W%W-%w")
|
|
self.date = make_aware(
|
|
datetime.datetime.strptime(
|
|
'%s-%s-%s 06:00' % (self.get_year(), date.month, date.day), '%Y-%m-%d %H:%M'
|
|
)
|
|
)
|
|
except ValueError:
|
|
raise Http404
|
|
return super().dispatch(request, *args, **kwargs)
|
|
|
|
def get_previous_week_url(self):
|
|
previous_week = self.get_previous_week(self.date.date())
|
|
return reverse(
|
|
'chrono-manager-resource-week-view',
|
|
kwargs={'pk': self.resource.pk, 'year': previous_week.year, 'week': previous_week.strftime('%W')},
|
|
)
|
|
|
|
def get_next_week_url(self):
|
|
next_week = self.get_next_week(self.date.date())
|
|
return reverse(
|
|
'chrono-manager-resource-week-view',
|
|
kwargs={'pk': self.resource.pk, 'year': next_week.year, 'week': next_week.strftime('%W')},
|
|
)
|
|
|
|
def get_next(self, date):
|
|
return self.get_next_week(date)
|
|
|
|
|
|
resource_weekly_view = ResourceWeekView.as_view()
|
|
|
|
|
|
class ResourceMonthView(ResourceWeekMonthMixin, DateMixin, MonthArchiveView):
|
|
template_name = 'chrono/manager_resource_month_view.html'
|
|
model = Event
|
|
month_format = '%m'
|
|
date_field = 'start_datetime'
|
|
allow_empty = True
|
|
allow_future = True
|
|
kind = 'month'
|
|
|
|
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(), 1), '%Y-%m-%d %H:%M'
|
|
)
|
|
)
|
|
except ValueError:
|
|
raise Http404
|
|
return super().dispatch(request, *args, **kwargs)
|
|
|
|
def get_previous_month_url(self):
|
|
previous_month = self.get_previous_month(self.date.date())
|
|
return reverse(
|
|
'chrono-manager-resource-month-view',
|
|
kwargs={'pk': self.resource.pk, 'year': previous_month.year, 'month': previous_month.month},
|
|
)
|
|
|
|
def get_next_month_url(self):
|
|
next_month = self.get_next_month(self.date.date())
|
|
return reverse(
|
|
'chrono-manager-resource-month-view',
|
|
kwargs={'pk': self.resource.pk, 'year': next_month.year, 'month': next_month.month},
|
|
)
|
|
|
|
def get_next(self, date):
|
|
return self.get_next_month(date)
|
|
|
|
|
|
resource_monthly_view = ResourceMonthView.as_view()
|
|
|
|
|
|
class ResourceAddView(CreateView):
|
|
template_name = 'chrono/manager_resource_form.html'
|
|
model = Resource
|
|
fields = ['label', 'description']
|
|
|
|
def dispatch(self, request, *args, **kwargs):
|
|
if not request.user.is_staff:
|
|
raise PermissionDenied()
|
|
return super().dispatch(request, *args, **kwargs)
|
|
|
|
def get_success_url(self):
|
|
return reverse('chrono-manager-resource-view', kwargs={'pk': self.object.id})
|
|
|
|
|
|
resource_add = ResourceAddView.as_view()
|
|
|
|
|
|
class ResourceEditView(UpdateView):
|
|
template_name = 'chrono/manager_resource_form.html'
|
|
model = Resource
|
|
fields = ['label', 'slug', 'description']
|
|
|
|
def dispatch(self, request, *args, **kwargs):
|
|
if not request.user.is_staff:
|
|
raise PermissionDenied()
|
|
return super().dispatch(request, *args, **kwargs)
|
|
|
|
def get_success_url(self):
|
|
return reverse('chrono-manager-resource-view', kwargs={'pk': self.object.id})
|
|
|
|
|
|
resource_edit = ResourceEditView.as_view()
|
|
|
|
|
|
class ResourceDeleteView(DeleteView):
|
|
template_name = 'chrono/manager_confirm_delete.html'
|
|
model = Resource
|
|
|
|
def dispatch(self, request, *args, **kwargs):
|
|
if not request.user.is_staff:
|
|
raise PermissionDenied()
|
|
return super().dispatch(request, *args, **kwargs)
|
|
|
|
def get_success_url(self):
|
|
return reverse('chrono-manager-resource-list')
|
|
|
|
|
|
resource_delete = ResourceDeleteView.as_view()
|
|
|
|
|
|
class CategoryListView(ListView):
|
|
template_name = 'chrono/manager_category_list.html'
|
|
model = Category
|
|
|
|
def dispatch(self, request, *args, **kwargs):
|
|
if not request.user.is_staff:
|
|
raise PermissionDenied()
|
|
return super().dispatch(request, *args, **kwargs)
|
|
|
|
|
|
category_list = CategoryListView.as_view()
|
|
|
|
|
|
class CategoryAddView(CreateView):
|
|
template_name = 'chrono/manager_category_form.html'
|
|
model = Category
|
|
fields = ['label']
|
|
|
|
def dispatch(self, request, *args, **kwargs):
|
|
if not request.user.is_staff:
|
|
raise PermissionDenied()
|
|
return super().dispatch(request, *args, **kwargs)
|
|
|
|
def get_success_url(self):
|
|
return reverse('chrono-manager-category-list')
|
|
|
|
|
|
category_add = CategoryAddView.as_view()
|
|
|
|
|
|
class CategoryEditView(UpdateView):
|
|
template_name = 'chrono/manager_category_form.html'
|
|
model = Category
|
|
fields = ['label', 'slug']
|
|
|
|
def dispatch(self, request, *args, **kwargs):
|
|
if not request.user.is_staff:
|
|
raise PermissionDenied()
|
|
return super().dispatch(request, *args, **kwargs)
|
|
|
|
def get_success_url(self):
|
|
return reverse('chrono-manager-category-list')
|
|
|
|
|
|
category_edit = CategoryEditView.as_view()
|
|
|
|
|
|
class CategoryDeleteView(DeleteView):
|
|
template_name = 'chrono/manager_confirm_delete.html'
|
|
model = Category
|
|
|
|
def dispatch(self, request, *args, **kwargs):
|
|
if not request.user.is_staff:
|
|
raise PermissionDenied()
|
|
return super().dispatch(request, *args, **kwargs)
|
|
|
|
def get_success_url(self):
|
|
return reverse('chrono-manager-category-list')
|
|
|
|
|
|
category_delete = CategoryDeleteView.as_view()
|
|
|
|
|
|
class 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')
|
|
|
|
def get_initial(self):
|
|
return {'booking_user_block_template': self.agenda.get_booking_user_block_template()}
|
|
|
|
|
|
agenda_booking_check_settings = AgendaBookingCheckSettingsView.as_view()
|
|
|
|
|
|
class AgendaDeleteView(DeleteView):
|
|
template_name = 'chrono/manager_confirm_delete.html'
|
|
model = Agenda
|
|
success_url = reverse_lazy('chrono-manager-homepage')
|
|
|
|
def dispatch(self, request, *args, **kwargs):
|
|
if not request.user.is_staff:
|
|
raise PermissionDenied()
|
|
return super().dispatch(request, *args, **kwargs)
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context['cannot_delete'] = Booking.objects.filter(
|
|
event__agenda=self.get_object(),
|
|
event__start_datetime__gt=now(),
|
|
cancellation_datetime__isnull=True,
|
|
).exists()
|
|
context['cannot_delete_msg'] = FUTURE_BOOKING_ERROR_MSG
|
|
return context
|
|
|
|
def delete(self, request, *args, **kwargs):
|
|
self.object = self.get_object()
|
|
context = self.get_context_data()
|
|
if context['cannot_delete']:
|
|
raise PermissionDenied()
|
|
return super().delete(request, *args, **kwargs)
|
|
|
|
|
|
agenda_delete = AgendaDeleteView.as_view()
|
|
|
|
|
|
class AgendaView(ViewableAgendaMixin, View):
|
|
def 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.month)
|
|
|
|
|
|
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, week=day.strftime('%W')
|
|
)
|
|
|
|
|
|
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.month, day=day.day
|
|
)
|
|
|
|
|
|
agenda_day_redirect_view = AgendaDayRedirectView.as_view()
|
|
|
|
|
|
class AgendaDateView(DateMixin, ViewableAgendaMixin):
|
|
model = Event
|
|
month_format = '%m'
|
|
week_format = '%W'
|
|
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)
|
|
else:
|
|
self.agenda.prefetch_desks_and_exceptions(min_date=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.month,
|
|
'day': previous_day.day,
|
|
},
|
|
)
|
|
|
|
def get_next_day_url(self):
|
|
next_day = self.date.date() + datetime.timedelta(days=1)
|
|
return reverse(
|
|
'chrono-manager-agenda-day-view',
|
|
kwargs={
|
|
'pk': self.agenda.id,
|
|
'year': next_day.year,
|
|
'month': next_day.month,
|
|
'day': next_day.day,
|
|
},
|
|
)
|
|
|
|
def get_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')
|
|
|
|
def get_dated_items(self):
|
|
date_list, object_list, extra_context = super().get_dated_items()
|
|
if self.agenda.kind == 'events':
|
|
min_start = make_aware(datetime.datetime.combine(extra_context[self.kind], datetime.time(0, 0)))
|
|
max_start = make_aware(
|
|
datetime.datetime.combine(extra_context['next_%s' % self.kind], datetime.time(0, 0))
|
|
)
|
|
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.date.isocalendar()[1]
|
|
last_week_number = first_week_number
|
|
if self.kind == 'month':
|
|
first_week_offset = int(
|
|
(hide_sunday and self.date.weekday() == 6) or (hide_weekend and self.date.weekday() == 5)
|
|
)
|
|
first_week_number = self.date.isocalendar()[1]
|
|
if first_week_number >= 52:
|
|
first_week_number = 0
|
|
last_day = self.get_next(self.date.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.date + datetime.timedelta(week_index * 7)
|
|
dow = date.isocalendar()[2]
|
|
start_date = date - datetime.timedelta(dow)
|
|
|
|
interval = datetime.timedelta(minutes=60)
|
|
period = self.date.replace(hour=self.min_display.hour, minute=0)
|
|
max_date = self.date.replace(hour=self.max_display.hour, minute=0)
|
|
|
|
periods = []
|
|
while period < max_date:
|
|
periods.append(period)
|
|
period = period + interval
|
|
|
|
return {
|
|
'days': [
|
|
self.get_day_timetable_infos(start_date + datetime.timedelta(i), interval)
|
|
for i in range(1, 8 - week_end_offset)
|
|
],
|
|
'periods': periods,
|
|
}
|
|
|
|
def get_day_timetable_infos(self, day, interval):
|
|
day = make_aware(make_naive(day)) # give day correct timezone
|
|
period = current_date = day.replace(hour=self.min_display.hour, minute=0)
|
|
timetable = {
|
|
'date': current_date,
|
|
'today': day.date() == datetime.date.today(),
|
|
'other_month': day.month != self.date.month,
|
|
'infos': {'opening_hours': [], 'exceptions': [], 'booked_slots': []},
|
|
}
|
|
|
|
desks = self.agenda.prefetched_desks
|
|
desks_len = len(desks)
|
|
max_date = day.replace(hour=self.max_display.hour, minute=0)
|
|
|
|
# compute booking and opening hours only for current month/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, WeekArchiveView):
|
|
week_format = "%W"
|
|
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_url(self):
|
|
previous_week = self.get_previous_week(self.date.date())
|
|
return reverse(
|
|
'chrono-manager-agenda-week-view',
|
|
kwargs={'pk': self.agenda.id, 'year': previous_week.year, 'week': previous_week.strftime('%W')},
|
|
)
|
|
|
|
def get_next_week_url(self):
|
|
next_week = self.get_next_week(self.date.date())
|
|
return reverse(
|
|
'chrono-manager-agenda-week-view',
|
|
kwargs={'pk': self.agenda.id, 'year': next_week.year, 'week': next_week.strftime('%W')},
|
|
)
|
|
|
|
def get_next(self, date):
|
|
return self.get_next_week(date)
|
|
|
|
def get_month(self):
|
|
date = datetime.datetime.strptime('%s-W%s-1' % (self.get_year(), self.get_week()), "%Y-W%W-%w")
|
|
return date.month
|
|
|
|
def get_day(self):
|
|
date = datetime.datetime.strptime('%s-W%s-1' % (self.get_year(), self.get_week()), "%Y-W%W-%w")
|
|
return date.day
|
|
|
|
def get_max_date(self):
|
|
return self.get_next_week(self.date.date())
|
|
|
|
|
|
agenda_weekly_view = AgendaWeekView.as_view()
|
|
|
|
|
|
class AgendaMonthView(AgendaWeekMonthMixin, AgendaDateView, MonthArchiveView):
|
|
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.date.date())
|
|
return reverse(
|
|
'chrono-manager-agenda-month-view',
|
|
kwargs={'pk': self.agenda.id, 'year': previous_month.year, 'month': previous_month.month},
|
|
)
|
|
|
|
def get_next_month_url(self):
|
|
next_month = self.get_next_month(self.date.date())
|
|
return reverse(
|
|
'chrono-manager-agenda-month-view',
|
|
kwargs={'pk': self.agenda.id, 'year': next_month.year, 'month': next_month.month},
|
|
)
|
|
|
|
def get_next(self, date):
|
|
return self.get_next_month(date)
|
|
|
|
def get_day(self):
|
|
return '1'
|
|
|
|
def get_max_date(self):
|
|
return self.get_next_month(self.date.date())
|
|
|
|
|
|
agenda_monthly_view = AgendaMonthView.as_view()
|
|
|
|
|
|
class AgendaOpenEventsView(ViewableAgendaMixin, DetailView):
|
|
model = Agenda
|
|
template_name = 'chrono/manager_agenda_open_events.html'
|
|
|
|
def set_agenda(self, **kwargs):
|
|
self.agenda = get_object_or_404(Agenda, pk=kwargs.get('pk'), kind='events')
|
|
|
|
def get_object(self, **kwargs):
|
|
return self.agenda
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context['user_can_manage'] = self.agenda.can_be_managed(self.request.user)
|
|
context['open_events'] = self.agenda.get_open_events()
|
|
return context
|
|
|
|
|
|
agenda_open_events_view = AgendaOpenEventsView.as_view()
|
|
|
|
|
|
class ManagedAgendaSubobjectMixin:
|
|
agenda = None
|
|
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.get_object()
|
|
context['booked'] = event.booking_set.filter(
|
|
cancellation_datetime__isnull=True, in_waiting_list=False
|
|
).order_by('creation_datetime')
|
|
context['waiting'] = event.booking_set.filter(
|
|
cancellation_datetime__isnull=True, in_waiting_list=True
|
|
).order_by('creation_datetime')
|
|
return context
|
|
|
|
|
|
event_view = EventDetailView.as_view()
|
|
|
|
|
|
class EventEditView(ManagedAgendaMixin, UpdateView):
|
|
template_name = 'chrono/manager_event_form.html'
|
|
model = Event
|
|
form_class = EventForm
|
|
pk_url_kwarg = 'event_pk'
|
|
|
|
def get_success_url(self):
|
|
if (
|
|
self.request.GET.get('next') == 'settings'
|
|
or self.request.POST.get('next') == 'settings'
|
|
or self.object.recurrence_days
|
|
):
|
|
return reverse('chrono-manager-agenda-settings', kwargs={'pk': self.agenda.id})
|
|
return reverse('chrono-manager-event-view', kwargs={'pk': self.agenda.id, 'event_pk': self.object.id})
|
|
|
|
|
|
event_edit = EventEditView.as_view()
|
|
|
|
|
|
class EventDeleteView(ManagedAgendaMixin, DeleteView):
|
|
template_name = 'chrono/manager_confirm_event_delete.html'
|
|
model = Event
|
|
pk_url_kwarg = 'event_pk'
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context['cannot_delete'] = bool(
|
|
self.object.booking_set.filter(cancellation_datetime__isnull=True).exists()
|
|
and self.object.start_datetime > now()
|
|
or self.object.has_recurrences_booked()
|
|
)
|
|
return context
|
|
|
|
def delete(self, request, *args, **kwargs):
|
|
self.object = self.get_object()
|
|
context = self.get_context_data()
|
|
if context['cannot_delete']:
|
|
raise PermissionDenied()
|
|
return super().delete(request, *args, **kwargs)
|
|
|
|
def get_success_url(self):
|
|
if self.request.GET.get('next') == 'settings' or self.request.POST.get('next') == 'settings':
|
|
return reverse('chrono-manager-agenda-settings', kwargs={'pk': self.agenda.id})
|
|
day = self.object.start_datetime
|
|
return reverse(
|
|
'chrono-manager-agenda-month-view',
|
|
kwargs={'pk': self.agenda.id, 'year': day.year, 'month': day.month},
|
|
)
|
|
|
|
|
|
event_delete = EventDeleteView.as_view()
|
|
|
|
|
|
class EventCheckView(ViewableAgendaMixin, DetailView):
|
|
template_name = 'chrono/manager_event_check.html'
|
|
model = Event
|
|
pk_url_kwarg = 'event_pk'
|
|
|
|
def set_agenda(self, **kwargs):
|
|
self.agenda = get_object_or_404(
|
|
Agenda,
|
|
pk=kwargs.get('pk'),
|
|
kind='events',
|
|
)
|
|
|
|
def get_queryset(self):
|
|
queryset = super().get_queryset()
|
|
return queryset.filter(agenda=self.agenda, start_datetime__date__lte=now().date(), cancelled=False)
|
|
|
|
def get_filters(self, booked_queryset, subscription_queryset):
|
|
agenda_filters = self.agenda.get_booking_check_filters()
|
|
filters = collections.defaultdict(set)
|
|
extra_data_from_booked = booked_queryset.filter(extra_data__has_any_keys=agenda_filters).values_list(
|
|
'extra_data', flat=True
|
|
)
|
|
extra_data_from_subscriptions = subscription_queryset.filter(
|
|
extra_data__has_any_keys=agenda_filters
|
|
).values_list('extra_data', flat=True)
|
|
for extra_data in list(extra_data_from_booked) + list(extra_data_from_subscriptions):
|
|
for k, v in extra_data.items():
|
|
if k in agenda_filters:
|
|
filters[k].add(v)
|
|
filters = sorted(filters.items())
|
|
filters = {k: sorted(list(v)) for k, v in filters}
|
|
return filters
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
|
|
event = self.object
|
|
|
|
# booking base queryset
|
|
booking_qs_kwargs = {}
|
|
if not self.agenda.subscriptions.exists():
|
|
booking_qs_kwargs = {'cancellation_datetime__isnull': True}
|
|
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.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
|
|
if context['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),
|
|
pk=kwargs.get('event_pk'),
|
|
agenda=self.agenda,
|
|
start_datetime__date__lte=now().date(),
|
|
cancelled=False,
|
|
)
|
|
|
|
def get_bookings(self):
|
|
return self.event.booking_set.filter(
|
|
event__agenda=self.agenda,
|
|
event__start_datetime__date__lte=now().date(),
|
|
event__cancelled=False,
|
|
cancellation_datetime__isnull=True,
|
|
in_waiting_list=False,
|
|
user_was_present__isnull=True,
|
|
)
|
|
|
|
def 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['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 = TimePeriodAddForm
|
|
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 AgendaAddDesk(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(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(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},
|
|
)
|
|
|
|
|
|
booking_cancel = BookingCancelView.as_view()
|
|
|
|
|
|
class BookingCheckMixin:
|
|
def get_booking(self, **kwargs):
|
|
return get_object_or_404(
|
|
Booking,
|
|
Q(event__checked=False) | Q(event__agenda__disable_check_update=False),
|
|
pk=kwargs['booking_pk'],
|
|
event__agenda=self.agenda,
|
|
event__start_datetime__date__lte=now().date(),
|
|
event__cancelled=False,
|
|
cancellation_datetime__isnull=True,
|
|
in_waiting_list=False,
|
|
primary_booking__isnull=True,
|
|
)
|
|
|
|
def 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}
|
|
)
|
|
return render(
|
|
request,
|
|
'chrono/manager_event_check_booking_fragment.html',
|
|
{'booking': booking, 'agenda': self.agenda},
|
|
)
|
|
return HttpResponseRedirect(
|
|
reverse(
|
|
'chrono-manager-event-check',
|
|
kwargs={'pk': booking.event.agenda_id, 'event_pk': booking.event.pk},
|
|
)
|
|
)
|
|
|
|
|
|
class BookingPresenceView(ViewableAgendaMixin, BookingCheckMixin, 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):
|
|
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)
|
|
|
|
|
|
booking_presence = BookingPresenceView.as_view()
|
|
|
|
|
|
class BookingAbsenceView(ViewableAgendaMixin, BookingCheckMixin, 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):
|
|
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)
|
|
|
|
|
|
booking_absence = BookingAbsenceView.as_view()
|
|
|
|
|
|
class EventCancelView(ViewableAgendaMixin, UpdateView):
|
|
template_name = 'chrono/manager_confirm_event_cancellation.html'
|
|
model = Event
|
|
pk_url_kwarg = 'event_pk'
|
|
form_class = EventCancelForm
|
|
|
|
def dispatch(self, request, *args, **kwargs):
|
|
self.event = self.get_object()
|
|
if self.event.cancellation_status:
|
|
raise PermissionDenied()
|
|
self.cancel_bookings = not (self.request.GET.get('force_cancellation'))
|
|
return super().dispatch(request, *args, **kwargs)
|
|
|
|
def form_valid(self, form):
|
|
self.event.cancel(self.cancel_bookings)
|
|
return HttpResponseRedirect(self.get_success_url())
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context['cancel_bookings'] = self.cancel_bookings
|
|
context['bookings_count'] = self.event.booking_set.filter(cancellation_datetime__isnull=True).count()
|
|
context['cancellation_forbidden'] = (
|
|
self.event.booking_set.filter(cancellation_datetime__isnull=True, cancel_callback_url='')
|
|
.exclude(backoffice_url='')
|
|
.exists()
|
|
)
|
|
return context
|
|
|
|
def get_success_url(self):
|
|
self.event.refresh_from_db()
|
|
if self.event.cancellation_scheduled:
|
|
messages.info(self.request, _('Event "%s" will be cancelled in a few minutes.') % self.event)
|
|
next_url = self.request.POST.get('next')
|
|
if next_url:
|
|
return next_url
|
|
day = self.event.start_datetime
|
|
return reverse(
|
|
'chrono-manager-agenda-month-view',
|
|
kwargs={'pk': self.event.agenda.pk, 'year': day.year, 'month': day.month},
|
|
)
|
|
|
|
|
|
event_cancel = EventCancelView.as_view()
|
|
|
|
|
|
class EventCancellationReportView(ViewableAgendaMixin, DetailView):
|
|
model = EventCancellationReport
|
|
template_name = 'chrono/manager_event_cancellation_report.html'
|
|
context_object_name = 'report'
|
|
pk_url_kwarg = 'report_pk'
|
|
|
|
def get(self, *args, **kwargs):
|
|
self.report = self.get_object()
|
|
self.report.seen = True
|
|
self.report.save()
|
|
return super().get(*args, **kwargs)
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
bookings = self.report.bookings.all()
|
|
errors = self.report.booking_errors
|
|
context['errors'] = {
|
|
booking: errors[str(booking.pk)] for booking in bookings if str(booking.pk) in errors
|
|
}
|
|
return context
|
|
|
|
|
|
event_cancellation_report = EventCancellationReportView.as_view()
|
|
|
|
|
|
class EventCancellationReportListView(ViewableAgendaMixin, ListView):
|
|
model = EventCancellationReport
|
|
context_object_name = 'cancellation_reports'
|
|
template_name = 'chrono/manager_event_cancellation_reports.html'
|
|
|
|
|
|
event_cancellation_report_list = EventCancellationReportListView.as_view()
|
|
|
|
|
|
class 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
|