Fix #785 -- Show availability in (sub)event list (#1112)

This commit is contained in:
Raphael Michel
2018-12-11 13:59:49 +01:00
committed by GitHub
parent eed220f14a
commit d267dfc682
17 changed files with 421 additions and 45 deletions

View File

@@ -11,7 +11,7 @@ from django.core.files.storage import default_storage
from django.core.mail import get_connection
from django.core.validators import RegexValidator
from django.db import models
from django.db.models import Exists, OuterRef, Q
from django.db.models import Exists, OuterRef, Prefetch, Q, Subquery
from django.template.defaultfilters import date as _date
from django.utils.crypto import get_random_string
from django.utils.functional import cached_property
@@ -22,6 +22,7 @@ from i18nfield.fields import I18nCharField, I18nTextField
from pretix.base.models.base import LoggedModel
from pretix.base.reldate import RelativeDateWrapper
from pretix.base.validators import EventSlugBlacklistValidator
from pretix.helpers.database import GroupConcat
from pretix.helpers.daterange import daterange
from pretix.helpers.json import safe_string
@@ -159,6 +160,79 @@ class EventMixin:
return safe_string(json.dumps(eventdict))
@classmethod
def annotated(cls, qs, channel='web'):
from pretix.base.models import Item, ItemVariation, Quota
sq_active_item = Item.objects.filter_available(channel=channel).filter(
Q(variations__isnull=True)
& Q(quotas__pk=OuterRef('pk'))
).order_by().values_list('quotas__pk').annotate(
items=GroupConcat('pk', delimiter=',')
).values('items')
sq_active_variation = ItemVariation.objects.filter(
Q(active=True)
& Q(item__active=True)
& Q(Q(item__available_from__isnull=True) | Q(item__available_from__lte=now()))
& Q(Q(item__available_until__isnull=True) | Q(item__available_until__gte=now()))
& Q(Q(item__category__isnull=True) | Q(item__category__is_addon=False))
& Q(item__sales_channels__contains=channel)
& Q(item__hide_without_voucher=False) # TODO: does this make sense?
& Q(quotas__pk=OuterRef('pk'))
).order_by().values_list('quotas__pk').annotate(
items=GroupConcat('pk', delimiter=',')
).values('items')
return qs.prefetch_related(
Prefetch(
'quotas',
to_attr='active_quotas',
queryset=Quota.objects.annotate(
active_items=Subquery(sq_active_item, output_field=models.TextField()),
active_variations=Subquery(sq_active_variation, output_field=models.TextField()),
).exclude(
Q(active_items="") & Q(active_variations="")
)
)
)
@cached_property
def best_availability_state(self):
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()
for q in self.active_quotas:
res = 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_available.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 not self.active_quotas:
return 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
@settings_hierarkey.add(parent_field='organizer', cache_namespace='event')
class Event(EventMixin, LoggedModel):
@@ -572,8 +646,10 @@ class Event(EventMixin, LoggedModel):
)
).order_by('date_from', 'name')
@property
def subevent_list_subevents(self):
def subevents_annotated(self, channel):
return SubEvent.annotated(self.subevents, channel)
def subevents_sorted(self, queryset):
ordering = self.settings.get('frontpage_subevent_ordering', default='date_ascending', as_type=str)
orderfields = {
'date_ascending': ('date_from', 'name'),
@@ -581,7 +657,7 @@ class Event(EventMixin, LoggedModel):
'name_ascending': ('name', 'date_from'),
'name_descending': ('-name', 'date_from'),
}[ordering]
subevs = self.subevents.filter(
subevs = queryset.filter(
Q(active=True) & (
Q(Q(date_to__isnull=True) & Q(date_from__gte=now()))
| Q(date_to__gte=now())

View File

@@ -153,6 +153,30 @@ class SubEventItemVariation(models.Model):
self.subevent.event.cache.clear()
class ItemQuerySet(models.QuerySet):
def filter_available(self, channel='web', voucher=None, allow_addons=False):
q = (
# IMPORTANT: If this is updated, also update the ItemVariation query
# in models/event.py: EventMixin.annotated()
Q(active=True)
& Q(Q(available_from__isnull=True) | Q(available_from__lte=now()))
& Q(Q(available_until__isnull=True) | Q(available_until__gte=now()))
& Q(sales_channels__contains=channel)
)
if not allow_addons:
q &= Q(Q(category__isnull=True) | Q(category__is_addon=False))
qs = self.filter(q)
vouchq = Q(hide_without_voucher=False)
if voucher:
if voucher.item_id:
vouchq |= Q(pk=voucher.item_id)
qs = qs.filter(pk=voucher.item_id)
elif voucher.quota_id:
qs = qs.filter(quotas__in=[voucher.quota_id])
return qs.filter(vouchq)
class Item(LoggedModel):
"""
An item is a thing which can be sold. It belongs to an event and may or may not belong to a category.
@@ -200,6 +224,8 @@ class Item(LoggedModel):
:type sales_channels: bool
"""
objects = ItemQuerySet.as_manager()
event = models.ForeignKey(
Event,
on_delete=models.PROTECT,
@@ -930,6 +956,16 @@ class Quota(LoggedModel):
:type size: int
:param items: The set of :py:class:`Item` objects this quota applies to
:param variations: The set of :py:class:`ItemVariation` objects this quota applies to
This model keeps a cache of the quota availability that is used in places where up-to-date
data is not important. This cache might be out of date even though a more recent quota was
calculated. This is intentional to keep database writes low. Currently, the cached values
are written whenever the quota is being calculated throughout the system and the cache is
at least 120 seconds old or if the new value is qualitatively "better" than the cached one
(i.e. more free quota).
There's also a cronjob that refreshes the cache of every quota if there is any log entry in
the event that is newer than the quota's cached time.
"""
AVAILABILITY_GONE = 0
@@ -1012,6 +1048,15 @@ class Quota(LoggedModel):
This method is used to determine whether Items or ItemVariations belonging
to this quota should currently be available for sale.
:param count_waitinglist: Whether or not take waiting list reservations into account. Defaults
to ``True``.
:param _cache: A dictionary mapping quota IDs to availabilities. If this quota is already
contained in that dictionary, this value will be used. Otherwise, the dict
will be populated accordingly.
:param allow_cache: Allow for values to be returned from the longer-term cache, see also
the documentation of this model class. Only works if ``count_waitinglist`` is
set to ``True``.
:returns: a tuple where the first entry is one of the ``Quota.AVAILABILITY_`` constants
and the second is the number of available tickets.
"""
@@ -1027,7 +1072,10 @@ class Quota(LoggedModel):
res = self._availability(now_dt, count_waitinglist)
self.event.cache.delete('item_quota_cache')
if count_waitinglist and not self.cache_is_hot(now_dt):
rewrite_cache = count_waitinglist and (
not self.cache_is_hot(now_dt) or res[0] > self.cached_availability_state
)
if rewrite_cache:
self.cached_availability_state = res[0]
self.cached_availability_number = res[1]
self.cached_availability_time = now_dt

View File

@@ -1,6 +1,9 @@
from datetime import timedelta
from django.db import models
from django.db.models import F, Max, OuterRef, Q, Subquery
from django.dispatch import receiver
from django.utils.timezone import now
from pretix.base.models import LogEntry, Quota
from pretix.celery_app import app
@@ -26,7 +29,8 @@ def refresh_quota_caches():
last_activity=Subquery(last_activity, output_field=models.DateTimeField())
).filter(
Q(cached_availability_time__isnull=True) |
Q(cached_availability_time__lt=F('last_activity'))
Q(cached_availability_time__lt=F('last_activity')) |
Q(cached_availability_time__lt=now() - timedelta(hours=2), last_activity__gt=now() - timedelta(days=7))
)
for q in quotas:
q.availability()

View File

@@ -204,6 +204,10 @@ DEFAULTS = {
'default': 'True',
'type': bool
},
'event_list_availability': {
'default': 'True',
'type': bool
},
'event_list_type': {
'default': 'list',
'type': str

View File

@@ -238,6 +238,13 @@ class OrganizerDisplaySettingsForm(SettingsForm):
('calendar', _('Calendar'))
)
)
event_list_availability = forms.BooleanField(
label=_('Show availability in event overviews'),
help_text=_('If checked, the list of events will show if events are sold out. This might '
'make for longer page loading times if you have lots of events and the shown status might be out '
'of date for up to two minutes.'),
required=False
)
organizer_link_back = forms.BooleanField(
label=_('Link back to organizer overview on all event pages'),
required=False

View File

@@ -12,6 +12,7 @@
{% bootstrap_field form.organizer_logo_image layout="control" %}
{% bootstrap_field form.organizer_homepage_text layout="control" %}
{% bootstrap_field form.event_list_type layout="control" %}
{% bootstrap_field form.event_list_availability layout="control" %}
{% bootstrap_field form.organizer_link_back layout="control" %}
</fieldset>
<fieldset>

View File

@@ -1,6 +1,7 @@
import contextlib
from django.db import transaction
from django.db.models import Aggregate
from django.db.models.expressions import OrderBy
@@ -57,3 +58,21 @@ class FixedOrderBy(OrderBy):
template = template or self.template
params = params * template.count('%(expression)s')
return (template % placeholders).rstrip(), params
class GroupConcat(Aggregate):
function = 'group_concat'
template = '%(function)s(%(field)s, "%(separator)s")'
def __init__(self, *expressions, **extra):
if 'separator' not in extra:
# For PostgreSQL separator is an obligatory
extra.update({'separator': ','})
super().__init__(*expressions, **extra)
def as_postgresql(self, compiler, connection):
return super().as_sql(
compiler, connection,
function='string_agg',
template="%(function)s(%(field)s::text, '%(separator)s')",
)

View File

@@ -2,10 +2,9 @@ from itertools import chain
from django import forms
from django.core.exceptions import ValidationError
from django.db.models import Count, Prefetch, Q
from django.db.models import Count, Prefetch
from django.utils.encoding import force_text
from django.utils.formats import number_format
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _
from pretix.base.forms.questions import (
@@ -204,12 +203,9 @@ class AddOnsForm(forms.Form):
ckey = '{}-{}'.format(subevent.pk if subevent else 0, category.pk)
if ckey not in item_cache:
# Get all items to possibly show
items = category.items.filter(
Q(active=True)
& Q(Q(available_from__isnull=True) | Q(available_from__lte=now()))
& Q(Q(available_until__isnull=True) | Q(available_until__gte=now()))
& Q(hide_without_voucher=False)
& Q(sales_channels__contains=self.sales_channel)
items = category.items.filter_available(
channel=self.sales_channel,
allow_addons=True
).select_related('tax_rule').prefetch_related(
Prefetch('quotas',
to_attr='_subevent_quotas',

View File

@@ -38,4 +38,4 @@
</div>
</div>
</form>
{% include "pretixpresale/fragment_calendar.html" %}
{% include "pretixpresale/fragment_calendar.html" with show_avail=event.settings.event_list_availability %}

View File

@@ -1,6 +1,6 @@
{% load i18n %}
{% load eventurl %}
{% for subev in event.subevent_list_subevents %}
{% for subev in subevent_list %}
<a href="{% eventurl event "presale:event.index" subevent=subev.id cart_namespace=cart_namespace %}"
class="subevent-row">
<div class="row">
@@ -16,7 +16,17 @@
{% endif %}
</div>
<div class="col-md-2 text-right">
{% if subev.presale_is_running %}
{% if subev.presale_is_running and event.settings.event_list_availability %}
{% if subev.best_availability_state == 100 %}
<span class="label label-success">{% trans "Tickets on sale" %}</span>
{% 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>
{% elif subev.best_availability_state < 20 %}
<span class="label label-danger">{% trans "Sold out" %}</span>
{% endif %}
{% elif subev.presale_is_running %}
<span class="label label-success">{% trans "Tickets on sale" %}</span>
{% elif subev.presale_has_ended %}
<span class="label label-danger">{% trans "Sale over" %}</span>

View File

@@ -38,7 +38,17 @@
</span>
{% endif %}
<span class="event-status">
{% if event.event.presale_is_running %}
{% if event.event.presale_is_running and show_avail %}
{% if event.event.best_availability_state == 100 %}
<span class="fa fa-ticket"></span> {% trans "Tickets on sale" %}
{% elif event.event.settings.waiting_list_enabled and event.event.best_availability_state >= 0 %}
<span class="fa fa-ticket"></span> {% trans "Waiting list" %}
{% elif event.event.best_availability_state == 20 %}
<span class="fa fa-ticket"></span> {% trans "Reserved" %}
{% elif event.event.best_availability_state < 20 %}
<span class="fa fa-ticket"></span> {% trans "Sold out" %}
{% endif %}
{% elif event.event.presale_is_running %}
<span class="fa fa-ticket"></span> {% trans "Tickets on sale" %}
{% elif event.event.presale_has_ended %}
<span class="fa fa-ticket"></span> {% trans "Sale over" %}

View File

@@ -64,7 +64,7 @@
</div>
</div>
</form>
{% include "pretixpresale/fragment_calendar.html" %}
{% include "pretixpresale/fragment_calendar.html" with show_avail=request.organizer.settings.event_list_availability %}
{% if multiple_timezones %}
<div class="alert alert-info">

View File

@@ -45,19 +45,50 @@
<tr>
<th>{% trans "Name" %}</th>
<th>{% trans "Date" %}</th>
<th>{% trans "Status" %}</th>
<th></th>
</tr>
</thead>
<tbody>
{% for e in events %}{% eventurl e "presale:event.index" as url %}
<tr>
<td><a href="{{ url }}">{{ e.name }}</a></td>
<td>
<a href="{{ url }}">{{ e.name }}</a>
</td>
<td>
{{ e.daterange|default:e.get_date_range_display }}
</td>
<td>
{% if e.has_subevents %}
<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 "Tickets on sale" %}</span>
{% 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>
{% elif e.best_availability_state < 20 %}
<span class="label label-danger">{% trans "Sold out" %}</span>
{% endif %}
{% elif e.presale_is_running %}
<span class="label label-success">{% trans "Tickets on sale" %}</span>
{% elif e.presale_has_ended %}
<span class="label label-danger">{% trans "Sale over" %}</span>
{% elif e.settings.presale_start_show_date %}
<span class="label label-warning">
{% blocktrans trimmed with date=subev.presale_start|date:"SHORT_DATE_FORMAT" %}
Sale starts {{ date }}
{% endblocktrans %}
</span>
{% else %}
<span class="label label-warning">{% trans "Not yet on sale" %}</span>
{% endif %}
</td>
<td class="text-right">
<a class="btn btn-primary" href="{{ url }}">
{% if e.presale_is_running %}{% trans "Buy tickets" %}
{% if e.has_subevents %}{% trans "Buy tickets" %}
{% elif e.presale_is_running and e.best_availability_state == 100 %}{% trans "Buy tickets" %}
{% else %}{% trans "More info" %}
{% endif %}
</a>

View File

@@ -7,7 +7,7 @@ from importlib import import_module
import pytz
from django.conf import settings
from django.core.exceptions import PermissionDenied
from django.db.models import Count, Prefetch, Q
from django.db.models import Count, Prefetch
from django.http import Http404, HttpResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.utils.decorators import method_decorator
@@ -48,23 +48,7 @@ def item_group_by_category(items):
def get_grouped_items(event, subevent=None, voucher=None, channel='web'):
items = event.items.all().filter(
Q(active=True)
& Q(Q(available_from__isnull=True) | Q(available_from__lte=now()))
& Q(Q(available_until__isnull=True) | Q(available_until__gte=now()))
& Q(Q(category__isnull=True) | Q(category__is_addon=False))
& Q(sales_channels__contains=channel)
)
vouchq = Q(hide_without_voucher=False)
if voucher:
if voucher.item_id:
vouchq |= Q(pk=voucher.item_id)
items = items.filter(pk=voucher.item_id)
elif voucher.quota_id:
items = items.filter(quotas__in=[voucher.quota_id])
items = items.filter(vouchq).select_related(
items = event.items.filter_available(channel=channel, voucher=voucher).select_related(
'category', 'tax_rule', # for re-grouping
).prefetch_related(
Prefetch('quotas',
@@ -291,12 +275,19 @@ class EventIndex(EventViewMixin, CartMixin, TemplateView):
context['after'] = after
ebd = defaultdict(list)
add_subevents_for_days(self.request.event.subevents.all(), before, after, ebd, set(), self.request.event,
kwargs.get('cart_namespace'))
add_subevents_for_days(
self.request.event.subevents_annotated(self.request.sales_channel),
before, after, ebd, set(), self.request.event,
kwargs.get('cart_namespace')
)
context['weeks'] = weeks_for_template(ebd, self.year, self.month)
context['months'] = [date(self.year, i + 1, 1) for i in range(12)]
context['years'] = range(now().year - 2, now().year + 3)
else:
context['subevent_list'] = self.request.event.subevents_sorted(
self.request.event.subevents_annotated(self.request.sales_channel)
)
context['show_cart'] = (
context['cart']['positions'] and (

View File

@@ -128,7 +128,7 @@ class OrganizerIndex(OrganizerViewMixin, ListView):
).annotate(
order_from=Coalesce('min_from', 'date_from'),
).order_by('order_from')
qs = filter_qs_by_attr(qs, self.request)
qs = Event.annotated(filter_qs_by_attr(qs, self.request))
return qs
def get_context_data(self, **kwargs):
@@ -314,14 +314,14 @@ class CalendarView(OrganizerViewMixin, TemplateView):
def _events_by_day(self, before, after):
ebd = defaultdict(list)
timezones = set()
add_events_for_days(self.request, self.request.organizer.events, before, after, ebd, timezones)
add_subevents_for_days(filter_qs_by_attr(SubEvent.objects.filter(
add_events_for_days(self.request, Event.annotated(self.request.organizer.events, 'web'), before, after, ebd, timezones)
add_subevents_for_days(filter_qs_by_attr(SubEvent.annotated(SubEvent.objects.filter(
event__organizer=self.request.organizer,
event__is_public=True,
event__live=True,
).prefetch_related(
'event___settings_objects', 'event__organizer___settings_objects'
), self.request), before, after, ebd, timezones)
)), self.request), before, after, ebd, timezones)
self._multiple_timezones = len(timezones) > 1
return ebd

View File

@@ -1085,6 +1085,51 @@ class ItemTest(TestCase):
i.active = False
assert not i.is_available()
def test_availability_filter(self):
i = Item.objects.create(
event=self.event, name="Ticket", default_price=23,
active=True, available_until=now() + timedelta(days=1),
)
assert Item.objects.filter_available().exists()
assert not Item.objects.filter_available(channel='foo').exists()
i.available_from = now() - timedelta(days=1)
i.save()
assert Item.objects.filter_available().exists()
i.available_from = now() + timedelta(days=1)
i.available_until = None
i.save()
assert not Item.objects.filter_available().exists()
i.available_from = None
i.available_until = now() - timedelta(days=1)
i.save()
assert not Item.objects.filter_available().exists()
i.available_from = None
i.available_until = None
i.save()
assert Item.objects.filter_available().exists()
i.active = False
i.save()
assert not Item.objects.filter_available().exists()
cat = ItemCategory.objects.create(
event=self.event, name='Foo', is_addon=True
)
i.active = True
i.category = cat
i.save()
assert not Item.objects.filter_available().exists()
assert Item.objects.filter_available(allow_addons=True).exists()
i.category = None
i.hide_without_voucher = True
i.save()
v = Voucher.objects.create(
event=self.event, item=i,
)
assert not Item.objects.filter_available().exists()
assert Item.objects.filter_available(voucher=v).exists()
class EventTest(TestCase):
@classmethod
@@ -1207,6 +1252,100 @@ class EventTest(TestCase):
assert event.presale_has_ended
assert not event.presale_is_running
def test_active_quotas_annotation(self):
event = Event.objects.create(
organizer=self.organizer, name='Download', slug='download',
date_from=now()
)
q = Quota.objects.create(event=event, name='Quota', size=2)
item = Item.objects.create(event=event, name='Early-bird ticket', default_price=0, active=True)
item2 = Item.objects.create(event=event, name='Early-bird ticket', default_price=0, active=False)
q.items.add(item)
q.items.add(item2)
assert Event.annotated(Event.objects).first().active_quotas == [q]
assert Event.annotated(Event.objects, 'foo').first().active_quotas == []
def test_active_quotas_annotation_product_inactive(self):
event = Event.objects.create(
organizer=self.organizer, name='Download', slug='download',
date_from=now()
)
q = Quota.objects.create(event=event, name='Quota', size=2)
item = Item.objects.create(event=event, name='Early-bird ticket', default_price=0, active=False)
q.items.add(item)
assert Event.annotated(Event.objects).first().active_quotas == []
def test_active_quotas_annotation_product_addon(self):
event = Event.objects.create(
organizer=self.organizer, name='Download', slug='download',
date_from=now()
)
q = Quota.objects.create(event=event, name='Quota', size=2)
item = Item.objects.create(event=event, name='Early-bird ticket', default_price=0, active=True)
cat = ItemCategory.objects.create(
event=event, name='Foo', is_addon=True
)
item.category = cat
item.save()
q.items.add(item)
assert Event.annotated(Event.objects).first().active_quotas == []
def test_active_quotas_annotation_product_unavailable(self):
event = Event.objects.create(
organizer=self.organizer, name='Download', slug='download',
date_from=now()
)
q = Quota.objects.create(event=event, name='Quota', size=2)
item = Item.objects.create(event=event, name='Early-bird ticket', default_price=0, active=True, available_until=now() - timedelta(days=1))
q.items.add(item)
assert Event.annotated(Event.objects).first().active_quotas == []
def test_active_quotas_annotation_variation_not_in_quota(self):
event = Event.objects.create(
organizer=self.organizer, name='Download', slug='download',
date_from=now()
)
q = Quota.objects.create(event=event, name='Quota', size=2)
item = Item.objects.create(event=event, name='Early-bird ticket', default_price=0, active=True)
item.variations.create(value="foo")
q.items.add(item)
assert Event.annotated(Event.objects).first().active_quotas == []
def test_active_quotas_annotation_variation(self):
event = Event.objects.create(
organizer=self.organizer, name='Download', slug='download',
date_from=now()
)
q = Quota.objects.create(event=event, name='Quota', size=2)
item = Item.objects.create(event=event, name='Early-bird ticket', default_price=0, active=True)
v = item.variations.create(value="foo")
item.variations.create(value="bar")
q.items.add(item)
q.variations.add(v)
assert Event.annotated(Event.objects).first().active_quotas == [q]
item.available_until = now() - timedelta(days=1)
item.save()
assert Event.annotated(Event.objects).first().active_quotas == []
item.available_until = None
item.available_from = now() + timedelta(days=1)
item.save()
assert Event.annotated(Event.objects).first().active_quotas == []
item.available_until = None
item.available_from = None
item.active = False
item.save()
assert Event.annotated(Event.objects).first().active_quotas == []
item.active = True
item.save()
assert Event.annotated(Event.objects).first().active_quotas == [q]
assert Event.annotated(Event.objects, 'foo').first().active_quotas == []
v.active = False
v.save()
assert Event.annotated(Event.objects).first().active_quotas == []
item.hide_without_voucher = True
item.save()
assert Event.annotated(Event.objects).first().active_quotas == []
class SubEventTest(TestCase):
@classmethod
@@ -1242,6 +1381,45 @@ class SubEventTest(TestCase):
v.pk: Decimal('30.00')
}
def test_active_quotas_annotation(self):
q = Quota.objects.create(event=self.event, name='Quota', size=2,
subevent=self.se)
item = Item.objects.create(event=self.event, name='Early-bird ticket', default_price=0, active=True)
q.items.add(item)
assert SubEvent.annotated(SubEvent.objects).first().active_quotas == [q]
assert SubEvent.annotated(SubEvent.objects, 'foo').first().active_quotas == []
def test_active_quotas_annotation_no_interference(self):
se2 = SubEvent.objects.create(
name='Testsub', date_from=now(), event=self.event
)
q = Quota.objects.create(event=self.event, name='Quota', size=2,
subevent=se2)
item = Item.objects.create(event=self.event, name='Early-bird ticket', default_price=0, active=True)
q.items.add(item)
assert SubEvent.annotated(SubEvent.objects).filter(pk=self.se.pk).first().active_quotas == []
assert SubEvent.annotated(SubEvent.objects).filter(pk=se2.pk).first().active_quotas == [q]
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)
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,
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
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
class CachedFileTestCase(TestCase):
def test_file_handling(self):