forked from CGM_Public/pretix_original
Co-authored-by: Richard Schreiber <schreiber@rami.io>
This commit is contained in:
co-authored by
Richard Schreiber
parent
aeb5c52bfe
commit
8bba1a2ea6
@@ -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):
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,6 +314,7 @@
|
|||||||
{% 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" %}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
Reference in New Issue
Block a user