The very basics of the plugin API

This commit is contained in:
Raphael Michel
2014-10-06 23:30:36 +02:00
parent 7ded9e88d8
commit 3c6f8b77cb
17 changed files with 587 additions and 3 deletions

View File

@@ -1,5 +1,5 @@
[run]
source = tixlbase,tixlcontrol,tixlpresale
source = tixlbase,tixlcontrol,tixlpresale,tixlplugins
omit = */migrations/*,*/urls.py,*/tests/*
[report]

View File

@@ -13,6 +13,7 @@ lxml
# Debugging requirements
django-debug-toolbar
ipython
# Testing requirements
pyflakes

View File

@@ -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
View File

View 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
View File

@@ -0,0 +1,5 @@
import django.dispatch
determine_availability = django.dispatch.Signal(
providing_args=["item", "variations", "context", "cache"]
)

View File

@@ -0,0 +1,2 @@
from pkgutil import extend_path
__path__ = extend_path(__path__, __name__)

View 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'

View 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"),
)

View 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

View 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'])