mirror of
https://github.com/pretix/pretix.git
synced 2026-05-03 14:54:04 +00:00
The very basics of the plugin API
This commit is contained in:
BIN
doc/development/api/.restriction.rst.swo
Normal file
BIN
doc/development/api/.restriction.rst.swo
Normal file
Binary file not shown.
10
doc/development/api/index.rst
Normal file
10
doc/development/api/index.rst
Normal file
@@ -0,0 +1,10 @@
|
||||
API details
|
||||
===========
|
||||
|
||||
Contents:
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
plugins
|
||||
restriction
|
||||
63
doc/development/api/plugins.rst
Normal file
63
doc/development/api/plugins.rst
Normal file
@@ -0,0 +1,63 @@
|
||||
.. highlight:: python
|
||||
:linenothreshold: 5
|
||||
|
||||
Plugin basics
|
||||
=============
|
||||
|
||||
It is possible to extend tixl with custom Python code using the official plugin
|
||||
API. Every plugin has to be implemented as an independent Django 'app' living
|
||||
either in an own python package either installed like any python module or in
|
||||
the ``tixlplugins/`` directory of your tixl installation. A plugin may only
|
||||
require two steps to install:
|
||||
|
||||
* Add it to the ``INSTALLED_APPS`` setting of Django in ``tixl/settings.py``
|
||||
* Perform database migrations by using ``python manage.py migrate``
|
||||
|
||||
The communication between tixl and the plugins happens via Django's
|
||||
`signal dispatcher`_ pattern. The core modules of tixl, ``tixlbase``,
|
||||
``tixlcontrol`` and ``tixlpresale`` expose a number of signals which are documented
|
||||
on the next pages.
|
||||
|
||||
.. _`pluginsetup`:
|
||||
|
||||
Creating a plugin
|
||||
-----------------
|
||||
|
||||
To create a new plugin, create a new python package as a subpackage to ``tixlplugins``.
|
||||
In order to do so, you can place your module into tixl's :file:`tixlplugins` folder *or
|
||||
anywhere else in your python import path* inside a folder called ``tixlplugins``.
|
||||
|
||||
.. IMPORTANT::
|
||||
This makes use of a design pattern called `namespace packages`_ which is only
|
||||
implicitly available as of Python 3.4. As we aim to support Python 3.2 for a bit
|
||||
longer, you **MUST** put **EXACLTY** the following content into ``tixlplugins/__init__.py``
|
||||
if you create a new ``tixlplugins`` folder somewhere in your path::
|
||||
|
||||
from pkgutil import extend_path
|
||||
__path__ = extend_path(__path__, __name__)
|
||||
|
||||
Otherwise it **will break** on Python 3.2 systems *depending on the python path's order*,
|
||||
which is not tolerable behaviour. Also, even on Python 3.4 the test runner seems to have
|
||||
problems without this workaround.
|
||||
|
||||
|
||||
Inside your newly created folder, you'll probably need the three python modules ``__init__.py``,
|
||||
``models.py`` and ``signals.py``, although this is up to you. You can take the following
|
||||
example, taken from the time restriction module (see next chapter) as a template for your
|
||||
``__init__.py`` module::
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class TimeRestrictionApp(AppConfig):
|
||||
name = 'tixlplugins.timerestriction'
|
||||
verbose_name = "Time restriction"
|
||||
|
||||
def ready(self):
|
||||
from . import signals
|
||||
|
||||
default_app_config = 'tixlplugins.timerestriction.TimeRestrictionApp'
|
||||
|
||||
|
||||
.. _signal dispatcher: https://docs.djangoproject.com/en/1.7/topics/signals/
|
||||
.. _namespace packages: http://legacy.python.org/dev/peps/pep-0420/
|
||||
105
doc/development/api/restriction.rst
Normal file
105
doc/development/api/restriction.rst
Normal file
@@ -0,0 +1,105 @@
|
||||
.. highlight:: python
|
||||
:linenothreshold: 5
|
||||
|
||||
Writing a restriction plugin
|
||||
============================
|
||||
|
||||
Please make sure you have read and understood the :ref:`basic idea being tixl's restrictions
|
||||
<restrictionconcept>`. 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 ``tixlbase.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:`tixlplugins/timerestriction/models.py`::
|
||||
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from tixlbase.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 ``tixlbase.signals.determine_availability``
|
||||
and is sent out every time some component of tixl wants to know whether a specific item or
|
||||
variation is available for sell.
|
||||
|
||||
It is sent out with several arguments:
|
||||
|
||||
item
|
||||
The instance of ``tixlbase.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 not: this is *not* the list of all possible variations, this is
|
||||
only the list of all variations the frontend likes to determine the status for.
|
||||
context
|
||||
A yet-to-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)
|
||||
|
||||
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::
|
||||
|
||||
TBD
|
||||
|
||||
.. IMPORTANT::
|
||||
Please note the copying of the ``variations`` list in the example above.
|
||||
|
||||
.. _caching feature: https://docs.djangoproject.com/en/1.7/topics/cache/
|
||||
@@ -1,4 +1,4 @@
|
||||
Implementation Concepts
|
||||
Implementation concepts
|
||||
=======================
|
||||
|
||||
Basic terminology
|
||||
@@ -74,6 +74,8 @@ 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
|
||||
^^^^^^^^^^^^
|
||||
|
||||
|
||||
@@ -11,3 +11,4 @@ Contents:
|
||||
setup
|
||||
style
|
||||
structure
|
||||
api/index
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[run]
|
||||
source = tixlbase,tixlcontrol,tixlpresale
|
||||
source = tixlbase,tixlcontrol,tixlpresale,tixlplugins
|
||||
omit = */migrations/*,*/urls.py,*/tests/*
|
||||
|
||||
[report]
|
||||
|
||||
@@ -13,6 +13,7 @@ lxml
|
||||
|
||||
# Debugging requirements
|
||||
django-debug-toolbar
|
||||
ipython
|
||||
|
||||
# Testing requirements
|
||||
pyflakes
|
||||
|
||||
@@ -8,8 +8,9 @@ For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/dev/ref/settings/
|
||||
"""
|
||||
|
||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||
import os
|
||||
|
||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
|
||||
|
||||
|
||||
@@ -43,6 +44,7 @@ INSTALLED_APPS = (
|
||||
'bootstrap3',
|
||||
'debug_toolbar.apps.DebugToolbarConfig',
|
||||
'djangoformsetjs',
|
||||
'tixlplugins.timerestriction',
|
||||
)
|
||||
|
||||
MIDDLEWARE_CLASSES = (
|
||||
|
||||
0
src/tixlbase/cache.py
Normal file
0
src/tixlbase/cache.py
Normal file
@@ -569,6 +569,9 @@ class Item(models.Model):
|
||||
|
||||
return result
|
||||
|
||||
def get_cache(self):
|
||||
return None
|
||||
|
||||
|
||||
class ItemVariation(models.Model):
|
||||
"""
|
||||
@@ -610,3 +613,24 @@ class ItemVariation(models.Model):
|
||||
class Meta:
|
||||
verbose_name = _("Item variation")
|
||||
verbose_name_plural = _("Item variations")
|
||||
|
||||
|
||||
class BaseRestriction(models.Model):
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
items = models.ManyToManyField(
|
||||
Item,
|
||||
related_name="restrictions_%(app_label)s_%(class)s",
|
||||
)
|
||||
variations = models.ManyToManyField(
|
||||
ItemVariation,
|
||||
related_name="restrictions_%(app_label)s_%(class)s",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
verbose_name = _("Restriction")
|
||||
verbose_name_plural = _("Restrictions")
|
||||
|
||||
5
src/tixlbase/signals.py
Normal file
5
src/tixlbase/signals.py
Normal file
@@ -0,0 +1,5 @@
|
||||
import django.dispatch
|
||||
|
||||
determine_availability = django.dispatch.Signal(
|
||||
providing_args=["item", "variations", "context", "cache"]
|
||||
)
|
||||
2
src/tixlplugins/__init__.py
Normal file
2
src/tixlplugins/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from pkgutil import extend_path
|
||||
__path__ = extend_path(__path__, __name__)
|
||||
11
src/tixlplugins/timerestriction/__init__.py
Normal file
11
src/tixlplugins/timerestriction/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class TimeRestrictionApp(AppConfig):
|
||||
name = 'tixlplugins.timerestriction'
|
||||
verbose_name = "Time restriction"
|
||||
|
||||
def ready(self):
|
||||
from . import signals
|
||||
|
||||
default_app_config = 'tixlplugins.timerestriction.TimeRestrictionApp'
|
||||
24
src/tixlplugins/timerestriction/models.py
Normal file
24
src/tixlplugins/timerestriction/models.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from tixlbase.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"),
|
||||
)
|
||||
61
src/tixlplugins/timerestriction/signals.py
Normal file
61
src/tixlplugins/timerestriction/signals.py
Normal file
@@ -0,0 +1,61 @@
|
||||
from django.dispatch import receiver
|
||||
from django.utils.timezone import now
|
||||
|
||||
from tixlbase.signals import determine_availability
|
||||
|
||||
from .models import TimeRestriction
|
||||
|
||||
|
||||
@receiver(determine_availability)
|
||||
def availability_handler(sender, **kwargs):
|
||||
# Handle the signal's input arguments
|
||||
item = kwargs['item']
|
||||
variations = kwargs['variations']
|
||||
cache = kwargs['cache'] # NOQA
|
||||
context = kwargs['context'] # NOQA
|
||||
|
||||
# Fetch all restriction objects applied to this item
|
||||
restrictions = list(TimeRestriction.objects.filter(
|
||||
items__in=(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]
|
||||
|
||||
# Walk through all variations we are asked for
|
||||
for v in variations:
|
||||
# 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
|
||||
price = None
|
||||
# Walk through all restriction objects applied to this item
|
||||
for restriction in restrictions:
|
||||
applied_to = list(restriction.variations.all())
|
||||
|
||||
# Only take this restriction into consideration if it either
|
||||
# is directly applied to this variation OR is applied to all
|
||||
# variations (e.g. the applied_to list is empty)
|
||||
if len(applied_to) > 0:
|
||||
if 'variation' not in v or v['variation'] not in applied_to:
|
||||
continue
|
||||
|
||||
if restriction.timeframe_from <= now() and restriction.timeframe_to >= now():
|
||||
# Selling this item is currently possible
|
||||
available = True
|
||||
# If multiple time frames are currently active, make sure to
|
||||
# get the cheapest price:
|
||||
if restriction.price is not None and (price is None or restriction.price < price):
|
||||
price = restriction.price
|
||||
|
||||
v['available'] = available
|
||||
v['price'] = price
|
||||
|
||||
return variations
|
||||
273
src/tixlplugins/timerestriction/tests.py
Normal file
273
src/tixlplugins/timerestriction/tests.py
Normal file
@@ -0,0 +1,273 @@
|
||||
from datetime import timedelta
|
||||
|
||||
from django.test import TestCase
|
||||
from django.utils.timezone import now
|
||||
|
||||
from tixlbase.models import (
|
||||
Event, Organizer, Item, Property, PropertyValue, ItemVariation
|
||||
)
|
||||
|
||||
# Do NOT use relative imports here
|
||||
from tixlplugins.timerestriction import signals
|
||||
from tixlplugins.timerestriction.models import TimeRestriction
|
||||
|
||||
|
||||
class TimeRestrictionTest(TestCase):
|
||||
"""
|
||||
This test case tests the various aspects of the time restriction
|
||||
plugin
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
o = Organizer.objects.create(name='Dummy', slug='dummy')
|
||||
self.event = Event.objects.create(
|
||||
organizer=o, name='Dummy', slug='dummy',
|
||||
date_from=now(),
|
||||
)
|
||||
self.item = Item.objects.create(event=self.event, name='Dummy', default_price=14)
|
||||
self.property = Property.objects.create(event=self.event, name='Size')
|
||||
self.value1 = PropertyValue.objects.create(prop=self.property, value='S')
|
||||
self.value2 = PropertyValue.objects.create(prop=self.property, value='M')
|
||||
self.value3 = PropertyValue.objects.create(prop=self.property, value='L')
|
||||
|
||||
def test_nothing(self):
|
||||
result = signals.availability_handler(
|
||||
None, item=self.item,
|
||||
variations=self.item.get_all_variations(),
|
||||
context=None, cache=self.item.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),
|
||||
price=12
|
||||
)
|
||||
r.items.add(self.item)
|
||||
result = signals.availability_handler(
|
||||
None, item=self.item,
|
||||
variations=self.item.get_all_variations(),
|
||||
context=None, cache=self.item.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),
|
||||
price=12
|
||||
)
|
||||
r.items.add(self.item)
|
||||
result = signals.availability_handler(
|
||||
None, item=self.item,
|
||||
variations=self.item.get_all_variations(),
|
||||
context=None, cache=self.item.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),
|
||||
price=12
|
||||
)
|
||||
r1.items.add(self.item)
|
||||
r2 = TimeRestriction.objects.create(
|
||||
timeframe_from=now() - timedelta(days=3),
|
||||
timeframe_to=now() + timedelta(days=5),
|
||||
price=8
|
||||
)
|
||||
r2.items.add(self.item)
|
||||
result = signals.availability_handler(
|
||||
None, item=self.item,
|
||||
variations=self.item.get_all_variations(),
|
||||
context=None, cache=self.item.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),
|
||||
price=12
|
||||
)
|
||||
r1.items.add(self.item)
|
||||
r2 = TimeRestriction.objects.create(
|
||||
timeframe_from=now() + timedelta(days=1),
|
||||
timeframe_to=now() + timedelta(days=7),
|
||||
price=8
|
||||
)
|
||||
r2.items.add(self.item)
|
||||
result = signals.availability_handler(
|
||||
None, item=self.item,
|
||||
variations=self.item.get_all_variations(),
|
||||
context=None, cache=self.item.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),
|
||||
price=12
|
||||
)
|
||||
r1.items.add(self.item)
|
||||
r2 = TimeRestriction.objects.create(
|
||||
timeframe_from=now() + timedelta(days=4),
|
||||
timeframe_to=now() + timedelta(days=7),
|
||||
price=8
|
||||
)
|
||||
r2.items.add(self.item)
|
||||
result = signals.availability_handler(
|
||||
None, item=self.item,
|
||||
variations=self.item.get_all_variations(),
|
||||
context=None, cache=self.item.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),
|
||||
price=12
|
||||
)
|
||||
r1.items.add(self.item)
|
||||
r2 = TimeRestriction.objects.create(
|
||||
timeframe_from=now() + timedelta(days=4),
|
||||
timeframe_to=now() + timedelta(days=7),
|
||||
price=8
|
||||
)
|
||||
r2.items.add(self.item)
|
||||
result = signals.availability_handler(
|
||||
None, item=self.item,
|
||||
variations=self.item.get_all_variations(),
|
||||
context=None, cache=self.item.get_cache()
|
||||
)
|
||||
self.assertEqual(len(result), 1)
|
||||
self.assertIn('available', result[0])
|
||||
self.assertFalse(result[0]['available'])
|
||||
|
||||
def test_variation_specific(self):
|
||||
self.item.properties.add(self.property)
|
||||
v1 = ItemVariation.objects.create(item=self.item)
|
||||
v1.values.add(self.value1)
|
||||
v2 = ItemVariation.objects.create(item=self.item)
|
||||
v2.values.add(self.value2)
|
||||
|
||||
r1 = TimeRestriction.objects.create(
|
||||
timeframe_from=now() - timedelta(days=5),
|
||||
timeframe_to=now() + timedelta(days=1),
|
||||
price=12
|
||||
)
|
||||
r1.items.add(self.item)
|
||||
r1.variations.add(v1)
|
||||
result = signals.availability_handler(
|
||||
None, item=self.item,
|
||||
variations=self.item.get_all_variations(),
|
||||
context=None, cache=self.item.get_cache()
|
||||
)
|
||||
self.assertEqual(len(result), 3)
|
||||
for v in result:
|
||||
if 'variation' in v and v['variation'].pk == v1.pk:
|
||||
self.assertTrue(v['available'])
|
||||
self.assertEqual(v['price'], 12)
|
||||
else:
|
||||
self.assertFalse(v['available'])
|
||||
|
||||
def test_variation_specific_and_general(self):
|
||||
self.item.properties.add(self.property)
|
||||
v1 = ItemVariation.objects.create(item=self.item)
|
||||
v1.values.add(self.value1)
|
||||
v2 = ItemVariation.objects.create(item=self.item)
|
||||
v2.values.add(self.value2)
|
||||
|
||||
r1 = TimeRestriction.objects.create(
|
||||
timeframe_from=now() - timedelta(days=5),
|
||||
timeframe_to=now() + timedelta(days=1),
|
||||
price=12
|
||||
)
|
||||
r1.items.add(self.item)
|
||||
r2 = TimeRestriction.objects.create(
|
||||
timeframe_from=now() - timedelta(days=5),
|
||||
timeframe_to=now() + timedelta(days=1),
|
||||
price=8
|
||||
)
|
||||
r2.items.add(self.item)
|
||||
r2.variations.add(v1)
|
||||
r3 = TimeRestriction.objects.create(
|
||||
timeframe_from=now() - timedelta(days=5),
|
||||
timeframe_to=now() - timedelta(days=1),
|
||||
price=10
|
||||
)
|
||||
r3.items.add(self.item)
|
||||
r3.variations.add(v2)
|
||||
result = signals.availability_handler(
|
||||
None, item=self.item,
|
||||
variations=self.item.get_all_variations(),
|
||||
context=None, cache=self.item.get_cache()
|
||||
)
|
||||
self.assertEqual(len(result), 3)
|
||||
for v in result:
|
||||
if 'variation' in v and v['variation'].pk == v1.pk:
|
||||
self.assertTrue(v['available'])
|
||||
self.assertEqual(v['price'], 8)
|
||||
else:
|
||||
self.assertTrue(v['available'])
|
||||
self.assertEqual(v['price'], 12)
|
||||
|
||||
def test_variation_specifics(self):
|
||||
self.item.properties.add(self.property)
|
||||
v1 = ItemVariation.objects.create(item=self.item)
|
||||
v1.values.add(self.value1)
|
||||
v2 = ItemVariation.objects.create(item=self.item)
|
||||
v2.values.add(self.value2)
|
||||
|
||||
r1 = TimeRestriction.objects.create(
|
||||
timeframe_from=now() - timedelta(days=5),
|
||||
timeframe_to=now() + timedelta(days=1),
|
||||
price=12
|
||||
)
|
||||
r1.items.add(self.item)
|
||||
r1.variations.add(v1)
|
||||
r2 = TimeRestriction.objects.create(
|
||||
timeframe_from=now() - timedelta(days=5),
|
||||
timeframe_to=now() + timedelta(days=1),
|
||||
price=8
|
||||
)
|
||||
r2.items.add(self.item)
|
||||
r2.variations.add(v1)
|
||||
r3 = TimeRestriction.objects.create(
|
||||
timeframe_from=now() - timedelta(days=5),
|
||||
timeframe_to=now() - timedelta(days=1),
|
||||
price=8
|
||||
)
|
||||
r3.items.add(self.item)
|
||||
r3.variations.add(v2)
|
||||
result = signals.availability_handler(
|
||||
None, item=self.item,
|
||||
variations=self.item.get_all_variations(),
|
||||
context=None, cache=self.item.get_cache()
|
||||
)
|
||||
self.assertEqual(len(result), 3)
|
||||
for v in result:
|
||||
if 'variation' in v and v['variation'].pk == v1.pk:
|
||||
self.assertTrue(v['available'])
|
||||
self.assertEqual(v['price'], 8)
|
||||
else:
|
||||
self.assertFalse(v['available'])
|
||||
Reference in New Issue
Block a user