mirror of
https://github.com/pretix/pretix.git
synced 2026-05-04 15:04:03 +00:00
Rename tixl to pretix
This commit is contained in:
2
src/pretixplugins/__init__.py
Normal file
2
src/pretixplugins/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from pkgutil import extend_path
|
||||
__path__ = extend_path(__path__, __name__)
|
||||
17
src/pretixplugins/testdummy/__init__.py
Normal file
17
src/pretixplugins/testdummy/__init__.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from django.apps import AppConfig
|
||||
from pretixbase.plugins import PluginType
|
||||
|
||||
|
||||
class TestDummyApp(AppConfig):
|
||||
name = 'pretixplugins.testdummy'
|
||||
verbose_name = '.testdummy'
|
||||
|
||||
class TixlPluginMeta:
|
||||
type = PluginType.RESTRICTION
|
||||
name = '.testdummy'
|
||||
version = '1.0.0'
|
||||
|
||||
def ready(self):
|
||||
from . import signals # NOQA
|
||||
|
||||
default_app_config = 'pretixplugins.testdummy.TestDummyApp'
|
||||
0
src/pretixplugins/testdummy/models.py
Normal file
0
src/pretixplugins/testdummy/models.py
Normal file
9
src/pretixplugins/testdummy/signals.py
Normal file
9
src/pretixplugins/testdummy/signals.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from django.dispatch import receiver
|
||||
|
||||
from pretixbase.signals import determine_availability
|
||||
|
||||
|
||||
@receiver(determine_availability)
|
||||
def availability_handler(sender, **kwargs):
|
||||
kwargs['sender'] = sender
|
||||
return kwargs
|
||||
22
src/pretixplugins/timerestriction/__init__.py
Normal file
22
src/pretixplugins/timerestriction/__init__.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from django.apps import AppConfig
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from pretixbase.plugins import PluginType
|
||||
|
||||
|
||||
class TimeRestrictionApp(AppConfig):
|
||||
name = 'pretixplugins.timerestriction'
|
||||
verbose_name = _("Time restriction")
|
||||
|
||||
class TixlPluginMeta:
|
||||
type = PluginType.RESTRICTION
|
||||
name = _("Restriction by time")
|
||||
author = _("the pretix team")
|
||||
version = '1.0.0'
|
||||
description = _("This plugin adds the possibility to restrict the sale " +
|
||||
"of a given item or variation to a certain timeframe " +
|
||||
"or change its price during a certain period.")
|
||||
|
||||
def ready(self):
|
||||
from . import signals # NOQA
|
||||
|
||||
default_app_config = 'pretixplugins.timerestriction.TimeRestrictionApp'
|
||||
38
src/pretixplugins/timerestriction/migrations/0001_initial.py
Normal file
38
src/pretixplugins/timerestriction/migrations/0001_initial.py
Normal file
@@ -0,0 +1,38 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import models, migrations
|
||||
import pretixbase.models
|
||||
import versions.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='TimeRestriction',
|
||||
fields=[
|
||||
('id', models.CharField(serialize=False, primary_key=True, max_length=36)),
|
||||
('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(null=True, blank=True, verbose_name='Price in time frame', max_digits=7, decimal_places=2)),
|
||||
('event', versions.models.VersionedForeignKey(to='pretixbase.Event', related_name='restrictions_timerestriction_timerestriction', verbose_name='Event')),
|
||||
('item', versions.models.VersionedForeignKey(to='pretixbase.Item', blank=True, null=True, related_name='restrictions_timerestriction_timerestriction', verbose_name='Item')),
|
||||
('variations', pretixbase.models.VariationsField(to='pretixbase.ItemVariation', blank=True, verbose_name='Variations', related_name='restrictions_timerestriction_timerestriction')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Restriction',
|
||||
'verbose_name_plural': 'Restrictions',
|
||||
'abstract': False,
|
||||
},
|
||||
bases=(models.Model,),
|
||||
),
|
||||
]
|
||||
24
src/pretixplugins/timerestriction/models.py
Normal file
24
src/pretixplugins/timerestriction/models.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from pretixbase.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"),
|
||||
)
|
||||
146
src/pretixplugins/timerestriction/signals.py
Normal file
146
src/pretixplugins/timerestriction/signals.py
Normal file
@@ -0,0 +1,146 @@
|
||||
from django.dispatch import receiver
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.forms.models import inlineformset_factory
|
||||
|
||||
from pretixbase.signals import determine_availability
|
||||
from pretixbase.models import Item
|
||||
from pretixcontrol.views.forms import VariationsField, RestrictionInlineFormset, RestrictionForm
|
||||
from pretixcontrol.signals import restriction_formset
|
||||
|
||||
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']
|
||||
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]
|
||||
|
||||
# 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:
|
||||
# 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
|
||||
|
||||
# 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
|
||||
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 len(v) != 0 and ('variation' not in v or v['variation'] not in applied_to):
|
||||
continue
|
||||
|
||||
if restriction.timeframe_from <= now() <= restriction.timeframe_to:
|
||||
# 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
|
||||
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)
|
||||
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.'
|
||||
}
|
||||
286
src/pretixplugins/timerestriction/tests.py
Normal file
286
src/pretixplugins/timerestriction/tests.py
Normal file
@@ -0,0 +1,286 @@
|
||||
from datetime import timedelta
|
||||
|
||||
from django.test import TestCase
|
||||
from django.utils.timezone import now
|
||||
|
||||
from pretixbase.models import (
|
||||
Event, Organizer, Item, Property, PropertyValue, ItemVariation
|
||||
)
|
||||
|
||||
# Do NOT use relative imports here
|
||||
from pretixplugins.timerestriction import signals
|
||||
from pretixplugins.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')
|
||||
self.variation1 = ItemVariation.objects.create(item=self.item)
|
||||
self.variation1.values.add(self.value1)
|
||||
self.variation2 = ItemVariation.objects.create(item=self.item)
|
||||
self.variation2.values.add(self.value2)
|
||||
self.variation3 = ItemVariation.objects.create(item=self.item)
|
||||
self.variation3.values.add(self.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.item.properties.add(self.property)
|
||||
|
||||
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.assertFalse(v['available'])
|
||||
|
||||
def test_variation_specifics(self):
|
||||
self.item.properties.add(self.property)
|
||||
|
||||
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)
|
||||
else:
|
||||
self.assertFalse(v['available'])
|
||||
Reference in New Issue
Block a user