diff --git a/doc/development/api/index.rst b/doc/development/api/index.rst index 3722efe524..1cdd44f3b5 100644 --- a/doc/development/api/index.rst +++ b/doc/development/api/index.rst @@ -7,7 +7,6 @@ Contents: :maxdepth: 2 plugins - restriction payment ticketoutput exporter diff --git a/doc/development/api/restriction.rst b/doc/development/api/restriction.rst deleted file mode 100644 index 19daf98804..0000000000 --- a/doc/development/api/restriction.rst +++ /dev/null @@ -1,306 +0,0 @@ -.. highlight:: python - :linenothreshold: 5 - -Writing a restriction plugin -============================ - -Please make sure you have read and understood the :ref:`basic idea ` 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 ` 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/ diff --git a/doc/development/concepts.rst b/doc/development/concepts.rst index ca3548b7dc..32de84a733 100644 --- a/doc/development/concepts.rst +++ b/doc/development/concepts.rst @@ -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 """"""""""""""""""""" diff --git a/src/pretix/base/models/__init__.py b/src/pretix/base/models/__init__.py index bf884178df..b9fedaa166 100644 --- a/src/pretix/base/models/__init__.py +++ b/src/pretix/base/models/__init__.py @@ -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' ] diff --git a/src/pretix/base/models/items.py b/src/pretix/base/models/items.py index e55b2575cf..d8ecca922e 100644 --- a/src/pretix/base/models/items.py +++ b/src/pretix/base/models/items.py @@ -80,8 +80,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. @@ -249,10 +247,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 +265,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 +298,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 +410,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 +473,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 +589,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 diff --git a/src/pretix/base/services/cart.py b/src/pretix/base/services/cart.py index 748d1c96b8..b7b6ca6de2 100644 --- a/src/pretix/base/services/cart.py +++ b/src/pretix/base/services/cart.py @@ -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.active: 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 diff --git a/src/pretix/base/services/orders.py b/src/pretix/base/services/orders.py index 557cf62bf8..94abb00fcb 100644 --- a/src/pretix/base/services/orders.py +++ b/src/pretix/base/services/orders.py @@ -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() diff --git a/src/pretix/base/signals.py b/src/pretix/base/signals.py index a40fa8707f..c9ec4ec964 100644 --- a/src/pretix/base/signals.py +++ b/src/pretix/base/signals.py @@ -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 diff --git a/src/pretix/control/forms/__init__.py b/src/pretix/control/forms/__init__.py index 97457f4d10..1966f65a23 100644 --- a/src/pretix/control/forms/__init__.py +++ b/src/pretix/control/forms/__init__.py @@ -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 diff --git a/src/pretix/control/templates/pretixcontrol/item/base.html b/src/pretix/control/templates/pretixcontrol/item/base.html index 646d812818..43eaadc09e 100644 --- a/src/pretix/control/templates/pretixcontrol/item/base.html +++ b/src/pretix/control/templates/pretixcontrol/item/base.html @@ -8,7 +8,6 @@
  • {% trans "General information" %}
  • {% trans "Properties" %}
  • {% trans "Variations" %}
  • -
  • {% trans "Restrictions" %}
  • {% else %}

    {% trans "Create product" %}

    diff --git a/src/pretix/control/templates/pretixcontrol/item/restrictions.html b/src/pretix/control/templates/pretixcontrol/item/restrictions.html deleted file mode 100644 index 59465f7975..0000000000 --- a/src/pretix/control/templates/pretixcontrol/item/restrictions.html +++ /dev/null @@ -1,72 +0,0 @@ -{% extends "pretixcontrol/item/base.html" %} -{% load i18n %} -{% load bootstrap3 %} -{% load formset_tags %} -{% block inside %} -

    {% 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 %}

    -
    - {% csrf_token %} - {% for set in formsets %} -
    - {{ set.title }} - {% bootstrap_formset_errors set %} -

    {{ set.description }}

    -
    -
    - {{ set.formset.management_form }} - {% for f in set.formset %} -
    - -
    -
    -
    - {% bootstrap_form f layout="horizontal" %} -
    -
    -
    -
    - {% endfor %} -
    - - -
    -
    - {% endfor %} -
    - -
    -
    - -{% endblock %} diff --git a/src/pretix/control/urls.py b/src/pretix/control/urls.py index 41f259979a..4517fae3ed 100644 --- a/src/pretix/control/urls.py +++ b/src/pretix/control/urls.py @@ -30,8 +30,6 @@ urlpatterns = [ url(r'^items/(?P[0-9a-f-]+)/$', item.ItemUpdateGeneral.as_view(), name='event.item'), url(r'^items/(?P[0-9a-f-]+)/variations$', item.ItemVariations.as_view(), name='event.item.variations'), - url(r'^items/(?P[0-9a-f-]+)/restrictions$', item.ItemRestrictions.as_view(), - name='event.item.restrictions'), url(r'^items/(?P[0-9a-f-]+)/properties$', item.ItemProperties.as_view(), name='event.item.properties'), url(r'^items/(?P[0-9a-f-]+)/up$', item.item_move_up, name='event.items.up'), diff --git a/src/pretix/control/views/item.py b/src/pretix/control/views/item.py index 1c68f0413e..bdfb49b8cc 100644 --- a/src/pretix/control/views/item.py +++ b/src/pretix/control/views/item.py @@ -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' diff --git a/src/pretix/plugins/timerestriction/__init__.py b/src/pretix/plugins/timerestriction/__init__.py deleted file mode 100644 index 91f12c91cb..0000000000 --- a/src/pretix/plugins/timerestriction/__init__.py +++ /dev/null @@ -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' diff --git a/src/pretix/plugins/timerestriction/migrations/0001_initial.py b/src/pretix/plugins/timerestriction/migrations/0001_initial.py deleted file mode 100644 index a6f1bab936..0000000000 --- a/src/pretix/plugins/timerestriction/migrations/0001_initial.py +++ /dev/null @@ -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', - }, - ), - ] diff --git a/src/pretix/plugins/timerestriction/migrations/__init__.py b/src/pretix/plugins/timerestriction/migrations/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/pretix/plugins/timerestriction/models.py b/src/pretix/plugins/timerestriction/models.py deleted file mode 100644 index e3ba537a4b..0000000000 --- a/src/pretix/plugins/timerestriction/models.py +++ /dev/null @@ -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"), - ) diff --git a/src/pretix/plugins/timerestriction/signals.py b/src/pretix/plugins/timerestriction/signals.py deleted file mode 100644 index 39a12087fb..0000000000 --- a/src/pretix/plugins/timerestriction/signals.py +++ /dev/null @@ -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.' - } diff --git a/src/pretix/settings.py b/src/pretix/settings.py index 937be967b8..e5c3b51273 100644 --- a/src/pretix/settings.py +++ b/src/pretix/settings.py @@ -149,7 +149,6 @@ INSTALLED_APPS = [ 'compressor', 'bootstrap3', 'djangoformsetjs', - 'pretix.plugins.timerestriction', 'pretix.plugins.banktransfer', 'pretix.plugins.stripe', 'pretix.plugins.paypal', diff --git a/src/tests/base/test_plugins.py b/src/tests/base/test_plugins.py index 728a8cb633..53fc315cb1 100644 --- a/src/tests/base/test_plugins.py +++ b/src/tests/base/test_plugins.py @@ -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]) diff --git a/src/tests/control/test_events.py b/src/tests/control/test_events.py index 70e8209394..074901117c 100644 --- a/src/tests/control/test_events.py +++ b/src/tests/control/test_events.py @@ -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) diff --git a/src/tests/control/test_permissions.py b/src/tests/control/test_permissions.py index 826e1f5d13..2872170703 100644 --- a/src/tests/control/test_permissions.py +++ b/src/tests/control/test_permissions.py @@ -36,7 +36,6 @@ event_urls = [ "items/abc/", "items/abc/variations", "items/abc/properties", - "items/abc/restrictions", "categories/", "categories/add", "categories/abc/", diff --git a/src/tests/plugins/test_timerestriction.py b/src/tests/plugins/test_timerestriction.py deleted file mode 100644 index b4ff31a7bf..0000000000 --- a/src/tests/plugins/test_timerestriction.py +++ /dev/null @@ -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']) diff --git a/src/tests/presale/test_cart.py b/src/tests/presale/test_cart.py index 51477e7184..15288e90e1 100644 --- a/src/tests/presale/test_cart.py +++ b/src/tests/presale/test_cart.py @@ -277,19 +277,6 @@ class CartTest(CartTestMixin, TestCase): 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, diff --git a/src/tests/testdummy/signals.py b/src/tests/testdummy/signals.py index 397a98e1f3..56955b6eaf 100644 --- a/src/tests/testdummy/signals.py +++ b/src/tests/testdummy/signals.py @@ -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")