general: add waiting list support (#11111)

This commit is contained in:
Frédéric Péters 2016-07-07 16:21:55 +02:00
parent 40a83b3f4c
commit 5aeb24d58a
5 changed files with 154 additions and 22 deletions

View File

@ -16,6 +16,7 @@
from django.core.urlresolvers import reverse
from django.db import models
from django.db import transaction
from django.db.models.expressions import RawSQL
from django.utils.formats import date_format
from django.utils.text import slugify
@ -51,23 +52,15 @@ class Agenda(models.Model):
return reverse('chrono-manager-agenda-view', kwargs={'pk': self.id})
class EventManager(models.Manager):
def get_queryset(self):
return super(EventManager, self).get_queryset().annotate(
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):
agenda = models.ForeignKey(Agenda)
start_datetime = models.DateTimeField(_('Date/time'))
places = models.PositiveIntegerField(_('Places'))
waiting_list_places = models.PositiveIntegerField(
_('Places in waiting list'), default=0)
label = models.CharField(_('Label'), max_length=50, null=True, blank=True,
help_text=_('Optional label to identify this date.'))
objects = EventManager()
full = models.BooleanField(default=False)
class Meta:
ordering = ['agenda', 'start_datetime']
@ -77,11 +70,39 @@ class Event(models.Model):
return self.label
return date_format(localtime(self.start_datetime), format='DATETIME_FORMAT')
@property
def booked_places(self):
return self.booking_set.filter(cancellation_datetime__isnull=True,
in_waiting_list=False).count()
@property
def waiting_list(self):
return self.booking_set.filter(cancellation_datetime__isnull=True,
in_waiting_list=True).count()
class Booking(models.Model):
event = models.ForeignKey(Event)
extra_data = JSONField(null=True)
cancellation_datetime = models.DateTimeField(null=True)
in_waiting_list = models.BooleanField(default=False)
creation_datetime = models.DateTimeField(auto_now_add=True)
def save(self, *args, **kwargs):
with transaction.atomic():
super(Booking, self).save(*args, **kwargs)
initial_value = self.event.full
self.event.full = bool(
(self.event.booked_places >= self.event.places and
self.event.waiting_list_places == 0) or
self.event.waiting_list >= self.event.waiting_list_places)
if self.event.full != initial_value:
self.event.save()
def cancel(self):
self.cancellation_datetime = now()
self.save()
def accept(self):
self.in_waiting_list = False
self.save()

View File

@ -25,4 +25,5 @@ urlpatterns = patterns('',
url(r'agenda/(?P<agenda_pk>\w+)/status/(?P<event_pk>\w+)/$', views.slot_status),
url(r'booking/(?P<booking_pk>\w+)/$', views.booking),
url(r'booking/(?P<booking_pk>\w+)/cancel/$', views.cancel_booking),
url(r'booking/(?P<booking_pk>\w+)/accept/$', views.accept_booking),
)

View File

@ -48,10 +48,9 @@ class Datetimes(GenericAPIView):
response = {'data': [{
'id': x.id,
'text': unicode(x)}
for x in Event.objects.filter(agenda=pk).filter(
places__gt=F('booked_places'),
start_datetime__gte=min_datetime)
]}
for x in Event.objects.filter(agenda=pk).filter(
start_datetime__gte=min_datetime,
full=False)]}
return Response(response)
datetimes = Datetimes.as_view()
@ -67,11 +66,26 @@ class Fillslot(GenericAPIView):
def post(self, request, agenda_pk=None, event_pk=None, format=None):
event = Event.objects.filter(id=event_pk)[0]
if event.booked_places >= event.places:
return Response({'err': 1, 'reason': 'sold out'}, status.HTTP_400_BAD_REQUEST)
new_booking = Booking(event_id=event_pk, extra_data=request.data)
if event.waiting_list_places:
if event.waiting_list >= event.waiting_list_places:
return Response({'err': 1, 'reason': 'sold out'}, status.HTTP_400_BAD_REQUEST)
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:
return Response({'err': 1, 'reason': 'sold out'}, status.HTTP_400_BAD_REQUEST)
new_booking.save()
response = {'err': 0, 'booking_id': new_booking.id}
response = {
'err': 0,
'in_waiting_list': new_booking.in_waiting_list,
'booking_id': new_booking.id,
}
return Response(response)
fillslot = Fillslot.as_view()
@ -105,6 +119,20 @@ class CancelBooking(APIView):
cancel_booking = CancelBooking.as_view()
class AcceptBooking(APIView):
permission_classes = (permissions.IsAuthenticated,)
def post(self, request, booking_pk=None, format=None):
booking = Booking.objects.get(id=booking_pk,
cancellation_datetime__isnull=True,
in_waiting_list=True)
booking.accept()
response = {'err': 0, 'booking_id': booking.id}
return Response(response)
accept_booking = AcceptBooking.as_view()
class SlotStatus(GenericAPIView):
serializer_class = SlotSerializer
permission_classes = (permissions.IsAuthenticated,)

View File

@ -25,9 +25,16 @@
{% for event in object.event_set.all %}
<li data-total="{{event.places}}" data-booked="{{event.booked_places}}"><a rel="popup" href="{% url 'chrono-manager-event-edit' pk=event.id %}">
{% if event.label %}{{event.label}} / {% endif %}
{% blocktrans with start=event.start_datetime places=event.places booked_places=event.booked_places %}
{{ start }} ({{ places }} places, {{ booked_places }} booked places)
{% endblocktrans %}</a>
{{ event.start_datetime }}
({% blocktrans with places=event.places booked_places=event.booked_places %}{{ places }} places, {{ booked_places }} booked places{% endblocktrans %}
{% if event.waiting_list_places %}
/
{% blocktrans with places=event.waiting_list_places waiting_places=event.waiting_list %}
{{waiting_places}} on {{ places }} in waiting list
{% endblocktrans %}
{% endif %}
)
</a>
<span class="occupation-bar"></span>
</li>
{% endfor %}

View File

@ -63,7 +63,6 @@ def test_datetimes_api(app, some_data):
assert 'data' in resp.json
assert len(resp.json['data']) == 3
def test_datetime_api_fr(app, some_data):
agenda_id = Agenda.objects.filter(label=u'Foo bar')[0].id
with override_settings(LANGUAGE_CODE='fr-fr'):
@ -159,3 +158,79 @@ def test_status(app, some_data, user):
assert resp.json['places']['total'] == 10
assert resp.json['places']['available'] == 9
assert resp.json['places']['reserved'] == 1
def test_waiting_list_datetimes(app, some_data, user):
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]
event.waiting_list_places = 5
event.save()
resp = app.get('/api/agenda/%s/datetimes/' % agenda_id)
assert len(resp.json['data']) == 3
assert event.id in [x['id'] for x in resp.json['data']]
for i in range(event.places):
Booking(event=event).save()
# all places are booked but all the dates are still displayed as there is a
# waiting list.
resp = app.get('/api/agenda/%s/datetimes/' % agenda_id)
assert len(resp.json['data']) == 3
# fill the waiting list
for i in range(event.waiting_list_places):
Booking(event=event, in_waiting_list=True).save()
# the event datetime should no longer be returned
resp = app.get('/api/agenda/%s/datetimes/' % agenda_id)
assert len(resp.json['data']) == 2
assert not event.id in [x['id'] for x in resp.json['data']]
def test_waiting_list_booking(app, some_data, user):
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]
event.waiting_list_places = 5
event.save()
for i in range(event.places):
Booking(event=event).save()
app.authorization = ('Basic', ('john.doe', 'password'))
resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event.id), status=200)
assert resp.json['err'] == 0
assert resp.json['in_waiting_list'] is True
# cancel a booking that was not on the waiting list
booking = Booking.objects.filter(event=event, in_waiting_list=False)[0]
booking.cancel()
# check new booking is still done on the waiting list
app.authorization = ('Basic', ('john.doe', 'password'))
resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event.id), status=200)
assert resp.json['err'] == 0
assert resp.json['in_waiting_list'] is True
# fill the waiting list
for i in range(event.waiting_list_places):
Booking(event=event, in_waiting_list=True).save()
app.authorization = ('Basic', ('john.doe', 'password'))
resp = app.post('/api/agenda/%s/fillslot/%s/' % (agenda_id, event.id), status=400)
assert resp.json['err'] == 1
def test_accept_booking(app, some_data, user):
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]
event.waiting_list_places = 5
event.save()
# create a booking on the waiting list
booking = Booking(event=event, in_waiting_list=True)
booking.save()
assert Booking.objects.filter(in_waiting_list=True).count() == 1
app.authorization = ('Basic', ('john.doe', 'password'))
resp = app.post('/api/booking/%s/accept/' % booking.id)
assert Booking.objects.filter(in_waiting_list=True).count() == 0
assert Booking.objects.filter(in_waiting_list=False).count() == 1