Projet

Général

Profil

0001-agendas-trigger-full-and-places-event-fields-54747.patch

Lauréline Guérin, 16 août 2021 16:19

Télécharger (52,4 ko)

Voir les différences:

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
chrono/agendas/migrations/0098_event_booked_places.py
1
from django.db import migrations, models
2

  
3

  
4
class Migration(migrations.Migration):
5

  
6
    dependencies = [
7
        ('agendas', '0097_min_booking_delay_working_days'),
8
    ]
9

  
10
    operations = [
11
        migrations.AddField(
12
            model_name='event',
13
            name='booked_places',
14
            field=models.PositiveSmallIntegerField(default=0),
15
        ),
16
        migrations.AddField(
17
            model_name='event',
18
            name='booked_waiting_list_places',
19
            field=models.PositiveSmallIntegerField(default=0),
20
        ),
21
    ]
chrono/agendas/migrations/0099_event_triggers.py
1
from django.db import migrations
2

  
3
sql_forwards = """
4
CREATE OR REPLACE FUNCTION update_event_full_fields() RETURNS TRIGGER AS $$
5
BEGIN
6
    IF (TG_OP = 'INSERT') THEN
7
        NEW.booked_places = 0;
8
        NEW.booked_waiting_list_places = 0;
9
    ELSE
10
        NEW.booked_places = (
11
            SELECT COUNT(*) FROM agendas_booking b
12
            WHERE b.event_id = NEW.id AND b.cancellation_datetime IS NULL AND b.in_waiting_list = false
13
        );
14
        NEW.booked_waiting_list_places = (
15
            SELECT COUNT(*) FROM agendas_booking b
16
            WHERE b.event_id = NEW.id AND b.cancellation_datetime IS NULL AND b.in_waiting_list = true
17
        );
18
    END IF;
19

  
20
    -- update almost_full field
21
    IF (NEW.booked_places >= NEW.places * 0.9) THEN
22
        NEW.almost_full = true;
23
    ELSE
24
        NEW.almost_full = false;
25
    END IF;
26

  
27
    -- update full field
28
    IF (NEW.booked_places >= NEW.places) AND (NEW.waiting_list_places = 0) THEN
29
        NEW.full = true;
30
    ELSIF (NEW.waiting_list_places > 0) AND (NEW.booked_waiting_list_places >= NEW.waiting_list_places) THEN
31
        NEW.full = true;
32
    ELSE
33
        NEW.full = false;
34
    END IF;
35

  
36
    RETURN NEW;
37
END;
38
$$ LANGUAGE plpgsql;
39

  
40
CREATE OR REPLACE FUNCTION update_event_places_fields() RETURNS TRIGGER AS $$
41
DECLARE
42
    e_id integer;
43
BEGIN
44
    IF (TG_OP = 'DELETE') THEN
45
        e_id = OLD.event_id;
46
    ELSE
47
        e_id = NEW.event_id;
48
    END IF;
49
    UPDATE agendas_event
50
    SET
51
        booked_places = (
52
            SELECT COUNT(*) FROM agendas_booking b
53
            WHERE b.event_id = e_id AND b.cancellation_datetime IS NULL AND b.in_waiting_list = false
54
        ),
55
        booked_waiting_list_places = (
56
            SELECT COUNT(*) FROM agendas_booking b
57
            WHERE b.event_id = e_id AND b.cancellation_datetime IS NULL AND b.in_waiting_list = true
58
        )
59
    WHERE id = e_id;
60

  
61
    RETURN NULL;
62
END;
63
$$ LANGUAGE plpgsql;
64

  
65
DROP TRIGGER IF EXISTS update_event_full_fields_trigger ON agendas_event;
66
CREATE TRIGGER update_event_full_fields_trigger BEFORE INSERT OR UPDATE ON agendas_event
67
    FOR EACH ROW EXECUTE PROCEDURE update_event_full_fields();
68

  
69
DROP TRIGGER IF EXISTS update_event_places_fields_trigger ON agendas_booking;
70
CREATE TRIGGER update_event_places_fields_trigger AFTER INSERT OR UPDATE OR DELETE ON agendas_booking
71
    FOR EACH ROW EXECUTE PROCEDURE update_event_places_fields();
72
"""
73

  
74
sql_backwards = """
75
DROP TRIGGER IF EXISTS update_event_full_fields_trigger ON agendas_event;
76
DROP TRIGGER IF EXISTS update_event_places_fields_trigger ON agendas_booking;
77

  
78
DROP FUNCTION IF EXISTS update_event_full_fields;
79
DROP FUNCTION IF EXISTS update_event_places_fields;
80
"""
81

  
82

  
83
class Migration(migrations.Migration):
84

  
85
    dependencies = [
86
        ('agendas', '0098_event_booked_places'),
87
    ]
88

  
89
    operations = [migrations.RunSQL(sql=sql_forwards, reverse_sql=sql_backwards)]
chrono/agendas/migrations/0100_event_dml.py
1
from django.db import migrations
2

  
3
# trigger places fields
4
sql_forwards = """UPDATE agendas_event SET id=id;"""
5

  
6

  
7
class Migration(migrations.Migration):
8

  
9
    dependencies = [
10
        ('agendas', '0099_event_triggers'),
11
    ]
12

  
13
    operations = [migrations.RunSQL(sql=sql_forwards, reverse_sql='')]
chrono/agendas/models.py
32 32
from django.core.exceptions import FieldDoesNotExist, ValidationError
33 33
from django.core.validators import MaxValueValidator, MinValueValidator
34 34
from django.db import IntegrityError, connection, models, transaction
35
from django.db.models import Count, IntegerField, Max, OuterRef, Q, Subquery, Value
36
from django.db.models.functions import Coalesce
35
from django.db.models import Count, Max, Q
37 36
from django.template import Context, Template, TemplateSyntaxError, VariableDoesNotExist, engines
38 37
from django.urls import reverse
39 38
from django.utils import functional
......
645 644
    def get_open_events(
646 645
        self,
647 646
        prefetched_queryset=False,
648
        annotate_queryset=False,
649 647
        include_full=True,
650 648
        min_start=None,
651 649
        max_start=None,
......
690 688
        if user_external_id and not prefetched_queryset:
691 689
            entries = Event.annotate_queryset_for_user(entries, user_external_id)
692 690

  
693
        if annotate_queryset and not prefetched_queryset:
694
            entries = Event.annotate_queryset(entries)
695

  
696 691
        if max_start:
697 692
            entries = self.add_event_recurrences(
698 693
                entries,
......
706 701

  
707 702
    def get_past_events(
708 703
        self,
709
        annotate_queryset=False,
710 704
        min_start=None,
711 705
        max_start=None,
712 706
        user_external_id=None,
......
729 723
        if user_external_id:
730 724
            entries = Event.annotate_queryset_for_user(entries, user_external_id)
731 725

  
732
        if annotate_queryset:
733
            entries = Event.annotate_queryset(entries)
734

  
735 726
        if min_start:
736 727
            entries = self.add_event_recurrences(
737 728
                entries,
......
1307 1298
    )
1308 1299
    pricing = models.CharField(_('Pricing'), max_length=150, null=True, blank=True)
1309 1300
    url = models.CharField(_('URL'), max_length=200, null=True, blank=True)
1301
    booked_places = models.PositiveSmallIntegerField(default=0)
1302
    booked_waiting_list_places = models.PositiveSmallIntegerField(default=0)
1310 1303
    almost_full = models.BooleanField(default=False)
1311 1304
    full = models.BooleanField(default=False)
1312 1305
    cancelled = models.BooleanField(default=False)
......
1340 1333
        assert self.agenda.kind != 'virtual', "an event can't reference a virtual agenda"
1341 1334
        assert not (self.slug and self.slug.isdigit()), 'slug cannot be a number'
1342 1335
        self.start_datetime = self.start_datetime.replace(second=0, microsecond=0)
1343
        self.check_full()
1344 1336
        if not self.slug:
1345 1337
            self.slug = generate_slug(self, seen_slugs=seen_slugs, agenda=self.agenda)
1346 1338
        return super().save(*args, **kwargs)
......
1350 1342
        # label can be empty
1351 1343
        return slugify(self.label or ('%s-event' % self.agenda.label))
1352 1344

  
1353
    @functional.cached_property
1354 1345
    def main_list_full(self):
1355 1346
        return bool(self.booked_places >= self.places)
1356 1347

  
1357
    def check_full(self):
1358
        self.full = bool(
1359
            (self.booked_places >= self.places and self.waiting_list_places == 0)
1360
            or (self.waiting_list_places and self.waiting_list >= self.waiting_list_places)
1361
        )
1362
        self.almost_full = bool(self.booked_places >= 0.9 * self.places)
1363

  
1364 1348
    def set_is_checked(self):
1365 1349
        if not self.agenda.mark_event_checked_auto:
1366 1350
            return
......
1395 1379
    def is_day_past(self):
1396 1380
        return self.start_datetime.date() <= now().date()
1397 1381

  
1398
    @staticmethod
1399
    def annotate_queryset(qs):
1400
        not_cancelled_bookings = Booking.objects.filter(
1401
            cancellation_datetime__isnull=True, event=OuterRef('pk')
1402
        )
1403

  
1404
        bookings = not_cancelled_bookings.filter(in_waiting_list=False).order_by().values('event')
1405
        count_bookings = bookings.annotate(count=Count('event')).values('count')
1406

  
1407
        waiting_list_bookings = not_cancelled_bookings.filter(in_waiting_list=True).order_by().values('event')
1408
        count_waiting_list = waiting_list_bookings.annotate(count=Count('event')).values('count')
1409

  
1410
        return qs.annotate(
1411
            booked_places_count=Coalesce(Subquery(count_bookings, output_field=IntegerField()), Value(0)),
1412
            waiting_list_count=Coalesce(Subquery(count_waiting_list, output_field=IntegerField()), Value(0)),
1413
        )
1414

  
1415 1382
    @staticmethod
1416 1383
    def annotate_queryset_for_user(qs, user_external_id):
1417 1384
        return qs.annotate(
......
1433 1400
            ),
1434 1401
        )
1435 1402

  
1436
    @property
1437
    def booked_places(self):
1438
        if hasattr(self, 'booked_places_count'):
1439
            return self.booked_places_count
1440
        return self.booking_set.filter(cancellation_datetime__isnull=True, in_waiting_list=False).count()
1441

  
1442 1403
    @property
1443 1404
    def remaining_places(self):
1444 1405
        return max(0, self.places - self.booked_places)
1445 1406

  
1446
    @property
1447
    def waiting_list(self):
1448
        if hasattr(self, 'waiting_list_count'):
1449
            return self.waiting_list_count
1450
        return self.booking_set.filter(cancellation_datetime__isnull=True, in_waiting_list=True).count()
1451

  
1452 1407
    @property
1453 1408
    def remaining_waiting_list_places(self):
1454
        return max(0, self.waiting_list_places - self.waiting_list)
1409
        return max(0, self.waiting_list_places - self.booked_waiting_list_places)
1455 1410

  
1456 1411
    @property
1457 1412
    def end_datetime(self):
......
1778 1733
    def user_name(self):
1779 1734
        return ('%s %s' % (self.user_first_name, self.user_last_name)).strip()
1780 1735

  
1781
    def save(self, *args, **kwargs):
1782
        with transaction.atomic():
1783
            super().save(*args, **kwargs)
1784
            initial_values = self.event.full, self.event.almost_full
1785
            self.event.check_full()
1786
            if (self.event.full, self.event.almost_full) != initial_values:
1787
                self.event.save()
1788

  
1789 1736
    def cancel(self, trigger_callback=False):
1790 1737
        timestamp = now()
1791 1738
        with transaction.atomic():
chrono/api/views.py
397 397

  
398 398

  
399 399
def get_event_places(event):
400
    available = event.places - event.booked_places
400
    available = event.remaining_places
401 401
    places = {
402 402
        'total': event.places,
403 403
        'reserved': event.booked_places,
......
408 408
    if event.waiting_list_places:
409 409
        places['has_waiting_list'] = True
410 410
        places['waiting_list_total'] = event.waiting_list_places
411
        places['waiting_list_reserved'] = event.waiting_list
412
        places['waiting_list_available'] = event.waiting_list_places - event.waiting_list
413
        places['waiting_list_activated'] = event.waiting_list > 0 or available <= 0
411
        places['waiting_list_reserved'] = event.booked_waiting_list_places
412
        places['waiting_list_available'] = event.remaining_waiting_list_places
413
        places['waiting_list_activated'] = event.booked_waiting_list_places > 0 or available <= 0
414 414
        # 'waiting_list_activated' means next booking will go into the waiting list
415 415

  
416 416
    return places
......
814 814
        entries = []
815 815
        if show_past:
816 816
            entries += agenda.get_past_events(
817
                annotate_queryset=True,
818 817
                min_start=start_datetime,
819 818
                max_start=end_datetime,
820 819
                user_external_id=booked_user_external_id or excluded_user_external_id,
821 820
            )
822 821
        if show_future:
823 822
            entries += agenda.get_open_events(
824
                annotate_queryset=True,
825 823
                min_start=start_datetime,
826 824
                max_start=end_datetime,
827 825
                user_external_id=booked_user_external_id or excluded_user_external_id,
......
1168 1166

  
1169 1167
    def validate(self, attrs):
1170 1168
        super().validate(attrs)
1171
        if not 'slots' in attrs:
1169
        if 'slots' not in attrs:
1172 1170
            raise serializers.ValidationError({'slots': _('This field is required.')})
1173 1171
        if not attrs.get('user_external_id'):
1174 1172
            raise serializers.ValidationError({'user_external_id': _('This field is required.')})
......
1438 1436
                    if (
1439 1437
                        payload.get('force_waiting_list')
1440 1438
                        or (event.booked_places + places_count) > event.places
1441
                        or event.waiting_list
1439
                        or event.booked_waiting_list_places
1442 1440
                    ):
1443 1441
                        # if this is full or there are people waiting, put new bookings
1444 1442
                        # in the waiting list.
1445 1443
                        in_waiting_list = True
1446
                        if (event.waiting_list + places_count) > event.waiting_list_places:
1444
                        if (event.booked_waiting_list_places + places_count) > event.waiting_list_places:
1447 1445
                            raise APIError(
1448 1446
                                _('sold out'),
1449 1447
                                err_class='sold out',
......
1618 1616
        full_events = list(events_to_book.filter(full=True))
1619 1617
        events_to_book = events_to_book.filter(full=False)
1620 1618

  
1621
        events_to_book = Event.annotate_queryset(events_to_book)
1622 1619
        events_to_book = events_to_book.annotate(
1623 1620
            in_waiting_list=ExpressionWrapper(
1624
                Q(booked_places_count__gte=F('places')) | Q(waiting_list_count__gt=0),
1621
                Q(booked_places__gte=F('places')) | Q(booked_waiting_list_places__gt=0),
1625 1622
                output_field=BooleanField(),
1626 1623
            )
1627 1624
        )
......
1629 1626
        extra_data = {k: v for k, v in request.data.items() if k not in payload}
1630 1627
        bookings = [make_booking(event, payload, extra_data) for event in events_to_book]
1631 1628

  
1632
        events_to_update = Event.annotate_queryset(
1633
            agenda.event_set.filter(pk__in=events_to_unbook + [event.pk for event in events_to_book])
1634
        )
1635 1629
        with transaction.atomic():
1636 1630
            deleted_count = Booking.objects.filter(
1637 1631
                user_external_id=user_external_id, event__in=events_to_unbook
1638 1632
            ).delete()[0]
1639 1633
            Booking.objects.bulk_create(bookings)
1640
            events_to_update.update(
1641
                full=Q(booked_places_count__gte=F('places'), waiting_list_places=0)
1642
                | Q(waiting_list_places__gt=0, waiting_list_count__gte=F('waiting_list_places')),
1643
                almost_full=Q(booked_places_count__gte=0.9 * F('places')),
1644
            )
1645 1634

  
1646 1635
        response = {
1647 1636
            'err': 0,
......
1675 1664
        user_external_id = payload['user_external_id']
1676 1665

  
1677 1666
        events = get_events_from_slots(payload['slots'], request, agenda, payload)
1678
        events = Event.annotate_queryset(events)
1679 1667

  
1680 1668
        already_booked_events = agenda.event_set.filter(booking__user_external_id=user_external_id)
1681 1669
        if start_datetime:
......
1694 1682

  
1695 1683
        events = events.annotate(
1696 1684
            in_waiting_list=ExpressionWrapper(
1697
                Q(booked_places_count__gte=F('places')) | Q(waiting_list_count__gt=0),
1685
                Q(booked_places__gte=F('places')) | Q(booked_waiting_list_places__gt=0),
1698 1686
                output_field=BooleanField(),
1699 1687
            )
1700 1688
        )
......
1703 1691
        extra_data = {k: v for k, v in request.data.items() if k not in payload}
1704 1692
        bookings = [make_booking(event, payload, extra_data) for event in events]
1705 1693

  
1706
        events_to_update = Event.annotate_queryset(
1707
            agenda.event_set.filter(pk__in=events_to_unbook + [event.pk for event in events])
1708
        )
1709 1694
        with transaction.atomic():
1710 1695
            deleted_count = Booking.objects.filter(
1711 1696
                user_external_id=user_external_id, event__in=events_to_unbook
1712 1697
            ).delete()[0]
1713 1698
            Booking.objects.bulk_create(bookings)
1714
            events_to_update.update(
1715
                full=Q(booked_places_count__gte=F('places'), waiting_list_places=0)
1716
                | Q(waiting_list_places__gt=0, waiting_list_count__gte=F('waiting_list_places')),
1717
                almost_full=Q(booked_places_count__gte=0.9 * F('places')),
1718
            )
1719 1699

  
1720 1700
        response = {
1721 1701
            'err': 0,
......
2126 2106
        # total places for the event (in waiting or main list, depending on the primary booking location)
2127 2107
        places = event.waiting_list_places if booking.in_waiting_list else event.places
2128 2108
        # total booked places for the event (in waiting or main list, depending on the primary booking location)
2129
        booked_places = event.waiting_list if booking.in_waiting_list else event.booked_places
2109
        booked_places = event.booked_waiting_list_places if booking.in_waiting_list else event.booked_places
2130 2110

  
2131 2111
        # places to book for this primary booking
2132 2112
        primary_wanted_places = payload['count']
chrono/manager/templates/chrono/manager_agenda_event_fragment.html
1 1
{% load i18n %}
2
<li class="{% if event.booked_places_count > event.places %}overbooking{% endif %}
2
<li class="{% if event.booked_places > event.places %}overbooking{% endif %}
3 3
           {% if event.main_list_full %}full{% endif %}
4 4
             {% if event.cancellation_status %}cancelled{% endif %}
5 5
           {% if not event.in_bookable_period %}not-{% endif %}bookable"
6 6
    {% if event.places %}
7
      data-total="{{ event.places }}" data-booked="{{ event.booked_places_count }}"
7
      data-total="{{ event.places }}" data-booked="{{ event.booked_places }}"
8 8
    {% elif event.waiting_list_places %}
9
      data-total="{{ event.waiting_list_places }}" data-booked="{{ event.waiting_list_count }}"
9
      data-total="{{ event.waiting_list_places }}" data-booked="{{ event.booked_waiting_list_places }}"
10 10
    {% endif %}
11 11
    ><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 %}">
12 12
    {% if event.cancellation_status %}
......
40 40
      ({% trans "Waiting list:" %}
41 41
      {% blocktrans count remaining_places=event.remaining_waiting_list_places %}{{ remaining_places }} remaining place{% plural %}{{ remaining_places }} remaining places{% endblocktrans %}
42 42
      -
43
      {% blocktrans with places=event.waiting_list_places count booked_places=event.waiting_list %}{{ booked_places }}/{{ places }} booking{% plural %}{{ booked_places }}/{{ places }} bookings{% endblocktrans %})
43
      {% 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 %})
44 44
    {% endif %}
45 45
    {% endif %}
46 46
    {% if view_mode == 'settings_view' and event.publication_date %}
chrono/manager/templates/chrono/manager_event_check.html
11 11
{% block content %}
12 12
<div class="section">
13 13
  <h3>
14
    {% blocktrans with places=object.places %}Bookings ({{ booked_places }}/{{ places }}){% endblocktrans %}
14
    {% blocktrans with booked_places=object.booked_places places=object.places %}Bookings ({{ booked_places }}/{{ places }}){% endblocktrans %}
15 15
  </h3>
16 16
  <div>
17 17
    <form class="check-bookings-filters">
......
76 76
{% if object.waiting_list_places %}
77 77
<div class="section">
78 78
  <h3>
79
    {% blocktrans with places=object.waiting_list_places %}Waiting List ({{ waiting_places }}/{{ places }}){% endblocktrans %}
79
    {% blocktrans with booked_places=object.booked_waiting_list_places places=object.waiting_list_places %}Waiting List ({{ booked_places }}/{{ places }}){% endblocktrans %}
80 80
  </h3>
81 81
  <div>
82 82
    <table class="main check-bookings">
chrono/manager/templates/chrono/manager_event_detail_fragment.html
12 12

  
13 13
<div class="section">
14 14
  <h3>
15
    {% blocktrans with places=object.places booked_places=booked|length count remaining_places=object.remaining_places %}
15
    {% blocktrans with places=object.places booked_places=object.booked_places count remaining_places=object.remaining_places %}
16 16
      Bookings ({{ booked_places }}/{{ places }}): {{ remaining_places }} remaining place
17 17
    {% plural %}
18 18
      Bookings ({{ booked_places }}/{{ places }}): {{ remaining_places }} remaining places
......
20 20
  </h3>
21 21
<div>
22 22

  
23
  {% if booked|length > event.places %}
23
  {% if object.booked_places > event.places %}
24 24
    <div class="errornotice"><p>{% trans "This event is overbooked." %}</p></div>
25
  {% elif booked|length == event.places %}
25
  {% elif object.booked_places == event.places %}
26 26
    <div class="infonotice"><p>{% trans "This event is full." %}</p></div>
27 27
  {% endif %}
28 28

  
......
44 44
{% if object.waiting_list_places %}
45 45
<div class="section">
46 46
  <h3>
47
    {% blocktrans with places=object.waiting_list_places booked_places=waiting|length count remaining_places=object.remaining_waiting_list_places %}
47
    {% blocktrans with places=object.waiting_list_places booked_places=object.booked_waiting_list_places count remaining_places=object.remaining_waiting_list_places %}
48 48
      Waiting List ({{ booked_places }}/{{ places }}): {{ remaining_places }} remaining place
49 49
    {% plural %}
50 50
      Waiting List ({{ booked_places }}/{{ places }}): {{ remaining_places }} remaining places
chrono/manager/views.py
25 25
from django.contrib import messages
26 26
from django.core.exceptions import PermissionDenied
27 27
from django.db import transaction
28
from django.db.models import BooleanField, Count, Max, Min, Q, Sum, Value
28
from django.db.models import BooleanField, Count, Max, Min, Q, Value
29 29
from django.http import Http404, HttpResponse, HttpResponseRedirect
30 30
from django.shortcuts import get_object_or_404, redirect, render
31 31
from django.template.defaultfilters import title
......
1123 1123
        qs = super().get_queryset()
1124 1124
        if self.agenda.kind != 'events':
1125 1125
            return qs
1126
        return Event.annotate_queryset(qs).order_by('start_datetime', 'label')
1126
        return qs.order_by('start_datetime', 'label')
1127 1127

  
1128 1128
    def get_dated_items(self):
1129 1129
        date_list, object_list, extra_context = super().get_dated_items()
......
1253 1253
        qs = super().get_queryset()
1254 1254
        if self.agenda.kind != 'events':
1255 1255
            return qs
1256
        return Event.annotate_queryset(qs).order_by('start_datetime', 'label')
1256
        return qs.order_by('start_datetime', 'label')
1257 1257

  
1258 1258
    def get_dated_items(self):
1259 1259
        date_list, object_list, extra_context = super().get_dated_items()
......
1466 1466
    def get_context_data(self, **kwargs):
1467 1467
        context = super().get_context_data(**kwargs)
1468 1468
        context['user_can_manage'] = self.agenda.can_be_managed(self.request.user)
1469
        context['open_events'] = self.agenda.get_open_events(annotate_queryset=True)
1469
        context['open_events'] = self.agenda.get_open_events()
1470 1470
        return context
1471 1471

  
1472 1472

  
......
1644 1644
        return context
1645 1645

  
1646 1646
    def get_events(self):
1647
        qs = self.agenda.event_set.filter(primary_event__isnull=True)
1648
        return Event.annotate_queryset(qs)
1647
        return self.agenda.event_set.filter(primary_event__isnull=True)
1649 1648

  
1650 1649
    def get_template_names(self):
1651 1650
        return ['chrono/manager_%s_agenda_settings.html' % self.agenda.kind]
......
2025 2024

  
2026 2025
        # build booking list
2027 2026
        context['booked'] = filterset.qs.order_by('user_last_name', 'user_first_name')
2028
        context['booked_places'] = context['booked'].aggregate(places=Sum('places_count'))['places']
2029 2027
        context['booked_without_status'] = any(e.user_was_present is None for e in context['booked'])
2030 2028
        if context['booked_without_status']:
2031 2029
            context['absence_form'] = BookingAbsenceReasonForm(agenda=self.agenda)
......
2041 2039
            + Count('secondary_booking_set', filter=Q(cancellation_datetime__isnull=True))
2042 2040
        )
2043 2041
        context['waiting'] = waiting_qs
2044
        context['waiting_places'] = waiting_qs.aggregate(places=Sum('places_count'))['places']
2045 2042

  
2046 2043
        return context
2047 2044

  
tests/api/test_all.py
168 168

  
169 169
    event1 = Event.objects.create(
170 170
        start_datetime=(localtime() + datetime.timedelta(days=5)).replace(hour=10, minute=0),
171
        places=20,
171
        places=1,
172 172
        agenda=event_agenda,
173 173
    )
174 174
    event2 = Event.objects.create(
175 175
        start_datetime=(localtime() + datetime.timedelta(days=10)).replace(hour=10, minute=0),
176
        places=20,
176
        places=1,
177 177
        agenda=event_agenda,
178 178
    )
179 179
    event3 = Event.objects.create(
180 180
        start_datetime=(localtime() + datetime.timedelta(days=15)).replace(hour=10, minute=0),
181
        places=20,
181
        places=1,
182 182
        agenda=event_agenda,
183 183
    )
184 184

  
......
187 187
    assert len(resp.json['data']) == 1
188 188

  
189 189
    # one event is full
190
    Event.objects.filter(pk=event1.pk).update(full=True)
190
    event1.booking_set.create()
191
    event1.refresh_from_db()
192
    assert event1.full is True
191 193
    resp = app.get('/api/agenda/', params={'with_open_events': '1'})
192 194
    assert len(resp.json['data']) == 1
193 195

  
194 196
    # all events are full
195
    Event.objects.update(full=True)
197
    event2.booking_set.create()
198
    event2.refresh_from_db()
199
    assert event2.full is True
200
    event3.booking_set.create()
201
    event3.refresh_from_db()
202
    assert event3.full is True
196 203
    resp = app.get('/api/agenda/', params={'with_open_events': '1'})
197 204
    assert len(resp.json['data']) == 0
198 205

  
199 206
    # event1 is not full but too soon
200
    Event.objects.filter(pk=event1.pk).update(full=False)
207
    event1.booking_set.all().delete()
208
    event1.refresh_from_db()
209
    assert event1.full is False
201 210
    event_agenda.minimal_booking_delay = 10
202 211
    event_agenda.save()
203 212
    assert list(event_agenda.get_open_events()) == [event2, event3]
......
205 214
    assert len(resp.json['data']) == 0
206 215

  
207 216
    # event3 is not full but too late
208
    Event.objects.filter(pk=event3.pk).update(full=False)
217
    event3.booking_set.all().delete()
218
    event3.refresh_from_db()
219
    assert event3.full is False
209 220
    event_agenda.maximal_booking_delay = 12
210 221
    event_agenda.save()
211 222
    assert list(event_agenda.get_open_events()) == [event2]
......
213 224
    assert len(resp.json['data']) == 0
214 225

  
215 226
    # events are not full but not published
216
    Event.objects.update(full=False)
227
    event2.booking_set.all().delete()
228
    event2.refresh_from_db()
229
    assert event2.full is False
217 230
    event_agenda.event_set.update(publication_date=now().date() + datetime.timedelta(days=20))
218 231
    assert list(event_agenda.get_open_events()) == []
219 232
    resp = app.get('/api/agenda/', params={'with_open_events': '1'})
......
366 379
    agenda = Agenda.objects.create(label='Foo bar', kind='events', minimal_booking_delay=0)
367 380
    event1 = Event.objects.create(
368 381
        start_datetime=(localtime() + datetime.timedelta(days=5)).replace(hour=10, minute=0),
369
        places=20,
382
        places=1,
370 383
        agenda=agenda,
371 384
    )
372 385
    event2 = Event.objects.create(
373 386
        start_datetime=(localtime() + datetime.timedelta(days=10)).replace(hour=10, minute=0),
374
        places=20,
387
        places=1,
375 388
        agenda=agenda,
376 389
    )
377 390
    event3 = Event.objects.create(
378 391
        start_datetime=(localtime() + datetime.timedelta(days=15)).replace(hour=10, minute=0),
379
        places=20,
392
        places=1,
380 393
        agenda=agenda,
381 394
    )
382 395
    resp = app.get('/api/agenda/%s/' % agenda.slug)
......
389 402
    assert data['api']['datetimes_url'] == 'http://testserver/api/agenda/foo-bar/datetimes/'
390 403

  
391 404
    # one event is full
392
    Event.objects.filter(pk=event1.pk).update(full=True)
405
    event1.booking_set.create()
406
    event1.refresh_from_db()
407
    assert event1.full is True
393 408
    resp = app.get('/api/agenda/%s/' % agenda.slug)
394 409
    assert resp.json['data']['opened_events_available'] is True
395 410

  
396 411
    # all events are full
397
    Event.objects.update(full=True)
412
    event2.booking_set.create()
413
    event2.refresh_from_db()
414
    assert event2.full is True
415
    event3.booking_set.create()
416
    event3.refresh_from_db()
417
    assert event3.full is True
398 418
    resp = app.get('/api/agenda/%s/' % agenda.slug)
399 419
    assert resp.json['data']['opened_events_available'] is False
400 420

  
401 421
    # event1 is not full but too soon
402
    Event.objects.filter(pk=event1.pk).update(full=False)
422
    event1.booking_set.all().delete()
423
    event1.refresh_from_db()
424
    assert event1.full is False
403 425
    agenda.minimal_booking_delay = 10
404 426
    agenda.save()
405 427
    resp = app.get('/api/agenda/%s/' % agenda.slug)
......
407 429
    assert resp.json['data']['opened_events_available'] is False
408 430

  
409 431
    # event3 is not full but too late
410
    Event.objects.filter(pk=event3.pk).update(full=False)
432
    event3.booking_set.all().delete()
433
    event3.refresh_from_db()
434
    assert event3.full is False
411 435
    agenda.maximal_booking_delay = 12
412 436
    agenda.save()
413 437
    resp = app.get('/api/agenda/%s/' % agenda.slug)
......
415 439
    assert resp.json['data']['opened_events_available'] is False
416 440

  
417 441
    # events are not full but not published
418
    Event.objects.update(full=False)
442
    event2.booking_set.all().delete()
443
    event2.refresh_from_db()
444
    assert event2.full is False
419 445
    agenda.event_set.update(publication_date=now().date() + datetime.timedelta(days=20))
420 446
    resp = app.get('/api/agenda/%s/' % agenda.slug)
421 447
    assert list(agenda.get_open_events()) == []
tests/api/test_fillslot.py
1130 1130

  
1131 1131
    resp3 = app.post('/api/agenda/%s/fillslot/%s/?count=5' % (agenda.slug, event.id))
1132 1132
    assert Event.objects.get(id=event.id).booked_places == 2
1133
    assert Event.objects.get(id=event.id).waiting_list == 5
1133
    assert Event.objects.get(id=event.id).booked_waiting_list_places == 5
1134 1134

  
1135 1135
    # check waiting list overflow
1136 1136
    resp = app.post('/api/agenda/%s/fillslot/%s/?count=5' % (agenda.slug, event.id))
......
1139 1139
    assert resp.json['err_class'] == 'sold out'
1140 1140
    assert resp.json['err_desc'] == 'sold out'
1141 1141
    assert Event.objects.get(id=event.id).booked_places == 2
1142
    assert Event.objects.get(id=event.id).waiting_list == 5
1142
    assert Event.objects.get(id=event.id).booked_waiting_list_places == 5
1143 1143

  
1144 1144
    # accept the waiting list
1145 1145
    resp = app.post(resp3.json['api']['accept_url'])
1146 1146
    assert Event.objects.get(id=event.id).booked_places == 7
1147
    assert Event.objects.get(id=event.id).waiting_list == 0
1147
    assert Event.objects.get(id=event.id).booked_waiting_list_places == 0
1148 1148

  
1149 1149
    # check with a short waiting list
1150 1150
    Booking.objects.all().delete()
......
1161 1161
    resp = app.post('/api/agenda/%s/fillslot/%s/?count=3' % (agenda.slug, event.id))
1162 1162
    assert resp.json['err'] == 0
1163 1163
    assert Event.objects.get(id=event.id).booked_places == 3
1164
    assert Event.objects.get(id=event.id).waiting_list == 0
1164
    assert Event.objects.get(id=event.id).booked_waiting_list_places == 0
1165 1165

  
1166 1166
    resp = app.post('/api/agenda/%s/fillslot/%s/?count=3' % (agenda.slug, event.id))
1167 1167
    assert resp.json['err'] == 1
......
1172 1172
    resp = app.post('/api/agenda/%s/fillslot/%s/?count=2' % (agenda.slug, event.id))
1173 1173
    assert resp.json['err'] == 0
1174 1174
    assert Event.objects.get(id=event.id).booked_places == 3
1175
    assert Event.objects.get(id=event.id).waiting_list == 2
1175
    assert Event.objects.get(id=event.id).booked_waiting_list_places == 2
1176 1176

  
1177 1177

  
1178 1178
def test_multiple_booking_api_fillslots(app, some_data, user):
......
1238 1238
    resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': slots, 'count': 5})
1239 1239
    for event in events:
1240 1240
        assert Event.objects.get(id=event.id).booked_places == 2
1241
        assert Event.objects.get(id=event.id).waiting_list == 5
1241
        assert Event.objects.get(id=event.id).booked_waiting_list_places == 5
1242 1242
    accept_url = resp.json['api']['accept_url']
1243 1243

  
1244 1244
    resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': slots, 'count': 5})
......
1248 1248
    assert resp.json['err_desc'] == 'sold out'
1249 1249
    for event in events:
1250 1250
        assert Event.objects.get(id=event.id).booked_places == 2
1251
        assert Event.objects.get(id=event.id).waiting_list == 5
1251
        assert Event.objects.get(id=event.id).booked_waiting_list_places == 5
1252 1252

  
1253 1253
    # accept the waiting list
1254 1254
    resp = app.post(accept_url)
1255 1255
    for event in events:
1256 1256
        assert Event.objects.get(id=event.id).booked_places == 7
1257
        assert Event.objects.get(id=event.id).waiting_list == 0
1257
        assert Event.objects.get(id=event.id).booked_waiting_list_places == 0
1258 1258

  
1259 1259
    # check with a short waiting list
1260 1260
    Booking.objects.all().delete()
......
1273 1273
    assert resp.json['err'] == 0
1274 1274
    for event in events:
1275 1275
        assert Event.objects.get(id=event.id).booked_places == 3
1276
        assert Event.objects.get(id=event.id).waiting_list == 0
1276
        assert Event.objects.get(id=event.id).booked_waiting_list_places == 0
1277 1277

  
1278 1278
    resp = app.post('/api/agenda/%s/fillslots/' % agenda.slug, params={'slots': slots, 'count': 3})
1279 1279
    assert resp.json['err'] == 1
......
1285 1285
    assert resp.json['err'] == 0
1286 1286
    for event in events:
1287 1287
        assert Event.objects.get(id=event.id).booked_places == 3
1288
        assert Event.objects.get(id=event.id).waiting_list == 2
1288
        assert Event.objects.get(id=event.id).booked_waiting_list_places == 2
1289 1289

  
1290 1290

  
1291 1291
def test_multiple_booking_move_booking(app, user):
......
2143 2143
    assert Booking.objects.filter(event__primary_event=event).count() == 104
2144 2144
    assert Booking.objects.filter(event__primary_event=sunday_event).count() == 52
2145 2145

  
2146
    events = Event.annotate_queryset(Event.objects.filter(primary_event__isnull=False))
2147
    assert events.filter(booked_places_count=1).count() == 156
2146
    events = Event.objects.filter(primary_event__isnull=False)
2147
    assert events.filter(booked_places=1).count() == 156
2148 2148

  
2149 2149
    # one recurrence is booked separately
2150 2150
    event = Event.objects.filter(primary_event__isnull=False).first()
......
2155 2155
    assert resp.json['booking_count'] == 156
2156 2156
    assert not resp.json['full_events']
2157 2157
    assert Booking.objects.count() == 313
2158
    events = Event.annotate_queryset(Event.objects.filter(primary_event__isnull=False))
2159
    assert events.filter(booked_places_count=2).count() == 156
2158
    events = Event.objects.filter(primary_event__isnull=False)
2159
    assert events.filter(booked_places=2).count() == 156
2160 2160
    # one booking has been put in waiting list
2161
    assert events.filter(waiting_list_count=1).count() == 1
2161
    assert events.filter(booked_waiting_list_places=1).count() == 1
2162 2162

  
2163 2163
    params['user_external_id'] = 'user_id_3'
2164 2164
    resp = app.post_json(fillslots_url, params=params)
2165 2165
    # everything goes in waiting list
2166
    assert events.filter(waiting_list_count=1).count() == 156
2166
    assert events.filter(booked_waiting_list_places=1).count() == 156
2167 2167
    # but an event was full
2168 2168
    assert resp.json['booking_count'] == 155
2169 2169
    assert len(resp.json['full_events']) == 1
......
2221 2221
    # create bookings in waiting list
2222 2222
    for recurrence in event.recurrences.all():
2223 2223
        Booking.objects.create(event=recurrence, in_waiting_list=True)
2224
    events = Event.annotate_queryset(Event.objects.filter(primary_event__isnull=False))
2225
    assert events.filter(waiting_list_count=1).count() == 5
2224
    events = Event.objects.filter(primary_event__isnull=False)
2225
    assert events.filter(booked_waiting_list_places=1).count() == 5
2226 2226

  
2227 2227
    # check that new bookings are put in waiting list despite free slots on main list
2228 2228
    params = {'user_external_id': 'user_id', 'slots': 'event:0'}
2229 2229
    resp = app.post_json('/api/agenda/%s/recurring-events/fillslots/' % agenda.slug, params=params)
2230 2230
    assert resp.json['booking_count'] == 5
2231
    assert events.filter(waiting_list_count=2).count() == 5
2231
    assert events.filter(booked_waiting_list_places=2).count() == 5
2232 2232

  
2233 2233

  
2234 2234
def test_recurring_events_api_fillslots_change_bookings(app, user, freezer):
......
2280 2280
    assert Booking.objects.count() == 208
2281 2281
    assert Booking.objects.filter(event__start_datetime__week_day=2).count() == 104
2282 2282
    assert Booking.objects.filter(event__start_datetime__week_day=5).count() == 52
2283
    events = Event.annotate_queryset(Event.objects.filter(primary_event__isnull=False))
2284
    assert events.filter(booked_places_count=1).count() == 156
2285
    assert events.filter(waiting_list_count=1).count() == 52
2283
    events = Event.objects.filter(primary_event__isnull=False)
2284
    assert events.filter(booked_places=1).count() == 156
2285
    assert events.filter(booked_waiting_list_places=1).count() == 52
2286 2286

  
2287 2287
    params['slots'] = 'event:1,event:4'
2288 2288
    resp = app.post_json(fillslots_url, params=params)
......
2291 2291
    assert Booking.objects.count() == 208
2292 2292
    assert Booking.objects.filter(event__start_datetime__week_day=3).count() == 104
2293 2293
    assert Booking.objects.filter(event__start_datetime__week_day=6).count() == 52
2294
    events = Event.annotate_queryset(Event.objects.filter(primary_event__isnull=False))
2295
    assert events.filter(booked_places_count=1).count() == 156
2296
    assert events.filter(waiting_list_count=1).count() == 52
2294
    events = Event.objects.filter(primary_event__isnull=False)
2295
    assert events.filter(booked_places=1).count() == 156
2296
    assert events.filter(booked_waiting_list_places=1).count() == 52
2297 2297

  
2298 2298
    # passing empty slots cancels all bookings
2299 2299
    params['slots'] = ''
......
2335 2335
    assert resp.json['booking_count'] == 2
2336 2336
    assert len(resp.json['waiting_list_events']) == 0
2337 2337

  
2338
    events = Event.annotate_queryset(Event.objects.all())
2339
    assert events.filter(booked_places_count=1).count() == 2
2338
    events = Event.objects.all()
2339
    assert events.filter(booked_places=1).count() == 2
2340 2340

  
2341 2341
    params['user_external_id'] = 'user_id_2'
2342 2342
    resp = app.post_json(fillslots_url, params=params)
......
2415 2415
    )
2416 2416

  
2417 2417
    params = {'user_external_id': 'user_id', 'slots': ','.join((event1.slug, event2.slug))}
2418
    resp = app.post_json('/api/agenda/%s/events/fillslots/' % agenda.slug, params)
2418
    resp = app.post_json('/api/agenda/%s/events/fillslots/' % agenda.slug, params=params)
2419 2419
    assert resp.json['err'] == 1
2420 2420
    assert resp.json['err_desc'] == 'event not bookable'
2421 2421

  
2422 2422
    params['events'] = 'future'
2423
    resp = app.post_json('/api/agenda/%s/events/fillslots/' % agenda.slug, params)
2423
    resp = app.post_json('/api/agenda/%s/events/fillslots/' % agenda.slug, params=params)
2424 2424
    assert resp.json['err'] == 1
2425 2425
    assert resp.json['err_desc'] == 'event not bookable'
2426 2426

  
2427 2427
    params['events'] = 'past'
2428
    resp = app.post_json('/api/agenda/%s/events/fillslots/' % agenda.slug, params)
2428
    resp = app.post_json('/api/agenda/%s/events/fillslots/' % agenda.slug, params=params)
2429 2429
    assert resp.json['err'] == 1
2430 2430
    assert resp.json['err_desc'] == 'event not bookable'
2431 2431

  
2432 2432
    params['events'] = 'all'
2433
    resp = app.post_json('/api/agenda/%s/events/fillslots/' % agenda.slug, params)
2433
    resp = app.post_json('/api/agenda/%s/events/fillslots/' % agenda.slug, params=params)
2434 2434
    assert resp.json['err'] == 0
tests/test_agendas.py
879 879
    assert Agenda.objects.get(id=agenda.id).edit_role is None
880 880

  
881 881

  
882
def test_event_bookings_annotation():
883
    agenda = Agenda(label='test', kind='events')
884
    agenda.save()
885
    event = Event(start_datetime=now(), label='foo', places=10, waiting_list_places=10, agenda=agenda)
886
    event.save()
887
    event2 = Event(start_datetime=now(), label='bar', places=10, waiting_list_places=10, agenda=agenda)
888
    event2.save()
889

  
890
    Booking(event=event).save()
891
    Booking(event=event).save()
892
    Booking(event=event, cancellation_datetime=now()).save()
893
    Booking(event=event, in_waiting_list=True).save()
894
    Booking(event=event, in_waiting_list=True, cancellation_datetime=now()).save()
895

  
896
    Booking(event=event2).save()
897
    Booking(event=event2).save()
898
    Booking(event=event2).save()
899

  
900
    for event in Event.annotate_queryset(Event.objects.filter(agenda=agenda)):
901
        if event.label == 'foo':
902
            assert event.booked_places_count == 2
903
            assert event.waiting_list_count == 1
904
        elif event.label == 'bar':
905
            assert event.booked_places_count == 3
906
            assert event.waiting_list_count == 0
907

  
908

  
909 882
def test_virtual_agenda_init():
910 883
    agenda1 = Agenda.objects.create(label='Agenda 1', kind='meetings')
911 884
    agenda2 = Agenda.objects.create(label='Agenda 2', kind='meetings')
......
1377 1350
    )
1378 1351
    desk = Desk.objects.create(agenda=agenda, slug='_exceptions_holder')
1379 1352
    desk.import_timeperiod_exceptions_from_settings(enable=True)
1380
    time_period_exception = TimePeriodException.objects.create(
1353
    TimePeriodException.objects.create(
1381 1354
        desk=desk,
1382 1355
        start_datetime=now(),
1383 1356
        end_datetime=now() + datetime.timedelta(minutes=30),
1384 1357
    )
1385 1358
    assert desk.timeperiodexception_set.count() == 34
1386
    settings = AgendaNotificationsSettings.objects.create(
1359
    AgendaNotificationsSettings.objects.create(
1387 1360
        agenda=agenda,
1388 1361
        full_event=AgendaNotificationsSettings.EMAIL_FIELD,
1389 1362
        full_event_emails=['hop@entrouvert.com', 'top@entrouvert.com'],
......
1795 1768
        assert 'Do no forget ID card.' in body
1796 1769
        assert 'Come !' in body
1797 1770
        assert 'Pricing: 10€' in body
1798
        assert not 'cancel' in body
1799
        assert not 'if present' in body  # assert separation with preview code
1771
        assert 'cancel' not in body
1772
        assert 'if present' not in body  # assert separation with preview code
1800 1773
    assert 'More information: https://example.party' in mail_bodies[0]
1801 1774
    assert '<a href="https://example.party">More information</a>' in mail_bodies[1]
1802 1775
    mailoutbox.clear()
......
2279 2252
        event.get_recurrence_display()
2280 2253
        == 'On Monday at 1:30 p.m., once every three weeks, from Jan. 7, 2021, until Jan. 14, 2021'
2281 2254
    )
2255

  
2256

  
2257
def test_event_triggered_fields():
2258
    agenda = Agenda.objects.create(label='Agenda', kind='events')
2259
    event = Event.objects.create(
2260
        agenda=agenda, start_datetime=now() + datetime.timedelta(days=10), places=10, label='Event'
2261
    )
2262
    event2 = Event.objects.create(
2263
        agenda=agenda, start_datetime=now() + datetime.timedelta(days=10), places=10, label='Event'
2264
    )
2265
    assert event.booked_places == 0
2266
    assert event.booked_waiting_list_places == 0
2267
    assert event.almost_full is False
2268
    assert event.full is False
2269

  
2270
    event.booked_places = 42
2271
    event.booked_waiting_list_places = 42
2272
    event.almost_full = True
2273
    event.full = True
2274
    event.save()
2275
    # computed by triggers
2276
    event.refresh_from_db()
2277
    assert event.booked_places == 0
2278
    assert event.booked_waiting_list_places == 0
2279
    assert event.almost_full is False
2280
    assert event.full is False
2281

  
2282
    # add bookings for other event: no impact
2283
    for _ in range(10):
2284
        Booking.objects.create(event=event2)
2285
    event.refresh_from_db()
2286
    assert event.booked_places == 0
2287
    assert event.booked_waiting_list_places == 0
2288
    assert event.almost_full is False
2289
    assert event.full is False
2290

  
2291
    # add bookings
2292
    for _ in range(9):
2293
        Booking.objects.create(event=event)
2294
    event.refresh_from_db()
2295
    assert event.booked_places == 9
2296
    assert event.booked_waiting_list_places == 0
2297
    assert event.almost_full is True
2298
    assert event.full is False
2299

  
2300
    Booking.objects.create(event=event)
2301
    event.refresh_from_db()
2302
    assert event.booked_places == 10
2303
    assert event.booked_waiting_list_places == 0
2304
    assert event.almost_full is True
2305
    assert event.full is True
2306

  
2307
    # cancel bookings for other event: no impact
2308
    event2.booking_set.filter(cancellation_datetime__isnull=True).first().cancel()
2309
    event.refresh_from_db()
2310
    assert event.booked_places == 10
2311
    assert event.booked_waiting_list_places == 0
2312
    assert event.almost_full is True
2313
    assert event.full is True
2314

  
2315
    # cancel bookings
2316
    event.booking_set.filter(cancellation_datetime__isnull=True).first().cancel()
2317
    event.refresh_from_db()
2318
    assert event.booked_places == 9
2319
    assert event.booked_waiting_list_places == 0
2320
    assert event.almost_full is True
2321
    assert event.full is False
2322
    event.booking_set.filter(cancellation_datetime__isnull=True).first().cancel()
2323
    event.refresh_from_db()
2324
    assert event.booked_places == 8
2325
    assert event.booked_waiting_list_places == 0
2326
    assert event.almost_full is False
2327
    assert event.full is False
2328

  
2329
    # update places
2330
    event.places = 20
2331
    event.save()
2332
    event.refresh_from_db()
2333
    assert event.booked_places == 8
2334
    assert event.booked_waiting_list_places == 0
2335
    assert event.almost_full is False
2336
    assert event.full is False
2337

  
2338
    Booking.objects.create(event=event)
2339
    Booking.objects.create(event=event)
2340
    event.places = 10
2341
    event.save()
2342
    event.refresh_from_db()
2343
    assert event.booked_places == 10
2344
    assert event.booked_waiting_list_places == 0
2345
    assert event.almost_full is True
2346
    assert event.full is True
2347

  
2348
    # with a waiting list
2349
    event.waiting_list_places = 5
2350
    event.save()
2351
    event.refresh_from_db()
2352
    assert event.booked_places == 10
2353
    assert event.booked_waiting_list_places == 0
2354
    assert event.almost_full is True
2355
    assert event.full is False
2356

  
2357
    # add bookings for other event: no impact
2358
    for _ in range(10):
2359
        Booking.objects.create(event=event2, in_waiting_list=True)
2360
    event.refresh_from_db()
2361
    assert event.booked_places == 10
2362
    assert event.booked_waiting_list_places == 0
2363
    assert event.almost_full is True
2364
    assert event.full is False
2365

  
2366
    # add bookings
2367
    Booking.objects.create(event=event, in_waiting_list=True)
2368
    event.refresh_from_db()
2369
    assert event.booked_places == 10
2370
    assert event.booked_waiting_list_places == 1
2371
    assert event.almost_full is True
2372
    assert event.full is False
2373
    for _ in range(1, 5):
2374
        Booking.objects.create(event=event, in_waiting_list=True)
2375
    event.refresh_from_db()
2376
    assert event.booked_places == 10
2377
    assert event.booked_waiting_list_places == 5
2378
    assert event.almost_full is True
2379
    assert event.full is True
2380

  
2381
    # cancel bookings for other event: no impact
2382
    event2.booking_set.filter(in_waiting_list=True, cancellation_datetime__isnull=True).first().cancel()
2383
    event.refresh_from_db()
2384
    assert event.booked_places == 10
2385
    assert event.booked_waiting_list_places == 5
2386
    assert event.almost_full is True
2387
    assert event.full is True
2388

  
2389
    # cancel bookings
2390
    event.booking_set.filter(in_waiting_list=True, cancellation_datetime__isnull=True).first().cancel()
2391
    event.refresh_from_db()
2392
    assert event.booked_places == 10
2393
    assert event.booked_waiting_list_places == 4
2394
    assert event.almost_full is True
2395
    assert event.full is False
2396

  
2397
    # update waiting list places
2398
    event.waiting_list_places = 4
2399
    event.save()
2400
    event.refresh_from_db()
2401
    assert event.booked_places == 10
2402
    assert event.booked_waiting_list_places == 4
2403
    assert event.almost_full is True
2404
    assert event.full is True
2405

  
2406
    # delete bookings
2407
    event.booking_set.filter(in_waiting_list=True, cancellation_datetime__isnull=True).first().delete()
2408
    event.refresh_from_db()
2409
    assert event.booked_places == 10
2410
    assert event.booked_waiting_list_places == 3
2411
    assert event.almost_full is True
2412
    assert event.full is False
2413
    event.booking_set.filter(in_waiting_list=False, cancellation_datetime__isnull=True).first().delete()
2414
    event.refresh_from_db()
2415
    assert event.booked_places == 9
2416
    assert event.booked_waiting_list_places == 3
2417
    assert event.almost_full is True
2418
    assert event.full is False
2419
    event.booking_set.filter(in_waiting_list=False, cancellation_datetime__isnull=True).first().delete()
2420
    event.refresh_from_db()
2421
    assert event.booked_places == 8
2422
    assert event.booked_waiting_list_places == 3
2423
    assert event.almost_full is False
2424
    assert event.full is False
2282
-