Projet

Général

Profil

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

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

Télécharger (53,2 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 |  27 +++
 chrono/agendas/migrations/0100_event_dml.py   |  13 ++
 chrono/agendas/models.py                      |  61 +-----
 .../event_booked_places_and_full_triggers.sql |  70 ++++++
 chrono/api/views.py                           |  41 +---
 .../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 +++++++++++++++---
 13 files changed, 407 insertions(+), 184 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
 create mode 100644 chrono/agendas/sql/event_booked_places_and_full_triggers.sql
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
import os
2

  
3
from django.db import migrations
4

  
5
with open(
6
    os.path.join(
7
        os.path.dirname(os.path.realpath(__file__)), '..', 'sql', 'event_booked_places_and_full_triggers.sql'
8
    )
9
) as sql_file:
10
    sql_forwards = sql_file.read()
11

  
12
sql_backwards = """
13
DROP TRIGGER IF EXISTS update_event_full_fields_trigger ON agendas_event;
14
DROP TRIGGER IF EXISTS update_event_places_fields_trigger ON agendas_booking;
15

  
16
DROP FUNCTION IF EXISTS update_event_full_fields;
17
DROP FUNCTION IF EXISTS update_event_places_fields;
18
"""
19

  
20

  
21
class Migration(migrations.Migration):
22

  
23
    dependencies = [
24
        ('agendas', '0098_event_booked_places'),
25
    ]
26

  
27
    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, Prefetch, Q, Subquery, Value
36
from django.db.models.functions import Coalesce
35
from django.db.models import Count, Max, Prefetch, 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,
......
1363 1354
    )
1364 1355
    pricing = models.CharField(_('Pricing'), max_length=150, null=True, blank=True)
1365 1356
    url = models.CharField(_('URL'), max_length=200, null=True, blank=True)
1357
    booked_places = models.PositiveSmallIntegerField(default=0)
1358
    booked_waiting_list_places = models.PositiveSmallIntegerField(default=0)
1366 1359
    almost_full = models.BooleanField(default=False)
1367 1360
    full = models.BooleanField(default=False)
1368 1361
    cancelled = models.BooleanField(default=False)
......
1396 1389
        assert self.agenda.kind != 'virtual', "an event can't reference a virtual agenda"
1397 1390
        assert not (self.slug and self.slug.isdigit()), 'slug cannot be a number'
1398 1391
        self.start_datetime = self.start_datetime.replace(second=0, microsecond=0)
1399
        self.check_full()
1400 1392
        if not self.slug:
1401 1393
            self.slug = generate_slug(self, seen_slugs=seen_slugs, agenda=self.agenda)
1402 1394
        return super().save(*args, **kwargs)
......
1406 1398
        # label can be empty
1407 1399
        return slugify(self.label or ('%s-event' % self.agenda.label))
1408 1400

  
1409
    @functional.cached_property
1410 1401
    def main_list_full(self):
1411 1402
        return bool(self.booked_places >= self.places)
1412 1403

  
1413
    def check_full(self):
1414
        self.full = bool(
1415
            (self.booked_places >= self.places and self.waiting_list_places == 0)
1416
            or (self.waiting_list_places and self.waiting_list >= self.waiting_list_places)
1417
        )
1418
        self.almost_full = bool(self.booked_places >= 0.9 * self.places)
1419

  
1420 1404
    def set_is_checked(self):
1421 1405
        if not self.agenda.mark_event_checked_auto:
1422 1406
            return
......
1451 1435
    def is_day_past(self):
1452 1436
        return self.start_datetime.date() <= now().date()
1453 1437

  
1454
    @staticmethod
1455
    def annotate_queryset(qs):
1456
        not_cancelled_bookings = Booking.objects.filter(
1457
            cancellation_datetime__isnull=True, event=OuterRef('pk')
1458
        )
1459

  
1460
        bookings = not_cancelled_bookings.filter(in_waiting_list=False).order_by().values('event')
1461
        count_bookings = bookings.annotate(count=Count('event')).values('count')
1462

  
1463
        waiting_list_bookings = not_cancelled_bookings.filter(in_waiting_list=True).order_by().values('event')
1464
        count_waiting_list = waiting_list_bookings.annotate(count=Count('event')).values('count')
1465

  
1466
        return qs.annotate(
1467
            booked_places_count=Coalesce(Subquery(count_bookings, output_field=IntegerField()), Value(0)),
1468
            waiting_list_count=Coalesce(Subquery(count_waiting_list, output_field=IntegerField()), Value(0)),
1469
        )
1470

  
1471 1438
    @staticmethod
1472 1439
    def annotate_queryset_for_user(qs, user_external_id):
1473 1440
        return qs.annotate(
......
1489 1456
            ),
1490 1457
        )
1491 1458

  
1492
    @property
1493
    def booked_places(self):
1494
        if hasattr(self, 'booked_places_count'):
1495
            return self.booked_places_count
1496
        return self.booking_set.filter(cancellation_datetime__isnull=True, in_waiting_list=False).count()
1497

  
1498 1459
    @property
1499 1460
    def remaining_places(self):
1500 1461
        return max(0, self.places - self.booked_places)
1501 1462

  
1502
    @property
1503
    def waiting_list(self):
1504
        if hasattr(self, 'waiting_list_count'):
1505
            return self.waiting_list_count
1506
        return self.booking_set.filter(cancellation_datetime__isnull=True, in_waiting_list=True).count()
1507

  
1508 1463
    @property
1509 1464
    def remaining_waiting_list_places(self):
1510
        return max(0, self.waiting_list_places - self.waiting_list)
1465
        return max(0, self.waiting_list_places - self.booked_waiting_list_places)
1511 1466

  
1512 1467
    @property
1513 1468
    def end_datetime(self):
......
1834 1789
    def user_name(self):
1835 1790
        return ('%s %s' % (self.user_first_name, self.user_last_name)).strip()
1836 1791

  
1837
    def save(self, *args, **kwargs):
1838
        with transaction.atomic():
1839
            super().save(*args, **kwargs)
1840
            initial_values = self.event.full, self.event.almost_full
1841
            self.event.check_full()
1842
            if (self.event.full, self.event.almost_full) != initial_values:
1843
                self.event.save()
1844

  
1845 1792
    def cancel(self, trigger_callback=False):
1846 1793
        timestamp = now()
1847 1794
        with transaction.atomic():
chrono/agendas/sql/event_booked_places_and_full_triggers.sql
1
CREATE OR REPLACE FUNCTION update_event_full_fields() RETURNS TRIGGER AS $$
2
BEGIN
3
    IF (TG_OP = 'INSERT') THEN
4
        NEW.booked_places = 0;
5
        NEW.booked_waiting_list_places = 0;
6
    ELSE
7
        -- update booked_places and booked_waiting_list_places fields
8
        SELECT
9
            COUNT(1) FILTER (WHERE NOT(in_waiting_list)),
10
            COUNT(1) FILTER (WHERE in_waiting_list)
11
            INTO NEW.booked_places, NEW.booked_waiting_list_places
12
            FROM agendas_booking b
13
            WHERE b.event_id = NEW.id AND b.cancellation_datetime IS NULL;
14
    END IF;
15

  
16
    -- update almost_full field
17
    IF (NEW.booked_places >= NEW.places * 0.9) THEN
18
        NEW.almost_full = true;
19
    ELSE
20
        NEW.almost_full = false;
21
    END IF;
22

  
23
    -- update full field
24
    IF (NEW.booked_places >= NEW.places) AND (NEW.waiting_list_places = 0) THEN
25
        NEW.full = true;
26
    ELSIF (NEW.waiting_list_places > 0) AND (NEW.booked_waiting_list_places >= NEW.waiting_list_places) THEN
27
        NEW.full = true;
28
    ELSE
29
        NEW.full = false;
30
    END IF;
31

  
32
    RETURN NEW;
33
END;
34
$$ LANGUAGE plpgsql;
35

  
36
CREATE OR REPLACE FUNCTION update_event_places_fields() RETURNS TRIGGER AS $$
37
DECLARE
38
    e_id integer;
39
BEGIN
40
    IF (TG_OP = 'DELETE') THEN
41
        e_id = OLD.event_id;
42
    ELSE
43
        e_id = NEW.event_id;
44
    END IF;
45
    -- update booked_places and booked_waiting_list_places fields on agendas_event
46
    WITH new_stats AS (
47
        SELECT
48
            COUNT(1) FILTER (WHERE NOT(in_waiting_list)) AS booked,
49
            COUNT(1) FILTER (WHERE in_waiting_list) AS waiting
50
            FROM agendas_booking b
51
            WHERE b.event_id = e_id AND b.cancellation_datetime IS NULL
52
    )
53
    UPDATE agendas_event
54
        SET booked_places = new_stats.booked,
55
            booked_waiting_list_places = new_stats.waiting
56
        FROM new_stats
57
        WHERE id = e_id AND (agendas_event.booked_places <> new_stats.booked
58
                             OR agendas_event.booked_waiting_list_places <> new_stats.waiting);
59

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

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

  
68
DROP TRIGGER IF EXISTS update_event_places_fields_trigger ON agendas_booking;
69
CREATE TRIGGER update_event_places_fields_trigger AFTER INSERT OR UPDATE OR DELETE ON agendas_booking
70
    FOR EACH ROW EXECUTE PROCEDURE update_event_places_fields();
chrono/api/views.py
396 396

  
397 397

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

  
415 415
    return places
......
755 755
        entries = []
756 756
        if show_past:
757 757
            entries += agenda.get_past_events(
758
                annotate_queryset=True,
759 758
                min_start=payload.get('date_start'),
760 759
                max_start=payload.get('date_end'),
761 760
                user_external_id=user_external_id,
762 761
            )
763 762
        if show_future:
764 763
            entries += agenda.get_open_events(
765
                annotate_queryset=True,
766 764
                min_start=payload.get('date_start'),
767 765
                max_start=payload.get('date_end'),
768 766
                user_external_id=user_external_id,
......
826 824
        user_external_id = payload.get('user_external_id') or payload.get('exclude_user_external_id')
827 825
        disable_booked = bool(payload.get('exclude_user_external_id'))
828 826
        agendas = Agenda.prefetch_events_and_exceptions(
829
            agendas, annotate_events=True, user_external_id=user_external_id
827
            agendas, user_external_id=user_external_id
830 828
        )
831 829

  
832 830
        entries = []
......
1392 1390
                    if (
1393 1391
                        payload.get('force_waiting_list')
1394 1392
                        or (event.booked_places + places_count) > event.places
1395
                        or event.waiting_list
1393
                        or event.booked_waiting_list_places
1396 1394
                    ):
1397 1395
                        # if this is full or there are people waiting, put new bookings
1398 1396
                        # in the waiting list.
1399 1397
                        in_waiting_list = True
1400
                        if (event.waiting_list + places_count) > event.waiting_list_places:
1398
                        if (event.booked_waiting_list_places + places_count) > event.waiting_list_places:
1401 1399
                            raise APIError(
1402 1400
                                _('sold out'),
1403 1401
                                err_class='sold out',
......
1572 1570
        full_events = list(events_to_book.filter(full=True))
1573 1571
        events_to_book = events_to_book.filter(full=False)
1574 1572

  
1575
        events_to_book = Event.annotate_queryset(events_to_book)
1576 1573
        events_to_book = events_to_book.annotate(
1577 1574
            in_waiting_list=ExpressionWrapper(
1578
                Q(booked_places_count__gte=F('places')) | Q(waiting_list_count__gt=0),
1575
                Q(booked_places__gte=F('places')) | Q(booked_waiting_list_places__gt=0),
1579 1576
                output_field=BooleanField(),
1580 1577
            )
1581 1578
        )
......
1583 1580
        extra_data = {k: v for k, v in request.data.items() if k not in payload}
1584 1581
        bookings = [make_booking(event, payload, extra_data) for event in events_to_book]
1585 1582

  
1586
        events_to_update = Event.annotate_queryset(
1587
            agenda.event_set.filter(pk__in=events_to_unbook + [event.pk for event in events_to_book])
1588
        )
1589 1583
        with transaction.atomic():
1590 1584
            deleted_count = Booking.objects.filter(
1591 1585
                user_external_id=user_external_id, event__in=events_to_unbook
1592 1586
            ).delete()[0]
1593 1587
            Booking.objects.bulk_create(bookings)
1594
            events_to_update.update(
1595
                full=Q(booked_places_count__gte=F('places'), waiting_list_places=0)
1596
                | Q(waiting_list_places__gt=0, waiting_list_count__gte=F('waiting_list_places')),
1597
                almost_full=Q(booked_places_count__gte=0.9 * F('places')),
1598
            )
1599 1588

  
1600 1589
        response = {
1601 1590
            'err': 0,
......
1632 1621
        user_external_id = payload['user_external_id']
1633 1622

  
1634 1623
        events = self.get_events(request, payload)
1635
        events = events.select_related('agenda')
1636
        events = Event.annotate_queryset(events)
1637 1624

  
1638 1625
        already_booked_events = self.get_already_booked_events(user_external_id)
1639 1626
        if start_datetime:
......
1652 1639

  
1653 1640
        events = events.annotate(
1654 1641
            in_waiting_list=ExpressionWrapper(
1655
                Q(booked_places_count__gte=F('places')) | Q(waiting_list_count__gt=0),
1642
                Q(booked_places__gte=F('places')) | Q(booked_waiting_list_places__gt=0),
1656 1643
                output_field=BooleanField(),
1657 1644
            )
1658 1645
        )
......
1661 1648
        extra_data = {k: v for k, v in request.data.items() if k not in payload}
1662 1649
        bookings = [make_booking(event, payload, extra_data) for event in events]
1663 1650

  
1664
        events_to_update = Event.annotate_queryset(
1665
            Event.objects.filter(pk__in=events_to_unbook + [event.pk for event in events])
1666
        )
1667 1651
        with transaction.atomic():
1668 1652
            deleted_count = Booking.objects.filter(
1669 1653
                user_external_id=user_external_id, event__in=events_to_unbook
1670 1654
            ).delete()[0]
1671 1655
            Booking.objects.bulk_create(bookings)
1672
            events_to_update.update(
1673
                full=Q(booked_places_count__gte=F('places'), waiting_list_places=0)
1674
                | Q(waiting_list_places__gt=0, waiting_list_count__gte=F('waiting_list_places')),
1675
                almost_full=Q(booked_places_count__gte=0.9 * F('places')),
1676
            )
1677 1656

  
1678 1657
        response = {
1679 1658
            'err': 0,
......
2105 2084
        # total places for the event (in waiting or main list, depending on the primary booking location)
2106 2085
        places = event.waiting_list_places if booking.in_waiting_list else event.places
2107 2086
        # total booked places for the event (in waiting or main list, depending on the primary booking location)
2108
        booked_places = event.waiting_list if booking.in_waiting_list else event.booked_places
2087
        booked_places = event.booked_waiting_list_places if booking.in_waiting_list else event.booked_places
2109 2088

  
2110 2089
        # places to book for this primary booking
2111 2090
        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
2435 2435

  
2436 2436

  
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
-