Fix #1251 -- Event list/calendar: Show "event almost sold out" state (#3063)

Co-authored-by: Richard Schreiber <schreiber@rami.io>
This commit is contained in:
Raphael Michel
2023-02-01 13:20:06 +01:00
committed by GitHub
co-authored by Richard Schreiber
parent aeb5c52bfe
commit 8bba1a2ea6
14 changed files with 248 additions and 69 deletions
+82 -41
View File
@@ -36,7 +36,7 @@ import logging
import os import os
import string import string
import uuid import uuid
from collections import OrderedDict from collections import Counter, OrderedDict, defaultdict
from datetime import datetime, time, timedelta from datetime import datetime, time, timedelta
from operator import attrgetter from operator import attrgetter
from urllib.parse import urljoin from urllib.parse import urljoin
@@ -340,64 +340,104 @@ class EventMixin:
) )
) )
@cached_property @property
def best_availability_state(self): def best_availability_state(self):
return self.best_availability[0]
@property
def best_availability_is_low(self):
"""
Returns ``True`` if the availability of tickets in this event is lower than the percentage
given in setting ``low_availability_percentage``.
"""
if not self.settings.low_availability_percentage:
return False
ba = self.best_availability
if ba[1] is None or not ba[2]:
return False
percentage = ba[1] / ba[2] * 100
return percentage < self.settings.low_availability_percentage
@cached_property
def best_availability(self):
"""
Returns a 3-tuple of
- The availability state of this event (one of the ``Quota.AVAILABILITY_*`` constants)
- The number of tickets currently available (or ``None``)
- The number of tickets "originally" available (or ``None``)
This can only be called on objects obtained through a queryset that has been passed through ``.annotated()``.
"""
from .items import Quota from .items import Quota
if not hasattr(self, 'active_quotas'): if not hasattr(self, 'active_quotas'):
raise TypeError("Call this only if you fetched the subevents via Event/SubEvent.annotated()") raise TypeError("Call this only if you fetched the subevents via Event/SubEvent.annotated()")
items_available = set()
vars_available = set()
items_reserved = set()
vars_reserved = set()
items_gone = set()
vars_gone = set()
items_disabled = set()
vars_disabled = set()
if hasattr(self, 'disabled_items'): # SubEventItem if hasattr(self, 'disabled_items'): # SubEventItem
items_disabled = set(self.disabled_items.split(",")) items_disabled = set(self.disabled_items.split(","))
else:
items_disabled = set()
if hasattr(self, 'disabled_vars'): # SubEventItemVariation if hasattr(self, 'disabled_vars'): # SubEventItemVariation
vars_disabled = set(self.disabled_vars.split(",")) vars_disabled = set(self.disabled_vars.split(","))
else:
vars_disabled = set()
# Compute the availability of all quotas and build a item→quotas mapping with all non-disabled items
r = getattr(self, '_quota_cache', {}) r = getattr(self, '_quota_cache', {})
quotas_for_item = defaultdict(list)
quotas_for_variation = defaultdict(list)
for q in self.active_quotas: for q in self.active_quotas:
res = r[q] if q in r else q.availability(allow_cache=True) if q not in r:
r[q] = q.availability(allow_cache=True)
if res[0] == Quota.AVAILABILITY_OK: if q.active_items:
if q.active_items: for item_id in q.active_items.split(","):
items_available.update(q.active_items.split(",")) if item_id not in items_disabled:
if q.active_variations: quotas_for_item[item_id].append(q)
vars_available.update(q.active_variations.split(",")) if q.active_variations:
elif res[0] == Quota.AVAILABILITY_RESERVED: for var_id in q.active_variations.split(","):
if q.active_items: if var_id not in vars_disabled:
items_reserved.update(q.active_items.split(",")) quotas_for_variation[var_id].append(q)
if q.active_variations:
vars_reserved.update(q.active_variations.split(","))
elif res[0] < Quota.AVAILABILITY_RESERVED:
if q.active_items:
items_gone.update(q.active_items.split(","))
if q.active_variations:
vars_gone.update(q.active_variations.split(","))
items_available -= items_disabled if not self.active_quotas or (not quotas_for_item and not quotas_for_variation):
items_reserved -= items_disabled # No item is enabled for this event, treat the event as "unknown"
items_gone -= items_disabled return None, None, None
vars_available -= vars_disabled
vars_reserved -= vars_disabled
vars_gone -= vars_disabled
if not self.active_quotas or ( # We iterate over all items and variations and keep track of
not items_available and not items_reserved and not items_gone and not vars_gone and not vars_available and not vars_reserved # - `best_state_found` - the best availability state we have seen so far. If one item is available, the event is available!
): # - `num_tickets_found` - the number of tickets currently available in total. We sum up all the items and variations, but keep
return None # track of them per-quota in `quota_used_for_found_tickets` to make sure we don't count the same tickets twice if two or more
# items share the same quota
# - `num_tickets_possible` - basically the same thing, just with the total size of quotas instead of their currently availability
# since we need that for the percentage calculation
best_state_found = Quota.AVAILABILITY_GONE
num_tickets_found = 0
num_tickets_possible = 0
quota_used_for_found_tickets = Counter()
quota_used_for_possible_tickets = Counter()
for quota_list in list(quotas_for_item.values()) + list(quotas_for_variation.values()):
worst_state_for_ticket = min(r[q][0] for q in quota_list)
quotas_that_are_not_unlimited = [q for q in quota_list if q.size is not None]
if not quotas_that_are_not_unlimited:
# We found an unlimited ticket, no more need to do anything else
return Quota.AVAILABILITY_OK, None, None
if items_available - items_reserved - items_gone or vars_available - vars_reserved - vars_gone: if worst_state_for_ticket == Quota.AVAILABILITY_OK:
return Quota.AVAILABILITY_OK availability_of_this = min(max(0, r[q][1] - quota_used_for_found_tickets[q]) for q in quotas_that_are_not_unlimited)
if items_reserved - items_gone or vars_reserved - vars_gone: num_tickets_found += availability_of_this
return Quota.AVAILABILITY_RESERVED for q in quota_list:
return Quota.AVAILABILITY_GONE quota_used_for_found_tickets[q] += availability_of_this
possible_of_this = min(max(0, q.size - quota_used_for_possible_tickets[q]) for q in quotas_that_are_not_unlimited)
num_tickets_possible += possible_of_this
for q in quota_list:
quota_used_for_possible_tickets[q] += possible_of_this
best_state_found = max(best_state_found, worst_state_for_ticket)
return best_state_found, num_tickets_found, num_tickets_possible
def free_seats(self, ignore_voucher=None, sales_channel='web', include_blocked=False): def free_seats(self, ignore_voucher=None, sales_channel='web', include_blocked=False):
qs_annotated = self._seats(ignore_voucher=ignore_voucher) qs_annotated = self._seats(ignore_voucher=ignore_voucher)
@@ -591,6 +631,7 @@ class Event(EventMixin, LoggedModel):
self.settings.invoice_email_attachment = True self.settings.invoice_email_attachment = True
self.settings.name_scheme = 'given_family' self.settings.name_scheme = 'given_family'
self.settings.payment_banktransfer_invoice_immediately = True self.settings.payment_banktransfer_invoice_immediately = True
self.settings.low_availability_percentage = 10
@property @property
def social_image(self): def social_image(self):
+19
View File
@@ -1308,6 +1308,25 @@ DEFAULTS = {
"the email. Does not affect orders performed through other sales channels."), "the email. Does not affect orders performed through other sales channels."),
) )
}, },
'low_availability_percentage': {
'default': None,
'type': int,
'serializer_class': serializers.IntegerField,
'form_class': forms.IntegerField,
'serializer_kwargs': dict(
min_value=0,
max_value=100,
),
'form_kwargs': dict(
label=_('Low availability threshold'),
help_text=_('If the availability of tickets falls below this percentage, the event (or a date, if it is an '
'event series) will be highlighted to have low availability in the event list or calendar. If '
'you keep this option empty, low availability will not be shown publicly.'),
min_value=0,
max_value=100,
required=False
)
},
'event_list_availability': { 'event_list_availability': {
'default': 'True', 'default': 'True',
'type': bool, 'type': bool,
+1
View File
@@ -507,6 +507,7 @@ class EventSettingsForm(SettingsForm):
'meta_noindex', 'meta_noindex',
'redirect_to_checkout_directly', 'redirect_to_checkout_directly',
'frontpage_subevent_ordering', 'frontpage_subevent_ordering',
'low_availability_percentage',
'event_list_type', 'event_list_type',
'event_list_available_only', 'event_list_available_only',
'frontpage_text', 'frontpage_text',
@@ -314,7 +314,8 @@
{% if sform.event_list_available_only %} {% if sform.event_list_available_only %}
{% bootstrap_field sform.event_list_available_only layout="control" %} {% bootstrap_field sform.event_list_available_only layout="control" %}
{% endif %} {% endif %}
{% bootstrap_field sform.low_availability_percentage layout="control" %}
{% url "control:organizer.edit" organizer=request.organizer.slug as org_url %} {% url "control:organizer.edit" organizer=request.organizer.slug as org_url %}
{% propagated request.event org_url "meta_noindex" %} {% propagated request.event org_url "meta_noindex" %}
{% bootstrap_field sform.meta_noindex layout="control" %} {% bootstrap_field sform.meta_noindex layout="control" %}
@@ -21,11 +21,15 @@
<div class="col-md-2 text-right flip"> <div class="col-md-2 text-right flip">
{% if subev.presale_is_running and event.settings.event_list_availability %} {% if subev.presale_is_running and event.settings.event_list_availability %}
{% if subev.best_availability_state == 100 %} {% if subev.best_availability_state == 100 %}
<span class="label label-success">{% trans "Book now" %}</span> {% if subev.best_availability_is_low %}
<span class="label label-success-warning">{% trans "Few tickets left" %}</span>
{% else %}
<span class="label label-success">{% trans "Book now" %}</span>
{% endif %}
{% elif event.settings.waiting_list_enabled and subev.best_availability_state >= 0 %} {% elif event.settings.waiting_list_enabled and subev.best_availability_state >= 0 %}
<span class="label label-warning">{% trans "Waiting list" %}</span> <span class="label label-warning">{% trans "Waiting list" %}</span>
{% elif subev.best_availability_state == 20 %} {% elif subev.best_availability_state == 20 %}
<span class="label label-warning">{% trans "Reserved" %}</span> <span class="label label-danger">{% trans "Reserved" %}</span>
{% elif subev.best_availability_state < 20 %} {% elif subev.best_availability_state < 20 %}
{% if subev.has_paid_item %} {% if subev.has_paid_item %}
<span class="label label-danger">{% trans "Sold out" %}</span> <span class="label label-danger">{% trans "Sold out" %}</span>
@@ -27,7 +27,7 @@
<li><a class="event {% if event.continued %}continued{% endif %} {% spaceless %} <li><a class="event {% if event.continued %}continued{% endif %} {% spaceless %}
{% if event.event.presale_is_running and show_avail %} {% if event.event.presale_is_running and show_avail %}
{% if event.event.best_availability_state == 100 %} {% if event.event.best_availability_state == 100 %}
available available {% if event.event.best_availability_is_low %} low {% endif %}
{% elif event.event.settings.waiting_list_enabled and event.event.best_availability_state >= 0 %} {% elif event.event.settings.waiting_list_enabled and event.event.best_availability_state >= 0 %}
waitinglist waitinglist
{% elif event.event.best_availability_state == 20 %} {% elif event.event.best_availability_state == 20 %}
@@ -68,7 +68,11 @@
<span class="event-status"> <span class="event-status">
{% if event.event.presale_is_running and show_avail %} {% if event.event.presale_is_running and show_avail %}
{% if event.event.best_availability_state == 100 %} {% if event.event.best_availability_state == 100 %}
<span class="fa fa-ticket" aria-hidden="true"></span> {% trans "Book now" %} {% if event.event.best_availability_is_low %}
<span class="fa fa-exclamation-circle" aria-hidden="true"></span> {% trans "Few tickets left" %}
{% else %}
<span class="fa fa-ticket" aria-hidden="true"></span> {% trans "Book now" %}
{% endif %}
{% elif event.event.settings.waiting_list_enabled and event.event.best_availability_state >= 0 %} {% elif event.event.settings.waiting_list_enabled and event.event.best_availability_state >= 0 %}
<span class="fa fa-ticket" aria-hidden="true"></span> {% trans "Waiting list" %} <span class="fa fa-ticket" aria-hidden="true"></span> {% trans "Waiting list" %}
{% elif event.event.best_availability_state == 20 %} {% elif event.event.best_availability_state == 20 %}
@@ -40,7 +40,7 @@
<a class="event {% if event.continued %}continued{% endif %} {% spaceless %} <a class="event {% if event.continued %}continued{% endif %} {% spaceless %}
{% if event.event.presale_is_running and show_avail %} {% if event.event.presale_is_running and show_avail %}
{% if event.event.best_availability_state == 100 %} {% if event.event.best_availability_state == 100 %}
available available {% if event.event.best_availability_is_low %} low {% endif %}
{% elif event.event.settings.waiting_list_enabled and event.event.best_availability_state >= 0 %} {% elif event.event.settings.waiting_list_enabled and event.event.best_availability_state >= 0 %}
waitinglist waitinglist
{% elif event.event.best_availability_state == 20 %} {% elif event.event.best_availability_state == 20 %}
@@ -89,12 +89,16 @@
<span class="event-status"> <span class="event-status">
{% if event.event.presale_is_running and show_avail %} {% if event.event.presale_is_running and show_avail %}
{% if event.event.best_availability_state == 100 %} {% if event.event.best_availability_state == 100 %}
<span class="fa fa-ticket" aria-hidden="true"></span> {% trans "Book now" %} {% if event.event.best_availability_is_low %}
{% elif event.event.settings.waiting_list_enabled and event.event.best_availability_state >= 0 %} <span class="fa fa-exclamation-circle" aria-hidden="true"></span> {% trans "Few tickets left" %}
{% else %}
<span class="fa fa-ticket" aria-hidden="true"></span> {% trans "Book now" %}
{% endif %}
{% elif event.event.settings.waiting_list_enabled and event.event.best_availability_state >= 0 %}
<span class="fa fa-ticket" aria-hidden="true"></span> {% trans "Waiting list" %} <span class="fa fa-ticket" aria-hidden="true"></span> {% trans "Waiting list" %}
{% elif event.event.best_availability_state == 20 %} {% elif event.event.best_availability_state == 20 %}
<span class="fa fa-ticket" aria-hidden="true"></span> {% trans "Reserved" %} <span class="fa fa-ticket" aria-hidden="true"></span> {% trans "Reserved" %}
{% elif event.event.best_availability_state < 20 %} {% elif event.event.best_availability_state < 20 %}
{% if event.event.has_paid_item %} {% if event.event.has_paid_item %}
<span class="fa fa-ticket" aria-hidden="true"></span> {% trans "Sold out" %} <span class="fa fa-ticket" aria-hidden="true"></span> {% trans "Sold out" %}
{% else %} {% else %}
@@ -104,9 +108,9 @@
{% endif %} {% endif %}
{% elif event.event.presale_is_running %} {% elif event.event.presale_is_running %}
<span class="fa fa-ticket" aria-hidden="true"></span> {% trans "Book now" %} <span class="fa fa-ticket" aria-hidden="true"></span> {% trans "Book now" %}
{% elif event.event.presale_has_ended %} {% elif event.event.presale_has_ended %}
<span class="fa fa-ticket" aria-hidden="true"></span> {% trans "Sale over" %} <span class="fa fa-ticket" aria-hidden="true"></span> {% trans "Sale over" %}
{% elif event.event.settings.presale_start_show_date and event.event.presale_start %} {% elif event.event.settings.presale_start_show_date and event.event.presale_start %}
<span class="fa fa-ticket" aria-hidden="true"></span> <span class="fa fa-ticket" aria-hidden="true"></span>
{% blocktrans with start_date=event.event.presale_start|date:"SHORT_DATE_FORMAT" %} {% blocktrans with start_date=event.event.presale_start|date:"SHORT_DATE_FORMAT" %}
from {{ start_date }} from {{ start_date }}
@@ -12,7 +12,7 @@
<li><a class="event {% if event.continued %}continued{% endif %} {% spaceless %} <li><a class="event {% if event.continued %}continued{% endif %} {% spaceless %}
{% if event.event.presale_is_running and show_avail %} {% if event.event.presale_is_running and show_avail %}
{% if event.event.best_availability_state == 100 %} {% if event.event.best_availability_state == 100 %}
available available {% if event.event.best_availability_is_low %} low {% endif %}
{% elif event.event.settings.waiting_list_enabled and event.event.best_availability_state >= 0 %} {% elif event.event.settings.waiting_list_enabled and event.event.best_availability_state >= 0 %}
waitinglist waitinglist
{% elif event.event.best_availability_state == 20 %} {% elif event.event.best_availability_state == 20 %}
@@ -53,7 +53,11 @@
<span class="event-status"> <span class="event-status">
{% if event.event.presale_is_running and show_avail %} {% if event.event.presale_is_running and show_avail %}
{% if event.event.best_availability_state == 100 %} {% if event.event.best_availability_state == 100 %}
<span class="fa fa-ticket" aria-hidden="true"></span> {% trans "Book now" %} {% if event.event.best_availability_is_low %}
<span class="fa fa-exclamation-circle" aria-hidden="true"></span> {% trans "Few tickets left" %}
{% else %}
<span class="fa fa-ticket" aria-hidden="true"></span> {% trans "Book now" %}
{% endif %}
{% elif event.event.settings.waiting_list_enabled and event.event.best_availability_state >= 0 %} {% elif event.event.settings.waiting_list_enabled and event.event.best_availability_state >= 0 %}
<span class="fa fa-ticket" aria-hidden="true"></span> {% trans "Waiting list" %} <span class="fa fa-ticket" aria-hidden="true"></span> {% trans "Waiting list" %}
{% elif event.event.best_availability_state == 20 %} {% elif event.event.best_availability_state == 20 %}
@@ -81,11 +81,15 @@
<span class="label label-default">{% trans "Event series" %}</span> <span class="label label-default">{% trans "Event series" %}</span>
{% elif e.presale_is_running and request.organizer.settings.event_list_availability %} {% elif e.presale_is_running and request.organizer.settings.event_list_availability %}
{% if e.best_availability_state == 100 %} {% if e.best_availability_state == 100 %}
<span class="label label-success">{% trans "Book now" %}</span> {% if e.best_availability_is_low %}
<span class="label label-success-warning">{% trans "Few tickets left" %}</span>
{% else %}
<span class="label label-success">{% trans "Book now" %}</span>
{% endif %}
{% elif e.settings.waiting_list_enabled and e.best_availability_state >= 0 %} {% elif e.settings.waiting_list_enabled and e.best_availability_state >= 0 %}
<span class="label label-warning">{% trans "Waiting list" %}</span> <span class="label label-warning">{% trans "Waiting list" %}</span>
{% elif e.best_availability_state == 20 %} {% elif e.best_availability_state == 20 %}
<span class="label label-warning">{% trans "Reserved" %}</span> <span class="label label-danger">{% trans "Reserved" %}</span>
{% elif e.best_availability_state < 20 %} {% elif e.best_availability_state < 20 %}
{% if e.has_paid_item %} {% if e.has_paid_item %}
<span class="label label-danger">{% trans "Sold out" %}</span> <span class="label label-danger">{% trans "Sold out" %}</span>
+9 -4
View File
@@ -368,15 +368,20 @@ class WidgetAPIProductList(EventListMixin, View):
availability = {} availability = {}
if ev.presale_is_running and event.settings.event_list_availability: if ev.presale_is_running and event.settings.event_list_availability:
if ev.best_availability_state == Quota.AVAILABILITY_OK: if ev.best_availability_state == Quota.AVAILABILITY_OK:
availability['color'] = 'green' if ev.best_availability_is_low:
availability['text'] = gettext('Book now') availability['color'] = 'green'
availability['reason'] = 'ok' availability['text'] = gettext('Few tickets left')
availability['reason'] = 'low'
else:
availability['color'] = 'green'
availability['text'] = gettext('Book now')
availability['reason'] = 'ok'
elif event.settings.waiting_list_enabled and (ev.best_availability_state is not None and ev.best_availability_state >= 0): elif event.settings.waiting_list_enabled and (ev.best_availability_state is not None and ev.best_availability_state >= 0):
availability['color'] = 'orange' availability['color'] = 'orange'
availability['text'] = gettext('Waiting list') availability['text'] = gettext('Waiting list')
availability['reason'] = 'waitinglist' availability['reason'] = 'waitinglist'
elif ev.best_availability_state == Quota.AVAILABILITY_RESERVED: elif ev.best_availability_state == Quota.AVAILABILITY_RESERVED:
availability['color'] = 'orange' availability['color'] = 'red'
availability['text'] = gettext('Reserved') availability['text'] = gettext('Reserved')
availability['reason'] = 'reserved' availability['reason'] = 'reserved'
elif ev.best_availability_state is not None and ev.best_availability_state < Quota.AVAILABILITY_RESERVED: elif ev.best_availability_state is not None and ev.best_availability_state < Quota.AVAILABILITY_RESERVED:
@@ -58,15 +58,35 @@
} }
} }
&.available, { &.available {
background: lighten($brand-success, 48%); background: lighten($brand-success, 48%);
border-color: lighten($brand-success, 30%); border-color: lighten($brand-success, 30%);
border-left-color: $brand-success; border-left-color: $brand-success;
color: darken($brand-success, 12%); color: darken($brand-success, 12%);
&.low {
border-left-color: lighten($brand-warning, 12%);
}
&:hover { &:hover {
background: lighten($brand-success, 50%); background: lighten($brand-success, 50%);
border-color: $brand-success; border-color: $brand-success;
&.low {
border-left-color: $brand-warning;
}
}
}
&.waitinglist {
background: lighten($brand-warning, 41%);
border-color: lighten($brand-warning, 30%);
border-left-color: lighten($brand-warning, 12%);
color: #963;
&:hover {
background: lighten($brand-warning, 43%);
border-color: $brand-warning;
} }
} }
@@ -82,7 +102,6 @@
} }
} }
&.available > *:first-child, &.available > *:first-child,
&.continued > *:first-child, &.continued > *:first-child,
&.soon > *:first-child { &.soon > *:first-child {
@@ -121,3 +121,33 @@ footer {
padding-bottom: 0; padding-bottom: 0;
} }
} }
.label-success-warning {
@include label-variant($label-success-bg);
padding-left: 2.5em;
position: relative;
&::before {
font-family: FontAwesome;
text-rendering: auto;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
color: white;
content: $fa-var-exclamation;
background: $label-warning-bg;
display: block;
position: absolute;
left: 0;
top: 0;
height: 100%;
width: 2em;
padding-top: .5em;
text-align: center;
border-top-left-radius: .25em;
border-bottom-left-radius: .25em;
}
}
@@ -495,6 +495,12 @@
.pretix-widget-event-availability-red.pretix-widget-event-calendar-event { .pretix-widget-event-availability-red.pretix-widget-event-calendar-event {
background-color: $brand-danger; background-color: $brand-danger;
} }
.pretix-widget-event-availability-low .pretix-widget-event-list-entry-availability span {
border-left: 10px solid $brand-warning;
}
.pretix-widget-event-availability-low.pretix-widget-event-calendar-event {
border-right: 10px solid $brand-warning;
}
.pretix-widget-event-calendar { .pretix-widget-event-calendar {
padding-top: 10px; padding-top: 10px;
+43 -6
View File
@@ -2295,24 +2295,61 @@ class SubEventTest(TestCase):
@classscope(attr='organizer') @classscope(attr='organizer')
def test_best_availability(self): def test_best_availability(self):
q = Quota.objects.create(event=self.event, name='Quota', size=0,
subevent=self.se)
item = Item.objects.create(event=self.event, name='Early-bird ticket', default_price=0, active=True) item = Item.objects.create(event=self.event, name='Early-bird ticket', default_price=0, active=True)
o = Order.objects.create(
code='FOO', event=self.event, email='dummy@dummy.test',
status=Order.STATUS_PAID,
datetime=now(), expires=now() + timedelta(days=10),
total=Decimal("30"), locale='en'
)
OrderPosition.objects.create(
order=o,
item=item,
subevent=self.se,
variation=None,
price=Decimal("12"),
)
self.event.settings.low_availability_percentage = 60
# 1 quota - 1 item
q = Quota.objects.create(event=self.event, name='Quota', size=1,
subevent=self.se)
q.items.add(item) q.items.add(item)
obj = SubEvent.annotated(SubEvent.objects).first() obj = SubEvent.annotated(SubEvent.objects).first()
assert len(obj.active_quotas) == 1 assert len(obj.active_quotas) == 1
assert obj.best_availability_state == Quota.AVAILABILITY_GONE assert obj.best_availability == (Quota.AVAILABILITY_GONE, 0, 1)
q2 = Quota.objects.create(event=self.event, name='Quota 2', size=1,
# 2 quotas - 1 item. Lowest quota wins.
q2 = Quota.objects.create(event=self.event, name='Quota 2', size=2,
subevent=self.se) subevent=self.se)
q2.items.add(item) q2.items.add(item)
obj = SubEvent.annotated(SubEvent.objects).first() obj = SubEvent.annotated(SubEvent.objects).first()
assert len(obj.active_quotas) == 2 assert len(obj.active_quotas) == 2
assert obj.best_availability_state == Quota.AVAILABILITY_GONE assert obj.best_availability == (Quota.AVAILABILITY_GONE, 0, 1)
# 2 quotas - 2 items. Higher quota wins since second item is only connected to second quota.
item2 = Item.objects.create(event=self.event, name='Regular ticket', default_price=10, active=True) item2 = Item.objects.create(event=self.event, name='Regular ticket', default_price=10, active=True)
q2.items.add(item2) q2.items.add(item2)
obj = SubEvent.annotated(SubEvent.objects).first() obj = SubEvent.annotated(SubEvent.objects).first()
assert len(obj.active_quotas) == 2 assert len(obj.active_quotas) == 2
assert obj.best_availability_state == Quota.AVAILABILITY_OK assert obj.best_availability == (Quota.AVAILABILITY_OK, 1, 2)
assert obj.best_availability_is_low
# 1 quota - 2 items. Quota is not counted twice!
q.size = 10
q.save()
q2.delete()
obj = SubEvent.annotated(SubEvent.objects).first()
assert len(obj.active_quotas) == 1
assert obj.best_availability == (Quota.AVAILABILITY_OK, 9, 10)
assert not obj.best_availability_is_low
# Unlimited quota
q.size = None
q.save()
obj = SubEvent.annotated(SubEvent.objects).first()
assert obj.best_availability == (Quota.AVAILABILITY_OK, None, None)
assert not obj.best_availability_is_low
class CachedFileTestCase(TestCase): class CachedFileTestCase(TestCase):