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 %} +
+ {% csrf_token %} +
+ {% trans "Installed plugins" %} + {% if "success" in request.GET %} +
+ {% trans "Your changes have been saved." %} +
+ {% endif %} + {% for plugin in plugins %} +
+
+
+
+

{{ plugin.name }}

+
+
+ {% if plugin.module in plugins_active %} + + {% else %} + + + {% endif %} +
+
+
+
+

Version {{ plugin.version }} by {{ plugin.author }}

+

{{ plugin.description }}

+
+
+ {% endfor %} +
+
+{% 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 %} -

{% trans "Settings" %}

- {% if "success" in request.GET %} -
- {% trans "Your changes have been saved." %} -
- {% endif %} +{% block inside %}
+ {% if "success" in request.GET %} +
+ {% trans "Your changes have been saved." %} +
+ {% endif %} {% csrf_token %}
{% trans "General information" %} diff --git a/src/tixlcontrol/templates/tixlcontrol/event/settings_base.html b/src/tixlcontrol/templates/tixlcontrol/event/settings_base.html new file mode 100644 index 000000000..09ac6dea3 --- /dev/null +++ b/src/tixlcontrol/templates/tixlcontrol/event/settings_base.html @@ -0,0 +1,13 @@ +{% extends "tixlcontrol/event/base.html" %} +{% load i18n %} +{% load bootstrap3 %} +{% block title %}{{ request.event.name }}{% endblock %} +{% block content %} +

{% trans "Settings" %}

+ + {% block inside %} + {% endblock %} +{% endblock %} diff --git a/src/tixlcontrol/urls.py b/src/tixlcontrol/urls.py index 2ee6de734..fdd058507 100644 --- a/src/tixlcontrol/urls.py +++ b/src/tixlcontrol/urls.py @@ -18,7 +18,8 @@ urlpatterns += patterns( patterns( 'tixlcontrol.views', url(r'^$', 'event.index', name='event.index'), - url(r'^settings$', event.EventUpdate.as_view(), name='event.settings'), + url(r'^settings/$', event.EventUpdate.as_view(), name='event.settings'), + url(r'^settings/plugins$', event.EventPlugins.as_view(), name='event.settings.plugins'), url(r'^items/$', item.ItemList.as_view(), name='event.items'), url(r'^items/(?P\d+)/$', item.ItemUpdateGeneral.as_view(), name='event.item'), url(r'^items/(?P\d+)/variations$', item.ItemVariations.as_view(), name='event.item.variations'), diff --git a/src/tixlcontrol/views/event.py b/src/tixlcontrol/views/event.py index ad97f4a9b..af84c9b4d 100644 --- a/src/tixlcontrol/views/event.py +++ b/src/tixlcontrol/views/event.py @@ -1,5 +1,7 @@ -from django.shortcuts import render +from django.shortcuts import render, redirect from django.views.generic.edit import UpdateView +from django.views.generic.base import TemplateView +from django.views.generic.detail import SingleObjectMixin from django import forms from django.utils.translation import ugettext_lazy as _ from django.core.urlresolvers import reverse @@ -60,5 +62,48 @@ class EventUpdate(EventPermissionRequiredMixin, UpdateView): }) + '?success=true' +class EventPlugins(EventPermissionRequiredMixin, TemplateView, SingleObjectMixin): + + model = Event + context_object_name = 'event' + permission = 'can_change_settings' + template_name = 'tixlcontrol/event/plugins.html' + + def get_object(self, queryset=None): + return self.request.event + + def get_context_data(self, *args, **kwargs): + from tixlbase.plugins import get_all_plugins + context = super().get_context_data(*args, **kwargs) + context['plugins'] = get_all_plugins() + context['plugins_active'] = self.object.get_plugins() + return context + + def get(self, request, *args, **kwargs): + self.object = self.get_object() + context = self.get_context_data(object=self.object) + return self.render_to_response(context) + + def post(self, request, *args, **kwargs): + self.object = self.get_object() + plugins_active = self.object.get_plugins() + for key, value in request.POST.items(): + if key.startswith("plugin:"): + module = key.split(":")[1] + if value == "enable": + plugins_active.append(module) + else: + plugins_active.remove(module) + self.object.plugins = ",".join(plugins_active) + self.object.save() + return redirect(self.get_success_url()) + + def get_success_url(self): + return reverse('control:event.settings.plugins', kwargs={ + 'organizer': self.get_object().organizer.slug, + 'event': self.get_object().slug, + }) + '?success=true' + + def index(request, organizer, event): return render(request, 'tixlcontrol/event/index.html', {}) diff --git a/src/tixlcontrol/views/item.py b/src/tixlcontrol/views/item.py index 431a511c7..5d90d65f3 100644 --- a/src/tixlcontrol/views/item.py +++ b/src/tixlcontrol/views/item.py @@ -461,10 +461,11 @@ class ItemVariationForm(forms.ModelForm): ] -class ItemVariations(TemplateView, SingleObjectMixin): +class ItemVariations(EventPermissionRequiredMixin, TemplateView, SingleObjectMixin): model = Item context_object_name = 'item' + permission = 'can_change_items' def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/src/tixlplugins/timerestriction/__init__.py b/src/tixlplugins/timerestriction/__init__.py index df92e1790..f98bb6248 100644 --- a/src/tixlplugins/timerestriction/__init__.py +++ b/src/tixlplugins/timerestriction/__init__.py @@ -1,11 +1,22 @@ 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' diff --git a/src/tixlplugins/timerestriction/tests.py b/src/tixlplugins/timerestriction/tests.py index 046a129bb..50c14f6a9 100644 --- a/src/tixlplugins/timerestriction/tests.py +++ b/src/tixlplugins/timerestriction/tests.py @@ -48,7 +48,7 @@ class TimeRestrictionTest(TestCase): ) r.items.add(self.item) result = signals.availability_handler( - None, item=self.item, + self.event, item=self.item, variations=self.item.get_all_variations(), context=None, cache=self.event.get_cache() ) @@ -66,7 +66,7 @@ class TimeRestrictionTest(TestCase): ) r.items.add(self.item) result = signals.availability_handler( - None, item=self.item, + self.event, item=self.item, variations=self.item.get_all_variations(), context=None, cache=self.event.get_cache() ) @@ -90,7 +90,7 @@ class TimeRestrictionTest(TestCase): ) r2.items.add(self.item) result = signals.availability_handler( - None, item=self.item, + self.event, item=self.item, variations=self.item.get_all_variations(), context=None, cache=self.event.get_cache() ) @@ -115,7 +115,7 @@ class TimeRestrictionTest(TestCase): ) r2.items.add(self.item) result = signals.availability_handler( - None, item=self.item, + self.event, item=self.item, variations=self.item.get_all_variations(), context=None, cache=self.event.get_cache() ) @@ -140,7 +140,7 @@ class TimeRestrictionTest(TestCase): ) r2.items.add(self.item) result = signals.availability_handler( - None, item=self.item, + self.event, item=self.item, variations=self.item.get_all_variations(), context=None, cache=self.event.get_cache() ) @@ -165,7 +165,7 @@ class TimeRestrictionTest(TestCase): ) r2.items.add(self.item) result = signals.availability_handler( - None, item=self.item, + self.event, item=self.item, variations=self.item.get_all_variations(), context=None, cache=self.event.get_cache() ) @@ -189,7 +189,7 @@ class TimeRestrictionTest(TestCase): r1.items.add(self.item) r1.variations.add(v1) result = signals.availability_handler( - None, item=self.item, + self.event, item=self.item, variations=self.item.get_all_variations(), context=None, cache=self.event.get_cache() ) @@ -232,7 +232,7 @@ class TimeRestrictionTest(TestCase): r3.items.add(self.item) r3.variations.add(v2) result = signals.availability_handler( - None, item=self.item, + self.event, item=self.item, variations=self.item.get_all_variations(), context=None, cache=self.event.get_cache() ) @@ -277,7 +277,7 @@ class TimeRestrictionTest(TestCase): r3.items.add(self.item) r3.variations.add(v2) result = signals.availability_handler( - None, item=self.item, + self.event, item=self.item, variations=self.item.get_all_variations(), context=None, cache=self.event.get_cache() )