2016-02-13 16:31:14 +01:00
|
|
|
# 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/>.
|
|
|
|
|
2016-09-11 11:31:29 +02:00
|
|
|
import datetime
|
|
|
|
|
2016-06-20 09:34:53 +02:00
|
|
|
from django.core.urlresolvers import reverse
|
2016-10-28 17:52:21 +02:00
|
|
|
from django.http import Http404
|
2016-07-20 13:22:58 +02:00
|
|
|
from django.shortcuts import get_object_or_404
|
2016-09-11 11:31:29 +02:00
|
|
|
from django.utils.timezone import now, make_aware
|
2016-02-13 16:31:14 +01:00
|
|
|
|
2016-07-20 13:29:09 +02:00
|
|
|
from rest_framework import permissions, serializers
|
2016-09-11 11:31:29 +02:00
|
|
|
from rest_framework.exceptions import APIException
|
2016-02-13 16:31:14 +01:00
|
|
|
from rest_framework.generics import GenericAPIView
|
|
|
|
from rest_framework.response import Response
|
2016-03-30 00:51:34 +02:00
|
|
|
from rest_framework.views import APIView
|
2016-02-13 16:31:14 +01:00
|
|
|
|
2016-09-11 11:31:29 +02:00
|
|
|
from ..agendas.models import Agenda, Event, Booking, MeetingType, TimePeriod
|
2016-06-19 21:23:23 +02:00
|
|
|
|
|
|
|
|
|
|
|
class Agendas(GenericAPIView):
|
2016-10-28 17:52:21 +02:00
|
|
|
def get(self, request, agenda_identifier=None, format=None):
|
2016-06-19 21:23:23 +02:00
|
|
|
response = {'data': [{
|
|
|
|
'id': x.id,
|
|
|
|
'slug': x.slug,
|
2016-06-20 09:34:53 +02:00
|
|
|
'api': {
|
|
|
|
'datetimes': request.build_absolute_uri(
|
2016-10-28 17:52:21 +02:00
|
|
|
reverse('api-agenda-datetimes',
|
|
|
|
kwargs={'agenda_identifier': x.slug})),
|
2016-06-20 09:34:53 +02:00
|
|
|
},
|
2016-06-19 21:23:23 +02:00
|
|
|
'text': x.label}
|
|
|
|
for x in Agenda.objects.all().order_by('label')]}
|
|
|
|
return Response(response)
|
|
|
|
|
|
|
|
agendas = Agendas.as_view()
|
2016-02-13 16:31:14 +01:00
|
|
|
|
|
|
|
|
|
|
|
class Datetimes(GenericAPIView):
|
2016-10-28 17:52:21 +02:00
|
|
|
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:
|
|
|
|
raise Http404()
|
2016-09-11 11:31:29 +02:00
|
|
|
if agenda.kind != 'events':
|
|
|
|
raise APIException('not an events agenda')
|
|
|
|
|
2016-09-11 19:16:15 +02:00
|
|
|
kwargs = {}
|
|
|
|
kwargs['start_datetime__gte'] = (now() + datetime.timedelta(days=agenda.minimal_booking_delay)).date()
|
|
|
|
if agenda.maximal_booking_delay:
|
|
|
|
kwargs['start_datetime__lt'] = (now() + datetime.timedelta(days=agenda.maximal_booking_delay)).date()
|
|
|
|
|
2016-10-28 17:52:21 +02:00
|
|
|
entries = Event.objects.filter(agenda=agenda).filter(**kwargs)
|
2016-09-11 11:31:29 +02:00
|
|
|
|
2016-10-09 11:37:51 +02:00
|
|
|
response = {'data': [{'id': x.id,
|
|
|
|
'text': unicode(x),
|
|
|
|
'disabled': bool(x.full)} for x in entries]}
|
2016-02-13 16:31:14 +01:00
|
|
|
return Response(response)
|
|
|
|
|
|
|
|
datetimes = Datetimes.as_view()
|
2016-02-13 16:52:04 +01:00
|
|
|
|
|
|
|
|
2016-09-11 11:31:29 +02:00
|
|
|
class MeetingDatetimes(GenericAPIView):
|
|
|
|
def get(self, request, pk=None, format=None):
|
|
|
|
meeting_type = MeetingType.objects.get(id=pk)
|
|
|
|
agenda = meeting_type.agenda
|
|
|
|
if agenda.kind != 'meetings':
|
|
|
|
raise APIException('not a meetings agenda')
|
|
|
|
|
2016-09-11 19:16:15 +02:00
|
|
|
now_datetime = now()
|
|
|
|
min_datetime = now() + datetime.timedelta(days=agenda.minimal_booking_delay)
|
|
|
|
max_datetime = now() + datetime.timedelta(days=agenda.maximal_booking_delay)
|
2016-09-11 11:31:29 +02:00
|
|
|
|
|
|
|
all_time_slots = []
|
|
|
|
for time_period in TimePeriod.objects.filter(agenda=agenda):
|
|
|
|
all_time_slots.extend(time_period.get_time_slots(
|
|
|
|
min_datetime=min_datetime,
|
|
|
|
max_datetime=max_datetime,
|
|
|
|
meeting_type=meeting_type))
|
|
|
|
|
|
|
|
busy_time_slots = Event.objects.filter(agenda=agenda,
|
|
|
|
start_datetime__gte=min_datetime,
|
|
|
|
start_datetime__lte=max_datetime + datetime.timedelta(meeting_type.duration))
|
|
|
|
busy_time_slots = list(busy_time_slots)
|
|
|
|
|
|
|
|
entries = []
|
|
|
|
# there's room for optimisations here, for a start both lists
|
|
|
|
# could be presorted and past busy time slots removed along the way.
|
|
|
|
for time_slot in all_time_slots:
|
2016-09-11 19:16:15 +02:00
|
|
|
if time_slot.start_datetime < now_datetime:
|
|
|
|
continue
|
2016-09-11 11:31:29 +02:00
|
|
|
if any((x for x in busy_time_slots if x.full and time_slot.intersects(x))):
|
|
|
|
continue
|
|
|
|
entries.append(time_slot)
|
|
|
|
|
|
|
|
entries.sort(key=lambda x: x.start_datetime)
|
|
|
|
|
|
|
|
response = {'data': [{'id': x.id, 'text': unicode(x)} for x in entries]}
|
|
|
|
return Response(response)
|
|
|
|
|
|
|
|
meeting_datetimes = MeetingDatetimes.as_view()
|
|
|
|
|
|
|
|
|
2016-02-13 16:52:04 +01:00
|
|
|
class SlotSerializer(serializers.Serializer):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
class Fillslot(GenericAPIView):
|
|
|
|
serializer_class = SlotSerializer
|
2016-06-18 11:59:26 +02:00
|
|
|
permission_classes = (permissions.IsAuthenticated,)
|
2016-02-13 16:52:04 +01:00
|
|
|
|
2016-10-28 17:52:21 +02:00
|
|
|
def post(self, request, agenda_identifier=None, event_pk=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:
|
|
|
|
raise Http404()
|
|
|
|
|
2016-09-11 11:31:29 +02:00
|
|
|
if agenda.kind == 'meetings':
|
|
|
|
# event is actually a timeslot, convert to a real event object
|
|
|
|
meeting_type_id, start_datetime_str = event_pk.split(':')
|
|
|
|
start_datetime = make_aware(datetime.datetime.strptime(
|
|
|
|
start_datetime_str, '%Y-%m-%d-%H%M'))
|
|
|
|
event, created = Event.objects.get_or_create(agenda=agenda,
|
|
|
|
meeting_type_id=meeting_type_id,
|
|
|
|
start_datetime=start_datetime,
|
|
|
|
defaults={'full': False, 'places': 1})
|
|
|
|
if created:
|
|
|
|
event.save()
|
|
|
|
event_pk = event.id
|
|
|
|
|
2016-02-13 17:52:32 +01:00
|
|
|
event = Event.objects.filter(id=event_pk)[0]
|
2016-06-20 14:52:56 +02:00
|
|
|
new_booking = Booking(event_id=event_pk, extra_data=request.data)
|
2016-07-07 16:21:55 +02:00
|
|
|
|
|
|
|
if event.waiting_list_places:
|
|
|
|
if event.waiting_list >= event.waiting_list_places:
|
2016-07-20 08:48:56 +02:00
|
|
|
return Response({'err': 1, 'reason': 'sold out'})
|
2016-07-07 16:21:55 +02:00
|
|
|
|
|
|
|
if event.booked_places >= event.places or event.waiting_list:
|
|
|
|
# if this is full or there are people waiting, put new bookings
|
|
|
|
# in the waiting list.
|
|
|
|
new_booking.in_waiting_list = True
|
|
|
|
else:
|
|
|
|
if event.booked_places >= event.places:
|
2016-07-20 08:48:56 +02:00
|
|
|
return Response({'err': 1, 'reason': 'sold out'})
|
2016-07-07 16:21:55 +02:00
|
|
|
|
2016-06-20 14:52:56 +02:00
|
|
|
new_booking.save()
|
2016-07-07 16:21:55 +02:00
|
|
|
response = {
|
|
|
|
'err': 0,
|
|
|
|
'in_waiting_list': new_booking.in_waiting_list,
|
|
|
|
'booking_id': new_booking.id,
|
|
|
|
}
|
2016-02-13 16:52:04 +01:00
|
|
|
return Response(response)
|
|
|
|
|
|
|
|
fillslot = Fillslot.as_view()
|
2016-03-30 00:51:34 +02:00
|
|
|
|
|
|
|
|
|
|
|
class BookingAPI(APIView):
|
2016-06-18 11:59:26 +02:00
|
|
|
permission_classes = (permissions.IsAuthenticated,)
|
|
|
|
|
2016-03-30 00:51:34 +02:00
|
|
|
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):
|
2016-06-23 19:31:12 +02:00
|
|
|
self.booking.cancel()
|
2016-03-30 00:51:34 +02:00
|
|
|
response = {'err': 0, 'booking_id': self.booking.id}
|
|
|
|
return Response(response)
|
|
|
|
|
|
|
|
booking = BookingAPI.as_view()
|
2016-06-18 12:24:21 +02:00
|
|
|
|
|
|
|
|
2016-06-23 19:31:12 +02:00
|
|
|
class CancelBooking(APIView):
|
2016-07-20 13:22:58 +02:00
|
|
|
'''
|
|
|
|
Cancel a booking.
|
|
|
|
|
|
|
|
It will return an error (code 1) if the booking was already cancelled.
|
|
|
|
'''
|
2016-06-23 19:31:12 +02:00
|
|
|
permission_classes = (permissions.IsAuthenticated,)
|
|
|
|
|
|
|
|
def post(self, request, booking_pk=None, format=None):
|
2016-07-20 13:22:58 +02:00
|
|
|
booking = get_object_or_404(Booking, id=booking_pk)
|
|
|
|
if booking.cancellation_datetime:
|
|
|
|
response = {'err': 1, 'reason': 'already cancelled'}
|
|
|
|
return Response(response)
|
2016-06-23 19:31:12 +02:00
|
|
|
booking.cancel()
|
|
|
|
response = {'err': 0, 'booking_id': booking.id}
|
|
|
|
return Response(response)
|
|
|
|
|
|
|
|
cancel_booking = CancelBooking.as_view()
|
|
|
|
|
|
|
|
|
2016-07-07 16:21:55 +02:00
|
|
|
class AcceptBooking(APIView):
|
2016-07-20 13:22:58 +02:00
|
|
|
'''
|
|
|
|
Accept a booking currently in the waiting list.
|
|
|
|
|
|
|
|
It will return error codes if the booking was cancelled before (code 1) and
|
|
|
|
if the booking was not in waiting list (code 2).
|
|
|
|
'''
|
2016-07-07 16:21:55 +02:00
|
|
|
permission_classes = (permissions.IsAuthenticated,)
|
|
|
|
|
|
|
|
def post(self, request, booking_pk=None, format=None):
|
2016-07-20 13:22:58 +02:00
|
|
|
booking = get_object_or_404(Booking, id=booking_pk)
|
|
|
|
if booking.cancellation_datetime:
|
|
|
|
response = {'err': 1, 'reason': 'booking is cancelled'}
|
|
|
|
return Response(response)
|
|
|
|
if not booking.in_waiting_list:
|
|
|
|
response = {'err': 2, 'reason': 'booking is not in waiting list'}
|
|
|
|
return Response(response)
|
2016-07-07 16:21:55 +02:00
|
|
|
booking.accept()
|
|
|
|
response = {'err': 0, 'booking_id': booking.id}
|
|
|
|
return Response(response)
|
|
|
|
|
|
|
|
accept_booking = AcceptBooking.as_view()
|
|
|
|
|
|
|
|
|
2016-06-18 12:24:21 +02:00
|
|
|
class SlotStatus(GenericAPIView):
|
|
|
|
serializer_class = SlotSerializer
|
|
|
|
permission_classes = (permissions.IsAuthenticated,)
|
|
|
|
|
2016-10-28 17:52:21 +02:00
|
|
|
def get(self, request, agenda_identifier=None, event_pk=None, format=None):
|
2016-07-22 13:45:53 +02:00
|
|
|
event = get_object_or_404(Event, id=event_pk)
|
2016-06-18 12:24:21 +02:00
|
|
|
response = {
|
|
|
|
'err': 0,
|
|
|
|
'places': {
|
|
|
|
'total': event.places,
|
|
|
|
'reserved': event.booked_places,
|
|
|
|
'available': event.places - event.booked_places,
|
|
|
|
}
|
|
|
|
}
|
2016-07-22 13:41:23 +02:00
|
|
|
if event.waiting_list_places:
|
|
|
|
response['places']['waiting_list_total'] = event.waiting_list_places
|
|
|
|
response['places']['waiting_list_reserved'] = event.waiting_list
|
|
|
|
response['places']['waiting_list_available'] = (event.waiting_list_places - event.waiting_list)
|
2016-06-18 12:24:21 +02:00
|
|
|
return Response(response)
|
|
|
|
|
|
|
|
slot_status = SlotStatus.as_view()
|