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::
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/

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

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"),
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,

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
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"]
)

View File

@@ -24,3 +24,7 @@ td > .form-group > .checkbox {
.opacity(.65);
.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 bootstrap3 %}
{% block title %}{{ request.event.name }}{% endblock %}
{% block content %}
<h1>{% trans "Settings" %}</h1>
{% if "success" in request.GET %}
<div class="alert alert-success">
{% trans "Your changes have been saved." %}
</div>
{% endif %}
{% block inside %}
<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 %}
<fieldset>
<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(
'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<item>\d+)/$', item.ItemUpdateGeneral.as_view(), name='event.item'),
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.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', {})

View File

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

View File

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

View File

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