mirror of
https://github.com/pretix/pretix.git
synced 2026-05-05 15:14:04 +00:00
Merge pull request #111 from pretix/remove-restrictions
Remove restrictions
This commit is contained in:
@@ -7,7 +7,6 @@ Contents:
|
||||
:maxdepth: 2
|
||||
|
||||
plugins
|
||||
restriction
|
||||
payment
|
||||
ticketoutput
|
||||
exporter
|
||||
|
||||
@@ -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/
|
||||
@@ -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
|
||||
"""""""""""""""""""""
|
||||
|
||||
|
||||
24
src/pretix/base/migrations/0005_auto_20151206_1652.py
Normal file
24
src/pretix/base/migrations/0005_auto_20151206_1652.py
Normal file
@@ -0,0 +1,24 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0004_auto_20151024_0848'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='item',
|
||||
name='available_from',
|
||||
field=models.DateTimeField(null=True, help_text='This product will not be sold before the given date.', blank=True, verbose_name='Available from'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='item',
|
||||
name='available_until',
|
||||
field=models.DateTimeField(null=True, help_text='This product will not be sold after the given date.', blank=True, verbose_name='Available to'),
|
||||
),
|
||||
]
|
||||
@@ -2,8 +2,8 @@ from .auth import User
|
||||
from .base import CachedFile, Versionable, cachedfile_name
|
||||
from .event import Event, EventLock, EventPermission, EventSetting
|
||||
from .items import (
|
||||
BaseRestriction, Item, ItemCategory, ItemVariation, Property,
|
||||
PropertyValue, Question, Quota, VariationsField, itempicture_upload_to,
|
||||
Item, ItemCategory, ItemVariation, Property, PropertyValue, Question,
|
||||
Quota, VariationsField, itempicture_upload_to,
|
||||
)
|
||||
from .orders import (
|
||||
CachedTicket, CartPosition, ObjectWithAnswers, Order, OrderPosition,
|
||||
@@ -14,7 +14,7 @@ from .organizer import Organizer, OrganizerPermission, OrganizerSetting
|
||||
__all__ = [
|
||||
'Versionable', 'User', 'CachedFile', 'Organizer', 'OrganizerPermission', 'Event', 'EventPermission',
|
||||
'ItemCategory', 'Item', 'Property', 'PropertyValue', 'ItemVariation', 'VariationsField', 'Question',
|
||||
'BaseRestriction', 'Quota', 'Order', 'CachedTicket', 'QuestionAnswer', 'ObjectWithAnswers', 'OrderPosition',
|
||||
'Quota', 'Order', 'CachedTicket', 'QuestionAnswer', 'ObjectWithAnswers', 'OrderPosition',
|
||||
'CartPosition', 'EventSetting', 'OrganizerSetting', 'EventLock', 'cachedfile_name', 'itempicture_upload_to',
|
||||
'generate_secret'
|
||||
]
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
from itertools import product
|
||||
|
||||
from django.db import models
|
||||
@@ -8,7 +7,7 @@ from django.db.models import Q, Case, Count, Sum, When
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from typing import List, Tuple, Union
|
||||
from typing import List, Tuple
|
||||
from versions.models import VersionedForeignKey, VersionedManyToManyField
|
||||
|
||||
from pretix.base.i18n import I18nCharField, I18nTextField
|
||||
@@ -80,8 +79,6 @@ class Item(Versionable):
|
||||
An item is a thing which can be sold. It belongs to an event and may or may not belong to a category.
|
||||
Items are often also called 'products' but are named 'items' internally due to historic reasons.
|
||||
|
||||
It has a default price which might by overriden by restrictions.
|
||||
|
||||
:param event: The event this belongs to.
|
||||
:type event: Event
|
||||
:param category: The category this belongs to. May be null.
|
||||
@@ -100,6 +97,10 @@ class Item(Versionable):
|
||||
:type admission: bool
|
||||
:param picture: A product picture to be shown next to the product description.
|
||||
:type picture: File
|
||||
:param available_from: The date this product goes on sale
|
||||
:type available_from: datetime
|
||||
:param available_until: The date until when the product is on sale
|
||||
:type available_until: datetime
|
||||
|
||||
"""
|
||||
|
||||
@@ -154,6 +155,16 @@ class Item(Versionable):
|
||||
null=True, blank=True,
|
||||
upload_to=itempicture_upload_to
|
||||
)
|
||||
available_from = models.DateTimeField(
|
||||
verbose_name=_("Available from"),
|
||||
null=True, blank=True,
|
||||
help_text=_('This product will not be sold before the given date.')
|
||||
)
|
||||
available_until = models.DateTimeField(
|
||||
verbose_name=_("Available until"),
|
||||
null=True, blank=True,
|
||||
help_text=_('This product will not be sold after the given date.')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Product")
|
||||
@@ -173,6 +184,19 @@ class Item(Versionable):
|
||||
if self.event:
|
||||
self.event.get_cache().clear()
|
||||
|
||||
def is_available(self) -> bool:
|
||||
"""
|
||||
Returns whether this item is available according to its ``active`` flag
|
||||
and its ``available_from`` and ``available_until`` fields
|
||||
"""
|
||||
if not self.active:
|
||||
return False
|
||||
if self.available_from and self.available_from > now():
|
||||
return False
|
||||
if self.available_until and self.available_until < now():
|
||||
return False
|
||||
return True
|
||||
|
||||
def get_all_variations(self, use_cache: bool=False) -> List[VariationDict]:
|
||||
"""
|
||||
This method returns a list containing all variations of this
|
||||
@@ -249,10 +273,9 @@ class Item(Versionable):
|
||||
def get_all_available_variations(self, use_cache: bool=False):
|
||||
"""
|
||||
This method returns a list of all variations which are theoretically
|
||||
possible for sale. It DOES call all activated restriction plugins, and it
|
||||
DOES only return variations which DO have an ItemVariation object, as all
|
||||
variations without one CAN NOT be part of a Quota and therefore can never
|
||||
be available for sale. The only exception is the empty variation
|
||||
possible for sale. It DOES only return variations which DO have an ItemVariation
|
||||
object, as all variations without one CAN NOT be part of a Quota and therefore can
|
||||
never be available for sale. The only exception is the empty variation
|
||||
for items without properties, which never has an ItemVariation object.
|
||||
|
||||
This DOES NOT take into account quotas itself. Use ``is_available`` on the
|
||||
@@ -268,39 +291,18 @@ class Item(Versionable):
|
||||
if use_cache and hasattr(self, '_get_all_available_variations_cache'):
|
||||
return self._get_all_available_variations_cache
|
||||
|
||||
from pretix.base.signals import determine_availability
|
||||
|
||||
variations = self._get_all_generated_variations()
|
||||
responses = determine_availability.send(
|
||||
self.event, item=self,
|
||||
variations=variations, context=None,
|
||||
cache=self.event.get_cache()
|
||||
)
|
||||
|
||||
for i, var in enumerate(variations):
|
||||
var['available'] = var['variation'].active if 'variation' in var else True
|
||||
if 'variation' in var:
|
||||
if var['variation'].default_price:
|
||||
if var['variation'].default_price is not None:
|
||||
var['price'] = var['variation'].default_price
|
||||
else:
|
||||
var['price'] = self.default_price
|
||||
else:
|
||||
var['price'] = self.default_price
|
||||
|
||||
# It is possible, that *multiple* restriction plugins change the default price.
|
||||
# In this case, the cheapest one wins. As soon as there is a restriction
|
||||
# that changes the price, the default price has no effect.
|
||||
|
||||
newprice = None
|
||||
for rec, response in responses:
|
||||
if 'available' in response[i] and not response[i]['available']:
|
||||
var['available'] = False
|
||||
break
|
||||
if 'price' in response[i] and response[i]['price'] is not None \
|
||||
and (newprice is None or response[i]['price'] < newprice):
|
||||
newprice = response[i]['price']
|
||||
var['price'] = newprice or var['price']
|
||||
|
||||
variations = [var for var in variations if var['available']]
|
||||
|
||||
self._get_all_available_variations_cache = variations
|
||||
@@ -322,38 +324,6 @@ class Item(Versionable):
|
||||
return min([q.availability() for q in self.quotas.all()],
|
||||
key=lambda s: (s[0], s[1] if s[1] is not None else sys.maxsize))
|
||||
|
||||
def check_restrictions(self):
|
||||
"""
|
||||
This method is used to determine whether this ItemVariation is restricted
|
||||
in sale by any restriction plugins.
|
||||
|
||||
:returns:
|
||||
|
||||
* ``False``, if the item is unavailable
|
||||
* the item's price, otherwise
|
||||
|
||||
:raises ValueError: if you call this on an item which has properties associated with it.
|
||||
Please use the method on the ItemVariation object you are interested in.
|
||||
"""
|
||||
if self.properties.count() > 0: # NOQA
|
||||
raise ValueError('Do not call this directly on items which have properties '
|
||||
'but call this on their ItemVariation objects')
|
||||
from pretix.base.signals import determine_availability
|
||||
|
||||
vd = VariationDict()
|
||||
responses = determine_availability.send(
|
||||
self.event, item=self,
|
||||
variations=[vd], context=None,
|
||||
cache=self.event.get_cache()
|
||||
)
|
||||
price = self.default_price
|
||||
for rec, response in responses:
|
||||
if 'available' in response[0] and not response[0]['available']:
|
||||
return False
|
||||
elif 'price' in response[0] and response[0]['price'] is not None and response[0]['price'] < price:
|
||||
price = response[0]['price']
|
||||
return price
|
||||
|
||||
|
||||
class Property(Versionable):
|
||||
"""
|
||||
@@ -466,8 +436,6 @@ class ItemVariation(Versionable):
|
||||
values by creating an ItemVariation object for them with active set to
|
||||
False.
|
||||
|
||||
Restrictions can be not only set to items but also directly to variations.
|
||||
|
||||
:param item: The item this variation belongs to
|
||||
:type item: Item
|
||||
:param values: A set of ``PropertyValue`` objects defining this variation
|
||||
@@ -531,31 +499,6 @@ class ItemVariation(Versionable):
|
||||
vd['variation'] = self
|
||||
return vd
|
||||
|
||||
def check_restrictions(self) -> Union[bool, Decimal]:
|
||||
"""
|
||||
This method is used to determine whether this ItemVariation is restricted
|
||||
in sale by any restriction plugins.
|
||||
|
||||
:returns:
|
||||
|
||||
* ``False``, if the item is unavailable
|
||||
* the item's price, otherwise
|
||||
"""
|
||||
from pretix.base.signals import determine_availability
|
||||
|
||||
responses = determine_availability.send(
|
||||
self.item.event, item=self.item,
|
||||
variations=[self.to_variation_dict()], context=None,
|
||||
cache=self.item.event.get_cache()
|
||||
)
|
||||
price = self.default_price if self.default_price is not None else self.item.default_price
|
||||
for rec, response in responses:
|
||||
if 'available' in response[0] and not response[0]['available']:
|
||||
return False
|
||||
elif 'price' in response[0] and response[0]['price'] is not None and response[0]['price'] < price:
|
||||
price = response[0]['price']
|
||||
return price
|
||||
|
||||
def add_values_from_string(self, pk):
|
||||
"""
|
||||
Add values to this ItemVariation using a serialized string of the form
|
||||
@@ -672,47 +615,6 @@ class Question(Versionable):
|
||||
self.event.get_cache().clear()
|
||||
|
||||
|
||||
class BaseRestriction(Versionable):
|
||||
"""
|
||||
A restriction is the abstract concept of a rule that limits the availability
|
||||
of Items or ItemVariations. This model is just an abstract base class to be
|
||||
extended by restriction plugins.
|
||||
"""
|
||||
event = VersionedForeignKey(
|
||||
Event,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="restrictions_%(app_label)s_%(class)s",
|
||||
verbose_name=_("Event"),
|
||||
)
|
||||
item = VersionedForeignKey(
|
||||
Item,
|
||||
blank=True, null=True,
|
||||
verbose_name=_("Item"),
|
||||
related_name="restrictions_%(app_label)s_%(class)s",
|
||||
)
|
||||
variations = VariationsField(
|
||||
'pretixbase.ItemVariation',
|
||||
blank=True,
|
||||
verbose_name=_("Variations"),
|
||||
related_name="restrictions_%(app_label)s_%(class)s",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
verbose_name = _("Restriction")
|
||||
verbose_name_plural = _("Restrictions")
|
||||
|
||||
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()
|
||||
|
||||
|
||||
class Quota(Versionable):
|
||||
"""
|
||||
A quota is a "pool of tickets". It is there to limit the number of items
|
||||
|
||||
@@ -90,15 +90,10 @@ def _add_items(event: Event, items: List[Tuple[str, Optional[str], int]],
|
||||
item = items_cache[i[0]]
|
||||
variation = variations_cache[i[1]] if i[1] is not None else None
|
||||
|
||||
# Execute restriction plugins to check whether they (a) change the price or
|
||||
# (b) make the item/variation unavailable. If neither is the case, check_restriction
|
||||
# will correctly return the default price
|
||||
price = item.check_restrictions() if variation is None else variation.check_restrictions()
|
||||
|
||||
# Fetch all quotas. If there are no quotas, this item is not allowed to be sold.
|
||||
quotas = list(item.quotas.all()) if variation is None else list(variation.quotas.all())
|
||||
|
||||
if price is False or len(quotas) == 0 or not item.active:
|
||||
if len(quotas) == 0 or not item.is_available():
|
||||
err = err or error_messages['unavailable']
|
||||
continue
|
||||
|
||||
@@ -121,11 +116,15 @@ def _add_items(event: Event, items: List[Tuple[str, Optional[str], int]],
|
||||
# Recreating
|
||||
cp = i[3].clone()
|
||||
cp.expires = expiry
|
||||
cp.price = price
|
||||
cp.price = item.default_price if variation is None else (
|
||||
variation.default_price if variation.default_price is not None else item.default_price)
|
||||
cp.save()
|
||||
else:
|
||||
CartPosition.objects.create(
|
||||
event=event, item=item, variation=variation, price=price, expires=expiry,
|
||||
event=event, item=item, variation=variation,
|
||||
price=item.default_price if variation is None else (
|
||||
variation.default_price if variation.default_price is not None else item.default_price),
|
||||
expires=expiry,
|
||||
cart_id=cart_id
|
||||
)
|
||||
return err
|
||||
|
||||
@@ -103,7 +103,8 @@ def _check_positions(event: Event, dt: datetime, positions: List[CartPosition]):
|
||||
if cp.expires >= dt:
|
||||
# Other checks are not necessary
|
||||
continue
|
||||
price = cp.item.check_restrictions() if cp.variation is None else cp.variation.check_restrictions()
|
||||
price = cp.item.default_price if cp.variation is None else (
|
||||
cp.variation.default_price if cp.variation.default_price is not None else cp.item.default_price)
|
||||
if price is False or len(quotas) == 0:
|
||||
err = err or error_messages['unavailable']
|
||||
cp.delete()
|
||||
|
||||
@@ -46,15 +46,6 @@ class EventPluginSignal(django.dispatch.Signal):
|
||||
responses.append((receiver, response))
|
||||
return responses
|
||||
|
||||
"""
|
||||
This signal is sent out every time some component of pretix wants to know whether a specific
|
||||
item or variation is available for sell. The item will only be sold, if all (active) receivers
|
||||
return a positive result (see plugin API documentation for details).
|
||||
"""
|
||||
determine_availability = EventPluginSignal(
|
||||
providing_args=["item", "variations", "context", "cache"]
|
||||
)
|
||||
|
||||
"""
|
||||
This signal is sent out to get all known payment providers. Receivers should return a
|
||||
subclass of pretix.base.payment.BasePaymentProvider
|
||||
|
||||
@@ -99,60 +99,6 @@ class TolerantFormsetModelForm(VersionedModelForm):
|
||||
return False
|
||||
|
||||
|
||||
class RestrictionForm(TolerantFormsetModelForm):
|
||||
"""
|
||||
The restriction form provides useful functionality for all forms
|
||||
representing a restriction instance. To be concret, this form does
|
||||
the necessary magic to make the 'variations' field work correctly
|
||||
and look beautiful.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
if 'item' in kwargs:
|
||||
self.item = kwargs['item']
|
||||
del kwargs['item']
|
||||
super().__init__(*args, **kwargs)
|
||||
if 'variations' in self.fields and isinstance(self.fields['variations'], VariationsField):
|
||||
self.fields['variations'].set_item(self.item)
|
||||
|
||||
|
||||
class RestrictionInlineFormset(forms.BaseInlineFormSet):
|
||||
"""
|
||||
This is the base class you should use for any formset you return
|
||||
from a ``restriction_formset`` signal receiver that contains
|
||||
RestrictionForm objects as its forms, as it correcly handles the
|
||||
necessary item parameter for the RestrictionForm. While this could
|
||||
be achieved with a regular formset, this also adds a
|
||||
``initialized_empty_form`` method which is the only way to correctly
|
||||
render a working empty form for a JavaScript-enabled restriction formset.
|
||||
"""
|
||||
|
||||
def __init__(self, data=None, files=None, instance=None,
|
||||
save_as_new=False, prefix=None, queryset=None, **kwargs):
|
||||
super().__init__(
|
||||
data, files, instance, save_as_new, prefix, queryset, **kwargs
|
||||
)
|
||||
if isinstance(self.instance, Item):
|
||||
self.queryset = self.queryset.as_of().prefetch_related("variations")
|
||||
|
||||
def initialized_empty_form(self):
|
||||
form = self.form(
|
||||
auto_id=self.auto_id,
|
||||
prefix=self.add_prefix('__prefix__'),
|
||||
empty_permitted=True,
|
||||
item=self.instance
|
||||
)
|
||||
self.add_fields(form, None)
|
||||
return form
|
||||
|
||||
def _construct_form(self, i, **kwargs):
|
||||
kwargs['item'] = self.instance
|
||||
return super()._construct_form(i, **kwargs)
|
||||
|
||||
class Meta:
|
||||
exclude = ['item']
|
||||
|
||||
|
||||
def selector(values, prop):
|
||||
# Given an iterable of PropertyValue objects, this will return a
|
||||
# list of their primary keys, ordered by the primary keys of the
|
||||
|
||||
@@ -137,6 +137,8 @@ class ItemFormGeneral(VersionedModelForm):
|
||||
'picture',
|
||||
'default_price',
|
||||
'tax_rate',
|
||||
'available_from',
|
||||
'available_until',
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
<li {% if "event.item" == url_name %}class="active"{% endif %}><a href="{% url 'control:event.item' organizer=request.event.organizer.slug event=request.event.slug item=object.identity %}">{% trans "General information" %}</a></li>
|
||||
<li {% if "event.item.properties" == url_name %}class="active"{% endif %}><a href="{% url 'control:event.item.properties' organizer=request.event.organizer.slug event=request.event.slug item=object.identity %}">{% trans "Properties" %}</a></li>
|
||||
<li {% if "event.item.variations" == url_name %}class="active"{% endif %}><a href="{% url 'control:event.item.variations' organizer=request.event.organizer.slug event=request.event.slug item=object.identity %}">{% trans "Variations" %}</a></li>
|
||||
<li {% if "event.item.restrictions" == url_name %}class="active"{% endif %}><a href="{% url 'control:event.item.restrictions' organizer=request.event.organizer.slug event=request.event.slug item=object.identity %}">{% trans "Restrictions" %}</a></li>
|
||||
</ul>
|
||||
{% else %}
|
||||
<h1>{% trans "Create product" %}</h1>
|
||||
|
||||
@@ -18,6 +18,11 @@
|
||||
{% bootstrap_field form.default_price layout="horizontal" %}
|
||||
{% bootstrap_field form.tax_rate layout="horizontal" %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Availability" %}</legend>
|
||||
{% bootstrap_field form.available_from layout="horizontal" %}
|
||||
{% bootstrap_field form.available_until layout="horizontal" %}
|
||||
</fieldset>
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
{% extends "pretixcontrol/item/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% load formset_tags %}
|
||||
{% block inside %}
|
||||
<p>{% blocktrans trimmed %}
|
||||
In this area, you can choose of a set of "restriction types" to restrict the availability of your product with
|
||||
certain conditions.
|
||||
{% endblocktrans %}</p>
|
||||
<form action="" method="post">
|
||||
{% csrf_token %}
|
||||
{% for set in formsets %}
|
||||
<fieldset>
|
||||
<legend>{{ set.title }}</legend>
|
||||
{% bootstrap_formset_errors set %}
|
||||
<p>{{ set.description }}</p>
|
||||
<div data-formset class="restriction-formset" data-formset-prefix="{{ set.formset.prefix }}">
|
||||
<div data-formset-body class="panel-group collapsible" id="accordion_{{ set.formset.prefix }}">
|
||||
{{ set.formset.management_form }}
|
||||
{% for f in set.formset %}
|
||||
<div class="panel panel-default" data-formset-form>
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<a data-toggle="collapse" data-parent="#accordion"
|
||||
href="#collapse{{ f.prefix }}">
|
||||
{{ set.title }}
|
||||
</a>
|
||||
</h4>
|
||||
</div>
|
||||
<div id="collapse{{ f.prefix }}" class="panel-collapse collapse in">
|
||||
<div class="panel-body">
|
||||
<div class="form-horizontal">
|
||||
{% bootstrap_form f layout="horizontal" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<script type="form-template" data-formset-empty-form>
|
||||
{% escapescript %}
|
||||
<div class="panel panel-default" data-formset-form>
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<a data-toggle="collapse" data-parent="#accordion" href="#collapse__prefix__">
|
||||
{% trans "New restriction" %}
|
||||
</a>
|
||||
</h4>
|
||||
</div>
|
||||
<div id="collapse__prefix__" class="panel-collapse collapse in">
|
||||
<div class="panel-body">
|
||||
<div class="form-horizontal">
|
||||
{% bootstrap_form set.formset.initialized_empty_form layout="horizontal" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endescapescript %}
|
||||
</script>
|
||||
<button type="button" class="btn btn-default" data-formset-add><i
|
||||
class="fa fa-plus"></i> {% trans "Add a new restriction" %}</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
{% endfor %}
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{% endblock %}
|
||||
@@ -30,8 +30,6 @@ urlpatterns = [
|
||||
url(r'^items/(?P<item>[0-9a-f-]+)/$', item.ItemUpdateGeneral.as_view(), name='event.item'),
|
||||
url(r'^items/(?P<item>[0-9a-f-]+)/variations$', item.ItemVariations.as_view(),
|
||||
name='event.item.variations'),
|
||||
url(r'^items/(?P<item>[0-9a-f-]+)/restrictions$', item.ItemRestrictions.as_view(),
|
||||
name='event.item.restrictions'),
|
||||
url(r'^items/(?P<item>[0-9a-f-]+)/properties$', item.ItemProperties.as_view(),
|
||||
name='event.item.properties'),
|
||||
url(r'^items/(?P<item>[0-9a-f-]+)/up$', item.item_move_up, name='event.items.up'),
|
||||
|
||||
@@ -740,68 +740,6 @@ class ItemVariations(ItemDetailMixin, EventPermissionRequiredMixin, TemplateView
|
||||
return context
|
||||
|
||||
|
||||
class ItemRestrictions(ItemDetailMixin, EventPermissionRequiredMixin, TemplateView):
|
||||
permission = 'can_change_items'
|
||||
template_name = 'pretixcontrol/item/restrictions.html'
|
||||
|
||||
def get_formsets(self):
|
||||
responses = restriction_formset.send(self.object.event, item=self.object)
|
||||
formsets = []
|
||||
for receiver, response in responses:
|
||||
response['formset'] = response['formsetclass'](
|
||||
self.request.POST if self.request.method == 'POST' else None,
|
||||
instance=self.object,
|
||||
prefix=response['prefix'],
|
||||
)
|
||||
formsets.append(response)
|
||||
return formsets
|
||||
|
||||
def main(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
self.request = request
|
||||
self.formsets = self.get_formsets()
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
self.main(request, *args, **kwargs)
|
||||
context = self.get_context_data(object=self.object)
|
||||
return self.render_to_response(context)
|
||||
|
||||
@transaction.atomic()
|
||||
def post(self, request, *args, **kwargs):
|
||||
self.main(request, *args, **kwargs)
|
||||
valid = True
|
||||
for f in self.formsets:
|
||||
valid &= f['formset'].is_valid()
|
||||
if valid:
|
||||
for f in self.formsets:
|
||||
for form in f['formset']:
|
||||
if 'DELETE' in form.cleaned_data and form.cleaned_data['DELETE'] is True:
|
||||
if form.instance.pk is None:
|
||||
continue
|
||||
form.instance.delete()
|
||||
else:
|
||||
form.instance.event = request.event
|
||||
form.instance.item = self.object
|
||||
form.save()
|
||||
messages.success(self.request, _('Your changes have been saved.'))
|
||||
return redirect(self.get_success_url())
|
||||
else:
|
||||
context = self.get_context_data(object=self.object)
|
||||
return self.render_to_response(context)
|
||||
|
||||
def get_context_data(self, *args, **kwargs) -> dict:
|
||||
context = super().get_context_data(*args, **kwargs)
|
||||
context['formsets'] = self.formsets
|
||||
return context
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
return reverse('control:event.item.restrictions', kwargs={
|
||||
'organizer': self.request.event.organizer.slug,
|
||||
'event': self.request.event.slug,
|
||||
'item': self.object.identity
|
||||
})
|
||||
|
||||
|
||||
class ItemDelete(EventPermissionRequiredMixin, DeleteView):
|
||||
model = Item
|
||||
template_name = 'pretixcontrol/item/delete.html'
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
from django.apps import AppConfig
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from pretix import __version__ as version
|
||||
from pretix.base.plugins import PluginType
|
||||
|
||||
|
||||
class TimeRestrictionApp(AppConfig):
|
||||
name = 'pretix.plugins.timerestriction'
|
||||
verbose_name = _("Time restriction")
|
||||
|
||||
class PretixPluginMeta:
|
||||
type = PluginType.RESTRICTION
|
||||
name = _("Restriction by time")
|
||||
author = _("the pretix team")
|
||||
version = version
|
||||
description = _("This plugin adds the possibility to restrict the sale " +
|
||||
"of a given product or variation to a certain timeframe " +
|
||||
"or change its price during a certain period.")
|
||||
|
||||
def ready(self):
|
||||
from . import signals # NOQA
|
||||
|
||||
default_app_config = 'pretix.plugins.timerestriction.TimeRestrictionApp'
|
||||
@@ -1,38 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import versions.models
|
||||
from django.db import migrations, models
|
||||
|
||||
import pretix.base.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '__first__'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='TimeRestriction',
|
||||
fields=[
|
||||
('id', models.CharField(serialize=False, max_length=36, primary_key=True)),
|
||||
('identity', models.CharField(max_length=36)),
|
||||
('version_start_date', models.DateTimeField()),
|
||||
('version_end_date', models.DateTimeField(null=True, blank=True, default=None)),
|
||||
('version_birth_date', models.DateTimeField()),
|
||||
('timeframe_from', models.DateTimeField(verbose_name='Start of time frame')),
|
||||
('timeframe_to', models.DateTimeField(verbose_name='End of time frame')),
|
||||
('price', models.DecimalField(decimal_places=2, max_digits=7, null=True, blank=True, verbose_name='Price in time frame')),
|
||||
('event', versions.models.VersionedForeignKey(verbose_name='Event', to='pretixbase.Event', related_name='restrictions_timerestriction_timerestriction')),
|
||||
('item', versions.models.VersionedForeignKey(related_name='restrictions_timerestriction_timerestriction', null=True, verbose_name='Item', to='pretixbase.Item', blank=True)),
|
||||
('variations', pretix.base.models.VariationsField(related_name='restrictions_timerestriction_timerestriction', blank=True, to='pretixbase.ItemVariation', verbose_name='Variations')),
|
||||
],
|
||||
options={
|
||||
'verbose_name_plural': 'Restrictions',
|
||||
'abstract': False,
|
||||
'verbose_name': 'Restriction',
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -1,24 +0,0 @@
|
||||
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"),
|
||||
)
|
||||
@@ -1,154 +0,0 @@
|
||||
from django.dispatch import receiver
|
||||
from django.forms.models import inlineformset_factory
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from pretix.base.models import Item
|
||||
from pretix.base.signals import determine_availability
|
||||
from pretix.control.forms import RestrictionForm, RestrictionInlineFormset
|
||||
from pretix.control.signals import restriction_formset
|
||||
|
||||
from .models import TimeRestriction
|
||||
|
||||
|
||||
# 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()
|
||||
|
||||
|
||||
@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.current.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]
|
||||
|
||||
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:%s:%s' % (
|
||||
item.identity,
|
||||
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
|
||||
|
||||
|
||||
class TimeRestrictionForm(RestrictionForm):
|
||||
|
||||
class Meta:
|
||||
model = TimeRestriction
|
||||
localized_fields = '__all__'
|
||||
fields = [
|
||||
'variations',
|
||||
'timeframe_from',
|
||||
'timeframe_to',
|
||||
'price',
|
||||
]
|
||||
|
||||
|
||||
@receiver(restriction_formset, dispatch_uid="restriction_time_formset")
|
||||
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. You can also change the price of '
|
||||
'variations for within the given timeframe. Please note, that if you change the price of '
|
||||
'variations here, this will overrule the price set in the "Variations" section.'
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import sys
|
||||
|
||||
from django.db.models import Count
|
||||
from django.db.models import Q, Count
|
||||
from django.utils.timezone import now
|
||||
from django.views.generic import TemplateView
|
||||
|
||||
from pretix.presale.views import CartMixin, EventViewMixin
|
||||
@@ -13,7 +14,9 @@ class EventIndex(EventViewMixin, CartMixin, TemplateView):
|
||||
context = super().get_context_data(**kwargs)
|
||||
# Fetch all items
|
||||
items = self.request.event.items.all().filter(
|
||||
active=True
|
||||
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()))
|
||||
).select_related(
|
||||
'category', # for re-grouping
|
||||
).prefetch_related(
|
||||
|
||||
@@ -149,7 +149,6 @@ INSTALLED_APPS = [
|
||||
'compressor',
|
||||
'bootstrap3',
|
||||
'djangoformsetjs',
|
||||
'pretix.plugins.timerestriction',
|
||||
'pretix.plugins.banktransfer',
|
||||
'pretix.plugins.stripe',
|
||||
'pretix.plugins.paypal',
|
||||
|
||||
@@ -425,6 +425,40 @@ class ItemCategoryTest(TestCase):
|
||||
assert c2 > c1
|
||||
|
||||
|
||||
class ItemTest(TestCase):
|
||||
"""
|
||||
This test case tests various methods around the item model.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
o = Organizer.objects.create(name='Dummy', slug='dummy')
|
||||
cls.event = Event.objects.create(
|
||||
organizer=o, name='Dummy', slug='dummy',
|
||||
date_from=now(),
|
||||
)
|
||||
|
||||
def test_is_available(self):
|
||||
i = Item.objects.create(
|
||||
event=self.event, name="Ticket", default_price=23,
|
||||
active=True, available_until=now() + timedelta(days=1),
|
||||
)
|
||||
assert i.is_available()
|
||||
i.available_from = now() - timedelta(days=1)
|
||||
assert i.is_available()
|
||||
i.available_from = now() + timedelta(days=1)
|
||||
i.available_until = None
|
||||
assert not i.is_available()
|
||||
i.available_from = None
|
||||
i.available_until = now() - timedelta(days=1)
|
||||
assert not i.is_available()
|
||||
i.available_from = None
|
||||
i.available_until = None
|
||||
assert i.is_available()
|
||||
i.active = False
|
||||
assert not i.is_available()
|
||||
|
||||
|
||||
class CachedFileTestCase(TestCase):
|
||||
def test_file_handling(self):
|
||||
cf = CachedFile()
|
||||
|
||||
@@ -4,7 +4,7 @@ from django.utils.timezone import now
|
||||
|
||||
from pretix.base.models import Event, Organizer
|
||||
from pretix.base.plugins import get_all_plugins
|
||||
from pretix.base.signals import determine_availability
|
||||
from pretix.base.signals import register_ticket_outputs
|
||||
|
||||
|
||||
class PluginRegistryTest(TestCase):
|
||||
@@ -45,13 +45,13 @@ class PluginSignalTest(TestCase):
|
||||
def test_no_plugins_active(self):
|
||||
self.event.plugins = ''
|
||||
self.event.save()
|
||||
responses = determine_availability.send(self.event)
|
||||
responses = register_ticket_outputs.send(self.event)
|
||||
self.assertEqual(len(responses), 0)
|
||||
|
||||
def test_one_plugin_active(self):
|
||||
self.event.plugins = 'tests.testdummy'
|
||||
self.event.save()
|
||||
payload = {'foo': 'bar'}
|
||||
responses = determine_availability.send(self.event, **payload)
|
||||
responses = register_ticket_outputs.send(self.event, **payload)
|
||||
self.assertEqual(len(responses), 1)
|
||||
self.assertIn('tests.testdummy.signals', [r[0].__module__ for r in responses])
|
||||
|
||||
@@ -66,9 +66,9 @@ class EventsTest(BrowserTest):
|
||||
def test_plugins(self):
|
||||
self.driver.get('%s/control/event/%s/%s/settings/plugins' % (self.live_server_url, self.orga1.slug,
|
||||
self.event1.slug))
|
||||
self.assertIn("Restriction by time", self.driver.find_element_by_class_name("form-plugins").text)
|
||||
self.assertIn("Enable", self.driver.find_element_by_name("plugin:pretix.plugins.timerestriction").text)
|
||||
self.driver.find_element_by_name("plugin:pretix.plugins.timerestriction").click()
|
||||
self.assertIn("Disable", self.driver.find_element_by_name("plugin:pretix.plugins.timerestriction").text)
|
||||
self.driver.find_element_by_name("plugin:pretix.plugins.timerestriction").click()
|
||||
self.assertIn("Enable", self.driver.find_element_by_name("plugin:pretix.plugins.timerestriction").text)
|
||||
self.assertIn("Bank transfer", self.driver.find_element_by_class_name("form-plugins").text)
|
||||
self.assertIn("Enable", self.driver.find_element_by_name("plugin:pretix.plugins.banktransfer").text)
|
||||
self.driver.find_element_by_name("plugin:pretix.plugins.banktransfer").click()
|
||||
self.assertIn("Disable", self.driver.find_element_by_name("plugin:pretix.plugins.banktransfer").text)
|
||||
self.driver.find_element_by_name("plugin:pretix.plugins.banktransfer").click()
|
||||
self.assertIn("Enable", self.driver.find_element_by_name("plugin:pretix.plugins.banktransfer").text)
|
||||
|
||||
@@ -36,7 +36,6 @@ event_urls = [
|
||||
"items/abc/",
|
||||
"items/abc/variations",
|
||||
"items/abc/properties",
|
||||
"items/abc/restrictions",
|
||||
"categories/",
|
||||
"categories/add",
|
||||
"categories/abc/",
|
||||
|
||||
@@ -1,289 +0,0 @@
|
||||
from datetime import timedelta
|
||||
|
||||
from django.test import TestCase
|
||||
from django.utils.timezone import now
|
||||
|
||||
from pretix.base.models import (
|
||||
Event, Item, ItemVariation, Organizer, Property, PropertyValue,
|
||||
)
|
||||
# Do NOT use relative imports here
|
||||
from pretix.plugins.timerestriction import signals
|
||||
from pretix.plugins.timerestriction.models import TimeRestriction
|
||||
|
||||
|
||||
class TimeRestrictionTest(TestCase):
|
||||
"""
|
||||
This test case tests the various aspects of the time restriction
|
||||
plugin
|
||||
"""
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
o = Organizer.objects.create(name='Dummy', slug='dummy')
|
||||
cls.event = Event.objects.create(
|
||||
organizer=o, name='Dummy', slug='dummy',
|
||||
date_from=now(),
|
||||
)
|
||||
cls.item = Item.objects.create(event=cls.event, name='Dummy', default_price=14)
|
||||
cls.property = Property.objects.create(event=cls.event, name='Size')
|
||||
cls.value1 = PropertyValue.objects.create(prop=cls.property, value='S')
|
||||
cls.value2 = PropertyValue.objects.create(prop=cls.property, value='M')
|
||||
cls.value3 = PropertyValue.objects.create(prop=cls.property, value='L')
|
||||
cls.variation1 = ItemVariation.objects.create(item=cls.item)
|
||||
cls.variation1.values.add(cls.value1)
|
||||
cls.variation2 = ItemVariation.objects.create(item=cls.item)
|
||||
cls.variation2.values.add(cls.value2)
|
||||
cls.variation3 = ItemVariation.objects.create(item=cls.item)
|
||||
cls.variation3.values.add(cls.value3)
|
||||
|
||||
def test_nothing(self):
|
||||
result = signals.availability_handler(
|
||||
None, item=self.item,
|
||||
variations=self.item.get_all_variations(),
|
||||
context=None, cache=self.event.get_cache()
|
||||
)
|
||||
self.assertEqual(len(result), 1)
|
||||
self.assertTrue('available' not in result[0] or result[0]['available'] is True)
|
||||
|
||||
def test_simple_case_available(self):
|
||||
r = TimeRestriction.objects.create(
|
||||
timeframe_from=now() - timedelta(days=3),
|
||||
timeframe_to=now() + timedelta(days=3),
|
||||
event=self.event,
|
||||
price=12
|
||||
)
|
||||
r.item = self.item
|
||||
r.save()
|
||||
result = signals.availability_handler(
|
||||
self.event, item=self.item,
|
||||
variations=self.item.get_all_variations(),
|
||||
context=None, cache=self.event.get_cache()
|
||||
)
|
||||
self.assertEqual(len(result), 1)
|
||||
self.assertIn('available', result[0])
|
||||
self.assertTrue(result[0]['available'])
|
||||
self.assertEqual(result[0]['price'], 12)
|
||||
|
||||
def test_cached_result(self):
|
||||
r = TimeRestriction.objects.create(
|
||||
timeframe_from=now() - timedelta(days=3),
|
||||
timeframe_to=now() + timedelta(days=3),
|
||||
event=self.event,
|
||||
price=12
|
||||
)
|
||||
r.item = self.item
|
||||
r.save()
|
||||
result = signals.availability_handler(
|
||||
self.event, item=self.item,
|
||||
variations=self.item.get_all_variations(),
|
||||
context=None, cache=self.event.get_cache()
|
||||
)
|
||||
self.assertEqual(len(result), 1)
|
||||
self.assertIn('available', result[0])
|
||||
self.assertTrue(result[0]['available'])
|
||||
self.assertEqual(result[0]['price'], 12)
|
||||
result = signals.availability_handler(
|
||||
self.event, item=self.item,
|
||||
variations=self.item.get_all_variations(),
|
||||
context=None, cache=self.event.get_cache()
|
||||
)
|
||||
self.assertEqual(len(result), 1)
|
||||
self.assertIn('available', result[0])
|
||||
self.assertTrue(result[0]['available'])
|
||||
self.assertEqual(result[0]['price'], 12)
|
||||
|
||||
def test_simple_case_unavailable(self):
|
||||
r = TimeRestriction.objects.create(
|
||||
timeframe_from=now() - timedelta(days=5),
|
||||
timeframe_to=now() - timedelta(days=3),
|
||||
event=self.event,
|
||||
price=12
|
||||
)
|
||||
r.item = self.item
|
||||
r.save()
|
||||
result = signals.availability_handler(
|
||||
self.event, item=self.item,
|
||||
variations=self.item.get_all_variations(),
|
||||
context=None, cache=self.event.get_cache()
|
||||
)
|
||||
self.assertEqual(len(result), 1)
|
||||
self.assertIn('available', result[0])
|
||||
self.assertFalse(result[0]['available'])
|
||||
|
||||
def test_multiple_overlapping_now(self):
|
||||
r1 = TimeRestriction.objects.create(
|
||||
timeframe_from=now() - timedelta(days=5),
|
||||
timeframe_to=now() + timedelta(days=3),
|
||||
event=self.event,
|
||||
price=12
|
||||
)
|
||||
r1.item = self.item
|
||||
r1.save()
|
||||
r2 = TimeRestriction.objects.create(
|
||||
timeframe_from=now() - timedelta(days=3),
|
||||
timeframe_to=now() + timedelta(days=5),
|
||||
event=self.event,
|
||||
price=8
|
||||
)
|
||||
r2.item = self.item
|
||||
r2.save()
|
||||
result = signals.availability_handler(
|
||||
self.event, item=self.item,
|
||||
variations=self.item.get_all_variations(),
|
||||
context=None, cache=self.event.get_cache()
|
||||
)
|
||||
self.assertEqual(len(result), 1)
|
||||
self.assertIn('available', result[0])
|
||||
self.assertTrue(result[0]['available'])
|
||||
self.assertEqual(result[0]['price'], 8)
|
||||
|
||||
def test_multiple_overlapping_tomorrow(self):
|
||||
r1 = TimeRestriction.objects.create(
|
||||
timeframe_from=now() - timedelta(days=5),
|
||||
timeframe_to=now() + timedelta(days=5),
|
||||
event=self.event,
|
||||
price=12
|
||||
)
|
||||
r1.item = self.item
|
||||
r1.save()
|
||||
r2 = TimeRestriction.objects.create(
|
||||
timeframe_from=now() + timedelta(days=1),
|
||||
timeframe_to=now() + timedelta(days=7),
|
||||
event=self.event,
|
||||
price=8
|
||||
)
|
||||
r2.item = self.item
|
||||
r2.save()
|
||||
result = signals.availability_handler(
|
||||
self.event, item=self.item,
|
||||
variations=self.item.get_all_variations(),
|
||||
context=None, cache=self.event.get_cache()
|
||||
)
|
||||
self.assertEqual(len(result), 1)
|
||||
self.assertIn('available', result[0])
|
||||
self.assertTrue(result[0]['available'])
|
||||
self.assertEqual(result[0]['price'], 12)
|
||||
|
||||
def test_multiple_distinct_available(self):
|
||||
r1 = TimeRestriction.objects.create(
|
||||
timeframe_from=now() - timedelta(days=5),
|
||||
timeframe_to=now() + timedelta(days=2),
|
||||
event=self.event,
|
||||
price=12
|
||||
)
|
||||
r1.item = self.item
|
||||
r1.save()
|
||||
r2 = TimeRestriction.objects.create(
|
||||
timeframe_from=now() + timedelta(days=4),
|
||||
timeframe_to=now() + timedelta(days=7),
|
||||
event=self.event,
|
||||
price=8
|
||||
)
|
||||
r2.item = self.item
|
||||
r2.save()
|
||||
result = signals.availability_handler(
|
||||
self.event, item=self.item,
|
||||
variations=self.item.get_all_variations(),
|
||||
context=None, cache=self.event.get_cache()
|
||||
)
|
||||
self.assertEqual(len(result), 1)
|
||||
self.assertIn('available', result[0])
|
||||
self.assertTrue(result[0]['available'])
|
||||
self.assertEqual(result[0]['price'], 12)
|
||||
|
||||
def test_multiple_distinct_unavailable(self):
|
||||
r1 = TimeRestriction.objects.create(
|
||||
timeframe_from=now() - timedelta(days=5),
|
||||
timeframe_to=now() - timedelta(days=1),
|
||||
event=self.event,
|
||||
price=12
|
||||
)
|
||||
r1.item = self.item
|
||||
r1.save()
|
||||
r2 = TimeRestriction.objects.create(
|
||||
timeframe_from=now() + timedelta(days=4),
|
||||
timeframe_to=now() + timedelta(days=7),
|
||||
event=self.event,
|
||||
price=8
|
||||
)
|
||||
r2.item = self.item
|
||||
r2.save()
|
||||
result = signals.availability_handler(
|
||||
self.event, item=self.item,
|
||||
variations=self.item.get_all_variations(),
|
||||
context=None, cache=self.event.get_cache()
|
||||
)
|
||||
self.assertEqual(len(result), 1)
|
||||
self.assertIn('available', result[0])
|
||||
self.assertFalse(result[0]['available'])
|
||||
|
||||
def test_variation_specific(self):
|
||||
self.property.item = self.item
|
||||
self.property.save()
|
||||
|
||||
r1 = TimeRestriction.objects.create(
|
||||
timeframe_from=now() - timedelta(days=5),
|
||||
timeframe_to=now() + timedelta(days=1),
|
||||
event=self.event,
|
||||
price=12
|
||||
)
|
||||
r1.item = self.item
|
||||
r1.save()
|
||||
r1.variations.add(self.variation1)
|
||||
result = signals.availability_handler(
|
||||
self.event, item=self.item,
|
||||
variations=self.item.get_all_variations(),
|
||||
context=None, cache=self.event.get_cache()
|
||||
)
|
||||
self.assertEqual(len(result), 3)
|
||||
for v in result:
|
||||
if 'variation' in v and v['variation'].pk == self.variation1.pk:
|
||||
self.assertTrue(v['available'])
|
||||
self.assertEqual(v['price'], 12)
|
||||
else:
|
||||
self.assertTrue(v['available'])
|
||||
|
||||
def test_variation_specifics(self):
|
||||
self.property.item = self.item
|
||||
self.property.save()
|
||||
|
||||
r1 = TimeRestriction.objects.create(
|
||||
timeframe_from=now() - timedelta(days=5),
|
||||
timeframe_to=now() + timedelta(days=1),
|
||||
event=self.event,
|
||||
price=12
|
||||
)
|
||||
r1.item = self.item
|
||||
r1.save()
|
||||
r1.variations.add(self.variation1)
|
||||
r2 = TimeRestriction.objects.create(
|
||||
timeframe_from=now() - timedelta(days=5),
|
||||
timeframe_to=now() + timedelta(days=1),
|
||||
event=self.event,
|
||||
price=8
|
||||
)
|
||||
r2.item = self.item
|
||||
r2.save()
|
||||
r2.variations.add(self.variation1)
|
||||
r3 = TimeRestriction.objects.create(
|
||||
timeframe_from=now() - timedelta(days=5),
|
||||
timeframe_to=now() - timedelta(days=1),
|
||||
event=self.event,
|
||||
price=8
|
||||
)
|
||||
r3.item = self.item
|
||||
r3.save()
|
||||
r3.variations.add(self.variation3)
|
||||
result = signals.availability_handler(
|
||||
self.event, item=self.item,
|
||||
variations=self.item.get_all_variations(),
|
||||
context=None, cache=self.event.get_cache()
|
||||
)
|
||||
self.assertEqual(len(result), 3)
|
||||
for v in result:
|
||||
if 'variation' in v and v['variation'].pk == self.variation1.pk:
|
||||
self.assertTrue(v['available'])
|
||||
self.assertEqual(v['price'], 8)
|
||||
elif 'variation' in v and v['variation'].pk == self.variation3.pk:
|
||||
self.assertFalse(v['available'])
|
||||
else:
|
||||
self.assertTrue(v['available'])
|
||||
@@ -1,12 +1,10 @@
|
||||
import datetime
|
||||
import time
|
||||
from datetime import timedelta
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
from django.conf import settings
|
||||
from django.test import TestCase
|
||||
from django.utils.timezone import now
|
||||
from tests.base import BrowserTest
|
||||
|
||||
from pretix.base.models import (
|
||||
CartPosition, Event, Item, ItemCategory, ItemVariation, Organizer,
|
||||
@@ -158,6 +156,31 @@ class CartTest(CartTestMixin, TestCase):
|
||||
self.assertIn('no longer available', doc.select('.alert-danger')[0].text)
|
||||
self.assertFalse(CartPosition.objects.filter(cart_id=self.session_key, event=self.event).exists())
|
||||
|
||||
def test_in_time_available(self):
|
||||
self.ticket.available_until = now() + timedelta(days=2)
|
||||
self.ticket.available_from = now() - timedelta(days=2)
|
||||
self.ticket.save()
|
||||
self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
|
||||
'item_' + self.ticket.identity: '1',
|
||||
}, follow=True)
|
||||
self.assertEqual(CartPosition.objects.filter(cart_id=self.session_key, event=self.event).count(), 1)
|
||||
|
||||
def test_no_longer_available(self):
|
||||
self.ticket.available_until = now() - timedelta(days=2)
|
||||
self.ticket.save()
|
||||
self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
|
||||
'item_' + self.ticket.identity: '1',
|
||||
}, follow=True)
|
||||
self.assertEqual(CartPosition.objects.filter(cart_id=self.session_key, event=self.event).count(), 0)
|
||||
|
||||
def test_not_yet_available(self):
|
||||
self.ticket.available_from = now() + timedelta(days=2)
|
||||
self.ticket.save()
|
||||
self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
|
||||
'item_' + self.ticket.identity: '1',
|
||||
}, follow=True)
|
||||
self.assertEqual(CartPosition.objects.filter(cart_id=self.session_key, event=self.event).count(), 0)
|
||||
|
||||
def test_max_items(self):
|
||||
CartPosition.objects.create(
|
||||
event=self.event, cart_id=self.session_key, item=self.ticket,
|
||||
@@ -262,34 +285,6 @@ class CartTest(CartTestMixin, TestCase):
|
||||
self.assertIn('no longer available', doc.select('.alert-danger')[0].text)
|
||||
self.assertFalse(CartPosition.objects.current.filter(identity=cp1.identity).exists())
|
||||
|
||||
def test_restriction_ok(self):
|
||||
self.event.plugins = 'tests.testdummy'
|
||||
self.event.save()
|
||||
self.event.settings.testdummy_available = 'yes'
|
||||
response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
|
||||
'item_' + self.ticket.identity: '1',
|
||||
}, follow=True)
|
||||
self.assertRedirects(response, '/%s/%s/' % (self.orga.slug, self.event.slug),
|
||||
target_status_code=200)
|
||||
objs = list(CartPosition.objects.current.filter(cart_id=self.session_key, event=self.event))
|
||||
self.assertEqual(len(objs), 1)
|
||||
self.assertEqual(objs[0].item, self.ticket)
|
||||
self.assertIsNone(objs[0].variation)
|
||||
self.assertEqual(objs[0].price, 23)
|
||||
|
||||
def test_restriction_failed(self):
|
||||
self.event.plugins = 'tests.testdummy'
|
||||
self.event.save()
|
||||
self.event.settings.testdummy_available = 'no'
|
||||
response = self.client.post('/%s/%s/cart/add' % (self.orga.slug, self.event.slug), {
|
||||
'item_' + self.ticket.identity: '1',
|
||||
}, follow=True)
|
||||
self.assertRedirects(response, '/%s/%s/' % (self.orga.slug, self.event.slug),
|
||||
target_status_code=200)
|
||||
doc = BeautifulSoup(response.rendered_content)
|
||||
self.assertIn('no longer available', doc.select('.alert-danger')[0].text)
|
||||
self.assertFalse(CartPosition.objects.filter(cart_id=self.session_key, event=self.event).exists())
|
||||
|
||||
def test_remove_simple(self):
|
||||
CartPosition.objects.create(
|
||||
event=self.event, cart_id=self.session_key, item=self.ticket,
|
||||
|
||||
@@ -54,6 +54,31 @@ class ItemDisplayTest(EventTestMixin, BrowserTest):
|
||||
self.driver.get('%s/%s/%s/' % (self.live_server_url, self.orga.slug, self.event.slug))
|
||||
self.assertIn("Early-bird", self.driver.find_element_by_css_selector("section .product-row:first-child").text)
|
||||
|
||||
def test_timely_available(self):
|
||||
q = Quota.objects.create(event=self.event, name='Quota', size=2)
|
||||
item = Item.objects.create(event=self.event, name='Early-bird ticket', default_price=0, active=True,
|
||||
available_until=now() + datetime.timedelta(days=2),
|
||||
available_from=now() - datetime.timedelta(days=2))
|
||||
q.items.add(item)
|
||||
self.driver.get('%s/%s/%s/' % (self.live_server_url, self.orga.slug, self.event.slug))
|
||||
self.assertIn("Early-bird", self.driver.find_element_by_css_selector("body").text)
|
||||
|
||||
def test_no_longer_available(self):
|
||||
q = Quota.objects.create(event=self.event, name='Quota', size=2)
|
||||
item = Item.objects.create(event=self.event, name='Early-bird ticket', default_price=0, active=True,
|
||||
available_until=now() - datetime.timedelta(days=2))
|
||||
q.items.add(item)
|
||||
self.driver.get('%s/%s/%s/' % (self.live_server_url, self.orga.slug, self.event.slug))
|
||||
self.assertNotIn("Early-bird", self.driver.find_element_by_css_selector("body").text)
|
||||
|
||||
def test_not_yet_available(self):
|
||||
q = Quota.objects.create(event=self.event, name='Quota', size=2)
|
||||
item = Item.objects.create(event=self.event, name='Early-bird ticket', default_price=0, active=True,
|
||||
available_from=now() + datetime.timedelta(days=2))
|
||||
q.items.add(item)
|
||||
self.driver.get('%s/%s/%s/' % (self.live_server_url, self.orga.slug, self.event.slug))
|
||||
self.assertNotIn("Early-bird", self.driver.find_element_by_css_selector("body").text)
|
||||
|
||||
def test_simple_with_category(self):
|
||||
c = ItemCategory.objects.create(event=self.event, name="Entry tickets", position=0)
|
||||
q = Quota.objects.create(event=self.event, name='Quota', size=2)
|
||||
|
||||
@@ -1,18 +1,6 @@
|
||||
from django.dispatch import receiver
|
||||
|
||||
from pretix.base.signals import determine_availability, register_ticket_outputs
|
||||
|
||||
|
||||
@receiver(determine_availability, dispatch_uid="restriction_dummy")
|
||||
def availability_handler(sender, **kwargs):
|
||||
kwargs['sender'] = sender
|
||||
if sender.settings.testdummy_available is not None:
|
||||
variations = kwargs['variations']
|
||||
variations = [d.copy() for d in variations]
|
||||
for v in variations:
|
||||
v['available'] = (sender.settings.testdummy_available == 'yes')
|
||||
return variations
|
||||
return []
|
||||
from pretix.base.signals import register_ticket_outputs
|
||||
|
||||
|
||||
@receiver(register_ticket_outputs, dispatch_uid="output_dummy")
|
||||
|
||||
Reference in New Issue
Block a user