Plugin registry

This commit is contained in:
Raphael Michel
2014-10-07 12:21:13 +02:00
parent 1ec224049d
commit 3bae6a6819
15 changed files with 240 additions and 33 deletions

View File

@@ -47,17 +47,31 @@ example, taken from the time restriction module (see next chapter) as a template
``__init__.py`` module:: ``__init__.py`` module::
from django.apps import AppConfig from django.apps import AppConfig
from django.utils.translation import ugettext_lazy as _
from tixlbase.plugins import PluginType
class TimeRestrictionApp(AppConfig): class TimeRestrictionApp(AppConfig):
name = 'tixlplugins.timerestriction' 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): def ready(self):
from . import signals from . import signals # NOQA
default_app_config = 'tixlplugins.timerestriction.TimeRestrictionApp' 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/ .. _signal dispatcher: https://docs.djangoproject.com/en/1.7/topics/signals/
.. _namespace packages: http://legacy.python.org/dev/peps/pep-0420/ .. _namespace packages: http://legacy.python.org/dev/peps/pep-0420/

View File

@@ -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 and is sent out every time some component of tixl wants to know whether a specific item or
variation is available for sell. 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. The instance of ``tixlbase.models.Item`` in question.
variations ``variations``
A list of dictionaries in the same format as ``Item.get_all_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 The list contains one dictionary per variation, where the ``Property`` IDs are
keys and the ``PropertyValue`` objects are values. If an ``ItemVariation`` object 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. 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`` Technically, you won't get ``dict`` objects but ``tixlbase.types.VariationDict``
objects, which behave exactly the same but add some extra methods. 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 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. 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) 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 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: 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 A boolean value whether or not this plugin allows this variation to be on sale. Defaults
to ``True``. to ``True``.
price ``price``
A price to be set for this variation. Set to ``None`` or omit to keep the default 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. of the variation or the item's base price.

View File

@@ -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,
),
]

View File

@@ -276,6 +276,10 @@ class Event(models.Model):
verbose_name=_("Last date of payments"), 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.") 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: class Meta:
verbose_name = _("Event") verbose_name = _("Event")
@@ -291,6 +295,11 @@ class Event(models.Model):
self.get_cache().clear() self.get_cache().clear()
return obj return obj
def get_plugins(self):
if self.plugins is None:
return []
return self.plugins.split(",")
def get_date_from_display(self): def get_date_from_display(self):
return _date( return _date(
self.date_from, self.date_from,

17
src/tixlbase/plugins.py Normal file
View File

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

View File

@@ -1,5 +1,39 @@
import django.dispatch 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"] providing_args=["item", "variations", "context", "cache"]
) )

View File

@@ -24,3 +24,7 @@ td > .form-group > .checkbox {
.opacity(.65); .opacity(.65);
.box-shadow(none); .box-shadow(none);
} }
.form-plugins .panel-title {
line-height: 34px;
}

View File

@@ -0,0 +1,39 @@
{% extends "tixlcontrol/event/settings_base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block inside %}
<form action="" method="post" class="form-horizontal form-plugins">
{% csrf_token %}
<fieldset>
<legend>{% trans "Installed plugins" %}</legend>
{% if "success" in request.GET %}
<div class="alert alert-success">
{% trans "Your changes have been saved." %}
</div>
{% endif %}
{% for plugin in plugins %}
<div class="panel panel-{% if plugin.module in plugins_active %}success{% else %}default{% endif %}">
<div class="panel-heading">
<div class="row">
<div class="col-sm-10">
<h3 class="panel-title">{{ plugin.name }}</h3>
</div>
<div class="col-sm-2">
{% if plugin.module in plugins_active %}
<button class="btn btn-default btn-block" name="plugin:{{ plugin.module }}" value="disable">{% trans "Disable" %}</button>
{% else %}
<button class="btn btn-primary btn-block" name="plugin:{{ plugin.module }}" value="enable">{% trans "Enable" %}</button>
{% endif %}
</div>
</div>
</div>
<div class="panel-body">
<p class="meta">Version {{ plugin.version }} by <em>{{ plugin.author }}</em></p>
<p>{{ plugin.description }}</p>
</div>
</div>
{% endfor %}
</fieldset>
</form>
{% endblock %}

View File

@@ -1,15 +1,13 @@
{% extends "tixlcontrol/event/base.html" %} {% extends "tixlcontrol/event/settings_base.html" %}
{% load i18n %} {% load i18n %}
{% load bootstrap3 %} {% load bootstrap3 %}
{% block title %}{{ request.event.name }}{% endblock %} {% block inside %}
{% block content %}
<h1>{% trans "Settings" %}</h1>
{% if "success" in request.GET %}
<div class="alert alert-success">
{% trans "Your changes have been saved." %}
</div>
{% endif %}
<form action="" method="post" class="form-horizontal"> <form action="" method="post" class="form-horizontal">
{% if "success" in request.GET %}
<div class="alert alert-success">
{% trans "Your changes have been saved." %}
</div>
{% endif %}
{% csrf_token %} {% csrf_token %}
<fieldset> <fieldset>
<legend>{% trans "General information" %}</legend> <legend>{% trans "General information" %}</legend>

View File

@@ -0,0 +1,13 @@
{% extends "tixlcontrol/event/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %}{{ request.event.name }}{% endblock %}
{% block content %}
<h1>{% trans "Settings" %}</h1>
<ul class="nav nav-pills">
<li {% if "event.settings" == url_name %}class="active"{% endif %}><a href="{% url 'control:event.settings' organizer=request.event.organizer.slug event=request.event.slug %}">{% trans "General settings" %}</a></li>
<li {% if "event.settings.plugins" == url_name %}class="active"{% endif %}><a href="{% url 'control:event.settings.plugins' organizer=request.event.organizer.slug event=request.event.slug %}">{% trans "Plugins" %}</a></li>
</ul>
{% block inside %}
{% endblock %}
{% endblock %}

View File

@@ -18,7 +18,8 @@ urlpatterns += patterns(
patterns( patterns(
'tixlcontrol.views', 'tixlcontrol.views',
url(r'^$', 'event.index', name='event.index'), 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/$', item.ItemList.as_view(), name='event.items'),
url(r'^items/(?P<item>\d+)/$', item.ItemUpdateGeneral.as_view(), name='event.item'), url(r'^items/(?P<item>\d+)/$', item.ItemUpdateGeneral.as_view(), name='event.item'),
url(r'^items/(?P<item>\d+)/variations$', item.ItemVariations.as_view(), name='event.item.variations'), url(r'^items/(?P<item>\d+)/variations$', item.ItemVariations.as_view(), name='event.item.variations'),

View File

@@ -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.edit import UpdateView
from django.views.generic.base import TemplateView
from django.views.generic.detail import SingleObjectMixin
from django import forms from django import forms
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
@@ -60,5 +62,48 @@ class EventUpdate(EventPermissionRequiredMixin, UpdateView):
}) + '?success=true' }) + '?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): def index(request, organizer, event):
return render(request, 'tixlcontrol/event/index.html', {}) return render(request, 'tixlcontrol/event/index.html', {})

View File

@@ -461,10 +461,11 @@ class ItemVariationForm(forms.ModelForm):
] ]
class ItemVariations(TemplateView, SingleObjectMixin): class ItemVariations(EventPermissionRequiredMixin, TemplateView, SingleObjectMixin):
model = Item model = Item
context_object_name = 'item' context_object_name = 'item'
permission = 'can_change_items'
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)

View File

@@ -1,11 +1,22 @@
from django.apps import AppConfig from django.apps import AppConfig
from django.utils.translation import ugettext_lazy as _
from tixlbase.plugins import PluginType
class TimeRestrictionApp(AppConfig): class TimeRestrictionApp(AppConfig):
name = 'tixlplugins.timerestriction' 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): def ready(self):
from . import signals from . import signals # NOQA
default_app_config = 'tixlplugins.timerestriction.TimeRestrictionApp' default_app_config = 'tixlplugins.timerestriction.TimeRestrictionApp'

View File

@@ -48,7 +48,7 @@ class TimeRestrictionTest(TestCase):
) )
r.items.add(self.item) r.items.add(self.item)
result = signals.availability_handler( result = signals.availability_handler(
None, item=self.item, self.event, item=self.item,
variations=self.item.get_all_variations(), variations=self.item.get_all_variations(),
context=None, cache=self.event.get_cache() context=None, cache=self.event.get_cache()
) )
@@ -66,7 +66,7 @@ class TimeRestrictionTest(TestCase):
) )
r.items.add(self.item) r.items.add(self.item)
result = signals.availability_handler( result = signals.availability_handler(
None, item=self.item, self.event, item=self.item,
variations=self.item.get_all_variations(), variations=self.item.get_all_variations(),
context=None, cache=self.event.get_cache() context=None, cache=self.event.get_cache()
) )
@@ -90,7 +90,7 @@ class TimeRestrictionTest(TestCase):
) )
r2.items.add(self.item) r2.items.add(self.item)
result = signals.availability_handler( result = signals.availability_handler(
None, item=self.item, self.event, item=self.item,
variations=self.item.get_all_variations(), variations=self.item.get_all_variations(),
context=None, cache=self.event.get_cache() context=None, cache=self.event.get_cache()
) )
@@ -115,7 +115,7 @@ class TimeRestrictionTest(TestCase):
) )
r2.items.add(self.item) r2.items.add(self.item)
result = signals.availability_handler( result = signals.availability_handler(
None, item=self.item, self.event, item=self.item,
variations=self.item.get_all_variations(), variations=self.item.get_all_variations(),
context=None, cache=self.event.get_cache() context=None, cache=self.event.get_cache()
) )
@@ -140,7 +140,7 @@ class TimeRestrictionTest(TestCase):
) )
r2.items.add(self.item) r2.items.add(self.item)
result = signals.availability_handler( result = signals.availability_handler(
None, item=self.item, self.event, item=self.item,
variations=self.item.get_all_variations(), variations=self.item.get_all_variations(),
context=None, cache=self.event.get_cache() context=None, cache=self.event.get_cache()
) )
@@ -165,7 +165,7 @@ class TimeRestrictionTest(TestCase):
) )
r2.items.add(self.item) r2.items.add(self.item)
result = signals.availability_handler( result = signals.availability_handler(
None, item=self.item, self.event, item=self.item,
variations=self.item.get_all_variations(), variations=self.item.get_all_variations(),
context=None, cache=self.event.get_cache() context=None, cache=self.event.get_cache()
) )
@@ -189,7 +189,7 @@ class TimeRestrictionTest(TestCase):
r1.items.add(self.item) r1.items.add(self.item)
r1.variations.add(v1) r1.variations.add(v1)
result = signals.availability_handler( result = signals.availability_handler(
None, item=self.item, self.event, item=self.item,
variations=self.item.get_all_variations(), variations=self.item.get_all_variations(),
context=None, cache=self.event.get_cache() context=None, cache=self.event.get_cache()
) )
@@ -232,7 +232,7 @@ class TimeRestrictionTest(TestCase):
r3.items.add(self.item) r3.items.add(self.item)
r3.variations.add(v2) r3.variations.add(v2)
result = signals.availability_handler( result = signals.availability_handler(
None, item=self.item, self.event, item=self.item,
variations=self.item.get_all_variations(), variations=self.item.get_all_variations(),
context=None, cache=self.event.get_cache() context=None, cache=self.event.get_cache()
) )
@@ -277,7 +277,7 @@ class TimeRestrictionTest(TestCase):
r3.items.add(self.item) r3.items.add(self.item)
r3.variations.add(v2) r3.variations.add(v2)
result = signals.availability_handler( result = signals.availability_handler(
None, item=self.item, self.event, item=self.item,
variations=self.item.get_all_variations(), variations=self.item.get_all_variations(),
context=None, cache=self.event.get_cache() context=None, cache=self.event.get_cache()
) )