From bf09a46d1bdc57e89fcc30953ce6041713d5583b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Mon, 16 Aug 2021 15:59:01 +0200
Subject: [PATCH] agendas: trigger full and places event fields (#54747)

---
 .../migrations/0098_event_booked_places.py    |  21 ++
 .../agendas/migrations/0099_event_triggers.py |  89 ++++++++
 chrono/agendas/migrations/0100_event_dml.py   |  13 ++
 chrono/agendas/models.py                      |  61 +-----
 chrono/api/views.py                           |  40 +---
 .../chrono/manager_agenda_event_fragment.html |   8 +-
 .../templates/chrono/manager_event_check.html |   4 +-
 .../chrono/manager_event_detail_fragment.html |   8 +-
 chrono/manager/views.py                       |  13 +-
 tests/api/test_all.py                         |  58 +++--
 tests/api/test_fillslot.py                    |  62 +++---
 tests/test_agendas.py                         | 205 +++++++++++++++---
 12 files changed, 399 insertions(+), 183 deletions(-)
 create mode 100644 chrono/agendas/migrations/0098_event_booked_places.py
 create mode 100644 chrono/agendas/migrations/0099_event_triggers.py
 create mode 100644 chrono/agendas/migrations/0100_event_dml.py

diff --git a/chrono/agendas/migrations/0098_event_booked_places.py b/chrono/agendas/migrations/0098_event_booked_places.py
new file mode 100644
index 0000000..e4dc5f3
--- /dev/null
+++ b/chrono/agendas/migrations/0098_event_booked_places.py
@@ -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),
+        ),
+    ]
diff --git a/chrono/agendas/migrations/0099_event_triggers.py b/chrono/agendas/migrations/0099_event_triggers.py
new file mode 100644
index 0000000..5c7c3c5
--- /dev/null
+++ b/chrono/agendas/migrations/0099_event_triggers.py
@@ -0,0 +1,89 @@
+from django.db import migrations
+
+sql_forwards = """
+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
+        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;
+    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();
+"""
+
+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)]
diff --git a/chrono/agendas/migrations/0100_event_dml.py b/chrono/agendas/migrations/0100_event_dml.py
new file mode 100644
index 0000000..491ede4
--- /dev/null
+++ b/chrono/agendas/migrations/0100_event_dml.py
@@ -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='')]
diff --git a/chrono/agendas/models.py b/chrono/agendas/models.py
index 54acd0b..d1f5a3f 100644
--- a/chrono/agendas/models.py
+++ b/chrono/agendas/models.py
@@ -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, Q, Subquery, Value
-from django.db.models.functions import Coalesce
+from django.db.models import Count, Max, 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,
@@ -1307,6 +1298,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)
@@ -1340,7 +1333,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)
@@ -1350,17 +1342,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
@@ -1395,23 +1379,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(
@@ -1433,25 +1400,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):
@@ -1778,14 +1733,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():
diff --git a/chrono/api/views.py b/chrono/api/views.py
index 59b2ab2..76ceb73 100644
--- a/chrono/api/views.py
+++ b/chrono/api/views.py
@@ -397,7 +397,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,
@@ -408,9 +408,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
@@ -814,14 +814,12 @@ class Datetimes(APIView):
         entries = []
         if show_past:
             entries += agenda.get_past_events(
-                annotate_queryset=True,
                 min_start=start_datetime,
                 max_start=end_datetime,
                 user_external_id=booked_user_external_id or excluded_user_external_id,
             )
         if show_future:
             entries += agenda.get_open_events(
-                annotate_queryset=True,
                 min_start=start_datetime,
                 max_start=end_datetime,
                 user_external_id=booked_user_external_id or excluded_user_external_id,
@@ -1168,7 +1166,7 @@ class EventsSlotsSerializer(SlotSerializer):
 
     def validate(self, attrs):
         super().validate(attrs)
-        if not 'slots' in attrs:
+        if 'slots' not in attrs:
             raise serializers.ValidationError({'slots': _('This field is required.')})
         if not attrs.get('user_external_id'):
             raise serializers.ValidationError({'user_external_id': _('This field is required.')})
@@ -1438,12 +1436,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',
@@ -1618,10 +1616,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(),
             )
         )
@@ -1629,19 +1626,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,
@@ -1675,7 +1664,6 @@ class EventsFillslots(APIView):
         user_external_id = payload['user_external_id']
 
         events = get_events_from_slots(payload['slots'], request, agenda, payload)
-        events = Event.annotate_queryset(events)
 
         already_booked_events = agenda.event_set.filter(booking__user_external_id=user_external_id)
         if start_datetime:
@@ -1694,7 +1682,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(),
             )
         )
@@ -1703,19 +1691,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(
-            agenda.event_set.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,
@@ -2126,7 +2106,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']
diff --git a/chrono/manager/templates/chrono/manager_agenda_event_fragment.html b/chrono/manager/templates/chrono/manager_agenda_event_fragment.html
index f155b45..f832427 100644
--- a/chrono/manager/templates/chrono/manager_agenda_event_fragment.html
+++ b/chrono/manager/templates/chrono/manager_agenda_event_fragment.html
@@ -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 %}
diff --git a/chrono/manager/templates/chrono/manager_event_check.html b/chrono/manager/templates/chrono/manager_event_check.html
index 277ae34..6050e3c 100644
--- a/chrono/manager/templates/chrono/manager_event_check.html
+++ b/chrono/manager/templates/chrono/manager_event_check.html
@@ -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">
diff --git a/chrono/manager/templates/chrono/manager_event_detail_fragment.html b/chrono/manager/templates/chrono/manager_event_detail_fragment.html
index 7d89a27..be3e3de 100644
--- a/chrono/manager/templates/chrono/manager_event_detail_fragment.html
+++ b/chrono/manager/templates/chrono/manager_event_detail_fragment.html
@@ -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
diff --git a/chrono/manager/views.py b/chrono/manager/views.py
index 9c104e5..97c55e0 100644
--- a/chrono/manager/views.py
+++ b/chrono/manager/views.py
@@ -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
 
diff --git a/tests/api/test_all.py b/tests/api/test_all.py
index 340a4bb..1e2b617 100644
--- a/tests/api/test_all.py
+++ b/tests/api/test_all.py
@@ -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()) == []
diff --git a/tests/api/test_fillslot.py b/tests/api/test_fillslot.py
index 4175f1a..f0134ce 100644
--- a/tests/api/test_fillslot.py
+++ b/tests/api/test_fillslot.py
@@ -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,20 +2415,20 @@ 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 not bookable'
 
     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 not bookable'
 
     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 not bookable'
 
     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
diff --git a/tests/test_agendas.py b/tests/test_agendas.py
index bbf25d4..7642dc7 100644
--- a/tests/test_agendas.py
+++ b/tests/test_agendas.py
@@ -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')
@@ -1377,13 +1350,13 @@ def test_agenda_events_duplicate():
     )
     desk = Desk.objects.create(agenda=agenda, slug='_exceptions_holder')
     desk.import_timeperiod_exceptions_from_settings(enable=True)
-    time_period_exception = TimePeriodException.objects.create(
+    TimePeriodException.objects.create(
         desk=desk,
         start_datetime=now(),
         end_datetime=now() + datetime.timedelta(minutes=30),
     )
     assert desk.timeperiodexception_set.count() == 34
-    settings = AgendaNotificationsSettings.objects.create(
+    AgendaNotificationsSettings.objects.create(
         agenda=agenda,
         full_event=AgendaNotificationsSettings.EMAIL_FIELD,
         full_event_emails=['hop@entrouvert.com', 'top@entrouvert.com'],
@@ -1795,8 +1768,8 @@ def test_agenda_reminders_email_content(mailoutbox, freezer):
         assert 'Do no forget ID card.' in body
         assert 'Come !' in body
         assert 'Pricing: 10€' in body
-        assert not 'cancel' in body
-        assert not 'if present' in body  # assert separation with preview code
+        assert 'cancel' not in body
+        assert 'if present' not in body  # assert separation with preview code
     assert 'More information: https://example.party' in mail_bodies[0]
     assert '<a href="https://example.party">More information</a>' in mail_bodies[1]
     mailoutbox.clear()
@@ -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
-- 
2.30.2

