mirror of
https://github.com/pretix/pretix.git
synced 2026-05-03 14:54:04 +00:00
Co-authored-by: Richard Schreiber <schreiber@rami.io>
This commit is contained in:
@@ -36,7 +36,7 @@ import logging
|
||||
import os
|
||||
import string
|
||||
import uuid
|
||||
from collections import OrderedDict
|
||||
from collections import Counter, OrderedDict, defaultdict
|
||||
from datetime import datetime, time, timedelta
|
||||
from operator import attrgetter
|
||||
from urllib.parse import urljoin
|
||||
@@ -340,64 +340,104 @@ class EventMixin:
|
||||
)
|
||||
)
|
||||
|
||||
@cached_property
|
||||
@property
|
||||
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
|
||||
|
||||
if not hasattr(self, 'active_quotas'):
|
||||
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
|
||||
items_disabled = set(self.disabled_items.split(","))
|
||||
else:
|
||||
items_disabled = set()
|
||||
|
||||
if hasattr(self, 'disabled_vars'): # SubEventItemVariation
|
||||
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', {})
|
||||
quotas_for_item = defaultdict(list)
|
||||
quotas_for_variation = defaultdict(list)
|
||||
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:
|
||||
items_available.update(q.active_items.split(","))
|
||||
if q.active_variations:
|
||||
vars_available.update(q.active_variations.split(","))
|
||||
elif res[0] == Quota.AVAILABILITY_RESERVED:
|
||||
if q.active_items:
|
||||
items_reserved.update(q.active_items.split(","))
|
||||
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(","))
|
||||
if q.active_items:
|
||||
for item_id in q.active_items.split(","):
|
||||
if item_id not in items_disabled:
|
||||
quotas_for_item[item_id].append(q)
|
||||
if q.active_variations:
|
||||
for var_id in q.active_variations.split(","):
|
||||
if var_id not in vars_disabled:
|
||||
quotas_for_variation[var_id].append(q)
|
||||
|
||||
items_available -= items_disabled
|
||||
items_reserved -= items_disabled
|
||||
items_gone -= items_disabled
|
||||
vars_available -= vars_disabled
|
||||
vars_reserved -= vars_disabled
|
||||
vars_gone -= vars_disabled
|
||||
if not self.active_quotas or (not quotas_for_item and not quotas_for_variation):
|
||||
# No item is enabled for this event, treat the event as "unknown"
|
||||
return None, None, None
|
||||
|
||||
if not self.active_quotas or (
|
||||
not items_available and not items_reserved and not items_gone and not vars_gone and not vars_available and not vars_reserved
|
||||
):
|
||||
return None
|
||||
# We iterate over all items and variations and keep track of
|
||||
# - `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
|
||||
# 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:
|
||||
return Quota.AVAILABILITY_OK
|
||||
if items_reserved - items_gone or vars_reserved - vars_gone:
|
||||
return Quota.AVAILABILITY_RESERVED
|
||||
return Quota.AVAILABILITY_GONE
|
||||
if worst_state_for_ticket == 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)
|
||||
num_tickets_found += availability_of_this
|
||||
for q in quota_list:
|
||||
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):
|
||||
qs_annotated = self._seats(ignore_voucher=ignore_voucher)
|
||||
@@ -591,6 +631,7 @@ class Event(EventMixin, LoggedModel):
|
||||
self.settings.invoice_email_attachment = True
|
||||
self.settings.name_scheme = 'given_family'
|
||||
self.settings.payment_banktransfer_invoice_immediately = True
|
||||
self.settings.low_availability_percentage = 10
|
||||
|
||||
@property
|
||||
def social_image(self):
|
||||
|
||||
@@ -1308,6 +1308,25 @@ DEFAULTS = {
|
||||
"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': {
|
||||
'default': 'True',
|
||||
'type': bool,
|
||||
|
||||
@@ -507,6 +507,7 @@ class EventSettingsForm(SettingsForm):
|
||||
'meta_noindex',
|
||||
'redirect_to_checkout_directly',
|
||||
'frontpage_subevent_ordering',
|
||||
'low_availability_percentage',
|
||||
'event_list_type',
|
||||
'event_list_available_only',
|
||||
'frontpage_text',
|
||||
|
||||
@@ -314,7 +314,8 @@
|
||||
{% if sform.event_list_available_only %}
|
||||
{% bootstrap_field sform.event_list_available_only layout="control" %}
|
||||
{% endif %}
|
||||
|
||||
{% bootstrap_field sform.low_availability_percentage layout="control" %}
|
||||
|
||||
{% url "control:organizer.edit" organizer=request.organizer.slug as org_url %}
|
||||
{% propagated request.event org_url "meta_noindex" %}
|
||||
{% bootstrap_field sform.meta_noindex layout="control" %}
|
||||
|
||||
@@ -21,11 +21,15 @@
|
||||
<div class="col-md-2 text-right flip">
|
||||
{% if subev.presale_is_running and event.settings.event_list_availability %}
|
||||
{% 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 %}
|
||||
<span class="label label-warning">{% trans "Waiting list" %}</span>
|
||||
{% 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 %}
|
||||
{% if subev.has_paid_item %}
|
||||
<span class="label label-danger">{% trans "Sold out" %}</span>
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
<li><a class="event {% if event.continued %}continued{% endif %} {% spaceless %}
|
||||
{% if event.event.presale_is_running and show_avail %}
|
||||
{% 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 %}
|
||||
waitinglist
|
||||
{% elif event.event.best_availability_state == 20 %}
|
||||
@@ -68,7 +68,11 @@
|
||||
<span class="event-status">
|
||||
{% if event.event.presale_is_running and show_avail %}
|
||||
{% 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 %}
|
||||
<span class="fa fa-ticket" aria-hidden="true"></span> {% trans "Waiting list" %}
|
||||
{% elif event.event.best_availability_state == 20 %}
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
<a class="event {% if event.continued %}continued{% endif %} {% spaceless %}
|
||||
{% if event.event.presale_is_running and show_avail %}
|
||||
{% 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 %}
|
||||
waitinglist
|
||||
{% elif event.event.best_availability_state == 20 %}
|
||||
@@ -89,12 +89,16 @@
|
||||
<span class="event-status">
|
||||
{% if event.event.presale_is_running and show_avail %}
|
||||
{% if event.event.best_availability_state == 100 %}
|
||||
<span class="fa fa-ticket" aria-hidden="true"></span> {% trans "Book now" %}
|
||||
{% elif event.event.settings.waiting_list_enabled and event.event.best_availability_state >= 0 %}
|
||||
{% 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 %}
|
||||
<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" %}
|
||||
{% elif event.event.best_availability_state < 20 %}
|
||||
{% elif event.event.best_availability_state < 20 %}
|
||||
{% if event.event.has_paid_item %}
|
||||
<span class="fa fa-ticket" aria-hidden="true"></span> {% trans "Sold out" %}
|
||||
{% else %}
|
||||
@@ -104,9 +108,9 @@
|
||||
{% endif %}
|
||||
{% elif event.event.presale_is_running %}
|
||||
<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" %}
|
||||
{% 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>
|
||||
{% blocktrans with start_date=event.event.presale_start|date:"SHORT_DATE_FORMAT" %}
|
||||
from {{ start_date }}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<li><a class="event {% if event.continued %}continued{% endif %} {% spaceless %}
|
||||
{% if event.event.presale_is_running and show_avail %}
|
||||
{% 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 %}
|
||||
waitinglist
|
||||
{% elif event.event.best_availability_state == 20 %}
|
||||
@@ -53,7 +53,11 @@
|
||||
<span class="event-status">
|
||||
{% if event.event.presale_is_running and show_avail %}
|
||||
{% 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 %}
|
||||
<span class="fa fa-ticket" aria-hidden="true"></span> {% trans "Waiting list" %}
|
||||
{% elif event.event.best_availability_state == 20 %}
|
||||
|
||||
@@ -81,11 +81,15 @@
|
||||
<span class="label label-default">{% trans "Event series" %}</span>
|
||||
{% elif e.presale_is_running and request.organizer.settings.event_list_availability %}
|
||||
{% 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 %}
|
||||
<span class="label label-warning">{% trans "Waiting list" %}</span>
|
||||
{% 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 %}
|
||||
{% if e.has_paid_item %}
|
||||
<span class="label label-danger">{% trans "Sold out" %}</span>
|
||||
|
||||
@@ -368,15 +368,20 @@ class WidgetAPIProductList(EventListMixin, View):
|
||||
availability = {}
|
||||
if ev.presale_is_running and event.settings.event_list_availability:
|
||||
if ev.best_availability_state == Quota.AVAILABILITY_OK:
|
||||
availability['color'] = 'green'
|
||||
availability['text'] = gettext('Book now')
|
||||
availability['reason'] = 'ok'
|
||||
if ev.best_availability_is_low:
|
||||
availability['color'] = 'green'
|
||||
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):
|
||||
availability['color'] = 'orange'
|
||||
availability['text'] = gettext('Waiting list')
|
||||
availability['reason'] = 'waitinglist'
|
||||
elif ev.best_availability_state == Quota.AVAILABILITY_RESERVED:
|
||||
availability['color'] = 'orange'
|
||||
availability['color'] = 'red'
|
||||
availability['text'] = gettext('Reserved')
|
||||
availability['reason'] = '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%);
|
||||
border-color: lighten($brand-success, 30%);
|
||||
border-left-color: $brand-success;
|
||||
color: darken($brand-success, 12%);
|
||||
|
||||
&.low {
|
||||
border-left-color: lighten($brand-warning, 12%);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: lighten($brand-success, 50%);
|
||||
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,
|
||||
&.continued > *:first-child,
|
||||
&.soon > *:first-child {
|
||||
|
||||
@@ -121,3 +121,33 @@ footer {
|
||||
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 {
|
||||
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 {
|
||||
padding-top: 10px;
|
||||
|
||||
@@ -2295,24 +2295,61 @@ class SubEventTest(TestCase):
|
||||
|
||||
@classscope(attr='organizer')
|
||||
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)
|
||||
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)
|
||||
obj = SubEvent.annotated(SubEvent.objects).first()
|
||||
assert len(obj.active_quotas) == 1
|
||||
assert obj.best_availability_state == Quota.AVAILABILITY_GONE
|
||||
q2 = Quota.objects.create(event=self.event, name='Quota 2', size=1,
|
||||
assert obj.best_availability == (Quota.AVAILABILITY_GONE, 0, 1)
|
||||
|
||||
# 2 quotas - 1 item. Lowest quota wins.
|
||||
q2 = Quota.objects.create(event=self.event, name='Quota 2', size=2,
|
||||
subevent=self.se)
|
||||
q2.items.add(item)
|
||||
obj = SubEvent.annotated(SubEvent.objects).first()
|
||||
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)
|
||||
q2.items.add(item2)
|
||||
obj = SubEvent.annotated(SubEvent.objects).first()
|
||||
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):
|
||||
|
||||
Reference in New Issue
Block a user