mirror of
https://github.com/pretix/pretix.git
synced 2026-05-05 15:14:04 +00:00
Add sub-events and relative date settings (#503)
* Data model * little crud * SubEventItemForm etc * Drop SubEventItem.active, quota editor * Fix failing tests * First frontend stuff * Addons form stuff * Quota calculation * net price display on EventIndex * Add tests, solve some bugs * Correct quota selection in more places, consolidate pricing logic * Fix failing quota tests * Fix TypeError * Add tests for checkout * Fixed a bug in QuotaForm * Prevent immutable cart if a quota was removed from an item * Add tests for pricing * Handle waiting list * Filter in check-in list * Fixed import lost in rebase * Fix waiting list widget * Voucher management * Voucher redemption * Fix broken tests * Add subevents to OrderChangeManager * Create a subevent during event creation * Fix bulk voucher creation * Introduce subevent.active * Copy from for subevents * Show active in list * ICal download for subevents * Check start and end of presale * Failing tests / show cart logic * Test * Rebase migrations * REST API integration of sub-events * Integrate quota calculation into the traditional quota form * Make subevent argument to add_position optional * Log-display foo * pretixdroid and subevents * Filter by subevent * Add more tests * Some mor tests * Rebase fixes * More tests * Relative dates * Restrict selection in relative datetime widgets * Filter subevent list * Re-label has_subevents * Rebase fixes, subevents in calendar view * Performance and caching issues * Refactor calendar templates * Permission tests * Calendar fixes and month selection * subevent selection * Rename subevents to dates * Add tests for calendar views
This commit is contained in:
@@ -10,13 +10,13 @@ from django.db import models
|
||||
from django.db.models import F, Func, Q, Sum
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
|
||||
from i18nfield.fields import I18nCharField, I18nTextField
|
||||
|
||||
from pretix.base.decimal import round_decimal
|
||||
from pretix.base.models.base import LoggedModel
|
||||
|
||||
from .event import Event
|
||||
from .event import Event, SubEvent
|
||||
|
||||
|
||||
class ItemCategory(LoggedModel):
|
||||
@@ -88,6 +88,40 @@ def itempicture_upload_to(instance, filename: str) -> str:
|
||||
)
|
||||
|
||||
|
||||
class SubEventItem(models.Model):
|
||||
"""
|
||||
This model can be used to change the price of a product for a single subevent (i.e. a
|
||||
date in an event series).
|
||||
|
||||
:param subevent: The date this belongs to
|
||||
:type subevent: SubEvent
|
||||
:param item: The item to modify the price for
|
||||
:type item: Item
|
||||
:param price: The modified price (or ``None`` for the original price)
|
||||
:type price: Decimal
|
||||
"""
|
||||
subevent = models.ForeignKey('SubEvent', on_delete=models.CASCADE)
|
||||
item = models.ForeignKey('Item', on_delete=models.CASCADE)
|
||||
price = models.DecimalField(max_digits=7, decimal_places=2, null=True, blank=True)
|
||||
|
||||
|
||||
class SubEventItemVariation(models.Model):
|
||||
"""
|
||||
This model can be used to change the price of a product variation for a single
|
||||
subevent (i.e. a date in an event series).
|
||||
|
||||
:param subevent: The date this belongs to
|
||||
:type subevent: SubEvent
|
||||
:param variation: The variation to modify the price for
|
||||
:type variation: ItemVariation
|
||||
:param price: The modified price (or ``None`` for the original price)
|
||||
:type price: Decimal
|
||||
"""
|
||||
subevent = models.ForeignKey('SubEvent', on_delete=models.CASCADE)
|
||||
variation = models.ForeignKey('ItemVariation', on_delete=models.CASCADE)
|
||||
price = models.DecimalField(max_digits=7, decimal_places=2, null=True, blank=True)
|
||||
|
||||
|
||||
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.
|
||||
@@ -271,7 +305,7 @@ class Item(LoggedModel):
|
||||
return False
|
||||
return True
|
||||
|
||||
def check_quotas(self, ignored_quotas=None, count_waitinglist=True, _cache=None):
|
||||
def check_quotas(self, ignored_quotas=None, count_waitinglist=True, subevent=None, _cache=None):
|
||||
"""
|
||||
This method is used to determine whether this Item is currently available
|
||||
for sale.
|
||||
@@ -285,12 +319,18 @@ class Item(LoggedModel):
|
||||
:raises ValueError: if you call this on an item which has variations associated with it.
|
||||
Please use the method on the ItemVariation object you are interested in.
|
||||
"""
|
||||
check_quotas = set(self.quotas.all())
|
||||
check_quotas = set(getattr(
|
||||
self, '_subevent_quotas', # Utilize cache in product list
|
||||
self.quotas.select_related('subevent').filter(subevent=subevent)
|
||||
if subevent else self.quotas.all()
|
||||
))
|
||||
if not subevent and self.event.has_subevents:
|
||||
raise TypeError('You need to supply a subevent.')
|
||||
if ignored_quotas:
|
||||
check_quotas -= set(ignored_quotas)
|
||||
if not check_quotas:
|
||||
return Quota.AVAILABILITY_OK, sys.maxsize
|
||||
if self.variations.count() > 0: # NOQA
|
||||
if self.has_variations: # NOQA
|
||||
raise ValueError('Do not call this directly on items which have variations '
|
||||
'but call this on their ItemVariation objects')
|
||||
return min([q.availability(count_waitinglist=count_waitinglist, _cache=_cache) for q in check_quotas],
|
||||
@@ -371,7 +411,7 @@ class ItemVariation(models.Model):
|
||||
if self.item:
|
||||
self.item.event.get_cache().clear()
|
||||
|
||||
def check_quotas(self, ignored_quotas=None, count_waitinglist=True, _cache=None) -> Tuple[int, int]:
|
||||
def check_quotas(self, ignored_quotas=None, count_waitinglist=True, subevent=None, _cache=None) -> Tuple[int, int]:
|
||||
"""
|
||||
This method is used to determine whether this ItemVariation is currently
|
||||
available for sale in terms of quotas.
|
||||
@@ -383,9 +423,15 @@ class ItemVariation(models.Model):
|
||||
:param count_waitinglist: If ``False``, waiting list entries will be ignored for quota calculation.
|
||||
:returns: any of the return codes of :py:meth:`Quota.availability()`.
|
||||
"""
|
||||
check_quotas = set(self.quotas.all())
|
||||
check_quotas = set(getattr(
|
||||
self, '_subevent_quotas', # Utilize cache in product list
|
||||
self.quotas.filter(subevent=subevent).select_related('subevent')
|
||||
if subevent else self.quotas.all()
|
||||
))
|
||||
if ignored_quotas:
|
||||
check_quotas -= set(ignored_quotas)
|
||||
if not subevent and self.item.event.has_subevents: # NOQA
|
||||
raise TypeError('You need to supply a subevent.')
|
||||
if not check_quotas:
|
||||
return Quota.AVAILABILITY_OK, sys.maxsize
|
||||
return min([q.availability(count_waitinglist=count_waitinglist, _cache=_cache) for q in check_quotas],
|
||||
@@ -402,6 +448,17 @@ class ItemAddOn(models.Model):
|
||||
An instance of this model indicates that buying a ticket of the time ``base_item``
|
||||
allows you to add up to ``max_count`` items from the category ``addon_category``
|
||||
to your order that will be associated with the base item.
|
||||
|
||||
:param base_item: The base item the add-ons are attached to
|
||||
:type base_item: Item
|
||||
:param addon_category: The category the add-on can be chosen from
|
||||
:type addon_category: ItemCategory
|
||||
:param min_count: The minimal number of add-ons to be chosen
|
||||
:type min_count: int
|
||||
:param max_count: The maximal number of add-ons to be chosen
|
||||
:type max_count: int
|
||||
:param position: An integer used for sorting
|
||||
:type position: int
|
||||
"""
|
||||
base_item = models.ForeignKey(
|
||||
Item,
|
||||
@@ -574,6 +631,8 @@ class Quota(LoggedModel):
|
||||
|
||||
:param event: The event this belongs to
|
||||
:type event: Event
|
||||
:param subevent: The event series date this belongs to, if event series are enabled
|
||||
:type subevent: SubEvent
|
||||
:param name: This quota's name
|
||||
:type name: str
|
||||
:param size: The number of items in this quota
|
||||
@@ -593,6 +652,13 @@ class Quota(LoggedModel):
|
||||
related_name="quotas",
|
||||
verbose_name=_("Event"),
|
||||
)
|
||||
subevent = models.ForeignKey(
|
||||
SubEvent,
|
||||
null=True, blank=True,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="quotas",
|
||||
verbose_name=pgettext_lazy('subevent', "Date"),
|
||||
)
|
||||
name = models.CharField(
|
||||
max_length=200,
|
||||
verbose_name=_("Name")
|
||||
@@ -687,11 +753,11 @@ class Quota(LoggedModel):
|
||||
now_dt = now_dt or now()
|
||||
if 'sqlite3' in settings.DATABASES['default']['ENGINE']:
|
||||
func = 'MAX'
|
||||
else:
|
||||
else: # NOQA
|
||||
func = 'GREATEST'
|
||||
|
||||
return Voucher.objects.filter(
|
||||
Q(event=self.event) &
|
||||
Q(event=self.event) & Q(subevent=self.subevent) &
|
||||
Q(block_quota=True) &
|
||||
Q(Q(valid_until__isnull=True) | Q(valid_until__gte=now_dt)) &
|
||||
Q(Q(self._position_lookup) | Q(quota=self))
|
||||
@@ -702,7 +768,7 @@ class Quota(LoggedModel):
|
||||
def count_waiting_list_pending(self) -> int:
|
||||
from pretix.base.models import WaitingListEntry
|
||||
return WaitingListEntry.objects.filter(
|
||||
Q(voucher__isnull=True) &
|
||||
Q(voucher__isnull=True) & Q(subevent=self.subevent) &
|
||||
self._position_lookup
|
||||
).distinct().count()
|
||||
|
||||
@@ -711,7 +777,7 @@ class Quota(LoggedModel):
|
||||
|
||||
now_dt = now_dt or now()
|
||||
return CartPosition.objects.filter(
|
||||
Q(event=self.event) &
|
||||
Q(event=self.event) & Q(subevent=self.subevent) &
|
||||
Q(expires__gte=now_dt) &
|
||||
~Q(
|
||||
Q(voucher__isnull=False) & Q(voucher__block_quota=True)
|
||||
@@ -725,14 +791,14 @@ class Quota(LoggedModel):
|
||||
|
||||
# This query has beeen benchmarked against a Count('id', distinct=True) aggregate and won by a small margin.
|
||||
return OrderPosition.objects.filter(
|
||||
self._position_lookup, order__status=Order.STATUS_PENDING, order__event=self.event
|
||||
self._position_lookup, order__status=Order.STATUS_PENDING, order__event=self.event, subevent=self.subevent
|
||||
).values('id').distinct().count()
|
||||
|
||||
def count_paid_orders(self):
|
||||
from pretix.base.models import Order, OrderPosition
|
||||
|
||||
return OrderPosition.objects.filter(
|
||||
self._position_lookup, order__status=Order.STATUS_PAID, order__event=self.event
|
||||
self._position_lookup, order__status=Order.STATUS_PAID, order__event=self.event, subevent=self.subevent
|
||||
).values('id').distinct().count()
|
||||
|
||||
@cached_property
|
||||
|
||||
Reference in New Issue
Block a user