mirror of
https://github.com/pretix/pretix.git
synced 2026-05-06 15:24:02 +00:00
Organizer-level plugins (#5305)
* Add version notes to the docs * Adapt signal handling * Add UI * Add API * API and tests * Fix registry * Update doc/development/api/plugins.rst Co-authored-by: Felix Rindt <felix@rindt.me> * Fix failing tests * Apply suggestions from code review Co-authored-by: Richard Schreiber <schreiber@rami.io> * Update src/pretix/control/templates/pretixcontrol/organizers/plugin_events.html Co-authored-by: luelista <weller@rami.io> * Update src/pretix/control/templates/pretixcontrol/organizers/plugins.html Co-authored-by: luelista <weller@rami.io> * Update src/pretix/control/templates/pretixcontrol/organizers/plugins.html Co-authored-by: luelista <weller@rami.io> * Update src/pretix/control/navigation.py Co-authored-by: luelista <weller@rami.io> * Update src/pretix/control/urls.py Co-authored-by: luelista <weller@rami.io> * Apply suggestion from @wiffbi * REbase migration * Fix review note * Fix test cases * Remove plugin from all events if disabled on org level * Update doc/development/api/plugins.rst * Unify registries * Rebase migration --------- Co-authored-by: Felix Rindt <felix@rindt.me> Co-authored-by: Richard Schreiber <schreiber@rami.io> Co-authored-by: luelista <weller@rami.io>
This commit is contained in:
@@ -70,9 +70,9 @@ from pretix.base.forms.widgets import (
|
||||
SplitDateTimePickerWidget, format_placeholders_help_text,
|
||||
)
|
||||
from pretix.base.models import (
|
||||
Customer, Device, EventMetaProperty, Gate, GiftCard, GiftCardAcceptance,
|
||||
Membership, MembershipType, OrderPosition, Organizer, ReusableMedium,
|
||||
SalesChannel, Team,
|
||||
Customer, Device, Event, EventMetaProperty, Gate, GiftCard,
|
||||
GiftCardAcceptance, Membership, MembershipType, OrderPosition, Organizer,
|
||||
ReusableMedium, SalesChannel, Team,
|
||||
)
|
||||
from pretix.base.models.customers import CustomerSSOClient, CustomerSSOProvider
|
||||
from pretix.base.models.organizer import OrganizerFooterLink
|
||||
@@ -1204,3 +1204,19 @@ class SalesChannelForm(I18nModelForm):
|
||||
)
|
||||
|
||||
return d
|
||||
|
||||
|
||||
class OrganizerPluginEventsForm(forms.Form):
|
||||
events = SafeEventMultipleChoiceField(
|
||||
queryset=Event.objects.none(),
|
||||
widget=forms.CheckboxSelectMultiple(attrs={
|
||||
'class': 'scrolling-multiple-choice scrolling-multiple-choice-large',
|
||||
}),
|
||||
label=_("Events with active plugin"),
|
||||
required=False,
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
events = kwargs.pop('events')
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['events'].queryset = events
|
||||
|
||||
@@ -783,6 +783,25 @@ class CoreLogEntryType(LogEntryType):
|
||||
pass
|
||||
|
||||
|
||||
@log_entry_types.new_from_dict({
|
||||
'pretix.organizer.plugins.enabled': _('The plugin has been enabled.'),
|
||||
'pretix.organizer.plugins.disabled': _('The plugin has been disabled.'),
|
||||
})
|
||||
class OrganizerPluginStateLogEntryType(LogEntryType):
|
||||
object_link_wrapper = _('Plugin {val}')
|
||||
|
||||
def get_object_link_info(self, logentry) -> Optional[dict]:
|
||||
if 'plugin' in logentry.parsed_data:
|
||||
app = app_cache.get(logentry.parsed_data['plugin'])
|
||||
if app and hasattr(app, 'PretixPluginMeta'):
|
||||
return {
|
||||
'href': reverse('control:organizer.settings.plugins', kwargs={
|
||||
'organizer': logentry.event.organizer.slug,
|
||||
}) + '#plugin_' + logentry.parsed_data['plugin'],
|
||||
'val': app.PretixPluginMeta.name
|
||||
}
|
||||
|
||||
|
||||
@log_entry_types.new_from_dict({
|
||||
'pretix.event.item_meta_property.added': _('A meta property has been added to this event.'),
|
||||
'pretix.event.item_meta_property.deleted': _('A meta property has been removed from this event.'),
|
||||
|
||||
@@ -495,6 +495,13 @@ def get_organizer_navigation(request):
|
||||
}),
|
||||
'active': url.url_name == 'organizer.edit',
|
||||
},
|
||||
{
|
||||
'label': _('Plugins'),
|
||||
'url': reverse('control:organizer.settings.plugins', kwargs={
|
||||
'organizer': request.organizer.slug,
|
||||
}),
|
||||
'active': url.url_name == 'organizer.settings.plugins' or url.url_name == 'organizer.settings.plugin-events',
|
||||
},
|
||||
{
|
||||
'label': _('Event metadata'),
|
||||
'url': reverse('control:organizer.properties', kwargs={
|
||||
|
||||
@@ -32,11 +32,11 @@
|
||||
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
from django.dispatch import Signal
|
||||
from pretix.base.signals import (
|
||||
DeprecatedSignal, EventPluginSignal, GlobalSignal, OrganizerPluginSignal,
|
||||
)
|
||||
|
||||
from pretix.base.signals import DeprecatedSignal, EventPluginSignal
|
||||
|
||||
html_page_start = Signal()
|
||||
html_page_start = GlobalSignal()
|
||||
"""
|
||||
This signal allows you to put code in the beginning of the main page for every
|
||||
page in the backend. You are expected to return HTML.
|
||||
@@ -80,7 +80,7 @@ in pretix.
|
||||
As with all plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
|
||||
nav_topbar = Signal()
|
||||
nav_topbar = GlobalSignal()
|
||||
"""
|
||||
Arguments: ``request``
|
||||
|
||||
@@ -99,7 +99,7 @@ This is no ``EventPluginSignal``, so you do not get the event in the ``sender``
|
||||
and you may get the signal regardless of whether your plugin is active.
|
||||
"""
|
||||
|
||||
nav_global = Signal()
|
||||
nav_global = GlobalSignal()
|
||||
"""
|
||||
Arguments: ``request``
|
||||
|
||||
@@ -150,7 +150,7 @@ As with all plugin signals, the ``sender`` keyword argument will contain the eve
|
||||
An additional keyword argument ``subevent`` *can* contain a sub-event.
|
||||
"""
|
||||
|
||||
user_dashboard_widgets = Signal()
|
||||
user_dashboard_widgets = GlobalSignal()
|
||||
"""
|
||||
Arguments: 'user'
|
||||
|
||||
@@ -221,7 +221,7 @@ Deprecated signal, no longer works. We just keep the definition so old plugins d
|
||||
break the installation.
|
||||
"""
|
||||
|
||||
nav_organizer = Signal()
|
||||
nav_organizer = OrganizerPluginSignal(allow_legacy_plugins=True)
|
||||
"""
|
||||
Arguments: 'organizer', 'request'
|
||||
|
||||
@@ -350,14 +350,14 @@ will be passed a ``form`` variable with your form.
|
||||
As with all plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
|
||||
oauth_application_registered = Signal()
|
||||
oauth_application_registered = GlobalSignal()
|
||||
"""
|
||||
Arguments: ``user``, ``application``
|
||||
|
||||
This signal will be called whenever a user registers a new OAuth application.
|
||||
"""
|
||||
|
||||
order_search_filter_q = Signal()
|
||||
order_search_filter_q = GlobalSignal()
|
||||
"""
|
||||
Arguments: ``query``
|
||||
|
||||
|
||||
@@ -87,6 +87,17 @@
|
||||
<span class="text-muted">{% trans "Not available" %}</span>
|
||||
</div>
|
||||
{% elif is_active %}
|
||||
{% if plugin.level == "organizer" %}
|
||||
<p class="text-muted">
|
||||
<span class="fa fa-group" aria-hidden="true"></span>
|
||||
{% trans "This plugin can only be disabled for the entire organizer account." %}
|
||||
</p>
|
||||
{% elif plugin.level == "event_organizer" %}
|
||||
<p class="text-muted">
|
||||
<span class="fa fa-group" aria-hidden="true"></span>
|
||||
{% trans "After disabling this plugin, some functionality may remain active in the organizer account." %}
|
||||
</p>
|
||||
{% endif %}
|
||||
<div class="plugin-action flip">
|
||||
{% if navigation_links %}
|
||||
<div class="btn-group">
|
||||
@@ -112,14 +123,42 @@
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
<button class="btn btn-default{% if plugin.featured %} btn-lg{% endif %}" name="plugin:{{ plugin.module }}"
|
||||
value="disable">{% trans "Disable" %}</button>
|
||||
{% if plugin.level == "organizer" %}
|
||||
<a href="{% url "control:organizer.settings.plugins" organizer=request.organizer.slug %}?q={{ plugin.module|urlencode }}"
|
||||
class="btn btn-default" target="_blank">
|
||||
<span class="fa fa-external-link" aria-hidden="true"></span>
|
||||
{% trans "Open in organizer settings" %}
|
||||
</a>
|
||||
{% else %}
|
||||
<button class="btn btn-default{% if plugin.featured %} btn-lg{% endif %}" name="plugin:{{ plugin.module }}"
|
||||
value="disable">{% trans "Disable" %}</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="plugin-action flip">
|
||||
<button class="btn btn-primary{% if plugin.featured %} btn-lg{% endif %}" name="plugin:{{ plugin.module }}"
|
||||
value="enable">{% trans "Enable" %}</button>
|
||||
</div>
|
||||
{% if plugin.level == "organizer" %}
|
||||
<p class="text-muted">
|
||||
<span class="fa fa-group" aria-hidden="true"></span>
|
||||
{% trans "This plugin can only be enabled for the entire organizer account." %}
|
||||
</p>
|
||||
<div class="plugin-action flip">
|
||||
<a href="{% url "control:organizer.settings.plugins" organizer=request.organizer.slug %}?q={{ plugin.module|urlencode }}"
|
||||
class="btn btn-default" target="_blank">
|
||||
<span class="fa fa-external-link" aria-hidden="true"></span>
|
||||
{% trans "Open in organizer settings" %}
|
||||
</a>
|
||||
</div>
|
||||
{% else %}
|
||||
{% if plugin.level == "event_organizer" and not plugin.module in request.organizer.get_plugins %}
|
||||
<p class="text-muted">
|
||||
<span class="fa fa-group" aria-hidden="true"></span>
|
||||
{% trans "Enabling this plugin will enable some of its functionality for the entire organizer account." %}
|
||||
</p>
|
||||
{% endif %}
|
||||
<div class="plugin-action flip">
|
||||
<button class="btn btn-primary{% if plugin.featured %} btn-lg{% endif %}" name="plugin:{{ plugin.module }}"
|
||||
value="enable">{% trans "Enable" %}</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if plugin.featured %}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
{% extends "pretixcontrol/organizers/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% load money %}
|
||||
{% block title %}
|
||||
{% blocktrans trimmed with name=plugin.name %}
|
||||
Events with plugin {{ name }}
|
||||
{% endblocktrans %}
|
||||
{% endblock %}
|
||||
{% block inner %}
|
||||
<h1>
|
||||
{% blocktrans trimmed with name=plugin.name %}
|
||||
Events with plugin {{ name }}
|
||||
{% endblocktrans %}
|
||||
</h1>
|
||||
{% if plugin.level == "event" %}
|
||||
<p>
|
||||
{% blocktrans trimmed with name=plugin.name %}
|
||||
The plugin "{{ name }}" can be enabled or disabled for every event individually.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
{% elif plugin.level == "event_organizer" %}
|
||||
<p>
|
||||
{% blocktrans trimmed with name=plugin.name %}
|
||||
The plugin "{{ name }}" is enabled for your organizer account, but also needs to be enabled for the
|
||||
specific events you want to use it with.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
{% endif %}
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
Using this form, you can quickly enable or disable it for many events. Note that it might still
|
||||
be necessary to configure the plugin for each event individually.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<form class="form-horizontal" action="" method="post">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form layout="control" %}
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,193 @@
|
||||
{% extends "pretixcontrol/organizers/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% load bootstrap3 %}
|
||||
{% block content %}
|
||||
<h1>{% trans "Available plugins" %}</h1>
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
On this page, you can choose plugins you want to enable for your organizer account. Plugins might bring additional
|
||||
software functionality, connect your events to third-party services, or apply other forms of customizations.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
{% if "success" in request.GET %}
|
||||
<div class="alert alert-success">
|
||||
{% trans "Your changes have been saved." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="row">
|
||||
<div class="col-lg-10">
|
||||
<p><input type="search" id="plugin_search_input" class="form-control" placeholder="{% trans "Search" %}"></p>
|
||||
</div>
|
||||
<div class="col-lg-2 text-right">
|
||||
<p class="btn-group btn-group-flex" data-toggle="buttons">
|
||||
<label class="btn btn-primary-if-active active"><input type="radio" name="plugin_state_filter" value="all" checked> {% trans "All" %}</label>
|
||||
<label class="btn btn-primary-if-active"><input type="radio" name="plugin_state_filter" value="active"> {% trans "Active" %}</label>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<form action="" method="post" class="form-horizontal form-plugins">
|
||||
{% csrf_token %}
|
||||
<div id="plugin_search_results" class="panel panel-default collapse">
|
||||
<div class="panel-heading">
|
||||
<button type="button" class="close" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
{% trans "Search results" %}
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="plugin-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="plugin_tabs"><div class="tabbed-form">
|
||||
{% for cat, catlabel, plist, has_pictures in plugins %}
|
||||
<fieldset data-plugin-category="{{ cat }}" data-plugin-category-label="{{ catlabel }}">
|
||||
<legend>{{ catlabel }}</legend>
|
||||
<div class="plugin-list">
|
||||
{% for plugin, is_active, settings_links, navigation_links, events_counter in plist %}
|
||||
<div class="plugin-container {% if plugin.featured %}featured-plugin{% endif %}" id="plugin_{{ plugin.module }}" data-plugin-module="{{ plugin.module }}" data-plugin-name="{{ plugin.name }}">
|
||||
{% if plugin.featured %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-body">
|
||||
{% endif %}
|
||||
<div class="plugin-text">
|
||||
{% if plugin.featured or plugin.experimental %}
|
||||
<p class="text-muted">
|
||||
{% if plugin.featured %}
|
||||
<span class="fa fa-thumbs-up" aria-hidden="true"></span>
|
||||
{% trans "Top recommendation" %}
|
||||
{% endif %}
|
||||
{% if plugin.experimental %}
|
||||
<span class="fa fa-flask" aria-hidden="true"></span>
|
||||
{% trans "Experimental feature" %}
|
||||
{% endif %}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if plugin.picture %}
|
||||
<p><img src="{% static plugin.picture %}" class="plugin-picture"></p>
|
||||
{% endif %}
|
||||
<h4>
|
||||
{{ plugin.name }}
|
||||
{% if show_meta %}
|
||||
<span class="text-muted text-sm">{{ plugin.version }}</span>
|
||||
{% endif %}
|
||||
{% if is_active and level == "organizer" %}
|
||||
<span class="label label-success" data-is-active>
|
||||
<span class="fa fa-check" aria-hidden="true"></span>
|
||||
{% trans "Active" %}
|
||||
</span>
|
||||
{% elif events_counter == events_total %}
|
||||
<span class="label label-success" data-is-active>
|
||||
<span class="fa fa-check" aria-hidden="true"></span>
|
||||
{% trans "Active (all events)" %}
|
||||
</span>
|
||||
{% elif events_counter %}
|
||||
<span class="label label-info" data-is-active>
|
||||
<span class="fa fa-check" aria-hidden="true"></span>
|
||||
{% blocktrans trimmed count count=events_counter %}
|
||||
Active ({{ count }} event)
|
||||
{% plural %}
|
||||
Active ({{ count }} events)
|
||||
{% endblocktrans %}
|
||||
</span>
|
||||
{% elif level == "event_organizer" %}
|
||||
<span class="label label-info" data-is-active>
|
||||
<span class="fa fa-check" aria-hidden="true"></span>
|
||||
{% blocktrans trimmed count count=0 %}
|
||||
Active ({{ count }} event)
|
||||
{% plural %}
|
||||
Active ({{ count }} events)
|
||||
{% endblocktrans %}
|
||||
</span>
|
||||
{% endif %}
|
||||
</h4>
|
||||
{% include "pretixcontrol/event/fragment_plugin_description.html" with plugin=plugin %}
|
||||
</div>
|
||||
{% if plugin.app.compatibility_errors %}
|
||||
<div class="plugin-action">
|
||||
<span class="text-muted">{% trans "Incompatible" %}</span>
|
||||
</div>
|
||||
{% elif plugin.restricted and plugin.module not in request.organizer.settings.allowed_restricted_plugins %}
|
||||
<div class="plugin-action">
|
||||
<span class="text-muted">{% trans "Not available" %}</span>
|
||||
</div>
|
||||
{% elif is_active %}
|
||||
{% if plugin.level == "event_organizer" %}
|
||||
<p class="text-muted">
|
||||
<span class="fa fa-calendar" aria-hidden="true"></span>
|
||||
{% trans "Parts of this plugin can be enabled or disabled for events individually." %}
|
||||
</p>
|
||||
{% endif %}
|
||||
<div class="plugin-action flip">
|
||||
{% if navigation_links %}
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-default dropdown-toggle{% if plugin.featured %} btn-lg{% endif %}" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" title="{% trans "Open plugin settings" %}">
|
||||
<span class="fa fa-compass"></span> {% trans "Go to" %} <span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
{% for link in navigation_links %}
|
||||
<li><a href="{{ link.0 }}">{{ link.1 }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if settings_links %}
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-default dropdown-toggle{% if plugin.featured %} btn-lg{% endif %}" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" title="{% trans "Open plugin settings" %}">
|
||||
<span class="fa fa-cog"></span> {% trans "Settings" %} <span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
{% for link in settings_links %}
|
||||
<li><a href="{{ link.0 }}">{{ link.1 }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
<button class="btn btn-default{% if plugin.featured %} btn-lg{% endif %}" name="plugin:{{ plugin.module }}"
|
||||
value="disable">{% trans "Disable" %}</button>
|
||||
{% if plugin.level == "event_organizer" %}
|
||||
<a class="btn btn-default {% if plugin.featured %} btn-lg{% endif %}"
|
||||
href="{% url "control:organizer.settings.plugin-events" organizer=request.organizer.slug plugin=plugin.module %}">
|
||||
{% trans "Manage events" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
{% if plugin.level == "organizer" %}
|
||||
<div class="plugin-action flip">
|
||||
<button class="btn btn-primary{% if plugin.featured %} btn-lg{% endif %}" name="plugin:{{ plugin.module }}"
|
||||
value="enable">{% trans "Enable" %}</button>
|
||||
</div>
|
||||
{% elif not plugin.level or plugin.level == "event" %}
|
||||
<p class="text-muted">
|
||||
<span class="fa fa-calendar" aria-hidden="true"></span>
|
||||
{% trans "This plugin can be enabled or disabled for events individually." %}
|
||||
</p>
|
||||
<div class="plugin-action flip">
|
||||
<a class="btn btn-default {% if plugin.featured %} btn-lg{% endif %}"
|
||||
href="{% url "control:organizer.settings.plugin-events" organizer=request.organizer.slug plugin=plugin.module %}">
|
||||
{% trans "Manage events" %}
|
||||
</a>
|
||||
</div>
|
||||
{% elif plugin.level == "event_organizer" %}
|
||||
<p class="text-muted">
|
||||
<span class="fa fa-calendar" aria-hidden="true"></span>
|
||||
{% trans "Parts of this plugin can be enabled or disabled for events individually." %}
|
||||
</p>
|
||||
<div class="plugin-action flip">
|
||||
<button class="btn btn-primary{% if plugin.featured %} btn-lg{% endif %}" name="plugin:{{ plugin.module }}"
|
||||
value="enable">{% trans "Enable" %}</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if plugin.featured %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</fieldset>
|
||||
{% endfor %}
|
||||
</div></div>
|
||||
</form>
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/plugins.js" %}"></script>
|
||||
{% endblock %}
|
||||
@@ -115,6 +115,10 @@ urlpatterns = [
|
||||
re_path(r'^organizers/select2$', typeahead.organizer_select2, name='organizers.select2'),
|
||||
re_path(r'^organizer/(?P<organizer>[^/]+)/$', organizer.OrganizerDetail.as_view(), name='organizer'),
|
||||
re_path(r'^organizer/(?P<organizer>[^/]+)/edit$', organizer.OrganizerUpdate.as_view(), name='organizer.edit'),
|
||||
re_path(r'^organizer/(?P<organizer>[^/]+)/settings/plugins$',
|
||||
organizer.OrganizerPlugins.as_view(), name='organizer.settings.plugins'),
|
||||
re_path(r'^organizer/(?P<organizer>[^/]+)/settings/plugins/(?P<plugin>[^/]+)/events$',
|
||||
organizer.OrganizerPluginEvents.as_view(), name='organizer.settings.plugin-events'),
|
||||
re_path(r'^organizer/(?P<organizer>[^/]+)/settings/email$',
|
||||
organizer.OrganizerMailSettings.as_view(), name='organizer.settings.mail'),
|
||||
re_path(r'^organizer/(?P<organizer>[^/]+)/settings/email/setup$',
|
||||
|
||||
@@ -61,7 +61,7 @@ from django.http import (
|
||||
JsonResponse,
|
||||
)
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.urls import reverse
|
||||
from django.urls import NoReverseMatch, reverse
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.html import conditional_escape, format_html
|
||||
from django.utils.http import url_has_allowed_host_and_scheme
|
||||
@@ -104,6 +104,10 @@ from ...base.i18n import language
|
||||
from ...base.models.items import (
|
||||
Item, ItemCategory, ItemMetaProperty, Question, Quota,
|
||||
)
|
||||
from ...base.plugins import (
|
||||
PLUGIN_LEVEL_EVENT, PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID,
|
||||
PLUGIN_LEVEL_ORGANIZER,
|
||||
)
|
||||
from ...base.services.mail import prefix_subject
|
||||
from ...base.services.placeholders import get_sample_context
|
||||
from ...base.settings import LazyI18nStringList
|
||||
@@ -349,43 +353,36 @@ class EventPlugins(EventSettingsViewMixin, EventPermissionRequiredMixin, Templat
|
||||
def available_plugins(self, event):
|
||||
from pretix.base.plugins import get_all_plugins
|
||||
|
||||
return (p for p in get_all_plugins(event) if not p.name.startswith('.')
|
||||
return (p for p in get_all_plugins(event=event) if not p.name.startswith('.')
|
||||
and getattr(p, 'visible', True))
|
||||
|
||||
def prepare_links(self, pluginmeta, key):
|
||||
links = getattr(pluginmeta, key, [])
|
||||
try:
|
||||
return [
|
||||
(
|
||||
reverse(urlname, kwargs={"organizer": self.request.organizer.slug, "event": self.request.event.slug, **kwargs}),
|
||||
" > ".join(map(str, linktext)) if isinstance(linktext, tuple) else linktext,
|
||||
) for linktext, urlname, kwargs in links
|
||||
]
|
||||
result = []
|
||||
for linktext, urlname, kwargs in links:
|
||||
try:
|
||||
result.append((
|
||||
reverse(urlname, kwargs={"organizer": self.request.organizer.slug, "event": self.request.event.slug, **kwargs}),
|
||||
" > ".join(map(str, linktext)) if isinstance(linktext, tuple) else linktext,
|
||||
))
|
||||
except NoReverseMatch:
|
||||
if pluginmeta.level != PLUGIN_LEVEL_EVENT:
|
||||
# Ignore, link might be for another level
|
||||
pass
|
||||
else:
|
||||
raise
|
||||
return result
|
||||
except:
|
||||
logger.exception('Failed to resolve settings links.')
|
||||
return []
|
||||
|
||||
def get_context_data(self, *args, **kwargs) -> dict:
|
||||
from pretix.base.plugins import CATEGORY_LABELS, CATEGORY_ORDER
|
||||
|
||||
context = super().get_context_data(*args, **kwargs)
|
||||
plugins = list(self.available_plugins(self.object))
|
||||
|
||||
order = [
|
||||
'FEATURE',
|
||||
'PAYMENT',
|
||||
'INTEGRATION',
|
||||
'CUSTOMIZATION',
|
||||
'FORMAT',
|
||||
'API',
|
||||
]
|
||||
labels = {
|
||||
'FEATURE': _('Features'),
|
||||
'PAYMENT': _('Payment providers'),
|
||||
'INTEGRATION': _('Integrations'),
|
||||
'CUSTOMIZATION': _('Customizations'),
|
||||
'FORMAT': _('Output and export formats'),
|
||||
'API': _('API features'),
|
||||
}
|
||||
|
||||
plugins_grouped = groupby(
|
||||
sorted(
|
||||
plugins,
|
||||
@@ -400,17 +397,24 @@ class EventPlugins(EventSettingsViewMixin, EventPermissionRequiredMixin, Templat
|
||||
plugins_grouped = [(c, list(plist)) for c, plist in plugins_grouped]
|
||||
|
||||
active_plugins = self.object.get_plugins()
|
||||
organizer_active_plugins = self.request.organizer.get_plugins()
|
||||
|
||||
def plugin_details(plugin):
|
||||
is_active = plugin.module in active_plugins
|
||||
if getattr(plugin, "level", PLUGIN_LEVEL_EVENT) == PLUGIN_LEVEL_ORGANIZER:
|
||||
is_active = plugin.module in organizer_active_plugins
|
||||
if getattr(plugin, "level", PLUGIN_LEVEL_EVENT) == PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID:
|
||||
is_active = is_active and plugin.module in organizer_active_plugins
|
||||
|
||||
settings_links = self.prepare_links(plugin, 'settings_links') if is_active else None
|
||||
navigation_links = self.prepare_links(plugin, 'navigation_links') if is_active else None
|
||||
return (plugin, is_active, settings_links, navigation_links)
|
||||
return plugin, is_active, settings_links, navigation_links
|
||||
|
||||
context['plugins'] = sorted([
|
||||
(c, labels.get(c, c), map(plugin_details, plist), any(getattr(p, 'picture', None) for p in plist))
|
||||
(c, CATEGORY_LABELS.get(c, c), map(plugin_details, plist), any(getattr(p, 'picture', None) for p in plist))
|
||||
for c, plist
|
||||
in plugins_grouped
|
||||
], key=lambda c: (order.index(c[0]), c[1]) if c[0] in order else (999, str(c[1])))
|
||||
], key=lambda c: (CATEGORY_ORDER.index(c[0]), c[1]) if c[0] in CATEGORY_ORDER else (999, str(c[1])))
|
||||
context['show_meta'] = settings.PRETIX_PLUGINS_SHOW_META
|
||||
return context
|
||||
|
||||
@@ -427,6 +431,7 @@ class EventPlugins(EventSettingsViewMixin, EventPermissionRequiredMixin, Templat
|
||||
}
|
||||
|
||||
with transaction.atomic():
|
||||
save_organizer = False
|
||||
for key, value in request.POST.items():
|
||||
if key.startswith("plugin:"):
|
||||
module = key.split(":")[1]
|
||||
@@ -436,8 +441,26 @@ class EventPlugins(EventSettingsViewMixin, EventPermissionRequiredMixin, Templat
|
||||
if module not in request.event.settings.allowed_restricted_plugins:
|
||||
continue
|
||||
|
||||
self.request.event.log_action('pretix.event.plugins.enabled', user=self.request.user,
|
||||
data={'plugin': module})
|
||||
if getattr(pluginmeta, 'level', PLUGIN_LEVEL_EVENT) not in (PLUGIN_LEVEL_EVENT, PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID):
|
||||
continue
|
||||
|
||||
if getattr(pluginmeta, 'level', PLUGIN_LEVEL_EVENT) == PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID:
|
||||
if not request.user.has_organizer_permission(request.organizer, "can_change_organizer_settings", request):
|
||||
messages.error(
|
||||
request,
|
||||
_("You do not have sufficient permission to enable plugins that need to be enabled "
|
||||
"for the entire organizer account.")
|
||||
)
|
||||
continue
|
||||
|
||||
if module not in self.object.organizer.get_plugins():
|
||||
self.object.organizer.log_action('pretix.organizer.plugins.enabled', user=self.request.user,
|
||||
data={'plugin': module})
|
||||
self.object.organizer.enable_plugin(module, allow_restricted=request.event.settings.allowed_restricted_plugins)
|
||||
save_organizer = True
|
||||
|
||||
self.object.log_action('pretix.event.plugins.enabled', user=self.request.user,
|
||||
data={'plugin': module})
|
||||
self.object.enable_plugin(module, allow_restricted=request.event.settings.allowed_restricted_plugins)
|
||||
|
||||
links = self.prepare_links(pluginmeta, 'settings_links')
|
||||
@@ -463,12 +486,14 @@ class EventPlugins(EventSettingsViewMixin, EventPermissionRequiredMixin, Templat
|
||||
self.object.disable_plugin(module)
|
||||
messages.success(self.request, _('The plugin has been disabled.'))
|
||||
self.object.save()
|
||||
if save_organizer:
|
||||
self.object.organizer.save()
|
||||
return redirect(self.get_success_url())
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
return reverse('control:event.settings.plugins', kwargs={
|
||||
'organizer': self.get_object().organizer.slug,
|
||||
'event': self.get_object().slug,
|
||||
'organizer': self.request.organizer.slug,
|
||||
'event': self.request.event.slug,
|
||||
})
|
||||
|
||||
|
||||
|
||||
@@ -33,10 +33,13 @@
|
||||
# License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
from collections import Counter
|
||||
from datetime import time, timedelta
|
||||
from decimal import Decimal
|
||||
from hashlib import sha1
|
||||
from itertools import groupby
|
||||
from json import JSONDecodeError
|
||||
|
||||
import bleach
|
||||
@@ -59,9 +62,11 @@ from django.http import (
|
||||
Http404, HttpResponse, HttpResponseBadRequest, JsonResponse,
|
||||
)
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
from django.urls import NoReverseMatch, reverse
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.html import format_html
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.timezone import get_current_timezone, now
|
||||
from django.utils.translation import gettext, gettext_lazy as _
|
||||
from django.views import View
|
||||
@@ -69,6 +74,7 @@ from django.views.decorators.http import require_http_methods
|
||||
from django.views.generic import (
|
||||
CreateView, DetailView, FormView, ListView, TemplateView, UpdateView,
|
||||
)
|
||||
from django.views.generic.detail import SingleObjectMixin
|
||||
|
||||
from pretix.api.models import ApiCall, WebHook
|
||||
from pretix.api.webhooks import manually_retry_all_calls
|
||||
@@ -91,6 +97,10 @@ from pretix.base.models.giftcards import (
|
||||
from pretix.base.models.orders import CancellationRequest
|
||||
from pretix.base.models.organizer import SalesChannel, TeamAPIToken
|
||||
from pretix.base.payment import PaymentException
|
||||
from pretix.base.plugins import (
|
||||
PLUGIN_LEVEL_EVENT, PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID,
|
||||
PLUGIN_LEVEL_ORGANIZER,
|
||||
)
|
||||
from pretix.base.services.export import multiexport, scheduled_organizer_export
|
||||
from pretix.base.services.mail import SendMailException, mail, prefix_subject
|
||||
from pretix.base.signals import register_multievent_data_exporters
|
||||
@@ -108,9 +118,9 @@ from pretix.control.forms.organizer import (
|
||||
GiftCardAcceptanceInviteForm, GiftCardCreateForm, GiftCardUpdateForm,
|
||||
KnownDomainFormset, MailSettingsForm, MembershipTypeForm,
|
||||
MembershipUpdateForm, OrganizerDeleteForm, OrganizerFooterLinkFormset,
|
||||
OrganizerForm, OrganizerSettingsForm, OrganizerUpdateForm,
|
||||
ReusableMediumCreateForm, ReusableMediumUpdateForm, SalesChannelForm,
|
||||
SSOClientForm, SSOProviderForm, TeamForm, WebHookForm,
|
||||
OrganizerForm, OrganizerPluginEventsForm, OrganizerSettingsForm,
|
||||
OrganizerUpdateForm, ReusableMediumCreateForm, ReusableMediumUpdateForm,
|
||||
SalesChannelForm, SSOClientForm, SSOProviderForm, TeamForm, WebHookForm,
|
||||
)
|
||||
from pretix.control.forms.rrule import RRuleForm
|
||||
from pretix.control.logdisplay import OVERVIEW_BANLIST
|
||||
@@ -129,6 +139,8 @@ from pretix.helpers.urls import build_absolute_uri as build_global_uri
|
||||
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||
from pretix.presale.forms.customer import TokenGenerator
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class OrganizerList(PaginationMixin, ListView):
|
||||
model = Organizer
|
||||
@@ -582,6 +594,263 @@ class OrganizerCreate(CreateView):
|
||||
})
|
||||
|
||||
|
||||
class OrganizerPlugins(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, TemplateView, SingleObjectMixin):
|
||||
model = Organizer
|
||||
context_object_name = 'organizer'
|
||||
permission = 'can_change_organizer_settings'
|
||||
template_name = 'pretixcontrol/organizers/plugins.html'
|
||||
|
||||
def get_object(self, queryset=None) -> Organizer:
|
||||
return self.request.organizer
|
||||
|
||||
def available_plugins(self, organizer):
|
||||
from pretix.base.plugins import get_all_plugins
|
||||
|
||||
return (p for p in get_all_plugins(organizer=organizer) if not p.name.startswith('.')
|
||||
and getattr(p, 'visible', True))
|
||||
|
||||
def prepare_links(self, pluginmeta, key):
|
||||
links = getattr(pluginmeta, key, [])
|
||||
try:
|
||||
result = []
|
||||
for linktext, urlname, kwargs in links:
|
||||
try:
|
||||
result.append((
|
||||
reverse(urlname, kwargs={"organizer": self.request.organizer.slug}),
|
||||
" > ".join(map(str, linktext)) if isinstance(linktext, tuple) else linktext,
|
||||
))
|
||||
except NoReverseMatch:
|
||||
if pluginmeta.level != PLUGIN_LEVEL_ORGANIZER:
|
||||
# Ignore, link might be for another level
|
||||
pass
|
||||
else:
|
||||
raise
|
||||
return result
|
||||
except:
|
||||
logger.exception('Failed to resolve settings links.')
|
||||
return []
|
||||
|
||||
def get_context_data(self, *args, **kwargs) -> dict:
|
||||
from pretix.base.plugins import CATEGORY_LABELS, CATEGORY_ORDER
|
||||
|
||||
context = super().get_context_data(*args, **kwargs)
|
||||
plugins = list(self.available_plugins(self.object))
|
||||
|
||||
active_counter = Counter()
|
||||
events_total = 0
|
||||
for e in self.object.events.only("plugins").iterator():
|
||||
events_total += 1
|
||||
for p in e.get_plugins():
|
||||
active_counter[p] += 1
|
||||
plugins_grouped = groupby(
|
||||
sorted(
|
||||
plugins,
|
||||
key=lambda p: (
|
||||
str(getattr(p, 'category', _('Other'))),
|
||||
(0 if getattr(p, 'featured', False) else 1),
|
||||
str(p.name).lower().replace('pretix ', '')
|
||||
),
|
||||
),
|
||||
lambda p: str(getattr(p, 'category', _('Other')))
|
||||
)
|
||||
plugins_grouped = [(c, list(plist)) for c, plist in plugins_grouped]
|
||||
|
||||
active_plugins = self.object.get_plugins()
|
||||
|
||||
def plugin_details(plugin):
|
||||
is_active = plugin.module in active_plugins
|
||||
events_counter = active_counter[plugin.module]
|
||||
settings_links = self.prepare_links(plugin, 'settings_links') if is_active else None
|
||||
navigation_links = self.prepare_links(plugin, 'navigation_links') if is_active else None
|
||||
return plugin, is_active, settings_links, navigation_links, events_counter
|
||||
|
||||
context['plugins'] = sorted([
|
||||
(c, CATEGORY_LABELS.get(c, c), map(plugin_details, plist), any(getattr(p, 'picture', None) for p in plist))
|
||||
for c, plist
|
||||
in plugins_grouped
|
||||
], key=lambda c: (CATEGORY_ORDER.index(c[0]), c[1]) if c[0] in CATEGORY_ORDER else (999, str(c[1])))
|
||||
context['show_meta'] = settings.PRETIX_PLUGINS_SHOW_META
|
||||
context['events_total'] = events_total
|
||||
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_available = {
|
||||
p.module: p for p in self.available_plugins(self.object)
|
||||
}
|
||||
choose_events_next = False
|
||||
with transaction.atomic():
|
||||
for key, value in request.POST.items():
|
||||
if key.startswith("plugin:"):
|
||||
module = key.split(":")[1]
|
||||
if value == "enable" and module in plugins_available:
|
||||
pluginmeta = plugins_available[module]
|
||||
if getattr(pluginmeta, 'restricted', False):
|
||||
if module not in request.organizer.settings.allowed_restricted_plugins:
|
||||
continue
|
||||
|
||||
level = getattr(pluginmeta, 'level', PLUGIN_LEVEL_EVENT)
|
||||
if level not in (PLUGIN_LEVEL_ORGANIZER, PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID):
|
||||
continue
|
||||
|
||||
if level == PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID:
|
||||
choose_events_next = module
|
||||
|
||||
self.object.log_action('pretix.organizer.plugins.enabled', user=self.request.user,
|
||||
data={'plugin': module})
|
||||
self.object.enable_plugin(module, allow_restricted=request.organizer.settings.allowed_restricted_plugins)
|
||||
|
||||
links = self.prepare_links(pluginmeta, 'settings_links')
|
||||
if links:
|
||||
info = [
|
||||
'<p>',
|
||||
format_html(_('The plugin {} is now active, you can configure it here:'),
|
||||
format_html("<strong>{}</strong>", pluginmeta.name)),
|
||||
'</p><p>',
|
||||
] + [
|
||||
format_html('<a href="{}" class="btn btn-default">{}</a> ', url, text)
|
||||
for url, text in links
|
||||
] + ['</p>']
|
||||
else:
|
||||
info = [
|
||||
format_html(_('The plugin {} is now active.'),
|
||||
format_html("<strong>{}</strong>", pluginmeta.name)),
|
||||
]
|
||||
messages.success(self.request, mark_safe("".join(info)))
|
||||
elif value == "disable" and module in plugins_available:
|
||||
pluginmeta = plugins_available[module]
|
||||
level = getattr(pluginmeta, 'level', PLUGIN_LEVEL_EVENT)
|
||||
if level not in (PLUGIN_LEVEL_ORGANIZER, PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID):
|
||||
continue
|
||||
|
||||
if level == PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID:
|
||||
events_to_disable = set(self.request.organizer.events.filter(
|
||||
plugins__regex='(^|,)' + module + '(,|$)'
|
||||
).values_list("pk", flat=True))
|
||||
logentries_to_save = []
|
||||
events_to_save = []
|
||||
|
||||
for e in self.request.organizer.events.filter(pk__in=events_to_disable):
|
||||
logentries_to_save.append(
|
||||
e.log_action('pretix.event.plugins.disabled', user=self.request.user,
|
||||
data={'plugin': module}, save=False)
|
||||
)
|
||||
e.disable_plugin(module)
|
||||
events_to_save.append(e)
|
||||
|
||||
Event.objects.bulk_update(events_to_save, fields=["plugins"])
|
||||
LogEntry.objects.bulk_create(logentries_to_save)
|
||||
|
||||
self.object.log_action('pretix.organizer.plugins.disabled', user=self.request.user,
|
||||
data={'plugin': module})
|
||||
self.object.disable_plugin(module)
|
||||
messages.success(self.request, _('The plugin has been disabled.'))
|
||||
self.object.save()
|
||||
if choose_events_next:
|
||||
return redirect(reverse('control:organizer.settings.plugin-events', kwargs={
|
||||
'organizer': self.request.organizer.slug,
|
||||
'plugin': choose_events_next,
|
||||
}))
|
||||
else:
|
||||
return redirect(self.get_success_url())
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
return reverse('control:organizer.settings.plugins', kwargs={
|
||||
'organizer': self.request.organizer.slug,
|
||||
})
|
||||
|
||||
|
||||
class OrganizerPluginEvents(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, FormView):
|
||||
model = Organizer
|
||||
context_object_name = 'organizer'
|
||||
permission = 'can_change_organizer_settings'
|
||||
template_name = 'pretixcontrol/organizers/plugin_events.html'
|
||||
form_class = OrganizerPluginEventsForm
|
||||
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs["events"] = self.request.user.get_events_with_permission(
|
||||
"can_change_event_settings", request=self.request
|
||||
).filter(organizer=self.request.organizer)
|
||||
kwargs["initial"] = {
|
||||
"events": self.request.organizer.events.filter(plugins__regex='(^|,)' + self.plugin.module + '(,|$)')
|
||||
}
|
||||
return kwargs
|
||||
|
||||
def available_plugins(self, organizer):
|
||||
from pretix.base.plugins import get_all_plugins
|
||||
|
||||
return (p for p in get_all_plugins(organizer=organizer) if not p.name.startswith('.')
|
||||
and getattr(p, 'visible', True))
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
return super().get_context_data(
|
||||
plugin=self.plugin,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
plugins_available = {
|
||||
p.module: p for p in self.available_plugins(self.request.organizer)
|
||||
}
|
||||
if kwargs["plugin"] not in plugins_available:
|
||||
raise Http404(_("Unknown plugin."))
|
||||
self.plugin = plugins_available[kwargs["plugin"]]
|
||||
level = getattr(self.plugin, "level", PLUGIN_LEVEL_EVENT)
|
||||
if level == PLUGIN_LEVEL_ORGANIZER:
|
||||
raise Http404(_("This plugin can only be enabled for the entire organizer account."))
|
||||
if level == PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID and self.plugin.module not in self.request.organizer.get_plugins():
|
||||
raise Http404(_("This plugin is currently not active on the organizer account."))
|
||||
|
||||
if getattr(self.plugin, 'restricted', False):
|
||||
if self.plugin.module not in request.organizer.settings.allowed_restricted_plugins:
|
||||
raise Http404(_("This plugin is currently not allowed for this organizer account."))
|
||||
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
return reverse('control:organizer.settings.plugins', kwargs={
|
||||
'organizer': self.request.organizer.slug,
|
||||
})
|
||||
|
||||
@transaction.atomic()
|
||||
def form_valid(self, form):
|
||||
enabled_events_before = set(
|
||||
self.request.organizer.events.filter(plugins__regex='(^|,)' + self.plugin.module + '(,|$)').values_list("pk", flat=True)
|
||||
)
|
||||
enabled_events_now = {e.pk for e in form.cleaned_data["events"]}
|
||||
|
||||
events_to_enable = enabled_events_now - enabled_events_before
|
||||
events_to_disable = enabled_events_before - enabled_events_now
|
||||
events_to_save = []
|
||||
logentries_to_save = []
|
||||
|
||||
for e in self.request.organizer.events.filter(pk__in=events_to_enable):
|
||||
logentries_to_save.append(
|
||||
e.log_action('pretix.event.plugins.enabled', user=self.request.user, data={'plugin': self.plugin.module}, save=False)
|
||||
)
|
||||
e.enable_plugin(self.plugin.module, allow_restricted=self.request.organizer.settings.allowed_restricted_plugins)
|
||||
events_to_save.append(e)
|
||||
|
||||
for e in self.request.organizer.events.filter(pk__in=events_to_disable):
|
||||
logentries_to_save.append(
|
||||
e.log_action('pretix.event.plugins.disabled', user=self.request.user, data={'plugin': self.plugin.module}, save=False)
|
||||
)
|
||||
e.disable_plugin(self.plugin.module)
|
||||
events_to_save.append(e)
|
||||
|
||||
Event.objects.bulk_update(events_to_save, fields=["plugins"])
|
||||
LogEntry.objects.bulk_create(logentries_to_save)
|
||||
messages.success(self.request, _("Your changes have been saved."))
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
class TeamListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, PaginationMixin, ListView):
|
||||
model = Team
|
||||
template_name = 'pretixcontrol/organizers/teams.html'
|
||||
|
||||
Reference in New Issue
Block a user