agendas: trigger full and places event fields (#54747)

This commit is contained in:
Lauréline Guérin 2021-08-16 15:59:01 +02:00
parent 5041ef37ce
commit 0b53360c0c
No known key found for this signature in database
GPG Key ID: 1FAB9B9B4F93D473
13 changed files with 403 additions and 182 deletions

View File

@ -0,0 +1,21 @@
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('agendas', '0097_min_booking_delay_working_days'),
]
operations = [
migrations.AddField(
model_name='event',
name='booked_places',
field=models.PositiveSmallIntegerField(default=0),
),
migrations.AddField(
model_name='event',
name='booked_waiting_list_places',
field=models.PositiveSmallIntegerField(default=0),
),
]

View File

@ -0,0 +1,27 @@
import os
from django.db import migrations
with open(
os.path.join(
os.path.dirname(os.path.realpath(__file__)), '..', 'sql', 'event_booked_places_and_full_triggers.sql'
)
) as sql_file:
sql_forwards = sql_file.read()
sql_backwards = """
DROP TRIGGER IF EXISTS update_event_full_fields_trigger ON agendas_event;
DROP TRIGGER IF EXISTS update_event_places_fields_trigger ON agendas_booking;
DROP FUNCTION IF EXISTS update_event_full_fields;
DROP FUNCTION IF EXISTS update_event_places_fields;
"""
class Migration(migrations.Migration):
dependencies = [
('agendas', '0098_event_booked_places'),
]
operations = [migrations.RunSQL(sql=sql_forwards, reverse_sql=sql_backwards)]

View File

@ -0,0 +1,13 @@
from django.db import migrations
# trigger places fields
sql_forwards = """UPDATE agendas_event SET id=id;"""
class Migration(migrations.Migration):
dependencies = [
('agendas', '0099_event_triggers'),
]
operations = [migrations.RunSQL(sql=sql_forwards, reverse_sql='')]

View File

@ -32,8 +32,7 @@ from django.contrib.postgres.fields import ArrayField, JSONField
from django.core.exceptions import FieldDoesNotExist, ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import IntegrityError, connection, models, transaction
from django.db.models import Count, IntegerField, Max, OuterRef, Prefetch, Q, Subquery, Value
from django.db.models.functions import Coalesce
from django.db.models import Count, Max, Prefetch, Q
from django.template import Context, Template, TemplateSyntaxError, VariableDoesNotExist, engines
from django.urls import reverse
from django.utils import functional
@ -645,7 +644,6 @@ class Agenda(models.Model):
def get_open_events(
self,
prefetched_queryset=False,
annotate_queryset=False,
include_full=True,
min_start=None,
max_start=None,
@ -690,9 +688,6 @@ class Agenda(models.Model):
if user_external_id and not prefetched_queryset:
entries = Event.annotate_queryset_for_user(entries, user_external_id)
if annotate_queryset and not prefetched_queryset:
entries = Event.annotate_queryset(entries)
if max_start:
entries = self.add_event_recurrences(
entries,
@ -706,7 +701,6 @@ class Agenda(models.Model):
def get_past_events(
self,
annotate_queryset=False,
min_start=None,
max_start=None,
user_external_id=None,
@ -729,9 +723,6 @@ class Agenda(models.Model):
if user_external_id:
entries = Event.annotate_queryset_for_user(entries, user_external_id)
if annotate_queryset:
entries = Event.annotate_queryset(entries)
if min_start:
entries = self.add_event_recurrences(
entries,
@ -1363,6 +1354,8 @@ class Event(models.Model):
)
pricing = models.CharField(_('Pricing'), max_length=150, null=True, blank=True)
url = models.CharField(_('URL'), max_length=200, null=True, blank=True)
booked_places = models.PositiveSmallIntegerField(default=0)
booked_waiting_list_places = models.PositiveSmallIntegerField(default=0)
almost_full = models.BooleanField(default=False)
full = models.BooleanField(default=False)
cancelled = models.BooleanField(default=False)
@ -1396,7 +1389,6 @@ class Event(models.Model):
assert self.agenda.kind != 'virtual', "an event can't reference a virtual agenda"
assert not (self.slug and self.slug.isdigit()), 'slug cannot be a number'
self.start_datetime = self.start_datetime.replace(second=0, microsecond=0)
self.check_full()
if not self.slug:
self.slug = generate_slug(self, seen_slugs=seen_slugs, agenda=self.agenda)
return super().save(*args, **kwargs)
@ -1406,17 +1398,9 @@ class Event(models.Model):
# label can be empty
return slugify(self.label or ('%s-event' % self.agenda.label))
@functional.cached_property
def main_list_full(self):
return bool(self.booked_places >= self.places)
def check_full(self):
self.full = bool(
(self.booked_places >= self.places and self.waiting_list_places == 0)
or (self.waiting_list_places and self.waiting_list >= self.waiting_list_places)
)
self.almost_full = bool(self.booked_places >= 0.9 * self.places)
def set_is_checked(self):
if not self.agenda.mark_event_checked_auto:
return
@ -1451,23 +1435,6 @@ class Event(models.Model):
def is_day_past(self):
return self.start_datetime.date() <= now().date()
@staticmethod
def annotate_queryset(qs):
not_cancelled_bookings = Booking.objects.filter(
cancellation_datetime__isnull=True, event=OuterRef('pk')
)
bookings = not_cancelled_bookings.filter(in_waiting_list=False).order_by().values('event')
count_bookings = bookings.annotate(count=Count('event')).values('count')
waiting_list_bookings = not_cancelled_bookings.filter(in_waiting_list=True).order_by().values('event')
count_waiting_list = waiting_list_bookings.annotate(count=Count('event')).values('count')
return qs.annotate(
booked_places_count=Coalesce(Subquery(count_bookings, output_field=IntegerField()), Value(0)),
waiting_list_count=Coalesce(Subquery(count_waiting_list, output_field=IntegerField()), Value(0)),
)
@staticmethod
def annotate_queryset_for_user(qs, user_external_id):
return qs.annotate(
@ -1489,25 +1456,13 @@ class Event(models.Model):
),
)
@property
def booked_places(self):
if hasattr(self, 'booked_places_count'):
return self.booked_places_count
return self.booking_set.filter(cancellation_datetime__isnull=True, in_waiting_list=False).count()
@property
def remaining_places(self):
return max(0, self.places - self.booked_places)
@property
def waiting_list(self):
if hasattr(self, 'waiting_list_count'):
return self.waiting_list_count
return self.booking_set.filter(cancellation_datetime__isnull=True, in_waiting_list=True).count()
@property
def remaining_waiting_list_places(self):
return max(0, self.waiting_list_places - self.waiting_list)
return max(0, self.waiting_list_places - self.booked_waiting_list_places)
@property
def end_datetime(self):
@ -1834,14 +1789,6 @@ class Booking(models.Model):
def user_name(self):
return ('%s %s' % (self.user_first_name, self.user_last_name)).strip()
def save(self, *args, **kwargs):
with transaction.atomic():
super().save(*args, **kwargs)
initial_values = self.event.full, self.event.almost_full
self.event.check_full()
if (self.event.full, self.event.almost_full) != initial_values:
self.event.save()
def cancel(self, trigger_callback=False):
timestamp = now()
with transaction.atomic():

View File

@ -0,0 +1,70 @@
CREATE OR REPLACE FUNCTION update_event_full_fields() RETURNS TRIGGER AS $$
BEGIN
IF (TG_OP = 'INSERT') THEN
NEW.booked_places = 0;
NEW.booked_waiting_list_places = 0;
ELSE
-- update booked_places and booked_waiting_list_places fields
SELECT
COUNT(1) FILTER (WHERE NOT(in_waiting_list)),
COUNT(1) FILTER (WHERE in_waiting_list)
INTO NEW.booked_places, NEW.booked_waiting_list_places
FROM agendas_booking b
WHERE b.event_id = NEW.id AND b.cancellation_datetime IS NULL;
END IF;
-- update almost_full field
IF (NEW.booked_places >= NEW.places * 0.9) THEN
NEW.almost_full = true;
ELSE
NEW.almost_full = false;
END IF;
-- update full field
IF (NEW.booked_places >= NEW.places) AND (NEW.waiting_list_places = 0) THEN
NEW.full = true;
ELSIF (NEW.waiting_list_places > 0) AND (NEW.booked_waiting_list_places >= NEW.waiting_list_places) THEN
NEW.full = true;
ELSE
NEW.full = false;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION update_event_places_fields() RETURNS TRIGGER AS $$
DECLARE
e_id integer;
BEGIN
IF (TG_OP = 'DELETE') THEN
e_id = OLD.event_id;
ELSE
e_id = NEW.event_id;
END IF;
-- update booked_places and booked_waiting_list_places fields on agendas_event
WITH new_stats AS (
SELECT
COUNT(1) FILTER (WHERE NOT(in_waiting_list)) AS booked,
COUNT(1) FILTER (WHERE in_waiting_list) AS waiting
FROM agendas_booking b
WHERE b.event_id = e_id AND b.cancellation_datetime IS NULL
)
UPDATE agendas_event
SET booked_places = new_stats.booked,
booked_waiting_list_places = new_stats.waiting
FROM new_stats
WHERE id = e_id AND (agendas_event.booked_places <> new_stats.booked
OR agendas_event.booked_waiting_list_places <> new_stats.waiting);
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS update_event_full_fields_trigger ON agendas_event;
CREATE TRIGGER update_event_full_fields_trigger BEFORE INSERT OR UPDATE ON agendas_event
FOR EACH ROW EXECUTE PROCEDURE update_event_full_fields();
DROP TRIGGER IF EXISTS update_event_places_fields_trigger ON agendas_booking;
CREATE TRIGGER update_event_places_fields_trigger AFTER INSERT OR UPDATE OR DELETE ON agendas_booking
FOR EACH ROW EXECUTE PROCEDURE update_event_places_fields();

View File

@ -396,7 +396,7 @@ def get_agenda_detail(request, agenda, check_events=False):
def get_event_places(event):
available = event.places - event.booked_places
available = event.remaining_places
places = {
'total': event.places,
'reserved': event.booked_places,
@ -407,9 +407,9 @@ def get_event_places(event):
if event.waiting_list_places:
places['has_waiting_list'] = True
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
places['waiting_list_activated'] = event.waiting_list > 0 or available <= 0
places['waiting_list_reserved'] = event.booked_waiting_list_places
places['waiting_list_available'] = event.remaining_waiting_list_places
places['waiting_list_activated'] = event.booked_waiting_list_places > 0 or available <= 0
# 'waiting_list_activated' means next booking will go into the waiting list
return places
@ -756,14 +756,12 @@ class Datetimes(APIView):
entries = []
if show_past:
entries += agenda.get_past_events(
annotate_queryset=True,
min_start=payload.get('date_start'),
max_start=payload.get('date_end'),
user_external_id=user_external_id,
)
if show_future:
entries += agenda.get_open_events(
annotate_queryset=True,
min_start=payload.get('date_start'),
max_start=payload.get('date_end'),
user_external_id=user_external_id,
@ -826,9 +824,7 @@ class MultipleAgendasDatetimes(APIView):
user_external_id = payload.get('user_external_id') or payload.get('exclude_user_external_id')
disable_booked = bool(payload.get('exclude_user_external_id'))
agendas = Agenda.prefetch_events_and_exceptions(
agendas, annotate_events=True, user_external_id=user_external_id
)
agendas = Agenda.prefetch_events_and_exceptions(agendas, user_external_id=user_external_id)
entries = []
for agenda in agendas:
@ -1393,12 +1389,12 @@ class Fillslots(APIView):
if (
payload.get('force_waiting_list')
or (event.booked_places + places_count) > event.places
or event.waiting_list
or event.booked_waiting_list_places
):
# 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:
if (event.booked_waiting_list_places + places_count) > event.waiting_list_places:
raise APIError(
_('sold out'),
err_class='sold out',
@ -1573,10 +1569,9 @@ class RecurringFillslots(APIView):
full_events = list(events_to_book.filter(full=True))
events_to_book = events_to_book.filter(full=False)
events_to_book = Event.annotate_queryset(events_to_book)
events_to_book = events_to_book.annotate(
in_waiting_list=ExpressionWrapper(
Q(booked_places_count__gte=F('places')) | Q(waiting_list_count__gt=0),
Q(booked_places__gte=F('places')) | Q(booked_waiting_list_places__gt=0),
output_field=BooleanField(),
)
)
@ -1584,19 +1579,11 @@ class RecurringFillslots(APIView):
extra_data = {k: v for k, v in request.data.items() if k not in payload}
bookings = [make_booking(event, payload, extra_data) for event in events_to_book]
events_to_update = Event.annotate_queryset(
agenda.event_set.filter(pk__in=events_to_unbook + [event.pk for event in events_to_book])
)
with transaction.atomic():
deleted_count = Booking.objects.filter(
user_external_id=user_external_id, event__in=events_to_unbook
).delete()[0]
Booking.objects.bulk_create(bookings)
events_to_update.update(
full=Q(booked_places_count__gte=F('places'), waiting_list_places=0)
| Q(waiting_list_places__gt=0, waiting_list_count__gte=F('waiting_list_places')),
almost_full=Q(booked_places_count__gte=0.9 * F('places')),
)
response = {
'err': 0,
@ -1633,8 +1620,6 @@ class EventsFillslots(APIView):
user_external_id = payload['user_external_id']
events = self.get_events(request, payload)
events = events.select_related('agenda')
events = Event.annotate_queryset(events)
already_booked_events = self.get_already_booked_events(user_external_id)
if start_datetime:
@ -1653,7 +1638,7 @@ class EventsFillslots(APIView):
events = events.annotate(
in_waiting_list=ExpressionWrapper(
Q(booked_places_count__gte=F('places')) | Q(waiting_list_count__gt=0),
Q(booked_places__gte=F('places')) | Q(booked_waiting_list_places__gt=0),
output_field=BooleanField(),
)
)
@ -1662,19 +1647,11 @@ class EventsFillslots(APIView):
extra_data = {k: v for k, v in request.data.items() if k not in payload}
bookings = [make_booking(event, payload, extra_data) for event in events]
events_to_update = Event.annotate_queryset(
Event.objects.filter(pk__in=events_to_unbook + [event.pk for event in events])
)
with transaction.atomic():
deleted_count = Booking.objects.filter(
user_external_id=user_external_id, event__in=events_to_unbook
).delete()[0]
Booking.objects.bulk_create(bookings)
events_to_update.update(
full=Q(booked_places_count__gte=F('places'), waiting_list_places=0)
| Q(waiting_list_places__gt=0, waiting_list_count__gte=F('waiting_list_places')),
almost_full=Q(booked_places_count__gte=0.9 * F('places')),
)
response = {
'err': 0,
@ -2106,7 +2083,7 @@ class ResizeBooking(APIView):
# total places for the event (in waiting or main list, depending on the primary booking location)
places = event.waiting_list_places if booking.in_waiting_list else event.places
# total booked places for the event (in waiting or main list, depending on the primary booking location)
booked_places = event.waiting_list if booking.in_waiting_list else event.booked_places
booked_places = event.booked_waiting_list_places if booking.in_waiting_list else event.booked_places
# places to book for this primary booking
primary_wanted_places = payload['count']

View File

@ -1,12 +1,12 @@
{% load i18n %}
<li class="{% if event.booked_places_count > event.places %}overbooking{% endif %}
<li class="{% if event.booked_places > event.places %}overbooking{% endif %}
{% if event.main_list_full %}full{% endif %}
{% if event.cancellation_status %}cancelled{% endif %}
{% if not event.in_bookable_period %}not-{% endif %}bookable"
{% if event.places %}
data-total="{{ event.places }}" data-booked="{{ event.booked_places_count }}"
data-total="{{ event.places }}" data-booked="{{ event.booked_places }}"
{% elif event.waiting_list_places %}
data-total="{{ event.waiting_list_places }}" data-booked="{{ event.waiting_list_count }}"
data-total="{{ event.waiting_list_places }}" data-booked="{{ event.booked_waiting_list_places }}"
{% endif %}
><a href="{% if view_mode == 'settings_view' %}{% url 'chrono-manager-event-edit' pk=agenda.pk event_pk=event.pk %}?next=settings{% elif event.pk %}{% url 'chrono-manager-event-view' pk=agenda.pk event_pk=event.pk %}{% else %}{% url 'chrono-manager-event-create-recurrence' pk=agenda.pk event_identifier=event.slug %}{% endif %}">
{% if event.cancellation_status %}
@ -40,7 +40,7 @@
({% trans "Waiting list:" %}
{% blocktrans count remaining_places=event.remaining_waiting_list_places %}{{ remaining_places }} remaining place{% plural %}{{ remaining_places }} remaining places{% endblocktrans %}
-
{% blocktrans with places=event.waiting_list_places count booked_places=event.waiting_list %}{{ booked_places }}/{{ places }} booking{% plural %}{{ booked_places }}/{{ places }} bookings{% endblocktrans %})
{% blocktrans with places=event.waiting_list_places count booked_places=event.booked_waiting_list_places %}{{ booked_places }}/{{ places }} booking{% plural %}{{ booked_places }}/{{ places }} bookings{% endblocktrans %})
{% endif %}
{% endif %}
{% if view_mode == 'settings_view' and event.publication_date %}

View File

@ -11,7 +11,7 @@
{% block content %}
<div class="section">
<h3>
{% blocktrans with places=object.places %}Bookings ({{ booked_places }}/{{ places }}){% endblocktrans %}
{% blocktrans with booked_places=object.booked_places places=object.places %}Bookings ({{ booked_places }}/{{ places }}){% endblocktrans %}
</h3>
<div>
<form class="check-bookings-filters">
@ -76,7 +76,7 @@
{% if object.waiting_list_places %}
<div class="section">
<h3>
{% blocktrans with places=object.waiting_list_places %}Waiting List ({{ waiting_places }}/{{ places }}){% endblocktrans %}
{% blocktrans with booked_places=object.booked_waiting_list_places places=object.waiting_list_places %}Waiting List ({{ booked_places }}/{{ places }}){% endblocktrans %}
</h3>
<div>
<table class="main check-bookings">

View File

@ -12,7 +12,7 @@
<div class="section">
<h3>
{% blocktrans with places=object.places booked_places=booked|length count remaining_places=object.remaining_places %}
{% blocktrans with places=object.places booked_places=object.booked_places count remaining_places=object.remaining_places %}
Bookings ({{ booked_places }}/{{ places }}): {{ remaining_places }} remaining place
{% plural %}
Bookings ({{ booked_places }}/{{ places }}): {{ remaining_places }} remaining places
@ -20,9 +20,9 @@
</h3>
<div>
{% if booked|length > event.places %}
{% if object.booked_places > event.places %}
<div class="errornotice"><p>{% trans "This event is overbooked." %}</p></div>
{% elif booked|length == event.places %}
{% elif object.booked_places == event.places %}
<div class="infonotice"><p>{% trans "This event is full." %}</p></div>
{% endif %}
@ -44,7 +44,7 @@
{% if object.waiting_list_places %}
<div class="section">
<h3>
{% blocktrans with places=object.waiting_list_places booked_places=waiting|length count remaining_places=object.remaining_waiting_list_places %}
{% blocktrans with places=object.waiting_list_places booked_places=object.booked_waiting_list_places count remaining_places=object.remaining_waiting_list_places %}
Waiting List ({{ booked_places }}/{{ places }}): {{ remaining_places }} remaining place
{% plural %}
Waiting List ({{ booked_places }}/{{ places }}): {{ remaining_places }} remaining places

View File

@ -25,7 +25,7 @@ import requests
from django.contrib import messages
from django.core.exceptions import PermissionDenied
from django.db import transaction
from django.db.models import BooleanField, Count, Max, Min, Q, Sum, Value
from django.db.models import BooleanField, Count, Max, Min, Q, Value
from django.http import Http404, HttpResponse, HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect, render
from django.template.defaultfilters import title
@ -1123,7 +1123,7 @@ class AgendaDayView(AgendaDateView, DayArchiveView):
qs = super().get_queryset()
if self.agenda.kind != 'events':
return qs
return Event.annotate_queryset(qs).order_by('start_datetime', 'label')
return qs.order_by('start_datetime', 'label')
def get_dated_items(self):
date_list, object_list, extra_context = super().get_dated_items()
@ -1253,7 +1253,7 @@ class AgendaMonthView(AgendaDateView, MonthArchiveView):
qs = super().get_queryset()
if self.agenda.kind != 'events':
return qs
return Event.annotate_queryset(qs).order_by('start_datetime', 'label')
return qs.order_by('start_datetime', 'label')
def get_dated_items(self):
date_list, object_list, extra_context = super().get_dated_items()
@ -1466,7 +1466,7 @@ class AgendaOpenEventsView(ViewableAgendaMixin, DetailView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['user_can_manage'] = self.agenda.can_be_managed(self.request.user)
context['open_events'] = self.agenda.get_open_events(annotate_queryset=True)
context['open_events'] = self.agenda.get_open_events()
return context
@ -1644,8 +1644,7 @@ class AgendaSettings(ManagedAgendaMixin, DetailView):
return context
def get_events(self):
qs = self.agenda.event_set.filter(primary_event__isnull=True)
return Event.annotate_queryset(qs)
return self.agenda.event_set.filter(primary_event__isnull=True)
def get_template_names(self):
return ['chrono/manager_%s_agenda_settings.html' % self.agenda.kind]
@ -2025,7 +2024,6 @@ class EventCheckView(ViewableAgendaMixin, DetailView):
# build booking list
context['booked'] = filterset.qs.order_by('user_last_name', 'user_first_name')
context['booked_places'] = context['booked'].aggregate(places=Sum('places_count'))['places']
context['booked_without_status'] = any(e.user_was_present is None for e in context['booked'])
if context['booked_without_status']:
context['absence_form'] = BookingAbsenceReasonForm(agenda=self.agenda)
@ -2041,7 +2039,6 @@ class EventCheckView(ViewableAgendaMixin, DetailView):
+ Count('secondary_booking_set', filter=Q(cancellation_datetime__isnull=True))
)
context['waiting'] = waiting_qs
context['waiting_places'] = waiting_qs.aggregate(places=Sum('places_count'))['places']
return context

View File

@ -168,17 +168,17 @@ def test_agendas_api(app):
event1 = Event.objects.create(
start_datetime=(localtime() + datetime.timedelta(days=5)).replace(hour=10, minute=0),
places=20,
places=1,
agenda=event_agenda,
)
event2 = Event.objects.create(
start_datetime=(localtime() + datetime.timedelta(days=10)).replace(hour=10, minute=0),
places=20,
places=1,
agenda=event_agenda,
)
event3 = Event.objects.create(
start_datetime=(localtime() + datetime.timedelta(days=15)).replace(hour=10, minute=0),
places=20,
places=1,
agenda=event_agenda,
)
@ -187,17 +187,26 @@ def test_agendas_api(app):
assert len(resp.json['data']) == 1
# one event is full
Event.objects.filter(pk=event1.pk).update(full=True)
event1.booking_set.create()
event1.refresh_from_db()
assert event1.full is True
resp = app.get('/api/agenda/', params={'with_open_events': '1'})
assert len(resp.json['data']) == 1
# all events are full
Event.objects.update(full=True)
event2.booking_set.create()
event2.refresh_from_db()
assert event2.full is True
event3.booking_set.create()
event3.refresh_from_db()
assert event3.full is True
resp = app.get('/api/agenda/', params={'with_open_events': '1'})
assert len(resp.json['data']) == 0
# event1 is not full but too soon
Event.objects.filter(pk=event1.pk).update(full=False)
event1.booking_set.all().delete()
event1.refresh_from_db()
assert event1.full is False
event_agenda.minimal_booking_delay = 10
event_agenda.save()
assert list(event_agenda.get_open_events()) == [event2, event3]
@ -205,7 +214,9 @@ def test_agendas_api(app):
assert len(resp.json['data']) == 0
# event3 is not full but too late
Event.objects.filter(pk=event3.pk).update(full=False)
event3.booking_set.all().delete()
event3.refresh_from_db()
assert event3.full is False
event_agenda.maximal_booking_delay = 12
event_agenda.save()
assert list(event_agenda.get_open_events()) == [event2]
@ -213,7 +224,9 @@ def test_agendas_api(app):
assert len(resp.json['data']) == 0
# events are not full but not published
Event.objects.update(full=False)
event2.booking_set.all().delete()
event2.refresh_from_db()
assert event2.full is False
event_agenda.event_set.update(publication_date=now().date() + datetime.timedelta(days=20))
assert list(event_agenda.get_open_events()) == []
resp = app.get('/api/agenda/', params={'with_open_events': '1'})
@ -366,17 +379,17 @@ def test_agenda_detail_api(app):
agenda = Agenda.objects.create(label='Foo bar', kind='events', minimal_booking_delay=0)
event1 = Event.objects.create(
start_datetime=(localtime() + datetime.timedelta(days=5)).replace(hour=10, minute=0),
places=20,
places=1,
agenda=agenda,
)
event2 = Event.objects.create(
start_datetime=(localtime() + datetime.timedelta(days=10)).replace(hour=10, minute=0),
places=20,
places=1,
agenda=agenda,
)
event3 = Event.objects.create(
start_datetime=(localtime() + datetime.timedelta(days=15)).replace(hour=10, minute=0),
places=20,
places=1,
agenda=agenda,
)
resp = app.get('/api/agenda/%s/' % agenda.slug)
@ -389,17 +402,26 @@ def test_agenda_detail_api(app):
assert data['api']['datetimes_url'] == 'http://testserver/api/agenda/foo-bar/datetimes/'
# one event is full
Event.objects.filter(pk=event1.pk).update(full=True)
event1.booking_set.create()
event1.refresh_from_db()
assert event1.full is True
resp = app.get('/api/agenda/%s/' % agenda.slug)
assert resp.json['data']['opened_events_available'] is True
# all events are full
Event.objects.update(full=True)
event2.booking_set.create()
event2.refresh_from_db()
assert event2.full is True
event3.booking_set.create()
event3.refresh_from_db()
assert event3.full is True
resp = app.get('/api/agenda/%s/' % agenda.slug)
assert resp.json['data']['opened_events_available'] is False
# event1 is not full but too soon
Event.objects.filter(pk=event1.pk).update(full=False)
event1.booking_set.all().delete()
event1.refresh_from_db()
assert event1.full is False
agenda.minimal_booking_delay = 10
agenda.save()
resp = app.get('/api/agenda/%s/' % agenda.slug)
@ -407,7 +429,9 @@ def test_agenda_detail_api(app):
assert resp.json['data']['opened_events_available'] is False
# event3 is not full but too late
Event.objects.filter(pk=event3.pk).update(full=False)
event3.booking_set.all().delete()
event3.refresh_from_db()
assert event3.full is False
agenda.maximal_booking_delay = 12
agenda.save()
resp = app.get('/api/agenda/%s/' % agenda.slug)
@ -415,7 +439,9 @@ def test_agenda_detail_api(app):
assert resp.json['data']['opened_events_available'] is False
# events are not full but not published
Event.objects.update(full=False)
event2.booking_set.all().delete()
event2.refresh_from_db()
assert event2.full is False
agenda.event_set.update(publication_date=now().date() + datetime.timedelta(days=20))
resp = app.get('/api/agenda/%s/' % agenda.slug)
assert list(agenda.get_open_events()) == []

View File

@ -1130,7 +1130,7 @@ def test_multiple_booking_api(app, some_data, user):
resp3 = app.post('/api/agenda/%s/fillslot/%s/?count=5' % (agenda.slug, event.id))
assert Event.objects.get(id=event.id).booked_places == 2
assert Event.objects.get(id=event.id).waiting_list == 5
assert Event.objects.get(id=event.id).booked_waiting_list_places == 5
# check waiting list overflow
resp = app.post('/api/agenda/%s/fillslot/%s/?count=5' % (agenda.slug, event.id))
@ -1139,12 +1139,12 @@ def test_multiple_booking_api(app, some_data, user):
assert resp.json['err_class'] == 'sold out'
assert resp.json['err_desc'] == 'sold out'
assert Event.objects.get(id=event.id).booked_places == 2
assert Event.objects.get(id=event.id).waiting_list == 5
assert Event.objects.get(id=event.id).booked_waiting_list_places == 5
# accept the waiting list
resp = app.post(resp3.json['api']['accept_url'])
assert Event.objects.get(id=event.id).booked_places == 7
assert Event.objects.get(id=event.id).waiting_list == 0
assert Event.objects.get(id=event.id).booked_waiting_list_places == 0
# check with a short waiting list
Booking.objects.all().delete()
@ -1161,7 +1161,7 @@ def test_multiple_booking_api(app, some_data, user):
resp = app.post('/api/agenda/%s/fillslot/%s/?count=3' % (agenda.slug, event.id))
assert resp.json['err'] == 0
assert Event.objects.get(id=event.id).booked_places == 3
assert Event.objects.get(id=event.id).waiting_list == 0
assert Event.objects.get(id=event.id).booked_waiting_list_places == 0
resp = app.post('/api/agenda/%s/fillslot/%s/?count=3' % (agenda.slug, event.id))
assert resp.json['err'] == 1
@ -1172,7 +1172,7 @@ def test_multiple_booking_api(app, some_data, user):
resp = app.post('/api/agenda/%s/fillslot/%s/?count=2' % (agenda.slug, event.id))
assert resp.json['err'] == 0
assert Event.objects.get(id=event.id).booked_places == 3
assert Event.objects.get(id=event.id).waiting_list == 2
assert Event.objects.get(id=event.id).booked_waiting_list_places == 2
def test_multiple_booking_api_fillslots(app, some_data, user):
@ -1238,7 +1238,7 @@ def test_multiple_booking_api_fillslots(app, some_data, user):
resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': slots, 'count': 5})
for event in events:
assert Event.objects.get(id=event.id).booked_places == 2
assert Event.objects.get(id=event.id).waiting_list == 5
assert Event.objects.get(id=event.id).booked_waiting_list_places == 5
accept_url = resp.json['api']['accept_url']
resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': slots, 'count': 5})
@ -1248,13 +1248,13 @@ def test_multiple_booking_api_fillslots(app, some_data, user):
assert resp.json['err_desc'] == 'sold out'
for event in events:
assert Event.objects.get(id=event.id).booked_places == 2
assert Event.objects.get(id=event.id).waiting_list == 5
assert Event.objects.get(id=event.id).booked_waiting_list_places == 5
# accept the waiting list
resp = app.post(accept_url)
for event in events:
assert Event.objects.get(id=event.id).booked_places == 7
assert Event.objects.get(id=event.id).waiting_list == 0
assert Event.objects.get(id=event.id).booked_waiting_list_places == 0
# check with a short waiting list
Booking.objects.all().delete()
@ -1273,7 +1273,7 @@ def test_multiple_booking_api_fillslots(app, some_data, user):
assert resp.json['err'] == 0
for event in events:
assert Event.objects.get(id=event.id).booked_places == 3
assert Event.objects.get(id=event.id).waiting_list == 0
assert Event.objects.get(id=event.id).booked_waiting_list_places == 0
resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': slots, 'count': 3})
assert resp.json['err'] == 1
@ -1285,7 +1285,7 @@ def test_multiple_booking_api_fillslots(app, some_data, user):
assert resp.json['err'] == 0
for event in events:
assert Event.objects.get(id=event.id).booked_places == 3
assert Event.objects.get(id=event.id).waiting_list == 2
assert Event.objects.get(id=event.id).booked_waiting_list_places == 2
def test_multiple_booking_move_booking(app, user):
@ -2143,8 +2143,8 @@ def test_recurring_events_api_fillslots(app, user, freezer):
assert Booking.objects.filter(event__primary_event=event).count() == 104
assert Booking.objects.filter(event__primary_event=sunday_event).count() == 52
events = Event.annotate_queryset(Event.objects.filter(primary_event__isnull=False))
assert events.filter(booked_places_count=1).count() == 156
events = Event.objects.filter(primary_event__isnull=False)
assert events.filter(booked_places=1).count() == 156
# one recurrence is booked separately
event = Event.objects.filter(primary_event__isnull=False).first()
@ -2155,15 +2155,15 @@ def test_recurring_events_api_fillslots(app, user, freezer):
assert resp.json['booking_count'] == 156
assert not resp.json['full_events']
assert Booking.objects.count() == 313
events = Event.annotate_queryset(Event.objects.filter(primary_event__isnull=False))
assert events.filter(booked_places_count=2).count() == 156
events = Event.objects.filter(primary_event__isnull=False)
assert events.filter(booked_places=2).count() == 156
# one booking has been put in waiting list
assert events.filter(waiting_list_count=1).count() == 1
assert events.filter(booked_waiting_list_places=1).count() == 1
params['user_external_id'] = 'user_id_3'
resp = app.post_json(fillslots_url, params=params)
# everything goes in waiting list
assert events.filter(waiting_list_count=1).count() == 156
assert events.filter(booked_waiting_list_places=1).count() == 156
# but an event was full
assert resp.json['booking_count'] == 155
assert len(resp.json['full_events']) == 1
@ -2221,14 +2221,14 @@ def test_recurring_events_api_fillslots_waiting_list(app, user, freezer):
# create bookings in waiting list
for recurrence in event.recurrences.all():
Booking.objects.create(event=recurrence, in_waiting_list=True)
events = Event.annotate_queryset(Event.objects.filter(primary_event__isnull=False))
assert events.filter(waiting_list_count=1).count() == 5
events = Event.objects.filter(primary_event__isnull=False)
assert events.filter(booked_waiting_list_places=1).count() == 5
# check that new bookings are put in waiting list despite free slots on main list
params = {'user_external_id': 'user_id', 'slots': 'event:0'}
resp = app.post_json('/api/agenda/%s/recurring-events/fillslots/' % agenda.slug, params=params)
assert resp.json['booking_count'] == 5
assert events.filter(waiting_list_count=2).count() == 5
assert events.filter(booked_waiting_list_places=2).count() == 5
def test_recurring_events_api_fillslots_change_bookings(app, user, freezer):
@ -2280,9 +2280,9 @@ def test_recurring_events_api_fillslots_change_bookings(app, user, freezer):
assert Booking.objects.count() == 208
assert Booking.objects.filter(event__start_datetime__week_day=2).count() == 104
assert Booking.objects.filter(event__start_datetime__week_day=5).count() == 52
events = Event.annotate_queryset(Event.objects.filter(primary_event__isnull=False))
assert events.filter(booked_places_count=1).count() == 156
assert events.filter(waiting_list_count=1).count() == 52
events = Event.objects.filter(primary_event__isnull=False)
assert events.filter(booked_places=1).count() == 156
assert events.filter(booked_waiting_list_places=1).count() == 52
params['slots'] = 'event:1,event:4'
resp = app.post_json(fillslots_url, params=params)
@ -2291,9 +2291,9 @@ def test_recurring_events_api_fillslots_change_bookings(app, user, freezer):
assert Booking.objects.count() == 208
assert Booking.objects.filter(event__start_datetime__week_day=3).count() == 104
assert Booking.objects.filter(event__start_datetime__week_day=6).count() == 52
events = Event.annotate_queryset(Event.objects.filter(primary_event__isnull=False))
assert events.filter(booked_places_count=1).count() == 156
assert events.filter(waiting_list_count=1).count() == 52
events = Event.objects.filter(primary_event__isnull=False)
assert events.filter(booked_places=1).count() == 156
assert events.filter(booked_waiting_list_places=1).count() == 52
# passing empty slots cancels all bookings
params['slots'] = ''
@ -2335,8 +2335,8 @@ def test_api_events_fillslots(app, user):
assert resp.json['booking_count'] == 2
assert len(resp.json['waiting_list_events']) == 0
events = Event.annotate_queryset(Event.objects.all())
assert events.filter(booked_places_count=1).count() == 2
events = Event.objects.all()
assert events.filter(booked_places=1).count() == 2
params['user_external_id'] = 'user_id_2'
resp = app.post_json(fillslots_url, params=params)
@ -2415,22 +2415,22 @@ def test_api_events_fillslots_past_event(app, user):
)
params = {'user_external_id': 'user_id', 'slots': ','.join((event1.slug, event2.slug))}
resp = app.post_json('/api/agenda/%s/events/fillslots/' % agenda.slug, params)
resp = app.post_json('/api/agenda/%s/events/fillslots/' % agenda.slug, params=params)
assert resp.json['err'] == 1
assert resp.json['err_desc'] == 'event %s is not bookable' % event1.slug
params['events'] = 'future'
resp = app.post_json('/api/agenda/%s/events/fillslots/' % agenda.slug, params)
resp = app.post_json('/api/agenda/%s/events/fillslots/' % agenda.slug, params=params)
assert resp.json['err'] == 1
assert resp.json['err_desc'] == 'event %s is not bookable' % event1.slug
params['events'] = 'past'
resp = app.post_json('/api/agenda/%s/events/fillslots/' % agenda.slug, params)
resp = app.post_json('/api/agenda/%s/events/fillslots/' % agenda.slug, params=params)
assert resp.json['err'] == 1
assert resp.json['err_desc'] == 'event %s is not bookable' % event2.slug
params['events'] = 'all'
resp = app.post_json('/api/agenda/%s/events/fillslots/' % agenda.slug, params)
resp = app.post_json('/api/agenda/%s/events/fillslots/' % agenda.slug, params=params)
assert resp.json['err'] == 0

View File

@ -879,33 +879,6 @@ def test_management_role_deletion():
assert Agenda.objects.get(id=agenda.id).edit_role is None
def test_event_bookings_annotation():
agenda = Agenda(label='test', kind='events')
agenda.save()
event = Event(start_datetime=now(), label='foo', places=10, waiting_list_places=10, agenda=agenda)
event.save()
event2 = Event(start_datetime=now(), label='bar', places=10, waiting_list_places=10, agenda=agenda)
event2.save()
Booking(event=event).save()
Booking(event=event).save()
Booking(event=event, cancellation_datetime=now()).save()
Booking(event=event, in_waiting_list=True).save()
Booking(event=event, in_waiting_list=True, cancellation_datetime=now()).save()
Booking(event=event2).save()
Booking(event=event2).save()
Booking(event=event2).save()
for event in Event.annotate_queryset(Event.objects.filter(agenda=agenda)):
if event.label == 'foo':
assert event.booked_places_count == 2
assert event.waiting_list_count == 1
elif event.label == 'bar':
assert event.booked_places_count == 3
assert event.waiting_list_count == 0
def test_virtual_agenda_init():
agenda1 = Agenda.objects.create(label='Agenda 1', kind='meetings')
agenda2 = Agenda.objects.create(label='Agenda 2', kind='meetings')
@ -2279,3 +2252,173 @@ def test_recurring_events_display(freezer):
event.get_recurrence_display()
== 'On Monday at 1:30 p.m., once every three weeks, from Jan. 7, 2021, until Jan. 14, 2021'
)
def test_event_triggered_fields():
agenda = Agenda.objects.create(label='Agenda', kind='events')
event = Event.objects.create(
agenda=agenda, start_datetime=now() + datetime.timedelta(days=10), places=10, label='Event'
)
event2 = Event.objects.create(
agenda=agenda, start_datetime=now() + datetime.timedelta(days=10), places=10, label='Event'
)
assert event.booked_places == 0
assert event.booked_waiting_list_places == 0
assert event.almost_full is False
assert event.full is False
event.booked_places = 42
event.booked_waiting_list_places = 42
event.almost_full = True
event.full = True
event.save()
# computed by triggers
event.refresh_from_db()
assert event.booked_places == 0
assert event.booked_waiting_list_places == 0
assert event.almost_full is False
assert event.full is False
# add bookings for other event: no impact
for _ in range(10):
Booking.objects.create(event=event2)
event.refresh_from_db()
assert event.booked_places == 0
assert event.booked_waiting_list_places == 0
assert event.almost_full is False
assert event.full is False
# add bookings
for _ in range(9):
Booking.objects.create(event=event)
event.refresh_from_db()
assert event.booked_places == 9
assert event.booked_waiting_list_places == 0
assert event.almost_full is True
assert event.full is False
Booking.objects.create(event=event)
event.refresh_from_db()
assert event.booked_places == 10
assert event.booked_waiting_list_places == 0
assert event.almost_full is True
assert event.full is True
# cancel bookings for other event: no impact
event2.booking_set.filter(cancellation_datetime__isnull=True).first().cancel()
event.refresh_from_db()
assert event.booked_places == 10
assert event.booked_waiting_list_places == 0
assert event.almost_full is True
assert event.full is True
# cancel bookings
event.booking_set.filter(cancellation_datetime__isnull=True).first().cancel()
event.refresh_from_db()
assert event.booked_places == 9
assert event.booked_waiting_list_places == 0
assert event.almost_full is True
assert event.full is False
event.booking_set.filter(cancellation_datetime__isnull=True).first().cancel()
event.refresh_from_db()
assert event.booked_places == 8
assert event.booked_waiting_list_places == 0
assert event.almost_full is False
assert event.full is False
# update places
event.places = 20
event.save()
event.refresh_from_db()
assert event.booked_places == 8
assert event.booked_waiting_list_places == 0
assert event.almost_full is False
assert event.full is False
Booking.objects.create(event=event)
Booking.objects.create(event=event)
event.places = 10
event.save()
event.refresh_from_db()
assert event.booked_places == 10
assert event.booked_waiting_list_places == 0
assert event.almost_full is True
assert event.full is True
# with a waiting list
event.waiting_list_places = 5
event.save()
event.refresh_from_db()
assert event.booked_places == 10
assert event.booked_waiting_list_places == 0
assert event.almost_full is True
assert event.full is False
# add bookings for other event: no impact
for _ in range(10):
Booking.objects.create(event=event2, in_waiting_list=True)
event.refresh_from_db()
assert event.booked_places == 10
assert event.booked_waiting_list_places == 0
assert event.almost_full is True
assert event.full is False
# add bookings
Booking.objects.create(event=event, in_waiting_list=True)
event.refresh_from_db()
assert event.booked_places == 10
assert event.booked_waiting_list_places == 1
assert event.almost_full is True
assert event.full is False
for _ in range(1, 5):
Booking.objects.create(event=event, in_waiting_list=True)
event.refresh_from_db()
assert event.booked_places == 10
assert event.booked_waiting_list_places == 5
assert event.almost_full is True
assert event.full is True
# cancel bookings for other event: no impact
event2.booking_set.filter(in_waiting_list=True, cancellation_datetime__isnull=True).first().cancel()
event.refresh_from_db()
assert event.booked_places == 10
assert event.booked_waiting_list_places == 5
assert event.almost_full is True
assert event.full is True
# cancel bookings
event.booking_set.filter(in_waiting_list=True, cancellation_datetime__isnull=True).first().cancel()
event.refresh_from_db()
assert event.booked_places == 10
assert event.booked_waiting_list_places == 4
assert event.almost_full is True
assert event.full is False
# update waiting list places
event.waiting_list_places = 4
event.save()
event.refresh_from_db()
assert event.booked_places == 10
assert event.booked_waiting_list_places == 4
assert event.almost_full is True
assert event.full is True
# delete bookings
event.booking_set.filter(in_waiting_list=True, cancellation_datetime__isnull=True).first().delete()
event.refresh_from_db()
assert event.booked_places == 10
assert event.booked_waiting_list_places == 3
assert event.almost_full is True
assert event.full is False
event.booking_set.filter(in_waiting_list=False, cancellation_datetime__isnull=True).first().delete()
event.refresh_from_db()
assert event.booked_places == 9
assert event.booked_waiting_list_places == 3
assert event.almost_full is True
assert event.full is False
event.booking_set.filter(in_waiting_list=False, cancellation_datetime__isnull=True).first().delete()
event.refresh_from_db()
assert event.booked_places == 8
assert event.booked_waiting_list_places == 3
assert event.almost_full is False
assert event.full is False