forked from CGM_Public/pretix_original
@@ -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())
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -204,6 +204,10 @@ DEFAULTS = {
|
||||
'default': 'True',
|
||||
'type': bool
|
||||
},
|
||||
'event_list_availability': {
|
||||
'default': 'True',
|
||||
'type': bool
|
||||
},
|
||||
'event_list_type': {
|
||||
'default': 'list',
|
||||
'type': str
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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')",
|
||||
)
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" %}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user