Fixed #108 -- Removed the restrictions system

This commit is contained in:
Raphael Michel
2015-12-06 17:49:02 +01:00
parent b26eaaa6c9
commit 4a1122a862
25 changed files with 26 additions and 1256 deletions
-1
View File
@@ -7,7 +7,6 @@ Contents:
:maxdepth: 2
plugins
restriction
payment
ticketoutput
exporter
-306
View File
@@ -1,306 +0,0 @@
.. highlight:: python
:linenothreshold: 5
Writing a restriction plugin
============================
Please make sure you have read and understood the :ref:`basic idea <restrictionconcept>` behind
what pretix calls *restrictions*. In this document, we will walk through the creation of a
restriction plugin using the example of a restriction by date and time.
Also, read :ref:`Creating a plugin <pluginsetup>` first.
The restriction model
---------------------
It is very likely that your new restriction plugin needs to store data. In order to do
so, it should define its own model with a name related to what your restriction does,
e.g. ``TimeRestriction``. This model should be a child class of ``pretix.base.models.BaseRestriction``.
You do not need to define custom fields, but you should create at least an empty model.
In our example, we put the following into :file:`pretix/plugins/timerestriction/models.py`::
from django.db import models
from django.utils.translation import ugettext_lazy as _
from pretix.base.models import BaseRestriction
class TimeRestriction(BaseRestriction):
"""
This restriction makes an item or variation only available
within a given time frame. The price of the item can be modified
during this time frame.
"""
timeframe_from = models.DateTimeField(
verbose_name=_("Start of time frame"),
)
timeframe_to = models.DateTimeField(
verbose_name=_("End of time frame"),
)
price = models.DecimalField(
null=True, blank=True,
max_digits=7, decimal_places=2,
verbose_name=_("Price in time frame"),
)
The basic signals
-----------------
Availability determination
^^^^^^^^^^^^^^^^^^^^^^^^^^
This is the one signal *every* restriction plugin has to listen for, as your plugin does not
restrict anything without doing so. It is available as ``pretix.base.signals.determine_availability``
and is sent out every time some component of pretix wants to know whether a specific item or
variation is available for sell.
It is sent out with several keyword arguments:
``item``
The instance of ``pretix.base.models.Item`` in question.
``variations``
A list of dictionaries in the same format as ``Item.get_all_variations``:
The list contains one dictionary per variation, where the ``Property`` IDs are
keys and the ``PropertyValue`` objects are values. If an ``ItemVariation`` object
exists, it is available in the dictionary via the special key ``'variation'``. If
the item does not have any properties, the list will contain exactly one empty
dictionary. Please note: this is *not* the list of all possible variations, this is
only the list of all variations the front end likes to determine the status for.
Technically, you won't get ``dict`` objects but ``pretix.base.types.VariationDict``
objects, which behave exactly the same but add some extra methods.
``context``
A yet-to-be-defined context object containing information about the user and the order
process. This is required to implement coupon-systems or similar restrictions.
``cache``
An object very similar to Django's own caching API (see tip below)
The positional argument ``sender`` contains the event.
All receivers **have to** return a copy of the given list of variation dictionaries where each
dictionary can be extended by the following two keys:
``available``
A boolean value whether or not this plugin allows this variation to be on sale. Defaults
to ``True``.
``price``
A price to be set for this variation. Set to ``None`` or omit to keep the default price
of the variation or the item's base price.
.. IMPORTANT::
As this signal might be called *a lot* under heavy load, you are expected to implement
your receiver with an eye to performance. We highly recommend making use of Django's
`caching feature`_. We cannot do this for you, as the possibility of caching highly
depends on the details of your restriction.
**Attention:** Please use the **cache object provided in the signal** instead of importing
it directly from django, so we can take care of invalidation whenever the organizer changes
the event or item settings. Please also **prefix all your cache keys** with your
plugin name.
In our example, the implementation could look like this::
from django.dispatch import receiver
from django.utils.timezone import now
from pretix.base.signals import determine_availability
from .models import TimeRestriction
@receiver(determine_availability, dispatch_uid="restriction_time")
def availability_handler(sender, **kwargs):
# Handle the signal's input arguments
item = kwargs['item']
variations = kwargs['variations']
cache = kwargs['cache']
context = kwargs['context'] # NOQA
# Fetch all restriction objects applied to this item
restrictions = list(TimeRestriction.objects.filter(
item=item,
).prefetch_related('variations'))
# If we do not know anything about this item, we are done here.
if len(restrictions) == 0:
return variations
# IMPORTANT:
# We need to make a two-level deep copy of the variations list before we
# modify it, becuase we need to to copy the dictionaries. Otherwise, we'll
# interfere with other plugins.
variations = [d.copy() for d in variations]
# The maximum validity of our cached values is the next date, one of our
# timeframe_from or tiemframe_to actions happens
def timediff(restrictions):
for r in restrictions:
if r.timeframe_from >= now():
yield (r.timeframe_from - now()).total_seconds()
if r.timeframe_to >= now():
yield (r.timeframe_to - now()).total_seconds()
try:
cache_validity = min(timediff(restrictions))
except ValueError:
# empty sequence
# If we get here, there are restrictions available but nothing will
# change about them any more. If it were not for the case of no
# restriction for the base item but restrictions for special
# variations, we could quit here with 'item not available'.
cache_validity = 3600
# Walk through all variations we are asked for
for v in variations:
var_restrictions = []
for restriction in restrictions:
applied_to = list(restriction.variations.current.all())
# Only take this restriction into consideration if it
# is directly applied to this variation or if the item
# has no variations
if v.empty() or ('variation' in v and v['variation'] in applied_to):
var_restrictions.append(restriction)
if not var_restrictions:
v['available'] = True
v['price'] = None
continue
# If this point is reached, there ARE time restrictions for this item
# Therefore, it is only available inside one of the timeframes, but not
# without any timeframe
available = False
# Make up some unique key for this variation
cachekey = 'timerestriction:%d:%s' % (
item.pk,
v.identify(),
)
# Fetch from cache, if available
cached = cache.get(cachekey)
if cached is not None:
v['available'] = (cached.split(":")[0] == 'True')
try:
v['price'] = float(cached.split(":")[1])
except ValueError:
v['price'] = None
continue
# Walk through all restriction objects applied to this item
prices = []
for restriction in var_restrictions:
if restriction.timeframe_from <= now() <= restriction.timeframe_to:
# Selling this item is currently possible
available = True
prices.append(restriction.price)
# Use the lowest of all prices set by restrictions
prices = [p for p in prices if p is not None]
price = min(prices) if prices else None
v['available'] = available
v['price'] = price
cache.set(
cachekey,
'%s:%s' % (
'True' if available else 'False',
str(price) if price else ''
),
cache_validity
)
return variations
.. IMPORTANT::
Please note the copying of the ``variations`` list in the example above (line 30).
If you do not copy down to the ``dict`` objects, you will run into
interference problems with other plugins.
Control interface formsets
^^^^^^^^^^^^^^^^^^^^^^^^^^
To make it possible for the event organizer to configure your restriction, there is a
'Restrictions' page in the item configuration. This page is able to show a formset for
each restriction plugin, but *you* are required to create this formset. This is why you
should listen to the ``pretix.control.signals.restriction_formset`` signal.
Currently, the signal comes with only one keyword argument:
``item``
The instance of ``pretix.base.models.Item`` we want a formset for.
You are expected to return a dict containing the following items:
``formsetclass``
An inline formset class (not a formset object).
``prefix``
A unique prefix for your queryset.
``title``
A title for your formset (normally your plugin name)
``description``
An short, explanatory text about your restriction.
Our time restriction example looks like this::
from django.utils.translation import ugettext_lazy as _
from django.dispatch import receiver
from django.forms.models import inlineformset_factory
from pretix.control.signals import restriction_formset
from pretix.base.models import Item
from pretix.control.forms import (
VariationsField, RestrictionInlineFormset, RestrictionForm
)
from .models import TimeRestriction
class TimeRestrictionForm(RestrictionForm):
class Meta:
model = TimeRestriction
localized_fields = '__all__'
fields = [
'variations',
'timeframe_from',
'timeframe_to',
'price',
]
@receiver(restriction_formset, dispatch_uid="restriction_formset_time")
def formset_handler(sender, **kwargs):
formset = inlineformset_factory(
Item,
TimeRestriction,
formset=RestrictionInlineFormset,
form=TimeRestrictionForm,
can_order=False,
can_delete=True,
extra=0,
)
return {
'title': _('Restriction by time'),
'formsetclass': formset,
'prefix': 'timerestriction',
'description': 'If you use this restriction type, the system will only '
'sell variations which are covered by at least one of the '
'timeframes you define below.'
}
.. NOTE::
If you do use the ``RestrictionInlineFormset``, ``RestrictionForm`` and
``VariationsField`` classes in your implementation, we will do a lot of magic for you
to display the ``variations`` field in the form in a nice and consistent way. So please,
use these base classes and test carefully, if you make any changes to the behaviour
of this field.
.. _caching feature: https://docs.djangoproject.com/en/1.7/topics/cache/
-43
View File
@@ -53,49 +53,6 @@ An item can be extended using **questions**. Questions enable items to be extend
additional information which can be entered by the user. Examples of possible questions
include 'Name' or 'age'.
.. _restrictionconcept:
Restrictions
^^^^^^^^^^^^
The probably most powerful concepts of pretix is the very abstract concept of **restrictions**.
We already know that **items** can come in very different **variations**, but a
**restriction** decides whether an variation is available for sale and assign **prices**
to **variations**. There are **restriction types** (pieces of code implementing the
restriction logic) and **restriction instances** (the specific configurations made by the
organizer). Although **restrictions** are a very abstract concept which can be used
to do nearly anything, there are a few obvious examples:
* One easy example is a restriction by time, which allows the sale of certain item variations
only within a certain time frame. As restrictions can also assign a price to a variation,
this can also be used to implement something like 'early-bird prices' for your tickets by
using multiple time restrictions with different prices.
* The most obvious example is the restriction by number, which limits the sale of the tickets to
a maximum number. You can use this either to stop selling tickets completely when your house
is full or for creating limited 'VIP tickets'. We'll come to this again later.
* A more advanced example is a restriction by user, for example reduced ticket prices for
users who are members of a special group.
* Arbitrary sophisticated features like coupon codes can also be implemented using
this feature.
Any number of **restrictions** can be applied to the whole of a **item** or even to a specific
**variation**. The processing of the restriction follows the following set of rules:
* Variation-specific rules have precedence over item-specific rules.
* The restrictions are being processed in random order (there may not be any assumptions about
the evaluation order).
* Multiple restriction instances of **different restriction types** are linked with *and*, so
if both a time frame and a restriction by number are applied to an item, the item is only available
for sale during the given time frame *and* only as long as items are available.
* Multiple restriction instances of the **same restriction type** are typically linked with *or*,
although this is the decision of the restriction logic itself and not mandatory. So for example
the restriction by time would implement this default logic, because if two time frames are applied
to an item, the item should be available for sale in both of the time frames (it just does not make
sense otherwise on an one-dimensional time axis).
* If multiple restrictions apply which set the price, the *cheapest* price determines the final price.
Restrictions can be implemented using a plugin system and do not require changes to the pretix codebase.
Restriction by number
"""""""""""""""""""""