diff --git a/doc/development/api/plugins.rst b/doc/development/api/plugins.rst index 7af03fda1..9fbe4faf8 100644 --- a/doc/development/api/plugins.rst +++ b/doc/development/api/plugins.rst @@ -47,17 +47,31 @@ example, taken from the time restriction module (see next chapter) as a template ``__init__.py`` module:: from django.apps import AppConfig + from django.utils.translation import ugettext_lazy as _ + from tixlbase.plugins import PluginType class TimeRestrictionApp(AppConfig): name = 'tixlplugins.timerestriction' - verbose_name = "Time restriction" + verbose_name = _("Time restriction") + + class TixlPluginMeta: + type = PluginType.RESTRICTION + name = _("Restriciton by time") + author = _("the tixl 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 + from . import signals # NOQA default_app_config = 'tixlplugins.timerestriction.TimeRestrictionApp' +.. IMPORTANT:: + You have to implement a ``TixlPluginMeta`` class like in the example to make your + plugin available to the users. .. _signal dispatcher: https://docs.djangoproject.com/en/1.7/topics/signals/ .. _namespace packages: http://legacy.python.org/dev/peps/pep-0420/ diff --git a/doc/development/api/restriction.rst b/doc/development/api/restriction.rst index ec80e027d..87998c7fe 100644 --- a/doc/development/api/restriction.rst +++ b/doc/development/api/restriction.rst @@ -56,11 +56,11 @@ restrict anything without doing so. It is available as ``tixlbase.signals.determ 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: +It is sent out with several keyword arguments: - item + ``item`` The instance of ``tixlbase.models.Item`` in question. - variations + ``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 @@ -70,19 +70,20 @@ It is sent out with several arguments: only the list of all variations the frontend likes to determine the status for. Technically, you won't get ``dict`` objects but ``tixlbase.types.VariationDict`` objects, which behave exactly the same but add some extra methods. - context + ``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 + ``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 + ``available`` A boolean value whether or not this plugin allows this variation to be on sale. Defaults to ``True``. - price + ``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. diff --git a/src/tixlbase/migrations/0016_event_plugins.py b/src/tixlbase/migrations/0016_event_plugins.py new file mode 100644 index 000000000..753cc19e3 --- /dev/null +++ b/src/tixlbase/migrations/0016_event_plugins.py @@ -0,0 +1,20 @@ +# -*- 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.AddField( + model_name='event', + name='plugins', + field=models.TextField(blank=True, verbose_name='Plugins', null=True), + preserve_default=True, + ), + ] diff --git a/src/tixlbase/models.py b/src/tixlbase/models.py index a2eb14a11..8cb83f1a6 100644 --- a/src/tixlbase/models.py +++ b/src/tixlbase/models.py @@ -276,6 +276,10 @@ class Event(models.Model): verbose_name=_("Last date of payments"), help_text=_("The last date any payments are accepted. This has precedence over the number of days configured above.") ) + plugins = models.TextField( + null=True, blank=True, + verbose_name=_("Plugins"), + ) class Meta: verbose_name = _("Event") @@ -291,6 +295,11 @@ class Event(models.Model): self.get_cache().clear() return obj + def get_plugins(self): + if self.plugins is None: + return [] + return self.plugins.split(",") + def get_date_from_display(self): return _date( self.date_from, diff --git a/src/tixlbase/plugins.py b/src/tixlbase/plugins.py new file mode 100644 index 000000000..a7c588616 --- /dev/null +++ b/src/tixlbase/plugins.py @@ -0,0 +1,17 @@ +from enum import Enum + +from django.apps import apps + + +class PluginType(Enum): + RESTRICTION = 1 + + +def get_all_plugins(): + plugins = [] + for app in apps.get_app_configs(): + if hasattr(app, 'TixlPluginMeta'): + meta = app.TixlPluginMeta + meta.module = app.name + plugins.append(meta) + return plugins diff --git a/src/tixlbase/signals.py b/src/tixlbase/signals.py index befaf947b..cccaaa92e 100644 --- a/src/tixlbase/signals.py +++ b/src/tixlbase/signals.py @@ -1,5 +1,39 @@ import django.dispatch +from django.apps import apps +from django.dispatch.dispatcher import NO_RECEIVERS -determine_availability = django.dispatch.Signal( + +class EventPluginSignal(django.dispatch.Signal): + + def send(self, sender, **named): + """ + Send signal from sender to all connected receivers that belong to + plugins enabled for the given Event. + + sender is required to be an instance of ``tixlbase.models.Event``. + """ + responses = [] + if not self.receivers or self.sender_receivers_cache.get(sender) is NO_RECEIVERS: + return responses + + for receiver in self._live_receivers(sender): + # Find the Django application this belongs to + searchpath = receiver.__module__ + app = None + while "." in searchpath: + try: + if apps.is_installed(searchpath): + app = apps.get_app_config(searchpath.split(".")[-1]) + except LookupError: + pass + searchpath, mod = searchpath.rsplit(".", 1) + + # Only fire receivers from active plugins + if app.name in sender.get_plugins(): + response = receiver(signal=self, sender=sender, **named) + responses.append((receiver, response)) + return responses + +determine_availability = EventPluginSignal( providing_args=["item", "variations", "context", "cache"] ) diff --git a/src/tixlcontrol/static/tixlcontrol/less/forms.less b/src/tixlcontrol/static/tixlcontrol/less/forms.less index 03f9f34e0..60c772ca7 100644 --- a/src/tixlcontrol/static/tixlcontrol/less/forms.less +++ b/src/tixlcontrol/static/tixlcontrol/less/forms.less @@ -24,3 +24,7 @@ td > .form-group > .checkbox { .opacity(.65); .box-shadow(none); } + +.form-plugins .panel-title { + line-height: 34px; +} diff --git a/src/tixlcontrol/templates/tixlcontrol/event/plugins.html b/src/tixlcontrol/templates/tixlcontrol/event/plugins.html new file mode 100644 index 000000000..09ffa5198 --- /dev/null +++ b/src/tixlcontrol/templates/tixlcontrol/event/plugins.html @@ -0,0 +1,39 @@ +{% extends "tixlcontrol/event/settings_base.html" %} +{% load i18n %} +{% load bootstrap3 %} +{% block inside %} +
+{% endblock %} diff --git a/src/tixlcontrol/templates/tixlcontrol/event/settings.html b/src/tixlcontrol/templates/tixlcontrol/event/settings.html index 7f3af20c8..abce92f02 100644 --- a/src/tixlcontrol/templates/tixlcontrol/event/settings.html +++ b/src/tixlcontrol/templates/tixlcontrol/event/settings.html @@ -1,15 +1,13 @@ -{% extends "tixlcontrol/event/base.html" %} +{% extends "tixlcontrol/event/settings_base.html" %} {% load i18n %} {% load bootstrap3 %} -{% block title %}{{ request.event.name }}{% endblock %} -{% block content %} -