0001-agendas-trigger-full-and-places-event-fields-54747.patch
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 |
SELECT |
|
11 |
COUNT(1) FILTER (WHERE NOT(in_waiting_list)), |
|
12 |
COUNT(1) FILTER (WHERE in_waiting_list) |
|
13 |
INTO NEW.booked_places, NEW.booked_waiting_list_places |
|
14 |
FROM agendas_booking b |
|
15 |
WHERE b.event_id = NEW.id AND b.cancellation_datetime IS NULL; |
|
16 |
END IF; |
|
17 | ||
18 |
-- update almost_full field |
|
19 |
IF (NEW.booked_places >= NEW.places * 0.9) THEN |
|
20 |
NEW.almost_full = true; |
|
21 |
ELSE |
|
22 |
NEW.almost_full = false; |
|
23 |
END IF; |
|
24 | ||
25 |
-- update full field |
|
26 |
IF (NEW.booked_places >= NEW.places) AND (NEW.waiting_list_places = 0) THEN |
|
27 |
NEW.full = true; |
|
28 |
ELSIF (NEW.waiting_list_places > 0) AND (NEW.booked_waiting_list_places >= NEW.waiting_list_places) THEN |
|
29 |
NEW.full = true; |
|
30 |
ELSE |
|
31 |
NEW.full = false; |
|
32 |
END IF; |
|
33 | ||
34 |
RETURN NEW; |
|
35 |
END; |
|
36 |
$$ LANGUAGE plpgsql; |
|
37 | ||
38 |
CREATE OR REPLACE FUNCTION update_event_places_fields() RETURNS TRIGGER AS $$ |
|
39 |
DECLARE |
|
40 |
e_id integer; |
|
41 |
BEGIN |
|
42 |
IF (TG_OP = 'DELETE') THEN |
|
43 |
e_id = OLD.event_id; |
|
44 |
ELSE |
|
45 |
e_id = NEW.event_id; |
|
46 |
END IF; |
|
47 |
WITH new_stats AS ( |
|
48 |
SELECT |
|
49 |
COUNT(1) FILTER (WHERE NOT(in_waiting_list)) AS booked, |
|
50 |
COUNT(1) FILTER (WHERE in_waiting_list) AS waiting |
|
51 |
FROM agendas_booking b |
|
52 |
WHERE b.event_id = e_id AND b.cancellation_datetime IS NULL |
|
53 |
) |
|
54 |
UPDATE agendas_event |
|
55 |
SET booked_places = new_stats.booked, |
|
56 |
booked_waiting_list_places = new_stats.waiting |
|
57 |
FROM new_stats |
|
58 |
WHERE id = e_id AND (agendas_event.booked_places <> new_stats.booked |
|
59 |
OR agendas_event.booked_waiting_list_places <> new_stats.waiting); |
|
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 |
- |