From 6bc22b0dfece9a21b120c5ff75b3ef20165e9040 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20P=C3=A9ters?= Date: Wed, 30 Mar 2016 00:51:34 +0200 Subject: [PATCH] general: make it possible to cancel bookings (#9999) --- .../0004_booking_cancellation_datetime.py | 20 +++++++++++++++++++ chrono/agendas/models.py | 8 ++++++-- chrono/api/urls.py | 1 + chrono/api/views.py | 16 +++++++++++++++ requirements.txt | 2 +- setup.py | 2 +- tests/test_agendas.py | 16 ++++++++++++++- tests/test_api.py | 9 +++++++++ 8 files changed, 69 insertions(+), 5 deletions(-) create mode 100644 chrono/agendas/migrations/0004_booking_cancellation_datetime.py diff --git a/chrono/agendas/migrations/0004_booking_cancellation_datetime.py b/chrono/agendas/migrations/0004_booking_cancellation_datetime.py new file mode 100644 index 00000000..2eac49c0 --- /dev/null +++ b/chrono/agendas/migrations/0004_booking_cancellation_datetime.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('agendas', '0003_booking'), + ] + + operations = [ + migrations.AddField( + model_name='booking', + name='cancellation_datetime', + field=models.DateTimeField(null=True), + preserve_default=True, + ), + ] diff --git a/chrono/agendas/models.py b/chrono/agendas/models.py index fe938cbd..a900d5be 100644 --- a/chrono/agendas/models.py +++ b/chrono/agendas/models.py @@ -16,7 +16,7 @@ from django.core.urlresolvers import reverse from django.db import models -from django.db.models import Count +from django.db.models.expressions import RawSQL from django.utils.text import slugify from django.utils.translation import ugettext_lazy as _ @@ -52,7 +52,10 @@ class Agenda(models.Model): class EventManager(models.Manager): def get_queryset(self): return super(EventManager, self).get_queryset().annotate( - booked_places=Count('booking')) + booked_places=RawSQL('''SELECT count(*) + FROM agendas_booking + WHERE agendas_booking.event_id = agendas_event.id + AND cancellation_datetime IS NULL''', ())) class Event(models.Model): @@ -69,3 +72,4 @@ class Event(models.Model): class Booking(models.Model): event = models.ForeignKey(Event) extra_data = JSONField(null=True) + cancellation_datetime = models.DateTimeField(null=True) diff --git a/chrono/api/urls.py b/chrono/api/urls.py index c6a663bb..b765c7e7 100644 --- a/chrono/api/urls.py +++ b/chrono/api/urls.py @@ -21,4 +21,5 @@ from . import views urlpatterns = patterns('', url(r'agenda/(?P\w+)/datetimes/', views.datetimes), url(r'agenda/(?P\w+)/fillslot/(?P\w+)/', views.fillslot), + url(r'booking/(?P\w+)/', views.booking), ) diff --git a/chrono/api/views.py b/chrono/api/views.py index be7376cb..025cb61f 100644 --- a/chrono/api/views.py +++ b/chrono/api/views.py @@ -21,6 +21,7 @@ from django.utils.timezone import localtime, now from rest_framework import serializers, status from rest_framework.generics import GenericAPIView from rest_framework.response import Response +from rest_framework.views import APIView from ..agendas.models import Event, Booking @@ -57,3 +58,18 @@ class Fillslot(GenericAPIView): return Response(response) fillslot = Fillslot.as_view() + + +class BookingAPI(APIView): + 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.cancellation_datetime = now() + self.booking.save() + response = {'err': 0, 'booking_id': self.booking.id} + return Response(response) + +booking = BookingAPI.as_view() diff --git a/requirements.txt b/requirements.txt index 7589f80e..80c146f0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -django>=1.7, <1.8 +django>=1.8, <1.9 gadjo djangorestframework>=3.1 django-jsonfield >= 0.9.3 diff --git a/setup.py b/setup.py index 61d38ea5..e8b5a395 100644 --- a/setup.py +++ b/setup.py @@ -102,7 +102,7 @@ setup( 'Programming Language :: Python', 'Programming Language :: Python :: 2', ], - install_requires=['django>=1.7, <1.8', + install_requires=['django>=1.8, <1.9', 'gadjo', 'djangorestframework>=3.1', 'django-jsonfield >= 0.9.3', diff --git a/tests/test_agendas.py b/tests/test_agendas.py index b4cdb2e7..46f36295 100644 --- a/tests/test_agendas.py +++ b/tests/test_agendas.py @@ -1,6 +1,8 @@ import pytest -from chrono.agendas.models import Agenda +from django.utils.timezone import now + +from chrono.agendas.models import Agenda, Event, Booking pytestmark = pytest.mark.django_db @@ -25,3 +27,15 @@ def test_duplicate_slugs(): agenda = Agenda(label=u'Foo baz') agenda.save() assert agenda.slug == 'foo-baz-2' + +def test_event_manager(): + agenda = Agenda(label=u'Foo baz') + agenda.save() + event = Event(start_datetime=now(), places=10, agenda=agenda) + event.save() + booking = Booking(event=event) + booking.save() + assert Event.objects.all()[0].booked_places == 1 + booking.cancellation_datetime = now() + booking.save() + assert Event.objects.all()[0].booked_places == 0 diff --git a/tests/test_api.py b/tests/test_api.py index 8b84843d..1f3f2f43 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -66,6 +66,15 @@ def test_booking_api_with_data(app, some_data): assert Booking.objects.count() == 1 assert Booking.objects.all()[0].extra_data == {'hello': 'world'} +def test_booking_cancellation_api(app, some_data): + agenda_id = Agenda.objects.filter(label=u'Foo bar')[0].id + event = Event.objects.filter(agenda_id=agenda_id)[0] + resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event.id)) + booking_id = resp.json['booking_id'] + assert Booking.objects.count() == 1 + resp = app.delete('/api/booking/%s/' % booking_id) + assert Booking.objects.filter(cancellation_datetime__isnull=False).count() == 1 + def test_soldout(app, some_data): agenda_id = Agenda.objects.filter(label=u'Foo bar')[0].id event = Event.objects.filter(agenda_id=agenda_id).exclude(start_datetime__lt=now())[0]