chrono/chrono/api/views.py

1076 lines
40 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/>.
from collections import defaultdict
import datetime
from django.db import transaction
from django.http import Http404, HttpResponse
from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.utils.dateparse import parse_date
from django.utils.encoding import force_text
from django.utils.timezone import now, make_aware, localtime
from django.utils.translation import gettext_noop
from django.utils.translation import ugettext_lazy as _
from rest_framework import permissions, serializers, status
from rest_framework.views import APIView
from chrono.api.utils import Response
from ..agendas.models import Agenda, Event, Booking, MeetingType, TimePeriod, Desk
from ..interval import Intervals
def format_response_datetime(dt):
return localtime(dt).strftime('%Y-%m-%d %H:%M:%S')
def get_exceptions_by_desk(agenda):
exceptions_by_desk = {}
for desk in Desk.objects.filter(agenda=agenda).prefetch_related('timeperiodexception_set'):
exceptions_by_desk[desk.id] = [
(exc.start_datetime, exc.end_datetime) for exc in desk.timeperiodexception_set.all()
]
return exceptions_by_desk
def get_min_datetime(agenda):
if agenda.minimal_booking_delay is None:
return None
min_datetime = now() + datetime.timedelta(days=agenda.minimal_booking_delay)
return min_datetime.replace(hour=0, minute=0, second=0, microsecond=0)
def get_max_datetime(agenda):
if agenda.maximal_booking_delay is None:
return None
max_datetime = now() + datetime.timedelta(days=agenda.maximal_booking_delay)
max_datetime = max_datetime.replace(hour=0, minute=0, second=0, microsecond=0)
return max_datetime + datetime.timedelta(days=1)
def get_all_slots(agenda, meeting_type):
min_datetime = get_min_datetime(agenda)
max_datetime = get_max_datetime(agenda)
time_period_filters = {
'min_datetime': min_datetime,
'max_datetime': max_datetime,
'meeting_type': meeting_type,
}
base_date = now().date()
agendas = agenda.get_real_agendas()
base_agenda = agenda
open_slots = {}
for agenda in agendas:
open_slots[agenda] = defaultdict(lambda: Intervals())
for agenda in agendas:
used_time_period_filters = time_period_filters.copy()
if used_time_period_filters['min_datetime'] is None:
used_time_period_filters['min_datetime'] = get_min_datetime(agenda)
if used_time_period_filters['max_datetime'] is None:
used_time_period_filters['max_datetime'] = get_max_datetime(agenda)
for raw_time_period in TimePeriod.objects.filter(desk__agenda=agenda):
for time_period in raw_time_period.get_effective_timeperiods(
base_agenda.excluded_timeperiods.all()
):
duration = (
datetime.datetime.combine(base_date, time_period.end_time)
- datetime.datetime.combine(base_date, time_period.start_time)
).seconds / 60
if duration < meeting_type.duration:
# skip time period that can't even hold a single meeting
continue
for slot in time_period.get_time_slots(**used_time_period_filters):
slot.full = False
open_slots[agenda][time_period.desk_id].add(slot.start_datetime, slot.end_datetime, slot)
# remove excluded slot
for agenda in agendas:
excluded_slot_by_desk = get_exceptions_by_desk(agenda)
for desk, excluded_interval in excluded_slot_by_desk.items():
for interval in excluded_interval:
begin, end = interval
open_slots[agenda][desk].remove_overlap(localtime(begin), localtime(end))
for agenda in agendas:
used_min_datetime = min_datetime
if used_min_datetime is None:
used_min_datetime = get_min_datetime(agenda)
used_max_datetime = max_datetime
if used_max_datetime is None:
used_max_datetime = get_max_datetime(agenda)
for event in (
agenda.event_set.filter(
agenda=agenda,
start_datetime__gte=used_min_datetime,
start_datetime__lte=used_max_datetime + datetime.timedelta(meeting_type.duration),
)
.select_related('meeting_type')
.exclude(booking__cancellation_datetime__isnull=False)
):
for slot in open_slots[agenda][event.desk_id].search_data(
event.start_datetime, event.end_datetime
):
slot.full = True
slots = []
for agenda in agendas:
for desk in open_slots[agenda]:
slots.extend(open_slots[agenda][desk].iter_data())
slots.sort(key=lambda slot: slot.start_datetime)
return slots
def get_agenda_detail(request, agenda):
agenda_detail = {
'id': agenda.slug,
'slug': agenda.slug, # kept for compatibility
'text': agenda.label,
'kind': agenda.kind,
'minimal_booking_delay': agenda.minimal_booking_delay,
'maximal_booking_delay': agenda.maximal_booking_delay,
}
if agenda.kind == 'events':
agenda_detail['api'] = {
'datetimes_url': request.build_absolute_uri(
reverse('api-agenda-datetimes', kwargs={'agenda_identifier': agenda.slug})
)
}
elif agenda.accept_meetings():
agenda_detail['api'] = {
'meetings_url': request.build_absolute_uri(
reverse('api-agenda-meetings', kwargs={'agenda_identifier': agenda.slug})
),
'desks_url': request.build_absolute_uri(
reverse('api-agenda-desks', kwargs={'agenda_identifier': agenda.slug})
),
}
agenda_detail['api']['fillslots_url'] = request.build_absolute_uri(
reverse('api-agenda-fillslots', kwargs={'agenda_identifier': agenda.slug})
)
return agenda_detail
def get_event_places(event):
places = {
'total': event.places,
'reserved': event.booked_places,
'available': event.places - event.booked_places,
}
if event.waiting_list_places:
places['waiting_list_total'] = event.waiting_list_places
places['waiting_list_reserved'] = event.waiting_list
places['waiting_list_available'] = event.waiting_list_places - event.waiting_list
return places
class Agendas(APIView):
permission_classes = ()
def get(self, request, format=None):
agendas_queryset = Agenda.objects.all().order_by('label')
if 'q' in request.GET:
if not request.GET['q']:
return Response({'data': []})
agendas_queryset = agendas_queryset.filter(slug__icontains=request.GET['q'])
agendas = [get_agenda_detail(request, agenda) for agenda in agendas_queryset]
return Response({'data': agendas})
agendas = Agendas.as_view()
class AgendaDetail(APIView):
'''
Retrieve an agenda instance.
'''
permission_classes = ()
def get(self, request, agenda_identifier):
agenda = get_object_or_404(Agenda, slug=agenda_identifier)
return Response({'data': get_agenda_detail(request, agenda)})
agenda_detail = AgendaDetail.as_view()
class Datetimes(APIView):
permission_classes = ()
def get(self, request, agenda_identifier=None, format=None):
try:
agenda = Agenda.objects.get(slug=agenda_identifier)
except Agenda.DoesNotExist:
try:
# legacy access by agenda id
agenda = Agenda.objects.get(id=int(agenda_identifier))
except (ValueError, Agenda.DoesNotExist):
raise Http404()
if agenda.kind != 'events':
raise Http404('agenda found, but it was not an events agenda')
entries = Event.objects.filter(agenda=agenda)
# we never want to allow booking for past events.
entries = entries.filter(start_datetime__gte=localtime(now()))
if agenda.minimal_booking_delay:
entries = entries.filter(
start_datetime__gte=localtime(
now() + datetime.timedelta(days=agenda.minimal_booking_delay)
).replace(hour=0, minute=0)
)
if agenda.maximal_booking_delay:
entries = entries.filter(
start_datetime__lt=localtime(
now() + datetime.timedelta(days=agenda.maximal_booking_delay)
).replace(hour=0, minute=0)
)
if 'date_start' in request.GET:
entries = entries.filter(
start_datetime__gte=make_aware(
datetime.datetime.combine(parse_date(request.GET['date_start']), datetime.time(0, 0))
)
)
if 'date_end' in request.GET:
entries = entries.filter(
start_datetime__lt=make_aware(
datetime.datetime.combine(parse_date(request.GET['date_end']), datetime.time(0, 0))
)
)
response = {
'data': [
{
'id': x.id,
'slug': x.slug,
'text': force_text(x),
'datetime': format_response_datetime(x.start_datetime),
'description': x.description,
'pricing': x.pricing,
'url': x.url,
'disabled': bool(x.full),
'api': {
'fillslot_url': request.build_absolute_uri(
reverse(
'api-fillslot',
kwargs={'agenda_identifier': agenda.slug, 'event_identifier': x.id,},
)
),
'status_url': request.build_absolute_uri(
reverse(
'api-event-status',
kwargs={'agenda_identifier': agenda.slug, 'event_identifier': x.id,},
)
),
},
}
for x in entries
]
}
return Response(response)
datetimes = Datetimes.as_view()
class MeetingDatetimes(APIView):
permission_classes = ()
def get(self, request, agenda_identifier=None, meeting_identifier=None, format=None):
try:
if agenda_identifier is None:
# legacy access by meeting id
meeting_type = MeetingType.objects.get(id=meeting_identifier)
agenda = meeting_type.agenda
else:
agenda = Agenda.objects.get(slug=agenda_identifier)
meeting_type = agenda.get_meetingtype(slug=meeting_identifier)
except (ValueError, MeetingType.DoesNotExist, Agenda.DoesNotExist):
raise Http404()
now_datetime = now()
slots = get_all_slots(agenda, meeting_type)
entries = {}
for slot in slots:
if slot.start_datetime < now_datetime:
continue
key = (slot.start_datetime, slot.end_datetime)
if key in entries and slot.full:
continue
entries[key] = slot
slots = sorted(entries.values(), key=lambda x: x.start_datetime)
# create fillslot API URL as a template, to avoid expensive calls
# to request.build_absolute_uri()
fake_event_identifier = '__event_identifier__'
fillslot_url = request.build_absolute_uri(
reverse(
'api-fillslot',
kwargs={'agenda_identifier': agenda.slug, 'event_identifier': fake_event_identifier,},
)
)
response = {
'data': [
{
'id': x.id,
'datetime': format_response_datetime(x.start_datetime),
'text': force_text(x),
'disabled': bool(x.full),
'api': {'fillslot_url': fillslot_url.replace(fake_event_identifier, str(x.id)),},
}
for x in slots
]
}
return Response(response)
meeting_datetimes = MeetingDatetimes.as_view()
class MeetingList(APIView):
permission_classes = ()
def get(self, request, agenda_identifier=None, format=None):
try:
agenda = Agenda.objects.get(slug=agenda_identifier)
except Agenda.DoesNotExist:
raise Http404()
if not agenda.accept_meetings():
raise Http404('agenda found, but it does not accept meetings')
meeting_types = []
for meeting_type in agenda.iter_meetingtypes():
meeting_types.append(
{
'text': meeting_type.label,
'id': meeting_type.slug,
'duration': meeting_type.duration,
'api': {
'datetimes_url': request.build_absolute_uri(
reverse(
'api-agenda-meeting-datetimes',
kwargs={
'agenda_identifier': agenda.slug,
'meeting_identifier': meeting_type.slug,
},
)
),
},
}
)
return Response({'data': meeting_types})
meeting_list = MeetingList.as_view()
class AgendaDeskList(APIView):
permission_classes = ()
def get(self, request, agenda_identifier=None, format=None):
try:
agenda = Agenda.objects.get(slug=agenda_identifier)
except Agenda.DoesNotExist:
raise Http404()
if agenda.kind != 'meetings':
raise Http404('agenda found, but it was not a meetings agenda')
desks = [{'id': x.slug, 'text': x.label} for x in agenda.desk_set.all()]
return Response({'data': desks})
agenda_desk_list = AgendaDeskList.as_view()
class SlotSerializer(serializers.Serializer):
'''
payload to fill one slot. The slot (event id) is in the URL.
'''
label = serializers.CharField(max_length=250, allow_blank=True)
user_name = serializers.CharField(max_length=250, allow_blank=True)
user_display_label = serializers.CharField(max_length=250, allow_blank=True)
backoffice_url = serializers.URLField(allow_blank=True)
count = serializers.IntegerField(min_value=1)
cancel_booking_id = serializers.CharField(max_length=250, allow_blank=True, allow_null=True)
force_waiting_list = serializers.BooleanField(default=False)
class StringOrListField(serializers.ListField):
def to_internal_value(self, data):
if isinstance(data, str):
data = [s.strip() for s in data.split(',')]
return super(StringOrListField, self).to_internal_value(data)
class SlotsSerializer(SlotSerializer):
'''
payload to fill multiple slots: same as SlotSerializer, but the
slots list is in the payload.
'''
slots = StringOrListField(required=True, child=serializers.CharField(max_length=64, allow_blank=False))
class Fillslots(APIView):
permission_classes = (permissions.IsAuthenticated,)
serializer_class = SlotsSerializer
def post(self, request, agenda_identifier=None, event_identifier=None, format=None):
return self.fillslot(request=request, agenda_identifier=agenda_identifier, format=format)
def fillslot(self, request, agenda_identifier=None, slots=[], format=None):
multiple_booking = bool(not slots)
try:
agenda = Agenda.objects.get(slug=agenda_identifier)
except Agenda.DoesNotExist:
try:
# legacy access by agenda id
agenda = Agenda.objects.get(id=int(agenda_identifier))
except (ValueError, Agenda.DoesNotExist):
raise Http404()
serializer = self.serializer_class(data=request.data, partial=True)
if not serializer.is_valid():
return Response(
{
'err': 1,
'err_class': 'invalid payload',
'err_desc': _('invalid payload'),
'errors': serializer.errors,
},
status=status.HTTP_400_BAD_REQUEST,
)
payload = serializer.validated_data
if 'slots' in payload:
slots = payload['slots']
if not slots:
return Response(
{
'err': 1,
'err_class': 'slots list cannot be empty',
'err_desc': _('slots list cannot be empty'),
},
status=status.HTTP_400_BAD_REQUEST,
)
if 'count' in payload:
places_count = payload['count']
elif 'count' in request.query_params:
# legacy: count in the query string
try:
places_count = int(request.query_params['count'])
except ValueError:
return Response(
{
'err': 1,
'err_class': 'invalid value for count (%s)' % request.query_params['count'],
'err_desc': _('invalid value for count (%s)') % request.query_params['count'],
},
status=status.HTTP_400_BAD_REQUEST,
)
else:
places_count = 1
if places_count <= 0:
return Response(
{
'err': 1,
'err_class': 'count cannot be less than or equal to zero',
'err_desc': _('count cannot be less than or equal to zero'),
},
status=status.HTTP_400_BAD_REQUEST,
)
to_cancel_booking = None
cancel_booking_id = None
if payload.get('cancel_booking_id'):
try:
cancel_booking_id = int(payload.get('cancel_booking_id'))
except (ValueError, TypeError):
return Response(
{
'err': 1,
'err_class': 'cancel_booking_id is not an integer',
'err_desc': _('cancel_booking_id is not an integer'),
},
status=status.HTTP_400_BAD_REQUEST,
)
if cancel_booking_id is not None:
cancel_error = None
try:
to_cancel_booking = Booking.objects.get(pk=cancel_booking_id)
if to_cancel_booking.cancellation_datetime:
cancel_error = gettext_noop('cancel booking: booking already cancelled')
else:
to_cancel_places_count = (
to_cancel_booking.secondary_booking_set.filter(event=to_cancel_booking.event).count()
+ 1
)
if places_count != to_cancel_places_count:
cancel_error = gettext_noop('cancel booking: count is different')
except Booking.DoesNotExist:
cancel_error = gettext_noop('cancel booking: booking does no exist')
if cancel_error:
return Response({'err': 1, 'err_class': cancel_error, 'err_desc': _(cancel_error),})
extra_data = {}
for k, v in request.data.items():
if k not in serializer.validated_data:
extra_data[k] = v
available_desk = None
if agenda.accept_meetings():
# slots are actually timeslot ids (meeting_type:start_datetime), not events ids.
# split them back to get both parts
meeting_type_id = slots[0].split(':')[0]
datetimes = set()
for slot in slots:
try:
meeting_type_id_, datetime_str = slot.split(':')
except ValueError:
return Response(
{
'err': 1,
'err_class': 'invalid slot: %s' % slot,
'err_desc': _('invalid slot: %s') % slot,
},
status=status.HTTP_400_BAD_REQUEST,
)
if meeting_type_id_ != meeting_type_id:
return Response(
{
'err': 1,
'err_class': 'all slots must have the same meeting type id (%s)'
% meeting_type_id,
'err_desc': _('all slots must have the same meeting type id (%s)')
% meeting_type_id,
},
status=status.HTTP_400_BAD_REQUEST,
)
datetimes.add(make_aware(datetime.datetime.strptime(datetime_str, '%Y-%m-%d-%H%M')))
# get all free slots and separate them by desk
all_slots = get_all_slots(agenda, agenda.get_meetingtype(id_=meeting_type_id))
all_free_slots = [slot for slot in all_slots if not slot.full]
datetimes_by_desk = defaultdict(set)
for slot in all_free_slots:
datetimes_by_desk[slot.desk.id].add(slot.start_datetime)
available_desk = None
if agenda.kind == 'virtual':
# Compute fill_rate by agenda/date
fill_rates = defaultdict(dict)
for slot in all_slots:
ref_date = slot.start_datetime.date()
if ref_date not in fill_rates[slot.desk.agenda]:
date_dict = fill_rates[slot.desk.agenda][ref_date] = {'free': 0, 'full': 0}
else:
date_dict = fill_rates[slot.desk.agenda][ref_date]
if slot.full:
date_dict['full'] += 1
else:
date_dict['free'] += 1
for dd in fill_rates.values():
for date_dict in dd.values():
date_dict['fill_rate'] = date_dict['full'] / (date_dict['full'] + date_dict['free'])
# select a desk on the agenda with min fill_rate on the given date
for available_desk_id in sorted(datetimes_by_desk.keys()):
if datetimes.issubset(datetimes_by_desk[available_desk_id]):
desk = Desk.objects.get(id=available_desk_id)
if available_desk is None:
available_desk = desk
available_desk_rate = 0
for dt in datetimes:
available_desk_rate += fill_rates[available_desk.agenda][dt.date()][
'fill_rate'
]
else:
for dt in datetimes:
desk_rate = 0
for dt in datetimes:
desk_rate += fill_rates[desk.agenda][dt.date()]['fill_rate']
if desk_rate < available_desk_rate:
available_desk = desk
available_desk_rate = desk_rate
else:
# meeting agenda
# search first desk where all requested slots are free
for available_desk_id in sorted(datetimes_by_desk.keys()):
if datetimes.issubset(datetimes_by_desk[available_desk_id]):
available_desk = Desk.objects.get(id=available_desk_id)
break
if available_desk is None:
return Response(
{
'err': 1,
'err_class': 'no more desk available',
'err_desc': _('no more desk available'),
}
)
# all datetimes are free, book them in order
datetimes = list(datetimes)
datetimes.sort()
# get a real meeting_type for virtual agenda
if agenda.kind == 'virtual':
meeting_type_id = MeetingType.objects.get(
agenda=available_desk.agenda, slug=meeting_type_id
).pk
# booking requires real Event objects (not lazy Timeslots);
# create them now, with data from the slots and the desk we found.
events = []
for start_datetime in datetimes:
events.append(
Event.objects.create(
agenda=available_desk.agenda,
meeting_type_id=meeting_type_id,
start_datetime=start_datetime,
full=False,
places=1,
desk=available_desk,
)
)
else:
try:
events = Event.objects.filter(id__in=[int(s) for s in slots]).order_by('start_datetime')
except ValueError:
events = Event.objects.filter(slug__in=slots).order_by('start_datetime')
if not events.count():
return Response(
{
'err': 1,
'err_class': 'unknown event identifiers or slugs',
'err_desc': _('unknown event identifiers or slugs'),
},
status=status.HTTP_400_BAD_REQUEST,
)
# search free places. Switch to waiting list if necessary.
in_waiting_list = False
for event in events:
if payload.get('force_waiting_list') and not event.waiting_list_places:
return Response({'err': 1, 'err_class': 'no waiting list', 'err_desc': _('no waiting list')})
if event.waiting_list_places:
if (
payload.get('force_waiting_list')
or (event.booked_places + places_count) > event.places
or event.waiting_list
):
# if this is full or there are people waiting, put new bookings
# in the waiting list.
in_waiting_list = True
if (event.waiting_list + places_count) > event.waiting_list_places:
return Response({'err': 1, 'err_class': 'sold out', 'err_desc': _('sold out')})
else:
if (event.booked_places + places_count) > event.places:
return Response({'err': 1, 'err_class': 'sold out', 'err_desc': _('sold out')})
with transaction.atomic():
if to_cancel_booking:
cancelled_booking_id = to_cancel_booking.pk
to_cancel_booking.cancel()
# now we have a list of events, book them.
primary_booking = None
for event in events:
for i in range(places_count):
new_booking = Booking(
event_id=event.id,
in_waiting_list=in_waiting_list,
label=payload.get('label', ''),
user_name=payload.get('user_name', ''),
backoffice_url=payload.get('backoffice_url', ''),
user_display_label=payload.get('user_display_label', ''),
extra_data=extra_data,
)
if primary_booking is not None:
new_booking.primary_booking = primary_booking
new_booking.save()
if primary_booking is None:
primary_booking = new_booking
response = {
'err': 0,
'in_waiting_list': in_waiting_list,
'booking_id': primary_booking.id,
'datetime': format_response_datetime(events[0].start_datetime),
'api': {
'cancel_url': request.build_absolute_uri(
reverse('api-cancel-booking', kwargs={'booking_pk': primary_booking.id})
),
'ics_url': request.build_absolute_uri(
reverse('api-booking-ics', kwargs={'booking_pk': primary_booking.id})
),
},
}
if in_waiting_list:
response['api']['accept_url'] = request.build_absolute_uri(
reverse('api-accept-booking', kwargs={'booking_pk': primary_booking.id})
)
else:
response['api']['suspend_url'] = request.build_absolute_uri(
reverse('api-suspend-booking', kwargs={'booking_pk': primary_booking.pk})
)
if agenda.accept_meetings():
response['end_datetime'] = format_response_datetime(events[-1].end_datetime)
response['duration'] = (events[-1].end_datetime - events[-1].start_datetime).seconds // 60
if available_desk:
response['desk'] = {'label': available_desk.label, 'slug': available_desk.slug}
if to_cancel_booking:
response['cancelled_booking_id'] = cancelled_booking_id
if agenda.kind == 'events' and not multiple_booking:
event = events[0]
response['places'] = get_event_places(event)
if agenda.kind == 'events' and multiple_booking:
response['events'] = [
{
'slug': x.slug,
'text': str(x),
'datetime': format_response_datetime(x.start_datetime),
'description': x.description,
}
for x in events
]
return Response(response)
fillslots = Fillslots.as_view()
class Fillslot(Fillslots):
serializer_class = SlotSerializer
def post(self, request, agenda_identifier=None, event_identifier=None, format=None):
return self.fillslot(
request=request,
agenda_identifier=agenda_identifier,
slots=[event_identifier], # fill a "list on one slot"
format=format,
)
fillslot = Fillslot.as_view()
class BookingAPI(APIView):
permission_classes = (permissions.IsAuthenticated,)
def initial(self, request, *args, **kwargs):
super(BookingAPI, self).initial(request, *args, **kwargs)
self.booking = Booking.objects.get(id=kwargs.get('booking_pk'), cancellation_datetime__isnull=True)
def delete(self, request, *args, **kwargs):
self.booking.cancel()
response = {'err': 0, 'booking_id': self.booking.id}
return Response(response)
booking = BookingAPI.as_view()
class CancelBooking(APIView):
'''
Cancel a booking.
It will return error codes if the booking was cancelled before (code 1) or
if the booking is not primary (code 2).
'''
permission_classes = (permissions.IsAuthenticated,)
def post(self, request, booking_pk=None, format=None):
booking = get_object_or_404(Booking, id=booking_pk)
if booking.cancellation_datetime:
response = {
'err': 1,
'err_class': 'already cancelled',
'err_desc': _('already cancelled'),
}
return Response(response)
if booking.primary_booking is not None:
response = {
'err': 2,
'err_class': 'secondary booking',
'err_desc': _('secondary booking'),
}
return Response(response)
booking.cancel()
response = {'err': 0, 'booking_id': booking.id}
return Response(response)
cancel_booking = CancelBooking.as_view()
class AcceptBooking(APIView):
'''
Accept a booking currently in the waiting list.
It will return error codes if the booking was cancelled before (code 1),
if the booking is not primary (code 2) or
if the booking was not in waiting list (code 3).
'''
permission_classes = (permissions.IsAuthenticated,)
def post(self, request, booking_pk=None, format=None):
booking = get_object_or_404(Booking, id=booking_pk)
if booking.cancellation_datetime:
response = {
'err': 1,
'err_class': 'booking is cancelled',
'err_desc': _('booking is cancelled'),
}
return Response(response)
if booking.primary_booking is not None:
response = {
'err': 2,
'err_class': 'secondary booking',
'err_desc': _('secondary booking'),
}
return Response(response)
if not booking.in_waiting_list:
response = {
'err': 3,
'err_class': 'booking is not in waiting list',
'err_desc': _('booking is not in waiting list'),
}
return Response(response)
booking.accept()
event = booking.event
response = {
'err': 0,
'booking_id': booking.pk,
'overbooked_places': max(0, event.booked_places - event.places),
}
return Response(response)
accept_booking = AcceptBooking.as_view()
class SuspendBooking(APIView):
'''
Suspend a accepted booking.
It will return error codes if the booking was cancelled before (code 1)
if the booking is not primary (code 2) or
if the booking is already in waiting list (code 3).
'''
permission_classes = (permissions.IsAuthenticated,)
def post(self, request, booking_pk=None, format=None):
booking = get_object_or_404(Booking, pk=booking_pk)
if booking.cancellation_datetime:
response = {
'err': 1,
'err_class': 'booking is cancelled',
'err_desc': _('booking is cancelled'),
}
return Response(response)
if booking.primary_booking is not None:
response = {
'err': 2,
'err_class': 'secondary booking',
'err_desc': _('secondary booking'),
}
return Response(response)
if booking.in_waiting_list:
response = {
'err': 3,
'err_class': 'booking is already in waiting list',
'err_desc': _('booking is already in waiting list'),
}
return Response(response)
booking.suspend()
response = {'err': 0, 'booking_id': booking.pk}
return Response(response)
suspend_booking = SuspendBooking.as_view()
class ResizeSerializer(serializers.Serializer):
count = serializers.IntegerField(min_value=1)
class ResizeBooking(APIView):
'''
Resize a booking.
It will return error codes if the booking was cancelled before (code 1)
if the booking is not primary (code 2)
if the event is sold out (code 3) or
if the booking is on multi events (code 4).
'''
permission_classes = (permissions.IsAuthenticated,)
serializer_class = ResizeSerializer
def post(self, request, booking_pk=None, format=None):
serializer = self.serializer_class(data=request.data)
if not serializer.is_valid():
return Response(
{
'err': 1,
'err_class': 'invalid payload',
'err_desc': _('invalid payload'),
'errors': serializer.errors,
},
status=status.HTTP_400_BAD_REQUEST,
)
payload = serializer.validated_data
booking = get_object_or_404(Booking, pk=booking_pk)
event = booking.event
if booking.cancellation_datetime:
response = {
'err': 1,
'err_class': 'booking is cancelled',
'err_desc': _('booking is cancelled'),
}
return Response(response)
if booking.primary_booking is not None:
response = {
'err': 2,
'err_class': 'secondary booking',
'err_desc': _('secondary booking'),
}
return Response(response)
event_ids = set([event.pk])
secondary_bookings = booking.secondary_booking_set.all().order_by('-creation_datetime')
for secondary in secondary_bookings:
event_ids.add(secondary.event_id)
if len(event_ids) > 1:
response = {
'err': 4,
'err_class': 'can not resize multi event booking',
'err_desc': _('can not resize multi event booking'),
}
return Response(response)
count = payload['count']
booked_places = event.waiting_list_places and event.waiting_list or event.booked_places
if booked_places < count:
if (
event.waiting_list
and count > event.waiting_list_places
or not event.waiting_list
and count > event.places
):
response = {
'err': 3,
'err_class': 'sold out',
'err_desc': _('sold out'),
}
return Response(response)
with transaction.atomic():
if booked_places > count:
# decrease places
for secondary in secondary_bookings[: booked_places - count]:
secondary.delete()
elif booked_places < count:
# increase places
bulk_bookings = []
for i in range(0, count - booked_places):
bulk_bookings.append(
booking.clone(
in_waiting_list=bool(event.waiting_list_places and event.waiting_list),
primary_booking=booking,
save=False,
)
)
Booking.objects.bulk_create(bulk_bookings)
response = {'err': 0, 'booking_id': booking.pk}
return Response(response)
resize_booking = ResizeBooking.as_view()
class SlotStatus(APIView):
permission_classes = (permissions.IsAuthenticated,)
def get_object(self, event_identifier):
try:
return Event.objects.get(slug=event_identifier)
except Event.DoesNotExist:
try:
# legacy access by event id
return Event.objects.get(pk=int(event_identifier))
except (ValueError, Event.DoesNotExist):
raise Http404()
def get(self, request, agenda_identifier=None, event_identifier=None, format=None):
event = self.get_object(event_identifier)
response = {
'err': 0,
'places': get_event_places(event),
}
return Response(response)
slot_status = SlotStatus.as_view()
class BookingICS(APIView):
permission_classes = (permissions.IsAuthenticated,)
def get(self, request, booking_pk=None, format=None):
booking = get_object_or_404(Booking, id=booking_pk)
response = HttpResponse(booking.get_ics(request), content_type='text/calendar')
return response
booking_ics = BookingICS.as_view()