mirror of
https://github.com/pretix/pretix.git
synced 2026-08-11 10:57:00 +00:00
Start implementing the Quota magic
This commit is contained in:
@@ -125,21 +125,21 @@ special care in the implementation to never sell more tickets than allowed, even
|
||||
|
||||
* There is a concept of **quotas**. A quota is basically a number of items combined with information
|
||||
about how many of them are still available.
|
||||
* Every time a user places a item in the cart, a **lock** object is created, reducing the number of
|
||||
* Every time a user places a item in the cart, a **cart lock** is created, reducing the number of
|
||||
available items in the pool by one. The lock is valid for a fixed time (e.g. 30 minutes), but not
|
||||
instantly deleted afther those 30 minutes (we'll get to that).
|
||||
* Every time a user places a binding order, the lock object is replaced by an **order** which behaves
|
||||
much the same as the lock. It reduces the number of available item and is valid for a fixed time, this
|
||||
time for the configured payment term (e.g. 14 days).
|
||||
* If the order is being paid, the **order** becomes permanent.
|
||||
* Once there are no available tickets left and a user wants to buy a ticket, a lock which is in place
|
||||
for more than the allowed time frame is being removed in favor of the new buyer. If there are no
|
||||
abandoned locks available, an unpaid order being older than the configured payment term is being
|
||||
removed. If there are none of them as well, this quota is sold out.
|
||||
* The same quota can apply to multiple items and one item can be affected by multiple quotas, to
|
||||
enable both of the following features at the same time:
|
||||
|
||||
* You'll want to make sure you never have more than X people at your event, so you'll create a quota
|
||||
applying to all ticket items.
|
||||
* You want to reduce the first Y tickets in price, so you'll create a restriction which is bound by
|
||||
a quota of Y and reduces the price.
|
||||
* Once there are no available tickets left and user A wants to buy a ticket, he can do so, as long as
|
||||
there are *expired* cart locks in the system. In this case, user A gets a new cart lock, so that there
|
||||
are more cart locks than available tickets and therefore have to remove one of the expired cart locks.
|
||||
However, we do not choose one by random, but keep the surplus in a way that leads to the deletion
|
||||
of the cart lock of the user who tries *last* to use his lock.
|
||||
* The same goes for orders which are not paid within the specified timeframe. This policy allows to
|
||||
sell as much items as possible, guarantees you to get your item if you checkout within the validity
|
||||
period of your lock or pay within the validity period of your order. It does not guarantee you anything
|
||||
any longer, but it tries to be *as tolerant as possible* to users who are paying after their payment
|
||||
period or click checkout after the expiry of their lock.
|
||||
* The same quota can apply to multiple items and one item can be affected by multiple quotas
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
import versions.models
|
||||
import pretixbase.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0002_auto_20150211_2031'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='quota',
|
||||
name='lock_cache',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='quota',
|
||||
name='order_cache',
|
||||
),
|
||||
]
|
||||
+110
-11
@@ -5,6 +5,8 @@ import uuid
|
||||
from django.db import models
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin
|
||||
from django.db.models import Q
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.template.defaultfilters import date as _date
|
||||
from django.core.validators import RegexValidator
|
||||
@@ -704,6 +706,13 @@ class Item(Versionable):
|
||||
return result
|
||||
|
||||
def get_all_available_variations(self):
|
||||
"""
|
||||
This method returns a list of all variations which are theoretically
|
||||
possible for sale. It DOES call all activated restriction plugins, but it
|
||||
DOES NOT take into account quotas. Use is_available on the ItemVariation
|
||||
objects (or the Item it self, if it does not have variations) to determine
|
||||
availability by the terms of quotas.
|
||||
"""
|
||||
from .signals import determine_availability
|
||||
|
||||
variations = self.get_all_variations()
|
||||
@@ -732,6 +741,16 @@ class Item(Versionable):
|
||||
|
||||
return variations
|
||||
|
||||
def availability(self):
|
||||
"""
|
||||
This method is used to determine whether this Item is currently available
|
||||
for sale. It may return any of the return codes of Quota.availability()
|
||||
"""
|
||||
if self.properties.exist():
|
||||
raise ValueError('Do not call this directly on items which have properties '
|
||||
'but call this on their ItemVariation objects')
|
||||
return max([q.availability() for q in self.quotas.all()])
|
||||
|
||||
|
||||
class ItemVariation(Versionable):
|
||||
"""
|
||||
@@ -784,6 +803,13 @@ class ItemVariation(Versionable):
|
||||
if self.item:
|
||||
self.item.event.get_cache().clear()
|
||||
|
||||
def availability(self):
|
||||
"""
|
||||
This method is used to determine whether this Item is currently available
|
||||
for sale. It may return any of the return codes of Quota.availability()
|
||||
"""
|
||||
return max([q.availability() for q in self.quotas.all()])
|
||||
|
||||
|
||||
class VariationsField(VersionedManyToManyField):
|
||||
"""
|
||||
@@ -867,10 +893,35 @@ class Quota(Versionable):
|
||||
speficied, the quota applies to all of them, and if there are variations
|
||||
specified, the quota applies to those.
|
||||
|
||||
This object holds two fields, "order_cache" and "lock_cache", which are
|
||||
implementation specific and are considered private. It is planned that they
|
||||
are being used as a fallback solution if redis is not available.
|
||||
Please read the documentation section on quotas carefully before doing
|
||||
anything with quotas. This might confuse you otherwise.
|
||||
http://docs.pretix.eu/en/latest/development/concepts.html#restriction-by-number
|
||||
|
||||
The AVAILABILITY_* constants represent varios states of an quota allowing
|
||||
its items/variations being for sale.
|
||||
|
||||
AVAILABILITY_OK
|
||||
This item is available for sale.
|
||||
|
||||
AVAILABILITY_RESERVED
|
||||
This item is currently not available for sale, because all available
|
||||
items are in people's shopping carts. It might become available
|
||||
again if those people do not proceed with checkout.
|
||||
|
||||
AVAILABILITY_ORDERED
|
||||
This item is currently not availalbe for sale, because all available
|
||||
items are ordered. It might become available again if those people
|
||||
do not pay.
|
||||
|
||||
AVAILABILITY_GONE
|
||||
This item is completely sold out.
|
||||
"""
|
||||
|
||||
AVAILABILITY_GONE = 30
|
||||
AVAILABILITY_ORDERED = 20
|
||||
AVAILABILITY_RESERVED = 10
|
||||
AVAILABILITY_OK = 0
|
||||
|
||||
event = VersionedForeignKey(
|
||||
Event,
|
||||
on_delete=models.CASCADE,
|
||||
@@ -896,14 +947,6 @@ class Quota(Versionable):
|
||||
blank=True,
|
||||
verbose_name=_("Variations")
|
||||
)
|
||||
order_cache = models.ManyToManyField(
|
||||
'OrderPosition',
|
||||
blank=True
|
||||
)
|
||||
lock_cache = models.ManyToManyField(
|
||||
'CartPosition',
|
||||
blank=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Quota")
|
||||
@@ -912,6 +955,62 @@ class Quota(Versionable):
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
super().delete(*args, **kwargs)
|
||||
if self.event:
|
||||
self.event.get_cache().clear()
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
super().save(*args, **kwargs)
|
||||
if self.event:
|
||||
self.event.get_cache().clear()
|
||||
|
||||
def availability(self):
|
||||
"""
|
||||
This method is used to determine whether Items or ItemVariations belonging
|
||||
to this quota should currently be available for sale. It returns one of the
|
||||
Quota.AVAILABILITY_ constants. 0 is returned if the item is available, a
|
||||
positive number depending on the reason, if not.
|
||||
"""
|
||||
# TODO: These lookups are highly inefficient. However, we'll wait with optimizing
|
||||
# until Django 1.8 is released, as the following feature might make it a
|
||||
# lot easier:
|
||||
# https://docs.djangoproject.com/en/1.8/ref/models/conditional-expressions/
|
||||
# TODO: Test for interference with old versions of Item-Quota-relations, etc.
|
||||
# TODO: Prevent corner-cases like people having ordered an item before it got
|
||||
# its first variationsadded
|
||||
quotalookup = (
|
||||
( # Orders for items which do not have any variations
|
||||
Q(variation__isnull=True)
|
||||
& Q(item__quotas__in=[self])
|
||||
) | ( # Orders for items which do have any variations
|
||||
Q(variation__quotas__in=[self])
|
||||
)
|
||||
)
|
||||
paid_orders = OrderPosition.objects.current.filter(
|
||||
Q(order__status=Order.STATUS_PAID)
|
||||
& quotalookup
|
||||
)
|
||||
if paid_orders >= self.size:
|
||||
return Quota.AVAILABILITY_GONE
|
||||
|
||||
pending_valid_orders = OrderPosition.objects.current.filter(
|
||||
Q(order__status=Order.STATUS_PENDING)
|
||||
& Q(order__expires__gte=now())
|
||||
& quotalookup
|
||||
)
|
||||
if (paid_orders + pending_valid_orders) >= self.size:
|
||||
return Quota.AVAILABILITY_ORDERED
|
||||
|
||||
valid_cart_positions = CartPosition.objects.current.filter(
|
||||
Q(order__expires__lt=now())
|
||||
& quotalookup
|
||||
)
|
||||
if (paid_orders + pending_valid_orders + valid_cart_positions) >= self.size:
|
||||
return Quota.AVAILABILITY_RESERVED
|
||||
|
||||
return Quota.AVAILABILITY_OK
|
||||
|
||||
|
||||
class Order(Versionable):
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user