chrono/chrono/manager/views.py

1250 lines
45 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 datetime
import json
from django.contrib import messages
from django.core.exceptions import PermissionDenied
from django.db.models import Q
from django.forms import ValidationError
from django.http import Http404, HttpResponse, HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.template.response import TemplateResponse
from django.urls import reverse, reverse_lazy
from django.utils.dates import MONTHS
from django.utils.timezone import now, make_aware, make_naive
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import ungettext
from django.utils.encoding import force_text
from django.views.generic import (
DetailView,
CreateView,
UpdateView,
ListView,
DeleteView,
FormView,
TemplateView,
DayArchiveView,
MonthArchiveView,
View,
)
from chrono.agendas.models import (
Agenda,
Event,
MeetingType,
TimePeriod,
Booking,
Desk,
TimePeriodException,
ICSError,
AgendaImportError,
TimePeriodExceptionSource,
VirtualMember,
)
from .forms import (
AgendaAddForm,
AgendaEditForm,
NewEventForm,
EventForm,
NewMeetingTypeForm,
MeetingTypeForm,
TimePeriodForm,
ImportEventsForm,
NewDeskForm,
DeskForm,
TimePeriodExceptionForm,
ExceptionsImportForm,
AgendasImportForm,
TimePeriodAddForm,
TimePeriodExceptionSourceReplaceForm,
VirtualMemberForm,
)
from .utils import import_site
FUTURE_BOOKING_ERROR_MSG = _('This cannot be removed as there are bookings for a future date.')
class HomepageView(ListView):
template_name = 'chrono/manager_home.html'
model = Agenda
def get_queryset(self):
queryset = super(HomepageView, self).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
homepage = HomepageView.as_view()
class AgendaAddView(CreateView):
template_name = 'chrono/manager_agenda_form.html'
model = Agenda
form_class = AgendaAddForm
def dispatch(self, request, *args, **kwargs):
if not request.user.is_staff:
raise PermissionDenied()
return super(AgendaAddView, self).dispatch(request, *args, **kwargs)
def form_valid(self, form):
model_form = super(AgendaAddView, self).form_valid(form)
if self.object.kind == 'meetings':
default_desk = Desk(agenda=self.object, label=_('Desk 1'))
default_desk.save()
return model_form
def get_success_url(self):
return reverse('chrono-manager-agenda-settings', kwargs={'pk': self.object.id})
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(AgendasImportView, self).dispatch(request, *args, **kwargs)
def form_valid(self, form):
try:
agendas_json = json.loads(force_text(self.request.FILES['agendas_json'].read()))
except ValueError:
form.add_error('agendas_json', _('File is not in the expected JSON format.'))
return self.form_invalid(form)
try:
results = import_site(agendas_json, overwrite=True)
except AgendaImportError as exc:
form.add_error('agendas_json', u'%s' % exc)
return self.form_invalid(form)
if results.get('created') == 0 and results.get('updated') == 0:
messages.info(self.request, _('No agendas were found.'))
else:
if results.get('created') == 0:
message1 = _('No agenda created.')
else:
message1 = ungettext(
'An agenda has been created.', '%(count)d agendas have been created.', results['created']
) % {'count': results['created']}
if results.get('updated') == 0:
message2 = _('No agenda updated.')
else:
message2 = ungettext(
'An agenda has been updated.', '%(count)d agendas have been updated.', results['updated']
) % {'count': results['updated']}
messages.info(self.request, u'%s %s' % (message1, message2))
return super(AgendasImportView, self).form_valid(form)
agendas_import = AgendasImportView.as_view()
class ViewableAgendaMixin(object):
agenda = None
def dispatch(self, request, *args, **kwargs):
self.agenda = get_object_or_404(Agenda, id=kwargs.get('pk'))
if not self.check_permissions(request.user):
raise PermissionDenied()
return super(ViewableAgendaMixin, self).dispatch(request, *args, **kwargs)
def check_permissions(self, user):
return self.agenda.can_be_viewed(user)
def get_context_data(self, **kwargs):
context = super(ViewableAgendaMixin, self).get_context_data(**kwargs)
context['agenda'] = self.agenda
return context
class ManagedAgendaMixin(ViewableAgendaMixin):
def check_permissions(self, user):
return self.agenda.can_be_managed(user)
def get_initial(self):
initial = super(ManagedAgendaMixin, self).get_initial()
initial['agenda'] = self.agenda.id
return initial
def get_success_url(self):
return reverse('chrono-manager-agenda-settings', kwargs={'pk': self.agenda.id})
class AgendaEditView(ManagedAgendaMixin, UpdateView):
template_name = 'chrono/manager_agenda_form.html'
model = Agenda
form_class = AgendaEditForm
def get_success_url(self):
return reverse('chrono-manager-agenda-settings', kwargs={'pk': self.object.id})
agenda_edit = AgendaEditView.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(AgendaDeleteView, self).dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super(AgendaDeleteView, self).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(AgendaDeleteView, self).delete(request, *args, **kwargs)
agenda_delete = AgendaDeleteView.as_view()
class AgendaView(ViewableAgendaMixin, View):
def get(self, request, *args, **kwargs):
today = datetime.date.today()
if self.agenda.kind == 'virtual':
real_agendas = [
(agenda, agenda.can_be_viewed(request.user)) for agenda in self.agenda.real_agendas.all()
]
return TemplateResponse(
request=request,
template='chrono/manager_virtual_agenda_view.html',
context={
'real_agendas': real_agendas,
'agenda': self.agenda,
'object': self.agenda,
'user_can_manage': self.agenda.can_be_managed(self.request.user),
},
)
if self.agenda.kind == 'meetings':
# redirect to today view
return HttpResponseRedirect(
reverse(
'chrono-manager-agenda-day-view',
kwargs={'pk': self.agenda.id, 'year': today.year, 'month': today.month, 'day': today.day},
)
)
if self.agenda.kind == 'events':
# redirect to monthly view, to first month where there are events,
# otherwise to latest month with events, otherwise to this month.
event = self.agenda.event_set.filter(
start_datetime__gte=datetime.date(today.year, today.month, 1)
).first()
if not event:
event = self.agenda.event_set.filter(
start_datetime__lte=datetime.date(today.year, today.month, 1)
).last()
if event:
day = event.start_datetime
else:
day = today
return HttpResponseRedirect(
reverse(
'chrono-manager-agenda-month-view',
kwargs={'pk': self.agenda.id, 'year': day.year, 'month': day.month},
)
)
agenda_view = AgendaView.as_view()
class AgendaDateView(ViewableAgendaMixin):
model = Event
month_format = '%m'
date_field = 'start_datetime'
allow_empty = True
allow_future = True
def dispatch(self, request, *args, **kwargs):
# specify 6am time to get the expected timezone on daylight saving time
# days.
try:
self.date = make_aware(
datetime.datetime.strptime(
'%s-%s-%s 06:00' % (self.get_year(), self.get_month(), self.get_day()), '%Y-%m-%d %H:%M'
)
)
except ValueError: # day is out of range for month
# redirect to last day of month
date = datetime.date(int(self.get_year()), int(self.get_month()), 1)
date += datetime.timedelta(days=40)
date = date.replace(day=1)
date -= datetime.timedelta(days=1)
return HttpResponseRedirect(
reverse(
'chrono-manager-agenda-day-view',
kwargs={'pk': kwargs['pk'], 'year': date.year, 'month': date.month, 'day': date.day},
)
)
return super(AgendaDateView, self).dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super(AgendaDateView, self).get_context_data(**kwargs)
context['agenda'] = self.agenda
if self.agenda.kind == 'meetings':
try:
context['hour_span'] = max(60 // self.agenda.get_base_meeting_duration(), 1)
except ValueError: # no meeting types defined
context['hour_span'] = 1
context['user_can_manage'] = self.agenda.can_be_managed(self.request.user)
return context
def get_queryset(self):
queryset = super(AgendaDateView, self).get_queryset()
queryset = queryset.filter(agenda=self.agenda).prefetch_related('booking_set')
return queryset
def get_days(self):
return [str(x) for x in range(1, 32)]
def get_months(self):
return [(str(x), MONTHS[x]) for x in range(1, 13)]
def get_years(self):
year = now().year
return [str(x) for x in range(year - 1, year + 5)]
class AgendaDayView(AgendaDateView, DayArchiveView):
template_name = 'chrono/manager_agenda_day_view.html'
def dispatch(self, request, *args, **kwargs):
# day view should only exist for meetings kind.
get_object_or_404(Agenda, id=kwargs.get('pk'), kind='meetings')
return super(AgendaDayView, self).dispatch(request, *args, **kwargs)
def get_previous_day_url(self):
previous_day = self.date.date() - datetime.timedelta(days=1)
return reverse(
'chrono-manager-agenda-day-view',
kwargs={
'pk': self.agenda.id,
'year': previous_day.year,
'month': previous_day.month,
'day': previous_day.day,
},
)
def get_next_day_url(self):
next_day = self.date.date() + datetime.timedelta(days=1)
return reverse(
'chrono-manager-agenda-day-view',
kwargs={
'pk': self.agenda.id,
'year': next_day.year,
'month': next_day.month,
'day': next_day.day,
},
)
def get_timetable_infos(self):
timeperiods = TimePeriod.objects.filter(desk__agenda=self.agenda, weekday=self.date.weekday(),)
if not timeperiods:
return
min_timeperiod = min([x.start_time for x in timeperiods])
max_timeperiod = max([x.end_time for x in timeperiods])
interval = datetime.timedelta(minutes=60)
current_date = self.date.replace(hour=min_timeperiod.hour, minute=0)
start_date = current_date
max_date = self.date.replace(hour=max_timeperiod.hour, minute=0)
if max_timeperiod.minute != 0:
# until the end of the last hour.
max_date += datetime.timedelta(hours=1)
desks = self.agenda.desk_set.all().prefetch_related('timeperiodexception_set')
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 desks:
info = {'desk': desk}
if first:
# use first row to include opening hours
info['opening_hours'] = opening_hours = []
for opening_hour in desk.get_opening_hours(current_date.date()):
opening_hours.append(
{
'css_top': 100 * (opening_hour.begin - start_date).seconds // 3600,
'css_height': 100 * (opening_hour.end - opening_hour.begin).seconds // 3600,
}
)
# and exceptions for this desk
info['exceptions'] = []
for exception in desk.timeperiodexception_set.all():
if exception.end_datetime < start_date:
continue
if exception.start_datetime > max_date:
continue
start_max = max(start_date, exception.start_datetime)
end_min = min(max_date, exception.end_datetime)
exception.css_top = int(100 * (start_max - start_date).seconds // 3600)
exception.css_height = int(100 * (end_min - start_max).seconds // 3600)
info['exceptions'].append(exception)
infos.append(info)
info['bookings'] = bookings = [] # bookings for this desk
finish_datetime = current_date + interval
for event in [
x
for x in self.object_list
if x.desk_id == desk.id
and x.start_datetime >= current_date
and x.start_datetime < finish_datetime
]:
# don't consider cancelled bookings
for booking in [x for x in event.booking_set.all() if not x.cancellation_datetime]:
booking.css_top = int(100 * event.start_datetime.minute / 60)
booking.css_height = int(100 * event.meeting_type.duration / 60)
bookings.append(booking)
yield current_date, infos
current_date += interval
first = False
agenda_day_view = AgendaDayView.as_view()
class AgendaMonthView(AgendaDateView, MonthArchiveView):
def get_queryset(self):
qs = super(AgendaMonthView, self).get_queryset()
if self.agenda.kind == 'meetings':
return qs
return Event.annotate_queryset(qs).order_by('start_datetime', 'label')
def get_template_names(self):
return ['chrono/manager_%s_agenda_month_view.html' % self.agenda.kind]
def get_context_data(self, **kwargs):
context = super(AgendaMonthView, self).get_context_data(**kwargs)
context['single_desk'] = bool(self.agenda.desk_set.count() == 1)
return context
def get_previous_month_url(self):
previous_month = self.get_previous_month(self.date.date())
return reverse(
'chrono-manager-agenda-month-view',
kwargs={'pk': self.agenda.id, 'year': previous_month.year, 'month': previous_month.month},
)
def get_next_month_url(self):
next_month = self.get_next_month(self.date.date())
return reverse(
'chrono-manager-agenda-month-view',
kwargs={'pk': self.agenda.id, 'year': next_month.year, 'month': next_month.month},
)
def get_day(self):
return '1'
def get_timetable_infos(self):
timeperiods = TimePeriod.objects.filter(desk__agenda=self.agenda)
if not timeperiods:
return
hide_sunday = not timeperiods.filter(weekday=6).exists()
hide_weekend = hide_sunday and not timeperiods.filter(weekday=5).exists()
# avoid displaying empty first week
first_week_offset = int(
(hide_sunday and self.date.weekday() == 6) or (hide_weekend and self.date.weekday() == 5)
)
first_week_number = self.date.isocalendar()[1]
if first_week_number >= 52:
first_week_number = 0
last_month_day = self.get_next_month(self.date.date()) - datetime.timedelta(days=1)
last_week_number = last_month_day.isocalendar()[1]
if last_week_number < first_week_number: # new year
last_week_number = 53
for week_number in range(first_week_number + first_week_offset, last_week_number + 1):
yield self.get_week_timetable_infos(
week_number - first_week_number,
timeperiods,
week_end_offset=int(hide_sunday) + int(hide_weekend),
)
def get_week_timetable_infos(self, week_index, timeperiods, week_end_offset=0):
date = self.date + datetime.timedelta(week_index * 7)
year, week_number, dow = date.isocalendar()
start_date = date - datetime.timedelta(dow)
self.min_timeperiod = min([x.start_time for x in timeperiods])
self.max_timeperiod = max([x.end_time for x in timeperiods])
interval = datetime.timedelta(minutes=60)
period = self.date.replace(hour=self.min_timeperiod.hour, minute=0)
max_date = self.date.replace(hour=self.max_timeperiod.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_timeperiod.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.desk_set.all().prefetch_related('timeperiodexception_set')
desks_len = len(desks)
max_date = day.replace(hour=self.max_timeperiod.hour, minute=0)
# compute booking and opening hours only for current month
if self.date.month != day.month:
return timetable
while period <= max_date:
left = 1
period_end = period + interval
for desk_index, desk in enumerate(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 hour in desk.get_opening_hours(current_date):
timetable['infos']['opening_hours'].append(
{
'css_top': 100 * (hour.begin - current_date).seconds // 3600,
'css_height': 100 * (hour.end - hour.begin).seconds // 3600,
'css_width': width,
'css_left': left,
}
)
for exception in desk.timeperiodexception_set.all():
if exception.end_datetime < current_date:
continue
if exception.start_datetime > max_date:
continue
start_max = max(current_date, exception.start_datetime)
end_min = min(max_date, exception.end_datetime)
exception.css_top = int(100 * (start_max - current_date).seconds // 3600)
exception.css_height = int(100 * (end_min - start_max).seconds // 3600)
exception.css_width = width
exception.css_left = left
timetable['infos']['exceptions'].append(exception)
left += width + 1
period += interval
return timetable
agenda_monthly_view = AgendaMonthView.as_view()
class ManagedAgendaSubobjectMixin(object):
agenda = None
def dispatch(self, request, *args, **kwargs):
self.agenda = self.get_object().agenda
if not self.agenda.can_be_managed(request.user):
raise PermissionDenied()
return super(ManagedAgendaSubobjectMixin, self).dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super(ManagedAgendaSubobjectMixin, self).get_context_data(**kwargs)
context['agenda'] = self.object.agenda
return context
def get_success_url(self):
return reverse('chrono-manager-agenda-settings', kwargs={'pk': self.agenda.id})
class ManagedDeskMixin(object):
desk = None
def dispatch(self, request, *args, **kwargs):
try:
self.desk = Desk.objects.get(id=kwargs.get('pk'))
except Desk.DoesNotExist:
raise Http404()
if not self.desk.agenda.can_be_managed(request.user):
raise PermissionDenied()
return super(ManagedDeskMixin, self).dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super(ManagedDeskMixin, self).get_context_data(**kwargs)
context['desk'] = self.desk
context['agenda'] = self.desk.agenda
return context
def get_initial(self):
initial = super(ManagedDeskMixin, self).get_initial()
initial['desk'] = self.desk
return initial
def get_success_url(self):
return reverse('chrono-manager-agenda-settings', kwargs={'pk': self.desk.agenda.id})
class ManagedDeskSubobjectMixin(object):
desk = None
def dispatch(self, request, *args, **kwargs):
self.desk = self.get_object().desk
if not self.desk.agenda.can_be_managed(request.user):
raise PermissionDenied()
return super(ManagedDeskSubobjectMixin, self).dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super(ManagedDeskSubobjectMixin, self).get_context_data(**kwargs)
context['desk'] = self.object.desk
context['agenda'] = self.object.desk.agenda
return context
def get_success_url(self):
return reverse('chrono-manager-agenda-settings', kwargs={'pk': self.desk.agenda.id})
class ManagedTimePeriodMixin(object):
agenda = None
def dispatch(self, request, *args, **kwargs):
self.time_period = self.get_object()
self.agenda = self.time_period.agenda
self.has_desk = False
if self.time_period.desk:
self.agenda = self.time_period.desk.agenda
self.has_desk = True
if not self.agenda.can_be_managed(request.user):
raise PermissionDenied()
return super(ManagedTimePeriodMixin, self).dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super(ManagedTimePeriodMixin, self).get_context_data(**kwargs)
context['agenda'] = self.agenda
return context
def get_success_url(self):
return reverse('chrono-manager-agenda-settings', kwargs={'pk': self.agenda.id})
class AgendaSettings(ManagedAgendaMixin, DetailView):
model = Agenda
def get_context_data(self, **kwargs):
context = super(AgendaSettings, self).get_context_data(**kwargs)
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()
]
context['meeting_types'] = self.object.iter_meetingtypes()
return context
def get_events(self):
return Event.annotate_queryset(Event.objects.filter(agenda=self.agenda).select_related('agenda'))
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')
json.dump({'agendas': [self.get_object().export_json()]}, response, indent=2)
return response
agenda_export = AgendaExport.as_view()
class AgendaAddEventView(ManagedAgendaMixin, CreateView):
template_name = 'chrono/manager_event_form.html'
model = Event
form_class = NewEventForm
agenda_add_event = AgendaAddEventView.as_view()
class AgendaImportEventsSampleView(TemplateView):
template_name = 'chrono/manager_sample_events.txt'
content_type = 'text/csv'
def get_context_data(self, **kwargs):
context = super(AgendaImportEventsSampleView, self).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 get_form_kwargs(self):
kwargs = super(AgendaImportEventsView, self).get_form_kwargs()
kwargs['agenda_pk'] = self.kwargs['pk']
return kwargs
def form_valid(self, form):
if form.events:
for event in form.events:
event.agenda_id = self.kwargs['pk']
if event.slug and Event.objects.filter(agenda_id=event.agenda_id, slug=event.slug).exists():
raise ValidationError(_('Duplicated event identifier'))
event.save()
messages.info(self.request, _('%d events have been imported.') % len(form.events))
return super(AgendaImportEventsView, self).form_valid(form)
agenda_import_events = AgendaImportEventsView.as_view()
class EventDetailView(ViewableAgendaMixin, DetailView):
model = Event
pk_url_kwarg = 'event_pk'
def get_template_names(self):
if self.request.is_ajax():
return ['chrono/manager_event_detail_fragment.html']
return ['chrono/manager_event_detail.html']
def get_context_data(self, **kwargs):
context = super(EventDetailView, self).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':
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(EventDeleteView, self).get_context_data(**kwargs)
context['cannot_delete'] = bool(
self.object.booking_set.filter(cancellation_datetime__isnull=True).exists()
and self.object.start_datetime > now()
)
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(EventDeleteView, self).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 AgendaAddMeetingTypeView(ManagedAgendaMixin, CreateView):
template_name = 'chrono/manager_meeting_type_form.html'
model = Event
form_class = NewMeetingTypeForm
agenda_add_meeting_type = AgendaAddMeetingTypeView.as_view()
class MeetingTypeEditView(ManagedAgendaSubobjectMixin, UpdateView):
template_name = 'chrono/manager_meeting_type_form.html'
model = MeetingType
form_class = MeetingTypeForm
meeting_type_edit = MeetingTypeEditView.as_view()
class MeetingTypeDeleteView(ManagedAgendaSubobjectMixin, DeleteView):
template_name = 'chrono/manager_confirm_delete.html'
model = MeetingType
def get_context_data(self, **kwargs):
context = super(MeetingTypeDeleteView, self).get_context_data(**kwargs)
cannot_delete = False
meeting_type = self.get_object()
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
):
cannot_delete = True
context['cannot_delete_msg'] = _(
'This cannot be removed as it used by a virtual agenda: %(agenda)s'
% {'agenda': virtual_agenda}
)
break
context['cannot_delete'] = cannot_delete
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(MeetingTypeDeleteView, self).delete(request, *args, **kwargs)
meeting_type_delete = MeetingTypeDeleteView.as_view()
def process_time_period_add_form(form, desk=None, agenda=None):
assert desk or agenda, "a time period requires a desk or a agenda"
for weekday in form.cleaned_data.get('weekdays'):
period = TimePeriod(
weekday=weekday,
start_time=form.cleaned_data['start_time'],
end_time=form.cleaned_data['end_time'],
)
if desk:
period.desk = desk
elif agenda:
period.agenda = agenda
period.save()
class AgendaAddTimePeriodView(ManagedDeskMixin, FormView):
template_name = 'chrono/manager_time_period_form.html'
form_class = TimePeriodAddForm
def form_valid(self, form):
process_time_period_add_form(form, desk=self.desk)
return super(AgendaAddTimePeriodView, self).form_valid(form)
agenda_add_time_period = AgendaAddTimePeriodView.as_view()
class VirtualAgendaAddTimePeriodView(ManagedAgendaMixin, FormView):
template_name = 'chrono/manager_time_period_form.html'
form_class = TimePeriodAddForm
def form_valid(self, form):
process_time_period_add_form(form, agenda=self.agenda)
return super(VirtualAgendaAddTimePeriodView, self).form_valid(form)
virtual_agenda_add_time_period = VirtualAgendaAddTimePeriodView.as_view()
class TimePeriodEditView(ManagedTimePeriodMixin, UpdateView):
template_name = 'chrono/manager_time_period_form.html'
model = TimePeriod
form_class = TimePeriodForm
def get_form_kwargs(self):
kwargs = super(TimePeriodEditView, self).get_form_kwargs()
kwargs['has_desk'] = self.has_desk
return kwargs
time_period_edit = TimePeriodEditView.as_view()
class TimePeriodDeleteView(ManagedTimePeriodMixin, DeleteView):
template_name = 'chrono/manager_confirm_delete.html'
model = TimePeriod
time_period_delete = TimePeriodDeleteView.as_view()
class AgendaAddDesk(ManagedAgendaMixin, CreateView):
template_name = 'chrono/manager_desk_form.html'
model = Desk
form_class = NewDeskForm
agenda_add_desk = AgendaAddDesk.as_view()
class DeskEditView(ManagedAgendaSubobjectMixin, UpdateView):
template_name = 'chrono/manager_desk_form.html'
model = Desk
form_class = DeskForm
desk_edit = DeskEditView.as_view()
class DeskDeleteView(ManagedAgendaSubobjectMixin, DeleteView):
template_name = 'chrono/manager_confirm_delete.html'
model = Desk
def get_context_data(self, **kwargs):
context = super(DeskDeleteView, self).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(DeskDeleteView, self).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(VirtualMemberAddView, self).get_form_kwargs()
kwargs['initial']['virtual_agenda'] = kwargs['initial']['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(VirtualMemberDeleteView, self).dispatch(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super(VirtualMemberDeleteView, self).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 = TimePeriodExceptionForm
agenda_add_time_period_exception = AgendaAddTimePeriodExceptionView.as_view()
class TimePeriodExceptionEditView(ManagedDeskSubobjectMixin, UpdateView):
template_name = 'chrono/manager_time_period_exception_form.html'
model = TimePeriodException
form_class = TimePeriodExceptionForm
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(desk=self.desk, end_datetime__gte=now())
def get_context_data(self, **kwargs):
context = super(TimePeriodExceptionListView, self).get_context_data(**kwargs)
context['user_can_manage'] = self.desk.agenda.can_be_managed(self.request.user)
return context
time_period_exception_list = TimePeriodExceptionListView.as_view()
class TimePeriodExceptionExtractListView(TimePeriodExceptionListView):
paginate_by = None # no pagination
def get_queryset(self):
# but limit queryset
return super(TimePeriodExceptionExtractListView, self).get_queryset()[:10]
time_period_exception_extract_list = TimePeriodExceptionExtractListView.as_view()
class TimePeriodExceptionDeleteView(ManagedDeskSubobjectMixin, DeleteView):
template_name = 'chrono/manager_confirm_exception_delete.html'
model = TimePeriodException
def get_success_url(self):
referer = self.request.META.get('HTTP_REFERER')
success_url = reverse('chrono-manager-time-period-exception-list', kwargs={'pk': self.desk.pk})
if success_url in referer:
return success_url
success_url = super(TimePeriodExceptionDeleteView, self).get_success_url()
if 'from_popup' in self.request.GET:
success_url = '{}?display_exceptions={}'.format(success_url, self.desk.pk)
return success_url
time_period_exception_delete = TimePeriodExceptionDeleteView.as_view()
class DeskImportTimePeriodExceptionsView(ManagedAgendaSubobjectMixin, UpdateView):
model = Desk
form_class = ExceptionsImportForm
template_name = 'chrono/manager_import_exceptions.html'
def get_context_data(self, **kwargs):
context = super(DeskImportTimePeriodExceptionsView, self).get_context_data(**kwargs)
context['exception_sources'] = self.get_object().timeperiodexceptionsource_set.all()
return context
def form_valid(self, form):
exceptions = None
try:
if form.cleaned_data['ics_file']:
exceptions = form.instance.import_timeperiod_exceptions_from_ics_file(
form.cleaned_data['ics_file']
)
elif form.cleaned_data['ics_url']:
exceptions = form.instance.import_timeperiod_exceptions_from_remote_ics(
form.cleaned_data['ics_url']
)
except ICSError as e:
form.add_error(None, force_text(e))
return self.form_invalid(form)
if exceptions is not None:
message = ungettext(
'An exception has been imported.', '%(count)d exceptions have been imported.', exceptions
)
message = message % {'count': exceptions}
messages.info(self.request, message)
return super(DeskImportTimePeriodExceptionsView, self).form_valid(form)
desk_import_time_period_exceptions = DeskImportTimePeriodExceptionsView.as_view()
class TimePeriodExceptionSourceDeleteView(ManagedDeskSubobjectMixin, DeleteView):
template_name = 'chrono/manager_confirm_source_delete.html'
model = TimePeriodExceptionSource
def get_success_url(self):
return reverse('chrono-manager-agenda-settings', kwargs={'pk': self.desk.agenda_id})
time_period_exception_source_delete = TimePeriodExceptionSourceDeleteView.as_view()
class TimePeriodExceptionSourceReplaceView(ManagedDeskSubobjectMixin, UpdateView):
model = TimePeriodExceptionSource
form_class = TimePeriodExceptionSourceReplaceForm
template_name = 'chrono/manager_replace_exceptions.html'
def get_queryset(self):
queryset = super(TimePeriodExceptionSourceReplaceView, self).get_queryset()
return queryset.filter(ics_filename__isnull=False)
def form_valid(self, form):
exceptions = None
try:
exceptions = form.instance.desk.import_timeperiod_exceptions_from_ics_file(
form.cleaned_data['ics_newfile'], source=form.instance
)
except ICSError as e:
form.add_error(None, force_text(e))
return self.form_invalid(form)
if exceptions is not None:
message = ungettext(
'An exception has been imported.', '%(count)d exceptions have been imported.', exceptions
)
message = message % {'count': exceptions}
messages.info(self.request, message)
return super(TimePeriodExceptionSourceReplaceView, self).form_valid(form)
time_period_exception_source_replace = TimePeriodExceptionSourceReplaceView.as_view()
class TimePeriodExceptionSourceRefreshView(ManagedDeskSubobjectMixin, DetailView):
model = TimePeriodExceptionSource
def get_queryset(self):
queryset = super(TimePeriodExceptionSourceRefreshView, self).get_queryset()
return queryset.filter(ics_url__isnull=False)
def get(self, request, *args, **kwargs):
try:
source = self.get_object()
exceptions = source.desk.import_timeperiod_exceptions_from_remote_ics(
source.ics_url, source=source
)
except ICSError as e:
messages.error(self.request, force_text(e))
else:
message = ungettext(
'An exception has been imported.', '%(count)d exceptions have been imported.', exceptions
)
message = message % {'count': exceptions}
messages.info(self.request, message)
# redirect to settings
return HttpResponseRedirect(
reverse('chrono-manager-agenda-settings', kwargs={'pk': source.desk.agenda_id})
)
time_period_exception_source_refresh = TimePeriodExceptionSourceRefreshView.as_view()
def menu_json(request):
label = _('Agendas')
json_str = json.dumps(
[
{
'label': force_text(label),
'slug': 'calendar',
'url': request.build_absolute_uri(reverse('chrono-manager-homepage')),
}
]
)
content_type = 'application/json'
for variable in ('jsonpCallback', 'callback'):
if variable in request.GET:
json_str = '%s(%s);' % (request.GET[variable], json_str)
content_type = 'application/javascript'
break
response = HttpResponse(content_type=content_type)
response.write(json_str)
return response