mirror of
https://github.com/pretix/pretix.git
synced 2026-05-11 16:13:59 +00:00
Plugin registry
This commit is contained in:
@@ -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/
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
20
src/tixlbase/migrations/0016_event_plugins.py
Normal file
20
src/tixlbase/migrations/0016_event_plugins.py
Normal 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,
|
||||
),
|
||||
]
|
||||
@@ -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
17
src/tixlbase/plugins.py
Normal 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
|
||||
@@ -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"]
|
||||
)
|
||||
|
||||
@@ -24,3 +24,7 @@ td > .form-group > .checkbox {
|
||||
.opacity(.65);
|
||||
.box-shadow(none);
|
||||
}
|
||||
|
||||
.form-plugins .panel-title {
|
||||
line-height: 34px;
|
||||
}
|
||||
|
||||
39
src/tixlcontrol/templates/tixlcontrol/event/plugins.html
Normal file
39
src/tixlcontrol/templates/tixlcontrol/event/plugins.html
Normal 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 %}
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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'),
|
||||
|
||||
@@ -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', {})
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user