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:
Raphael Michel
2017-07-11 13:56:00 +02:00
committed by GitHub
parent 554800c06f
commit 8123effa65
141 changed files with 5920 additions and 1012 deletions

View File

@@ -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