diff --git a/doc/development/api/restriction.rst b/doc/development/api/restriction.rst index 1139a88e5..ffb9f2d0a 100644 --- a/doc/development/api/restriction.rst +++ b/doc/development/api/restriction.rst @@ -97,9 +97,119 @@ dictionary can be extended by the following two keys: In our example, the implementation could look like this:: - TBD + 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'] + 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] + + # 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:%d:%s' % ( + item.pk, + ",".join(sorted( + [str(v[1].pk) for v in v.items() if v[0] != 'variation'] + )) + ) + + # 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.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 + 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. + 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. .. _caching feature: https://docs.djangoproject.com/en/1.7/topics/cache/ diff --git a/src/tixlbase/migrations/0015_auto_20141006_2205.py b/src/tixlbase/migrations/0015_auto_20141006_2205.py new file mode 100644 index 000000000..5dee309cf --- /dev/null +++ b/src/tixlbase/migrations/0015_auto_20141006_2205.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('tixlbase', '0014_auto_20141005_1037'), + ] + + operations = [ + migrations.AlterField( + model_name='item', + name='questions', + field=models.ManyToManyField(blank=True, related_name='items', verbose_name='Questions', help_text='The user will be asked to fill in answers for the selected questions', to='tixlbase.Question'), + ), + migrations.AlterField( + model_name='question', + name='event', + field=models.ForeignKey(to='tixlbase.Event', related_name='questions'), + ), + ] diff --git a/src/tixlplugins/timerestriction/migrations/0001_initial.py b/src/tixlplugins/timerestriction/migrations/0001_initial.py new file mode 100644 index 000000000..817f266ca --- /dev/null +++ b/src/tixlplugins/timerestriction/migrations/0001_initial.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import models, migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('tixlbase', '0015_auto_20141006_2205'), + ] + + operations = [ + migrations.CreateModel( + name='TimeRestriction', + fields=[ + ('id', models.AutoField(primary_key=True, serialize=False, verbose_name='ID', auto_created=True)), + ('timeframe_from', models.DateTimeField(verbose_name='Start of time frame')), + ('timeframe_to', models.DateTimeField(verbose_name='End of time frame')), + ('price', models.DecimalField(max_digits=7, verbose_name='Price in time frame', decimal_places=2, null=True, blank=True)), + ('event', models.ForeignKey(related_name='restrictions_timerestriction_timerestriction', to='tixlbase.Event', verbose_name='Event')), + ('items', models.ManyToManyField(to='tixlbase.Item', related_name='restrictions_timerestriction_timerestriction')), + ('variations', models.ManyToManyField(to='tixlbase.ItemVariation', related_name='restrictions_timerestriction_timerestriction')), + ], + options={ + 'abstract': False, + 'verbose_name_plural': 'Restrictions', + 'verbose_name': 'Restriction', + }, + bases=(models.Model,), + ), + ] diff --git a/src/tixlplugins/timerestriction/migrations/__init__.py b/src/tixlplugins/timerestriction/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/tixlplugins/timerestriction/signals.py b/src/tixlplugins/timerestriction/signals.py index e0bc01b1f..4ef032d9e 100644 --- a/src/tixlplugins/timerestriction/signals.py +++ b/src/tixlplugins/timerestriction/signals.py @@ -59,7 +59,9 @@ def availability_handler(sender, **kwargs): # Make up some unique key for this variation cachekey = 'timerestriction:%d:%s' % ( item.pk, - ",".join(sorted([str(v[1].pk) for v in v.items() if v[0] != 'variation'])) + ",".join(sorted( + [str(v[1].pk) for v in v.items() if v[0] != 'variation'] + )) ) # Fetch from cache, if available @@ -83,19 +85,24 @@ def availability_handler(sender, **kwargs): if 'variation' not in v or v['variation'] not in applied_to: continue - if restriction.timeframe_from <= now() and restriction.timeframe_to >= now(): + 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): + 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 ''), + '%s:%s' % ( + 'True' if available else 'False', + str(price) if price else '' + ), cache_validity )