forked from CGM_Public/pretix_original
upstream/2025.7.1 #7
@@ -424,9 +424,9 @@ Endpoints
|
||||
:param organizer: The ``slug`` field of the organizer of the event to create.
|
||||
:param event: The ``slug`` field of the event to copy settings and items from.
|
||||
:statuscode 201: no error
|
||||
:statuscode 400: The event could not be created due to invalid submitted data.
|
||||
:statuscode 400: The event could not be updated due to invalid submitted data.
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to create this resource.
|
||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to update this resource.
|
||||
|
||||
|
||||
.. http:patch:: /api/v1/organizers/(organizer)/events/(event)/
|
||||
|
||||
@@ -19,6 +19,11 @@ name string The organizer's
|
||||
slug string A short form of the name, used e.g. in URLs.
|
||||
public_url string The public, customer-facing URL of the organizer, where
|
||||
the list of all events can be found (read-only).
|
||||
plugins list A list of package names of the enabled plugins for this
|
||||
organizer. Note that most plugins are enabled on the
|
||||
event level (or both levels). If you remove a plugin
|
||||
that is also enabled on some events, it will
|
||||
automatically be removed from all events as well.
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
|
||||
@@ -53,7 +58,10 @@ Endpoints
|
||||
{
|
||||
"name": "Big Events LLC",
|
||||
"slug": "Big Events",
|
||||
"public_url": "https://pretix.eu/bigevents/"
|
||||
"public_url": "https://pretix.eu/bigevents/",
|
||||
"plugins": [
|
||||
"pretix_datev"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -87,7 +95,10 @@ Endpoints
|
||||
{
|
||||
"name": "Big Events LLC",
|
||||
"slug": "Big Events",
|
||||
"public_url": "https://pretix.eu/bigevents/"
|
||||
"public_url": "https://pretix.eu/bigevents/",
|
||||
"plugins": [
|
||||
"pretix_datev"
|
||||
]
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to fetch
|
||||
@@ -95,6 +106,50 @@ Endpoints
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to view it.
|
||||
|
||||
.. http:patch:: /api/v1/organizers/(organizer)/
|
||||
|
||||
Updates an organizer. Currently only the ``plugins`` field may be updated.
|
||||
|
||||
Permission required: "Can change organizer settings"
|
||||
|
||||
**Example request**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
PATCH /api/v1/organizers/bigevents/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Accept: application/json, text/javascript
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"plugins": [
|
||||
"pretix_seating"
|
||||
]
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Vary: Accept
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"name": "Big Events LLC",
|
||||
"slug": "Big Events",
|
||||
"public_url": "https://pretix.eu/bigevents/",
|
||||
"plugins": [
|
||||
"pretix_seating"
|
||||
]
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to update
|
||||
:statuscode 200: no error
|
||||
:statuscode 400: The organizer could not be updated due to invalid submitted data.
|
||||
:statuscode 401: Authentication failure
|
||||
:statuscode 403: The requested organizer does not exist **or** you have no permission to update this resource.
|
||||
|
||||
Organizer settings
|
||||
------------------
|
||||
|
||||
|
||||
@@ -38,6 +38,10 @@ available_number integer Number of avail
|
||||
slightly out of date. ``null`` means unlimited.
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 2025.7
|
||||
|
||||
The attribute ``ignore_for_event_availability`` has been added.
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
|
||||
@@ -54,6 +54,10 @@ budget money (string) The budget a vo
|
||||
budget_used money (string) The amount of budget the voucher has already used up.
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 2025.7
|
||||
|
||||
The attributes ``created``, ``budget``, and ``budget_used`` have been added.
|
||||
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
@@ -56,6 +56,20 @@ restricted boolean (optional) ``False`` by default, restricts a plugin
|
||||
for an event by system administrators / superusers.
|
||||
experimental boolean (optional) ``False`` by default, marks a plugin as an experimental feature in the plugins list.
|
||||
compatibility string Specifier for compatible pretix versions.
|
||||
level string System level the plugin can be activated at.
|
||||
Set to ``pretix.base.plugins.PLUGIN_LEVEL_EVENT`` for plugins that can be activated
|
||||
at event level and then be active for that event only.
|
||||
Set to ``pretix.base.plugins.PLUGIN_LEVEL_ORGANIZER`` for plugins that can be
|
||||
activated only for the organizer as a whole and are active for any event within
|
||||
that organizer.
|
||||
Set to ``pretix.base.plugins.PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID`` for plugins that
|
||||
can be activated at organizer level but are considered active only within events
|
||||
for which they have also been specifically activated.
|
||||
More levels, e.g. user-level plugins, might be invented in the future.
|
||||
settings_links list List of ``((menu name, submenu name, …), urlname, url_kwargs)`` tuples that point
|
||||
to the plugin's settings.
|
||||
navigation_links list List of ``((menu name, submenu name, …), urlname, url_kwargs)`` tuples that point
|
||||
to the plugin's system pages.
|
||||
================== ==================== ===========================================================
|
||||
|
||||
A working example would be:
|
||||
@@ -63,9 +77,9 @@ A working example would be:
|
||||
.. code-block:: python
|
||||
|
||||
try:
|
||||
from pretix.base.plugins import PluginConfig
|
||||
from pretix.base.plugins import PluginConfig, PLUGIN_LEVEL_EVENT
|
||||
except ImportError:
|
||||
raise RuntimeError("Please use pretix 2.7 or above to run this plugin!")
|
||||
raise RuntimeError("Please use pretix 2025.7 or above to run this plugin!")
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
@@ -79,6 +93,7 @@ A working example would be:
|
||||
version = '1.0.0'
|
||||
category = 'PAYMENT'
|
||||
picture = 'pretix_paypal/paypal_logo.svg'
|
||||
level = PLUGIN_LEVEL_EVENT
|
||||
visible = True
|
||||
featured = False
|
||||
restricted = False
|
||||
@@ -142,14 +157,14 @@ method to make your receivers available:
|
||||
from . import signals # NOQA
|
||||
|
||||
You can optionally specify code that is executed when your plugin is activated for an event
|
||||
in the ``installed`` method:
|
||||
or organizer in the ``installed`` method:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
class PaypalApp(AppConfig):
|
||||
…
|
||||
|
||||
def installed(self, event):
|
||||
def installed(self, event_or_organizer):
|
||||
pass # Your code here
|
||||
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ from rest_framework.relations import SlugRelatedField
|
||||
from pretix.api.serializers import (
|
||||
CompatibleJSONField, SalesChannelMigrationMixin,
|
||||
)
|
||||
from pretix.api.serializers.fields import PluginsField
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.api.serializers.settings import SettingsSerializer
|
||||
from pretix.base.models import (
|
||||
@@ -61,6 +62,9 @@ from pretix.base.models.items import (
|
||||
ItemMetaProperty, SubEventItem, SubEventItemVariation,
|
||||
)
|
||||
from pretix.base.models.tax import CustomRulesValidator
|
||||
from pretix.base.plugins import (
|
||||
PLUGIN_LEVEL_EVENT, PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID,
|
||||
)
|
||||
from pretix.base.services.seating import (
|
||||
SeatProtected, generate_seats, validate_plan_change,
|
||||
)
|
||||
@@ -126,22 +130,6 @@ class SeatCategoryMappingField(Field):
|
||||
}
|
||||
|
||||
|
||||
class PluginsField(Field):
|
||||
|
||||
def to_representation(self, obj):
|
||||
from pretix.base.plugins import get_all_plugins
|
||||
|
||||
return sorted([
|
||||
p.module for p in get_all_plugins()
|
||||
if not p.name.startswith('.') and getattr(p, 'visible', True) and p.module in obj.get_plugins()
|
||||
])
|
||||
|
||||
def to_internal_value(self, data):
|
||||
return {
|
||||
'plugins': data
|
||||
}
|
||||
|
||||
|
||||
class TimeZoneField(ChoiceField):
|
||||
def get_attribute(self, instance):
|
||||
return instance.cache.get_or_set(
|
||||
@@ -283,17 +271,28 @@ class EventSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
||||
from pretix.base.plugins import get_all_plugins
|
||||
|
||||
plugins_available = {
|
||||
p.module: p for p in get_all_plugins(self.instance)
|
||||
p.module: p for p in get_all_plugins(event=self.instance)
|
||||
if not p.name.startswith('.') and getattr(p, 'visible', True)
|
||||
}
|
||||
current_plugins = self.instance.get_plugins() if self.instance and self.instance.pk else []
|
||||
settings_holder = self.instance if self.instance and self.instance.pk else self.context['organizer']
|
||||
|
||||
allowed_levels = (PLUGIN_LEVEL_EVENT, PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID)
|
||||
for plugin in value.get('plugins'):
|
||||
if plugin not in plugins_available:
|
||||
raise ValidationError(_('Unknown plugin: \'{name}\'.').format(name=plugin))
|
||||
if getattr(plugins_available[plugin], 'restricted', False):
|
||||
if plugin not in settings_holder.settings.allowed_restricted_plugins:
|
||||
raise ValidationError(_('Restricted plugin: \'{name}\'.').format(name=plugin))
|
||||
level = getattr(plugins_available[plugin], 'level', PLUGIN_LEVEL_EVENT)
|
||||
if level not in allowed_levels:
|
||||
raise ValidationError('Plugin cannot be enabled on this level: \'{name}\'.'.format(name=plugin))
|
||||
|
||||
if level == PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID and plugin not in self.context['organizer'].get_plugins():
|
||||
if plugin not in current_plugins:
|
||||
# Technically, this is allowed, but consumers might be confused if the API call doesn't do anything
|
||||
# so we prevent this change.
|
||||
raise ValidationError('Plugin should be enabled on organizer level first: \'{name}\'.'.format(name=plugin))
|
||||
|
||||
return value
|
||||
|
||||
|
||||
@@ -109,3 +109,19 @@ class UploadedFileField(serializers.Field):
|
||||
return None
|
||||
request = self.context['request']
|
||||
return request.build_absolute_uri(url)
|
||||
|
||||
|
||||
class PluginsField(serializers.Field):
|
||||
|
||||
def to_representation(self, obj):
|
||||
from pretix.base.plugins import get_all_plugins
|
||||
|
||||
return sorted([
|
||||
p.module for p in get_all_plugins()
|
||||
if not p.name.startswith('.') and getattr(p, 'visible', True) and p.module in obj.get_plugins()
|
||||
])
|
||||
|
||||
def to_internal_value(self, data):
|
||||
return {
|
||||
'plugins': data
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ from decimal import Decimal
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.db import transaction
|
||||
from django.db.models import Q
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
@@ -32,6 +33,7 @@ from rest_framework.exceptions import ValidationError
|
||||
|
||||
from pretix.api.auth.devicesecurity import get_all_security_profiles
|
||||
from pretix.api.serializers import AsymmetricField
|
||||
from pretix.api.serializers.fields import PluginsField
|
||||
from pretix.api.serializers.i18n import I18nAwareModelSerializer
|
||||
from pretix.api.serializers.order import CompatibleJSONField
|
||||
from pretix.api.serializers.settings import SettingsSerializer
|
||||
@@ -43,6 +45,10 @@ from pretix.base.models import (
|
||||
SalesChannel, SeatingPlan, Team, TeamAPIToken, TeamInvite, User,
|
||||
)
|
||||
from pretix.base.models.seating import SeatingPlanLayoutValidator
|
||||
from pretix.base.plugins import (
|
||||
PLUGIN_LEVEL_EVENT, PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID,
|
||||
PLUGIN_LEVEL_ORGANIZER,
|
||||
)
|
||||
from pretix.base.services.mail import SendMailException, mail
|
||||
from pretix.base.settings import validate_organizer_settings
|
||||
from pretix.helpers.urls import build_absolute_uri as build_global_uri
|
||||
@@ -53,13 +59,47 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
class OrganizerSerializer(I18nAwareModelSerializer):
|
||||
public_url = serializers.SerializerMethodField('get_organizer_url', read_only=True)
|
||||
plugins = PluginsField(required=False, source='*')
|
||||
name = serializers.CharField(read_only=True)
|
||||
slug = serializers.CharField(read_only=True)
|
||||
|
||||
def get_organizer_url(self, organizer):
|
||||
return build_absolute_uri(organizer, 'presale:organizer.index')
|
||||
|
||||
class Meta:
|
||||
model = Organizer
|
||||
fields = ('name', 'slug', 'public_url')
|
||||
fields = ('name', 'slug', 'public_url', 'plugins')
|
||||
|
||||
def validate_plugins(self, value):
|
||||
from pretix.base.plugins import get_all_plugins
|
||||
|
||||
plugins_available = {
|
||||
p.module: p for p in get_all_plugins(organizer=self.instance)
|
||||
if not p.name.startswith('.') and getattr(p, 'visible', True)
|
||||
}
|
||||
settings_holder = self.instance
|
||||
|
||||
allowed_levels = (PLUGIN_LEVEL_ORGANIZER, PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID)
|
||||
for plugin in value.get('plugins'):
|
||||
if plugin not in plugins_available:
|
||||
raise ValidationError(_('Unknown plugin: \'{name}\'.').format(name=plugin))
|
||||
if getattr(plugins_available[plugin], 'restricted', False):
|
||||
if plugin not in settings_holder.settings.allowed_restricted_plugins:
|
||||
raise ValidationError(_('Restricted plugin: \'{name}\'.').format(name=plugin))
|
||||
if getattr(plugins_available[plugin], 'level', PLUGIN_LEVEL_EVENT) not in allowed_levels:
|
||||
raise ValidationError('Plugin cannot be enabled on this level: \'{name}\'.'.format(name=plugin))
|
||||
|
||||
return value
|
||||
|
||||
@transaction.atomic
|
||||
def update(self, instance, validated_data):
|
||||
plugins = validated_data.pop('plugins', None)
|
||||
organizer = super().update(instance, validated_data)
|
||||
# Plugins
|
||||
if plugins is not None:
|
||||
organizer.set_active_plugins(plugins)
|
||||
organizer.save()
|
||||
return organizer
|
||||
|
||||
|
||||
class SeatingPlanSerializer(I18nAwareModelSerializer):
|
||||
|
||||
@@ -21,22 +21,22 @@
|
||||
#
|
||||
from datetime import timedelta
|
||||
|
||||
from django.dispatch import Signal, receiver
|
||||
from django.dispatch import receiver
|
||||
from django.utils.timezone import now
|
||||
from django_scopes import scopes_disabled
|
||||
|
||||
from pretix.api.models import ApiCall, WebHookCall
|
||||
from pretix.base.signals import EventPluginSignal, periodic_task
|
||||
from pretix.base.signals import EventPluginSignal, GlobalSignal, periodic_task
|
||||
from pretix.helpers.periodic import minimum_interval
|
||||
|
||||
register_webhook_events = Signal()
|
||||
register_webhook_events = GlobalSignal()
|
||||
"""
|
||||
This signal is sent out to get all known webhook events. Receivers should return an
|
||||
instance of a subclass of ``pretix.api.webhooks.WebhookEvent`` or a list of such
|
||||
instances.
|
||||
"""
|
||||
|
||||
register_device_security_profile = Signal()
|
||||
register_device_security_profile = GlobalSignal()
|
||||
"""
|
||||
This signal is sent out to get all known device security_profiles. Receivers should
|
||||
return an instance of a subclass of ``pretix.api.auth.devicesecurity.BaseSecurityProfile``
|
||||
|
||||
@@ -19,7 +19,9 @@
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import operator
|
||||
from decimal import Decimal
|
||||
from functools import reduce
|
||||
|
||||
import django_filters
|
||||
from django.contrib.auth.hashers import make_password
|
||||
@@ -48,15 +50,18 @@ from pretix.api.serializers.organizer import (
|
||||
TeamInviteSerializer, TeamMemberSerializer, TeamSerializer,
|
||||
)
|
||||
from pretix.base.models import (
|
||||
Customer, Device, GiftCard, GiftCardTransaction, Membership,
|
||||
MembershipType, Organizer, SalesChannel, SeatingPlan, Team, TeamAPIToken,
|
||||
TeamInvite, User,
|
||||
Customer, Device, Event, GiftCard, GiftCardTransaction, LogEntry,
|
||||
Membership, MembershipType, Organizer, SalesChannel, SeatingPlan, Team,
|
||||
TeamAPIToken, TeamInvite, User,
|
||||
)
|
||||
from pretix.base.plugins import (
|
||||
PLUGIN_LEVEL_EVENT, PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID,
|
||||
)
|
||||
from pretix.helpers import OF_SELF
|
||||
from pretix.helpers.dicts import merge_dicts
|
||||
|
||||
|
||||
class OrganizerViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
class OrganizerViewSet(mixins.UpdateModelMixin, viewsets.ReadOnlyModelViewSet):
|
||||
serializer_class = OrganizerSerializer
|
||||
queryset = Organizer.objects.none()
|
||||
lookup_field = 'slug'
|
||||
@@ -65,6 +70,7 @@ class OrganizerViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
filter_backends = (TotalOrderingFilter,)
|
||||
ordering = ('slug',)
|
||||
ordering_fields = ('name', 'slug')
|
||||
write_permission = "can_change_organizer_settings"
|
||||
|
||||
def get_queryset(self):
|
||||
if self.request.user.is_authenticated:
|
||||
@@ -83,6 +89,67 @@ class OrganizerViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
else:
|
||||
return Organizer.objects.filter(pk=self.request.auth.team.organizer_id)
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_update(self, serializer):
|
||||
from pretix.base.plugins import get_all_plugins
|
||||
|
||||
original_data = self.get_serializer(instance=serializer.instance).data
|
||||
|
||||
current_plugins_value = serializer.instance.get_plugins()
|
||||
updated_plugins_value = serializer.validated_data.get('plugins', None)
|
||||
|
||||
super().perform_update(serializer)
|
||||
|
||||
if serializer.data == original_data:
|
||||
# Performance optimization: If nothing was changed, we do not need to save or log anything.
|
||||
# This costs us a few cycles on save, but avoids thousands of lines in our log.
|
||||
return
|
||||
|
||||
if updated_plugins_value is not None and set(updated_plugins_value) != set(current_plugins_value):
|
||||
enabled = {m: 'enabled' for m in updated_plugins_value if m not in current_plugins_value}
|
||||
disabled = {m: 'disabled' for m in current_plugins_value if m not in updated_plugins_value}
|
||||
changed = merge_dicts(enabled, disabled)
|
||||
|
||||
plugins_available = {
|
||||
p.module: p
|
||||
for p in get_all_plugins(organizer=serializer.instance)
|
||||
if not p.name.startswith('.') and getattr(p, 'visible', True)
|
||||
}
|
||||
qs = []
|
||||
for module in disabled:
|
||||
pluginmeta = plugins_available[module]
|
||||
level = getattr(pluginmeta, 'level', PLUGIN_LEVEL_EVENT)
|
||||
if level == PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID:
|
||||
qs.append(Q(plugins__regex='(^|,)' + module + '(,|$)'))
|
||||
|
||||
if qs:
|
||||
events_to_disable = set(self.request.organizer.events.filter(
|
||||
reduce(operator.or_, qs)
|
||||
).values_list("pk", flat=True))
|
||||
logentries_to_save = []
|
||||
events_to_save = []
|
||||
|
||||
for e in self.request.organizer.events.filter(pk__in=events_to_disable):
|
||||
for module in disabled:
|
||||
if module in e.get_plugins():
|
||||
logentries_to_save.append(
|
||||
e.log_action('pretix.event.plugins.disabled', user=self.request.user, auth=self.request.auth,
|
||||
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)
|
||||
|
||||
for module, operation in changed.items():
|
||||
serializer.instance.log_action(
|
||||
'pretix.organizer.plugins.' + operation,
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data={'plugin': module}
|
||||
)
|
||||
|
||||
|
||||
class SeatingPlanViewSet(viewsets.ModelViewSet):
|
||||
serializer_class = SeatingPlanSerializer
|
||||
|
||||
@@ -38,13 +38,13 @@ from pretix.base.datasync.sourcefields import (
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.logentrytype_registry import make_link
|
||||
from pretix.base.models.datasync import OrderSyncQueue, OrderSyncResult
|
||||
from pretix.base.signals import EventPluginRegistry
|
||||
from pretix.base.signals import PluginAwareRegistry
|
||||
from pretix.helpers import OF_SELF
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
datasync_providers = EventPluginRegistry({"identifier": lambda o: o.identifier})
|
||||
datasync_providers = PluginAwareRegistry({"identifier": lambda o: o.identifier})
|
||||
|
||||
|
||||
class BaseSyncError(Exception):
|
||||
|
||||
@@ -26,7 +26,7 @@ from django.urls import reverse
|
||||
from django.utils.html import format_html
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from pretix.base.signals import EventPluginRegistry
|
||||
from pretix.base.signals import PluginAwareRegistry
|
||||
|
||||
|
||||
def make_link(a_map, wrapper, is_active=True, event=None, plugin_name=None):
|
||||
@@ -55,7 +55,7 @@ def make_link(a_map, wrapper, is_active=True, event=None, plugin_name=None):
|
||||
return format_html(wrapper, **a_map)
|
||||
|
||||
|
||||
class LogEntryTypeRegistry(EventPluginRegistry):
|
||||
class LogEntryTypeRegistry(PluginAwareRegistry):
|
||||
def __init__(self):
|
||||
super().__init__({'action_type': lambda o: getattr(o, 'action_type')})
|
||||
|
||||
|
||||
18
src/pretix/base/migrations/0287_organizer_plugins.py
Normal file
18
src/pretix/base/migrations/0287_organizer_plugins.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.2.17 on 2025-07-12 09:25
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("pretixbase", "0286_settingsstore_unique"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="organizer",
|
||||
name="plugins",
|
||||
field=models.TextField(default=""),
|
||||
),
|
||||
]
|
||||
@@ -551,8 +551,7 @@ class Event(EventMixin, LoggedModel):
|
||||
:type presale_end: datetime
|
||||
:param location: venue
|
||||
:type location: str
|
||||
:param plugins: A comma-separated list of plugin names that are active for this
|
||||
event.
|
||||
:param plugins: A comma-separated list of plugin names that are active for this event.
|
||||
:type plugins: str
|
||||
:param has_subevents: Enable event series functionality
|
||||
:type has_subevents: bool
|
||||
@@ -1393,7 +1392,7 @@ class Event(EventMixin, LoggedModel):
|
||||
from pretix.base.plugins import get_all_plugins
|
||||
|
||||
return {
|
||||
p.module: p for p in get_all_plugins(self)
|
||||
p.module: p for p in get_all_plugins(event=self)
|
||||
if not p.name.startswith('.') and getattr(p, 'visible', True)
|
||||
}
|
||||
|
||||
@@ -1412,12 +1411,20 @@ class Event(EventMixin, LoggedModel):
|
||||
self.plugins = ",".join(modules)
|
||||
|
||||
def enable_plugin(self, module, allow_restricted=frozenset()):
|
||||
"""
|
||||
Adds a plugin to the list of plugins, calling its ``installed`` hook (if available).
|
||||
It is the caller's responsibility to save the event object.
|
||||
"""
|
||||
plugins_active = self.get_plugins()
|
||||
if module not in plugins_active:
|
||||
plugins_active.append(module)
|
||||
self.set_active_plugins(plugins_active, allow_restricted=allow_restricted)
|
||||
|
||||
def disable_plugin(self, module):
|
||||
"""
|
||||
Adds a plugin to the list of plugins, calling its ``uninstalled`` hook (if available).
|
||||
It is the caller's responsibility to save the event object.
|
||||
"""
|
||||
plugins_active = self.get_plugins()
|
||||
if module in plugins_active:
|
||||
plugins_active.remove(module)
|
||||
|
||||
@@ -40,9 +40,6 @@ from django.contrib.contenttypes.models import ContentType
|
||||
from django.db import connections, models
|
||||
from django.utils.functional import cached_property
|
||||
|
||||
from pretix.base.logentrytype_registry import log_entry_types, make_link
|
||||
from pretix.base.signals import is_app_active, logentry_object_link
|
||||
|
||||
|
||||
class VisibleOnlyManager(models.Manager):
|
||||
def get_queryset(self):
|
||||
@@ -91,6 +88,8 @@ class LogEntry(models.Model):
|
||||
indexes = [models.Index(fields=["datetime", "id"])]
|
||||
|
||||
def display(self):
|
||||
from pretix.base.logentrytype_registry import log_entry_types
|
||||
|
||||
log_entry_type, meta = log_entry_types.get(action_type=self.action_type)
|
||||
if log_entry_type:
|
||||
return log_entry_type.display(self, self.parsed_data)
|
||||
@@ -128,6 +127,11 @@ class LogEntry(models.Model):
|
||||
|
||||
@cached_property
|
||||
def display_object(self):
|
||||
from pretix.base.logentrytype_registry import (
|
||||
log_entry_types, make_link,
|
||||
)
|
||||
from pretix.base.signals import is_app_active, logentry_object_link
|
||||
|
||||
from . import (
|
||||
Discount, Event, Item, Order, Question, Quota, SubEvent, Voucher,
|
||||
)
|
||||
|
||||
@@ -68,6 +68,8 @@ class Organizer(LoggedModel):
|
||||
:param slug: A globally unique, short name for this organizer, to be used
|
||||
in URLs and similar places.
|
||||
:type slug: str
|
||||
:param plugins: A comma-separated list of plugin names that are active for this organizer.
|
||||
:type plugins: str
|
||||
"""
|
||||
|
||||
settings_namespace = 'organizer'
|
||||
@@ -91,6 +93,10 @@ class Organizer(LoggedModel):
|
||||
verbose_name=_("Short form"),
|
||||
unique=True
|
||||
)
|
||||
plugins = models.TextField(
|
||||
verbose_name=_("Plugins"),
|
||||
null=False, blank=True, default="",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Organizer")
|
||||
@@ -119,6 +125,11 @@ class Organizer(LoggedModel):
|
||||
"""
|
||||
self.settings.cookie_consent = True
|
||||
|
||||
plugins = [p for p in settings.PRETIX_PLUGINS_ORGANIZER_DEFAULT.split(",") if p]
|
||||
if plugins:
|
||||
self.set_active_plugins(plugins, allow_restricted=plugins)
|
||||
self.save()
|
||||
|
||||
def get_cache(self):
|
||||
"""
|
||||
Returns an :py:class:`ObjectRelatedCache` object. This behaves equivalent to
|
||||
@@ -143,6 +154,61 @@ class Organizer(LoggedModel):
|
||||
|
||||
return ObjectRelatedCache(self)
|
||||
|
||||
def get_plugins(self):
|
||||
"""
|
||||
Returns the names of the plugins activated for this organizer as a list.
|
||||
"""
|
||||
if not self.plugins:
|
||||
return []
|
||||
return self.plugins.split(",")
|
||||
|
||||
def get_available_plugins(self):
|
||||
from pretix.base.plugins import get_all_plugins
|
||||
|
||||
return {
|
||||
p.module: p for p in get_all_plugins(organizer=self)
|
||||
if not p.name.startswith('.') and getattr(p, 'visible', True)
|
||||
}
|
||||
|
||||
def set_active_plugins(self, modules, allow_restricted=frozenset()):
|
||||
plugins_active = self.get_plugins()
|
||||
plugins_available = self.get_available_plugins()
|
||||
|
||||
enable = [m for m in modules if m not in plugins_active and m in plugins_available]
|
||||
|
||||
for module in enable:
|
||||
if getattr(plugins_available[module].app, 'restricted', False) and module not in allow_restricted:
|
||||
modules.remove(module)
|
||||
elif hasattr(plugins_available[module].app, 'installed'):
|
||||
getattr(plugins_available[module].app, 'installed')(self)
|
||||
|
||||
self.plugins = ",".join(modules)
|
||||
|
||||
def enable_plugin(self, module, allow_restricted=frozenset()):
|
||||
"""
|
||||
Adds a plugin to the list of plugins, calling its ``installed`` hook (if available).
|
||||
It is the caller's responsibility to save the organizer object.
|
||||
"""
|
||||
plugins_active = self.get_plugins()
|
||||
if module not in plugins_active:
|
||||
plugins_active.append(module)
|
||||
self.set_active_plugins(plugins_active, allow_restricted=allow_restricted)
|
||||
|
||||
def disable_plugin(self, module):
|
||||
"""
|
||||
Removes a plugin from the list of plugins, calling its ``uninstalled`` hook (if available).
|
||||
It is the caller's responsibility to save the organizer object and, in case of a hybrid organizer-event plugin,
|
||||
to remove it from all events.
|
||||
"""
|
||||
plugins_active = self.get_plugins()
|
||||
if module in plugins_active:
|
||||
plugins_active.remove(module)
|
||||
self.set_active_plugins(plugins_active)
|
||||
|
||||
plugins_available = self.get_available_plugins()
|
||||
if module in plugins_available and hasattr(plugins_available[module].app, 'uninstalled'):
|
||||
getattr(plugins_available[module].app, 'uninstalled')(self)
|
||||
|
||||
@property
|
||||
def timezone(self):
|
||||
return pytz_deprecation_shim.timezone(self.settings.timezone)
|
||||
|
||||
@@ -28,8 +28,13 @@ import importlib_metadata as metadata
|
||||
from django.apps import AppConfig, apps
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from packaging.requirements import Requirement
|
||||
|
||||
PLUGIN_LEVEL_EVENT = 'event'
|
||||
PLUGIN_LEVEL_ORGANIZER = 'organizer'
|
||||
PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID = 'event_organizer'
|
||||
|
||||
|
||||
class PluginType(Enum):
|
||||
"""
|
||||
@@ -43,11 +48,14 @@ class PluginType(Enum):
|
||||
EXPORT = 4
|
||||
|
||||
|
||||
def get_all_plugins(event=None) -> List[type]:
|
||||
def get_all_plugins(*, event=None, organizer=None) -> List[type]:
|
||||
"""
|
||||
Returns the PretixPluginMeta classes of all plugins found in the installed Django apps.
|
||||
"""
|
||||
assert not event or not organizer
|
||||
plugins = []
|
||||
event_fallback = None
|
||||
event_fallback_used = False
|
||||
for app in apps.get_app_configs():
|
||||
if hasattr(app, 'PretixPluginMeta'):
|
||||
meta = app.PretixPluginMeta
|
||||
@@ -56,8 +64,26 @@ def get_all_plugins(event=None) -> List[type]:
|
||||
if app.name in settings.PRETIX_PLUGINS_EXCLUDE:
|
||||
continue
|
||||
|
||||
if hasattr(app, 'is_available') and event:
|
||||
if not app.is_available(event):
|
||||
level = getattr(app, "level", PLUGIN_LEVEL_EVENT)
|
||||
if level == PLUGIN_LEVEL_EVENT:
|
||||
if event and hasattr(app, 'is_available'):
|
||||
if not app.is_available(event):
|
||||
continue
|
||||
elif organizer and hasattr(app, 'is_available'):
|
||||
if not event_fallback_used:
|
||||
event_fallback = organizer.events.first()
|
||||
event_fallback_used = True
|
||||
if not event_fallback or not app.is_available(event_fallback):
|
||||
continue
|
||||
elif level == PLUGIN_LEVEL_ORGANIZER:
|
||||
if organizer and hasattr(app, 'is_available'):
|
||||
if not app.is_available(organizer):
|
||||
continue
|
||||
elif event and hasattr(app, 'is_available'):
|
||||
if not app.is_available(event.organizer):
|
||||
continue
|
||||
elif level == PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID and (event or organizer) and hasattr(app, 'is_available'):
|
||||
if not app.is_available(event or organizer):
|
||||
continue
|
||||
|
||||
plugins.append(meta)
|
||||
@@ -91,3 +117,26 @@ class PluginConfig(AppConfig, metaclass=PluginConfigMeta):
|
||||
self.name, req, requirement_version
|
||||
))
|
||||
sys.exit(1)
|
||||
|
||||
if not hasattr(self.PretixPluginMeta, 'level'):
|
||||
self.PretixPluginMeta.level = PLUGIN_LEVEL_EVENT
|
||||
if self.PretixPluginMeta.level not in (PLUGIN_LEVEL_EVENT, PLUGIN_LEVEL_ORGANIZER, PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID):
|
||||
raise ImproperlyConfigured(f"Unknown plugin level '{self.PretixPluginMeta.level}'")
|
||||
|
||||
|
||||
CATEGORY_ORDER = [
|
||||
'FEATURE',
|
||||
'PAYMENT',
|
||||
'INTEGRATION',
|
||||
'CUSTOMIZATION',
|
||||
'FORMAT',
|
||||
'API',
|
||||
]
|
||||
CATEGORY_LABELS = {
|
||||
'FEATURE': _('Features'),
|
||||
'PAYMENT': _('Payment providers'),
|
||||
'INTEGRATION': _('Integrations'),
|
||||
'CUSTOMIZATION': _('Customizations'),
|
||||
'FORMAT': _('Output and export formats'),
|
||||
'API': _('API features'),
|
||||
}
|
||||
|
||||
@@ -114,7 +114,7 @@ def restricted_plugin_kwargs():
|
||||
from pretix.base.plugins import get_all_plugins
|
||||
|
||||
plugins_available = [
|
||||
(p.module, p.name) for p in get_all_plugins(None)
|
||||
(p.module, p.name) for p in get_all_plugins()
|
||||
if (
|
||||
not p.name.startswith('.') and
|
||||
getattr(p, 'restricted', False) and
|
||||
|
||||
@@ -33,16 +33,23 @@
|
||||
# License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
import warnings
|
||||
from typing import Any, Callable, List, Tuple
|
||||
from typing import Any, Callable, Generic, List, Tuple, TypeVar
|
||||
|
||||
import django.dispatch
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.dispatch.dispatcher import NO_RECEIVERS
|
||||
|
||||
from .models import Event
|
||||
from .models.event import Event
|
||||
from .models.organizer import Organizer
|
||||
from .plugins import (
|
||||
PLUGIN_LEVEL_EVENT, PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID,
|
||||
PLUGIN_LEVEL_ORGANIZER,
|
||||
)
|
||||
|
||||
app_cache = {}
|
||||
T = TypeVar('T')
|
||||
|
||||
|
||||
def _populate_app_cache():
|
||||
@@ -56,6 +63,9 @@ def get_defining_app(o):
|
||||
if "sentry" in o.__module__:
|
||||
o = o.__wrapped__
|
||||
|
||||
if hasattr(o, "__mocked_app"):
|
||||
return o.__mocked_app
|
||||
|
||||
# Find the Django application this belongs to
|
||||
searchpath = o.__module__
|
||||
|
||||
@@ -74,43 +84,71 @@ def get_defining_app(o):
|
||||
return app
|
||||
|
||||
|
||||
def is_app_active(sender, app):
|
||||
def is_app_active(sender, app, allow_legacy_plugins=False):
|
||||
if app == 'CORE':
|
||||
return True
|
||||
|
||||
excluded = settings.PRETIX_PLUGINS_EXCLUDE
|
||||
if sender and app and app.name in sender.get_plugins() and app.name not in excluded:
|
||||
if not sender or not app or app.name in excluded:
|
||||
return False
|
||||
|
||||
level = getattr(app.PretixPluginMeta, "level", PLUGIN_LEVEL_EVENT)
|
||||
if level == PLUGIN_LEVEL_EVENT:
|
||||
if isinstance(sender, Event):
|
||||
enabled = app.name in sender.get_plugins()
|
||||
elif isinstance(sender, Organizer) and allow_legacy_plugins:
|
||||
# Deprecated behaviour: Event plugins that are registered on organizer level are considered active for
|
||||
# all organizers in the context of signals that used to be global signals before the introduction of
|
||||
# organizer-level plugin. A deprecation warning is emitted at .connect() time.
|
||||
enabled = True
|
||||
else:
|
||||
raise ImproperlyConfigured(f"Cannot check if event-level plugin is active on {type(sender)}")
|
||||
elif level == PLUGIN_LEVEL_ORGANIZER:
|
||||
if isinstance(sender, Organizer):
|
||||
enabled = app.name in sender.get_plugins()
|
||||
elif isinstance(sender, Event):
|
||||
enabled = app.name in sender.organizer.get_plugins()
|
||||
else:
|
||||
raise ImproperlyConfigured(f"Cannot check if organizer-level plugin is active on {type(sender)}")
|
||||
elif level == PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID:
|
||||
if isinstance(sender, Organizer):
|
||||
enabled = app.name in sender.get_plugins()
|
||||
elif isinstance(sender, Event):
|
||||
enabled = app.name in sender.get_plugins() and app.name in sender.organizer.get_plugins()
|
||||
else:
|
||||
raise ImproperlyConfigured(f"Cannot check if hybrid event/organizer-level plugin is active on {type(sender)}")
|
||||
else:
|
||||
raise ImproperlyConfigured("Unknown plugin level")
|
||||
|
||||
if enabled:
|
||||
if not hasattr(app, 'compatibility_errors') or not app.compatibility_errors:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def is_receiver_active(sender, receiver):
|
||||
def is_receiver_active(sender, receiver, allow_legacy_plugins=False):
|
||||
if sender is None:
|
||||
# Send to all events!
|
||||
return True
|
||||
|
||||
app = get_defining_app(receiver)
|
||||
|
||||
return is_app_active(sender, app)
|
||||
return is_app_active(sender, app, allow_legacy_plugins)
|
||||
|
||||
|
||||
class EventPluginSignal(django.dispatch.Signal):
|
||||
"""
|
||||
This is an extension to Django's built-in signals which differs in a way that it sends
|
||||
out it's events only to receivers which belong to plugins that are enabled for the given
|
||||
Event.
|
||||
"""
|
||||
class PluginSignal(Generic[T], django.dispatch.Signal):
|
||||
type = None
|
||||
|
||||
def send(self, sender: Event, **named) -> List[Tuple[Callable, Any]]:
|
||||
def _is_receiver_active(self, sender, receiver):
|
||||
return is_receiver_active(sender, receiver)
|
||||
|
||||
def send(self, sender: T, **named) -> List[Tuple[Callable, Any]]:
|
||||
"""
|
||||
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 ``pretix.base.models.Event``.
|
||||
plugins enabled for the given event / organizer.
|
||||
"""
|
||||
if sender and not isinstance(sender, Event):
|
||||
raise ValueError("Sender needs to be an event.")
|
||||
if sender and not isinstance(sender, self.type):
|
||||
raise ValueError(f"Sender needs to be of type {self.type}.")
|
||||
|
||||
responses = []
|
||||
if not self.receivers or self.sender_receivers_cache.get(sender) is NO_RECEIVERS:
|
||||
@@ -120,20 +158,18 @@ class EventPluginSignal(django.dispatch.Signal):
|
||||
_populate_app_cache()
|
||||
|
||||
for receiver in self._sorted_receivers(sender):
|
||||
if is_receiver_active(sender, receiver):
|
||||
if self._is_receiver_active(sender, receiver):
|
||||
response = receiver(signal=self, sender=sender, **named)
|
||||
responses.append((receiver, response))
|
||||
return responses
|
||||
|
||||
def send_chained(self, sender: Event, chain_kwarg_name, **named) -> List[Tuple[Callable, Any]]:
|
||||
def send_chained(self, sender: T, chain_kwarg_name, **named) -> List[Tuple[Callable, Any]]:
|
||||
"""
|
||||
Send signal from sender to all connected receivers. The return value of the first receiver
|
||||
will be used as the keyword argument specified by ``chain_kwarg_name`` in the input to the
|
||||
second receiver and so on. The return value of the last receiver is returned by this method.
|
||||
|
||||
sender is required to be an instance of ``pretix.base.models.Event``.
|
||||
"""
|
||||
if sender and not isinstance(sender, Event):
|
||||
if sender and not isinstance(sender, self.type):
|
||||
raise ValueError("Sender needs to be an event.")
|
||||
|
||||
response = named.get(chain_kwarg_name)
|
||||
@@ -144,20 +180,18 @@ class EventPluginSignal(django.dispatch.Signal):
|
||||
_populate_app_cache()
|
||||
|
||||
for receiver in self._sorted_receivers(sender):
|
||||
if is_receiver_active(sender, receiver):
|
||||
if self._is_receiver_active(sender, receiver):
|
||||
named[chain_kwarg_name] = response
|
||||
response = receiver(signal=self, sender=sender, **named)
|
||||
return response
|
||||
|
||||
def send_robust(self, sender: Event, **named) -> List[Tuple[Callable, Any]]:
|
||||
def send_robust(self, sender: T, **named) -> List[Tuple[Callable, Any]]:
|
||||
"""
|
||||
Send signal from sender to all connected receivers. If a receiver raises an exception
|
||||
instead of returning a value, the exception is included as the result instead of
|
||||
stopping the response chain at the offending receiver.
|
||||
|
||||
sender is required to be an instance of ``pretix.base.models.Event``.
|
||||
"""
|
||||
if sender and not isinstance(sender, Event):
|
||||
if sender and not isinstance(sender, self.type):
|
||||
raise ValueError("Sender needs to be an event.")
|
||||
|
||||
responses = []
|
||||
@@ -171,7 +205,7 @@ class EventPluginSignal(django.dispatch.Signal):
|
||||
_populate_app_cache()
|
||||
|
||||
for receiver in self._sorted_receivers(sender):
|
||||
if is_receiver_active(sender, receiver):
|
||||
if self._is_receiver_active(sender, receiver):
|
||||
try:
|
||||
response = receiver(signal=self, sender=sender, **named)
|
||||
except Exception as err:
|
||||
@@ -193,6 +227,67 @@ class EventPluginSignal(django.dispatch.Signal):
|
||||
return sorted_list
|
||||
|
||||
|
||||
class EventPluginSignal(PluginSignal[Event]):
|
||||
"""
|
||||
This is an extension to Django's built-in signals which differs in a way that it sends
|
||||
out it's events only to receivers which belong to plugins that are enabled for the given
|
||||
Event.
|
||||
"""
|
||||
type = Event
|
||||
|
||||
def connect(self, receiver, sender=None, weak=True, dispatch_uid=None):
|
||||
app = get_defining_app(receiver)
|
||||
if app != "CORE":
|
||||
if not hasattr(app, "PretixPluginMeta"):
|
||||
raise ImproperlyConfigured(
|
||||
f"{app} uses an EventPluginSignal but is not a pretix plugin"
|
||||
)
|
||||
allowed_levels = (PLUGIN_LEVEL_ORGANIZER, PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID, PLUGIN_LEVEL_EVENT)
|
||||
if getattr(app.PretixPluginMeta, "level", PLUGIN_LEVEL_EVENT) not in allowed_levels:
|
||||
# This check is redundant for now, but will be useful if we ever add other levels
|
||||
raise ImproperlyConfigured(
|
||||
f"{app} uses an EventPluginSignal but is not a plugin that can be active on event or organizer level"
|
||||
)
|
||||
return super().connect(receiver, sender, weak, dispatch_uid)
|
||||
|
||||
|
||||
class OrganizerPluginSignal(PluginSignal[Organizer]):
|
||||
"""
|
||||
This is an extension to Django's built-in signals which differs in a way that it sends
|
||||
out it's events only to receivers which belong to plugins that are enabled for the given
|
||||
Organizer.
|
||||
"""
|
||||
type = Organizer
|
||||
|
||||
def __init__(self, allow_legacy_plugins=False):
|
||||
self.allow_legacy_plugins = allow_legacy_plugins
|
||||
super().__init__()
|
||||
|
||||
def _is_receiver_active(self, sender, receiver):
|
||||
return is_receiver_active(sender, receiver, allow_legacy_plugins=self.allow_legacy_plugins)
|
||||
|
||||
def connect(self, receiver, sender=None, weak=True, dispatch_uid=None):
|
||||
app = get_defining_app(receiver)
|
||||
if app != "CORE":
|
||||
if not hasattr(app, "PretixPluginMeta"):
|
||||
raise ImproperlyConfigured(
|
||||
f"{app} uses an OrganizerPluginSignal but is not a pretix plugin"
|
||||
)
|
||||
allowed_levels = (PLUGIN_LEVEL_ORGANIZER, PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID)
|
||||
if getattr(app.PretixPluginMeta, "level", PLUGIN_LEVEL_EVENT) not in allowed_levels:
|
||||
if getattr(app.PretixPluginMeta, "level", PLUGIN_LEVEL_EVENT) == PLUGIN_LEVEL_EVENT and self.allow_legacy_plugins:
|
||||
warnings.warn(
|
||||
'This signal will soon be only available for plugins that declare to be organizer-level',
|
||||
stacklevel=3,
|
||||
category=DeprecationWarning,
|
||||
)
|
||||
else:
|
||||
raise ImproperlyConfigured(
|
||||
f"{app} uses an OrganizerPluginSignal but is not a plugin that can be active on organizer level"
|
||||
)
|
||||
return super().connect(receiver, sender, weak, dispatch_uid)
|
||||
|
||||
|
||||
class GlobalSignal(django.dispatch.Signal):
|
||||
def send_chained(self, sender: Event, chain_kwarg_name, **named) -> List[Tuple[Callable, Any]]:
|
||||
"""
|
||||
@@ -211,10 +306,14 @@ class GlobalSignal(django.dispatch.Signal):
|
||||
return response
|
||||
|
||||
|
||||
class DeprecatedSignal(django.dispatch.Signal):
|
||||
class DeprecatedSignal(GlobalSignal):
|
||||
|
||||
def connect(self, receiver, sender=None, weak=True, dispatch_uid=None):
|
||||
warnings.warn('This signal is deprecated and will soon be removed', stacklevel=3)
|
||||
warnings.warn(
|
||||
'This signal is deprecated and will soon be removed',
|
||||
stacklevel=3,
|
||||
category=DeprecationWarning,
|
||||
)
|
||||
super().connect(receiver, sender=None, weak=True, dispatch_uid=None)
|
||||
|
||||
|
||||
@@ -324,20 +423,39 @@ class Registry:
|
||||
)
|
||||
|
||||
|
||||
class EventPluginRegistry(Registry):
|
||||
class PluginAwareRegistry(Registry):
|
||||
"""
|
||||
A Registry which automatically annotates entries with a "plugin" key, specifying which plugin
|
||||
the entry is defined in. This allows the consumer of entries to determine whether an entry is
|
||||
enabled for a given event, or filter only for entries defined by enabled plugins.
|
||||
enabled for a given event or organizer, or filter only for entries defined by enabled plugins.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
logtype, meta = my_registry.find(action_type="foo.bar.baz")
|
||||
# meta["plugin"] contains the django app name of the defining plugin
|
||||
"""
|
||||
allowed_levels = [
|
||||
PLUGIN_LEVEL_EVENT,
|
||||
PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID,
|
||||
PLUGIN_LEVEL_ORGANIZER,
|
||||
]
|
||||
|
||||
def __init__(self, keys):
|
||||
super().__init__({"plugin": lambda o: get_defining_app(o), **keys})
|
||||
def get_plugin(o):
|
||||
app = get_defining_app(o)
|
||||
if app != "CORE":
|
||||
if not hasattr(app, "PretixPluginMeta"):
|
||||
raise ImproperlyConfigured(
|
||||
f"{app} uses an PluginAwareRegistry but is not a pretix plugin"
|
||||
)
|
||||
level = getattr(app.PretixPluginMeta, "level", PLUGIN_LEVEL_EVENT)
|
||||
if level not in self.allowed_levels:
|
||||
raise ImproperlyConfigured(
|
||||
f"{app} has level {level} but should have one of {self.allowed_levels} to use this registry"
|
||||
)
|
||||
return app
|
||||
|
||||
super().__init__({"plugin": get_plugin, **keys})
|
||||
|
||||
def filter(self, active_in=None, **kwargs):
|
||||
result = super().filter(**kwargs)
|
||||
@@ -357,6 +475,9 @@ class EventPluginRegistry(Registry):
|
||||
return item, meta
|
||||
|
||||
|
||||
EventPluginRegistry = PluginAwareRegistry # for backwards compatibility
|
||||
|
||||
|
||||
event_live_issues = EventPluginSignal()
|
||||
"""
|
||||
This signal is sent out to determine whether an event can be taken live. If you want to
|
||||
@@ -449,7 +570,7 @@ This signal is sent out when a notification is sent.
|
||||
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
|
||||
register_sales_channel_types = django.dispatch.Signal()
|
||||
register_sales_channel_types = GlobalSignal()
|
||||
"""
|
||||
This signal is sent out to get all known sales channels types. Receivers should return an
|
||||
instance of a subclass of ``pretix.base.channels.SalesChannelType`` or a list of such
|
||||
@@ -467,10 +588,8 @@ subclass of pretix.base.exporter.BaseExporter
|
||||
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
|
||||
register_multievent_data_exporters = django.dispatch.Signal()
|
||||
register_multievent_data_exporters = OrganizerPluginSignal(allow_legacy_plugins=True)
|
||||
"""
|
||||
Arguments: ``event``
|
||||
|
||||
This signal is sent out to get all known data exporters, which support exporting data for
|
||||
multiple events. Receivers should return a subclass of pretix.base.exporter.BaseExporter
|
||||
|
||||
@@ -742,7 +861,7 @@ The ``sender`` keyword argument will contain the event. The ``target`` will cont
|
||||
copy to, the ``source`` keyword argument will contain the product to **copy from**.
|
||||
"""
|
||||
|
||||
periodic_task = django.dispatch.Signal()
|
||||
periodic_task = GlobalSignal()
|
||||
"""
|
||||
This is a regular django signal (no pretix event signal) that we send out every
|
||||
time the periodic task cronjob runs. This interval is not sharply defined, it can
|
||||
@@ -751,13 +870,13 @@ idempotent, i.e. it should not make a difference if this is sent out more often
|
||||
than expected.
|
||||
"""
|
||||
|
||||
register_global_settings = django.dispatch.Signal()
|
||||
register_global_settings = GlobalSignal()
|
||||
"""
|
||||
All plugins that are installed may send fields for the global settings form, as
|
||||
an OrderedDict of (setting name, form field).
|
||||
"""
|
||||
|
||||
gift_card_transaction_display = django.dispatch.Signal()
|
||||
gift_card_transaction_display = GlobalSignal() # todo: replace with OrganizerPluginSignal?
|
||||
"""
|
||||
Arguments: ``transaction``, ``customer_facing``
|
||||
|
||||
@@ -969,7 +1088,7 @@ return a dictionary mapping names of attributes in the settings store to DRF ser
|
||||
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
|
||||
customer_created = GlobalSignal()
|
||||
customer_created = OrganizerPluginSignal(allow_legacy_plugins=True)
|
||||
"""
|
||||
Arguments: ``customer``
|
||||
|
||||
@@ -979,7 +1098,7 @@ object is given as the first argument.
|
||||
The ``sender`` keyword argument will contain the organizer.
|
||||
"""
|
||||
|
||||
customer_signed_in = GlobalSignal()
|
||||
customer_signed_in = OrganizerPluginSignal(allow_legacy_plugins=True)
|
||||
"""
|
||||
Arguments: ``customer``
|
||||
|
||||
@@ -989,7 +1108,7 @@ is given as the first argument.
|
||||
The ``sender`` keyword argument will contain the organizer.
|
||||
"""
|
||||
|
||||
device_info_updated = django.dispatch.Signal()
|
||||
device_info_updated = GlobalSignal() # todo: replace with OrganizerPluginSignal?
|
||||
"""
|
||||
Arguments: ``old_device``, ``new_device``
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -31,12 +31,9 @@
|
||||
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
|
||||
# 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 pretix.base.signals import EventPluginSignal, GlobalSignal
|
||||
|
||||
from django.dispatch import Signal
|
||||
|
||||
from pretix.base.signals import EventPluginSignal
|
||||
|
||||
global_html_head = Signal()
|
||||
global_html_head = GlobalSignal()
|
||||
"""
|
||||
Arguments: ``request``
|
||||
|
||||
@@ -47,7 +44,7 @@ of every page in the frontend. You will get the request as the keyword argument
|
||||
This signal is called regardless of whether your plugin is active for all pages of the system.
|
||||
"""
|
||||
|
||||
global_html_page_header = Signal()
|
||||
global_html_page_header = GlobalSignal()
|
||||
"""
|
||||
Arguments: ``request``
|
||||
|
||||
@@ -58,7 +55,7 @@ of every page in the frontend. You will get the request as the keyword argument
|
||||
This signal is called regardless of whether your plugin is active for all pages of the system.
|
||||
"""
|
||||
|
||||
global_html_footer = Signal()
|
||||
global_html_footer = GlobalSignal()
|
||||
"""
|
||||
Arguments: ``request``
|
||||
|
||||
@@ -134,7 +131,7 @@ are expected to return a dictionary containing the keys ``label`` and ``url``.
|
||||
As with all plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
|
||||
global_footer_link = Signal()
|
||||
global_footer_link = GlobalSignal()
|
||||
"""
|
||||
Arguments: ``request``
|
||||
|
||||
|
||||
@@ -26,11 +26,10 @@ from urllib.parse import urljoin, urlsplit
|
||||
import sass
|
||||
from django.conf import settings
|
||||
from django.contrib.staticfiles import finders
|
||||
from django.dispatch import Signal
|
||||
from django.templatetags.static import static as _static
|
||||
|
||||
from pretix.base.models import Event, Organizer
|
||||
from pretix.base.signals import EventPluginSignal
|
||||
from pretix.base.signals import EventPluginSignal, GlobalSignal
|
||||
from pretix.multidomain.urlreverse import (
|
||||
get_event_domain, get_organizer_domain,
|
||||
)
|
||||
@@ -38,7 +37,7 @@ from pretix.multidomain.urlreverse import (
|
||||
logger = logging.getLogger('pretix.presale.style')
|
||||
|
||||
|
||||
register_fonts = Signal()
|
||||
register_fonts = GlobalSignal()
|
||||
"""
|
||||
Return a dictionaries of the following structure. Paths should be relative to static root or an absolute URL. In the
|
||||
latter case, the fonts won't be available for PDF-rendering.
|
||||
|
||||
@@ -217,6 +217,8 @@ if config.getboolean('pretix', 'trust_x_forwarded_proto', fallback=False):
|
||||
|
||||
PRETIX_PLUGINS_DEFAULT = config.get('pretix', 'plugins_default',
|
||||
fallback='pretix.plugins.sendmail,pretix.plugins.statistics,pretix.plugins.checkinlists')
|
||||
PRETIX_PLUGINS_ORGANIZER_DEFAULT = config.get('pretix', 'plugins_organizer_default',
|
||||
fallback='')
|
||||
PRETIX_PLUGINS_EXCLUDE = config.get('pretix', 'plugins_exclude', fallback='').split(',')
|
||||
PRETIX_PLUGINS_SHOW_META = config.getboolean('pretix', 'plugins_show_meta', fallback=True)
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ filterwarnings =
|
||||
ignore::django.utils.deprecation.RemovedInDjango51Warning:django.core.files.storage
|
||||
ignore:.*index_together.*:django.utils.deprecation.RemovedInDjango51Warning:
|
||||
ignore:.*get_storage_class.*:django.utils.deprecation.RemovedInDjango51Warning:compressor
|
||||
ignore:.*This signal will soon be only available for plugins that declare to be organizer-level.*:DeprecationWarning:
|
||||
ignore::DeprecationWarning:mt940
|
||||
ignore::DeprecationWarning:cbor2
|
||||
ignore::DeprecationWarning:markdown
|
||||
|
||||
@@ -855,6 +855,39 @@ def test_event_update_plugins_validation(token_client, organizer, event, item, m
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
|
||||
resp = token_client.patch(
|
||||
'/api/v1/organizers/{}/events/{}/'.format(organizer.slug, event.slug),
|
||||
{
|
||||
"plugins": ["tests.testdummyorga"]
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert resp.data == {"plugins": ["Plugin cannot be enabled on this level: 'tests.testdummyorga'."]}
|
||||
|
||||
resp = token_client.patch(
|
||||
'/api/v1/organizers/{}/events/{}/'.format(organizer.slug, event.slug),
|
||||
{
|
||||
"plugins": ["tests.testdummyhybrid"]
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert resp.data == {"plugins": ["Plugin should be enabled on organizer level first: 'tests.testdummyhybrid'."]}
|
||||
|
||||
with scopes_disabled():
|
||||
organizer.enable_plugin("tests.testdummyhybrid")
|
||||
organizer.save()
|
||||
|
||||
resp = token_client.patch(
|
||||
'/api/v1/organizers/{}/events/{}/'.format(organizer.slug, event.slug),
|
||||
{
|
||||
"plugins": ["tests.testdummyhybrid"]
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
|
||||
resp = token_client.patch(
|
||||
'/api/v1/organizers/{}/events/{}/'.format(organizer.slug, event.slug),
|
||||
{
|
||||
|
||||
@@ -423,7 +423,7 @@ def test_use_token_for_access_one_organizer(client, admin_user, organizer, appli
|
||||
assert resp.status_code == 200
|
||||
data = json.loads(resp.content.decode())
|
||||
assert data == {'count': 1, 'next': None, 'previous': None, 'results': [
|
||||
{'name': 'Dummy', 'slug': 'dummy', 'public_url': 'http://example.com/dummy/'}
|
||||
{'name': 'Dummy', 'slug': 'dummy', 'public_url': 'http://example.com/dummy/', 'plugins': []}
|
||||
]}
|
||||
resp = client.get('/api/v1/organizers/dummy/events/', HTTP_AUTHORIZATION='Bearer %s' % access_token)
|
||||
assert resp.status_code == 200
|
||||
@@ -470,8 +470,8 @@ def test_use_token_for_access_two_organizers(client, admin_user, organizer, appl
|
||||
assert resp.status_code == 200
|
||||
data = json.loads(resp.content.decode())
|
||||
assert data == {'count': 2, 'next': None, 'previous': None, 'results': [
|
||||
{'name': 'A', 'slug': 'a', 'public_url': 'http://example.com/a/'},
|
||||
{'name': 'Dummy', 'slug': 'dummy', 'public_url': 'http://example.com/dummy/'},
|
||||
{'name': 'A', 'slug': 'a', 'public_url': 'http://example.com/a/', 'plugins': []},
|
||||
{'name': 'Dummy', 'slug': 'dummy', 'public_url': 'http://example.com/dummy/', 'plugins': []},
|
||||
]}
|
||||
resp = client.get('/api/v1/organizers/dummy/events/', HTTP_AUTHORIZATION='Bearer %s' % access_token)
|
||||
assert resp.status_code == 200
|
||||
|
||||
@@ -19,14 +19,19 @@
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import zoneinfo
|
||||
from datetime import datetime
|
||||
|
||||
import pytest
|
||||
from django.core.files.base import ContentFile
|
||||
from django_scopes import scopes_disabled
|
||||
from tests.const import SAMPLE_PNG
|
||||
|
||||
TEST_ORGANIZER_RES = {
|
||||
"name": "Dummy",
|
||||
"slug": "dummy",
|
||||
"public_url": "http://example.com/dummy/"
|
||||
"public_url": "http://example.com/dummy/",
|
||||
"plugins": [],
|
||||
}
|
||||
|
||||
|
||||
@@ -45,24 +50,57 @@ def test_organizer_detail(token_client, organizer):
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_get_settings(token_client, organizer):
|
||||
organizer.settings.event_list_type = "week"
|
||||
resp = token_client.get(
|
||||
'/api/v1/organizers/{}/settings/'.format(organizer.slug,),
|
||||
def test_organizer_patch(token_client, organizer):
|
||||
with scopes_disabled():
|
||||
# An event needs to exist for the backwards-compatibility mechanism in get_all_plugins to trigger
|
||||
event = organizer.events.create(
|
||||
name="Event", slug="e2", live=True,
|
||||
date_from=datetime(2020, 1, 10, 16, 0, tzinfo=zoneinfo.ZoneInfo("UTC")),
|
||||
date_to=datetime(2020, 1, 10, 17, 0, tzinfo=zoneinfo.ZoneInfo("UTC")),
|
||||
)
|
||||
resp = token_client.patch(
|
||||
'/api/v1/organizers/{}/'.format(organizer.slug),
|
||||
{
|
||||
'slug': 'willbeignored',
|
||||
'name': 'Willbeignored',
|
||||
'plugins': ['tests.testdummyorga', 'tests.testdummyhybrid']
|
||||
},
|
||||
format='json',
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert resp.data['event_list_type'] == "week"
|
||||
assert resp.data['slug'] == 'dummy'
|
||||
assert resp.data['name'] == 'Dummy'
|
||||
assert set(resp.data['plugins']) == {'tests.testdummyorga', 'tests.testdummyhybrid'}
|
||||
|
||||
resp = token_client.get(
|
||||
'/api/v1/organizers/{}/settings/?explain=true'.format(organizer.slug),
|
||||
resp = token_client.patch(
|
||||
'/api/v1/organizers/{}/'.format(organizer.slug),
|
||||
{
|
||||
'slug': 'willbeignored',
|
||||
'name': 'Willbeignored',
|
||||
'plugins': ['pretix.plugins.statistics']
|
||||
},
|
||||
format='json',
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert resp.data == {
|
||||
"plugins": ["Plugin cannot be enabled on this level: 'pretix.plugins.statistics'."]
|
||||
}
|
||||
|
||||
event.plugins = "tests.testdummyhybrid,tests.testdummy"
|
||||
event.save()
|
||||
resp = token_client.patch(
|
||||
'/api/v1/organizers/{}/'.format(organizer.slug),
|
||||
{
|
||||
'slug': 'willbeignored',
|
||||
'name': 'Willbeignored',
|
||||
'plugins': ['tests.testdummyorga']
|
||||
},
|
||||
format='json',
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert resp.data['event_list_type'] == {
|
||||
"value": "week",
|
||||
"label": "Default overview style",
|
||||
"help_text": "If your event series has more than 50 dates in the future, only the month or week calendar can be used.",
|
||||
"readonly": False,
|
||||
}
|
||||
|
||||
event.refresh_from_db()
|
||||
assert event.plugins == "tests.testdummy"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
||||
@@ -203,6 +203,7 @@ event_permission_sub_urls = [
|
||||
]
|
||||
|
||||
org_permission_sub_urls = [
|
||||
('patch', 'can_change_organizer_settings', '', 200),
|
||||
('get', 'can_change_organizer_settings', 'settings/', 200),
|
||||
('patch', 'can_change_organizer_settings', 'settings/', 200),
|
||||
('get', 'can_change_organizer_settings', 'webhooks/', 200),
|
||||
|
||||
@@ -182,6 +182,11 @@ def _register_with_fake_plugin_name(registry, obj, plugin_name):
|
||||
|
||||
class App:
|
||||
name = plugin_name
|
||||
|
||||
class PretixPluginMeta:
|
||||
pass
|
||||
|
||||
obj.__mocked_app = App
|
||||
registry.register(obj)
|
||||
registry.registered_entries[obj]['plugin'] = App
|
||||
|
||||
|
||||
@@ -22,10 +22,12 @@
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
from django.apps import apps
|
||||
|
||||
from pretix.base.logentrytypes import (
|
||||
ItemLogEntryType, LogEntryType, LogEntryTypeRegistry,
|
||||
)
|
||||
from pretix.base.models import Event
|
||||
from pretix.base.signals import Registry
|
||||
|
||||
|
||||
@@ -140,29 +142,38 @@ def test_logentrytype_registry():
|
||||
reg = LogEntryTypeRegistry()
|
||||
|
||||
with mock.patch('pretix.base.signals.get_defining_app') as mock_get_defining_app:
|
||||
mock_get_defining_app.return_value = 'my_plugin'
|
||||
mock_get_defining_app.return_value = apps.get_app_config("testdummy")
|
||||
|
||||
@reg.new("foo.mytype")
|
||||
class MyType(LogEntryType):
|
||||
pass
|
||||
|
||||
@reg.new("foo.myothertype")
|
||||
class MyOtherType(LogEntryType):
|
||||
pass
|
||||
with mock.patch('pretix.base.signals.get_defining_app') as mock_get_defining_app:
|
||||
mock_get_defining_app.return_value = "CORE"
|
||||
|
||||
@reg.new("foo.myothertype")
|
||||
class MyOtherType(LogEntryType):
|
||||
pass
|
||||
|
||||
typ, meta = reg.get(action_type="foo.mytype")
|
||||
assert isinstance(typ, MyType)
|
||||
assert meta['action_type'] == "foo.mytype"
|
||||
assert meta['plugin'] == 'my_plugin'
|
||||
assert meta['plugin'] == apps.get_app_config("testdummy")
|
||||
|
||||
typ, meta = reg.get(action_type="foo.myothertype")
|
||||
assert isinstance(typ, MyOtherType)
|
||||
assert meta['action_type'] == "foo.myothertype"
|
||||
assert meta['plugin'] is None
|
||||
assert meta['plugin'] == "CORE"
|
||||
|
||||
by_my_plugin = reg.filter(plugin='my_plugin')
|
||||
by_my_plugin = reg.filter(plugin=apps.get_app_config("testdummy"))
|
||||
assert set(type(typ) for typ, meta in by_my_plugin) == {MyType}
|
||||
|
||||
by_active_plugin = reg.filter(active_in=Event(plugins=""))
|
||||
assert set(type(typ) for typ, meta in by_active_plugin) == {MyOtherType}
|
||||
|
||||
by_active_plugin = reg.filter(active_in=Event(plugins="tests.testdummy"))
|
||||
assert set(type(typ) for typ, meta in by_active_plugin) == {MyType, MyOtherType}
|
||||
|
||||
|
||||
def test_logentrytype_registry_validation():
|
||||
reg = LogEntryTypeRegistry()
|
||||
|
||||
@@ -327,6 +327,28 @@ class EventsTest(SoupTest):
|
||||
self.event1.refresh_from_db()
|
||||
assert "testdummyrestricted" in self.event1.plugins
|
||||
|
||||
self.post_doc('/control/event/%s/%s/settings/plugins' % (self.orga1.slug, self.event1.slug),
|
||||
{'plugin:tests.testdummyorga': 'enable'})
|
||||
self.event1.refresh_from_db()
|
||||
assert "testdummyorga" not in self.event1.plugins
|
||||
|
||||
self.post_doc('/control/event/%s/%s/settings/plugins' % (self.orga1.slug, self.event1.slug),
|
||||
{'plugin:tests.testdummyhybrid': 'enable'})
|
||||
self.event1.refresh_from_db()
|
||||
assert "tests.testdummyhybrid" not in self.event1.plugins
|
||||
self.orga1.refresh_from_db()
|
||||
assert "tests.testdummyhybrid" not in self.orga1.plugins
|
||||
|
||||
t2 = Team.objects.create(organizer=self.orga1, can_change_organizer_settings=True)
|
||||
t2.members.add(self.user)
|
||||
|
||||
self.post_doc('/control/event/%s/%s/settings/plugins' % (self.orga1.slug, self.event1.slug),
|
||||
{'plugin:tests.testdummyhybrid': 'enable'})
|
||||
self.event1.refresh_from_db()
|
||||
assert "tests.testdummyhybrid" in self.event1.plugins
|
||||
self.orga1.refresh_from_db()
|
||||
assert "tests.testdummyhybrid" in self.orga1.plugins
|
||||
|
||||
def test_testmode_enable(self):
|
||||
self.event1.testmode = False
|
||||
self.event1.save()
|
||||
|
||||
@@ -380,3 +380,76 @@ class OrganizerTest(SoupTest):
|
||||
assert doc.select('.alert-danger')
|
||||
with scopes_disabled():
|
||||
assert self.orga1.sales_channels.filter(identifier="web").exists()
|
||||
|
||||
def test_plugins(self):
|
||||
doc = self.get_doc('/control/organizer/%s/settings/plugins' % self.orga1.slug)
|
||||
self.assertIn("Stripe", doc.select(".form-plugins")[0].text)
|
||||
self.assertIn("Enable", doc.select("[name=\"plugin:tests.testdummyorga\"]")[0].text)
|
||||
self.assertIn("Enable", doc.select("[name=\"plugin:tests.testdummyhybrid\"]")[0].text)
|
||||
assert not doc.select("[name=\"plugin:pretix.plugins.stripe\"]")
|
||||
assert not doc.select("[name=\"plugin:tests.testdummy\"]")
|
||||
assert not doc.select("[name=\"plugin:tests.testdummyrestricted\"]")
|
||||
assert not doc.select("[name=\"plugin:tests.testdummyorgarestricted\"]")
|
||||
assert not doc.select("[name=\"plugin:tests.testdummyhidden\"]")
|
||||
|
||||
doc = self.post_doc('/control/organizer/%s/settings/plugins' % self.orga1.slug,
|
||||
{'plugin:tests.testdummyorga': 'enable'})
|
||||
self.assertIn("Disable", doc.select("[name=\"plugin:tests.testdummyorga\"]")[0].text)
|
||||
|
||||
doc = self.post_doc('/control/organizer/%s/settings/plugins' % self.orga1.slug,
|
||||
{'plugin:tests.testdummyhybrid': 'enable'})
|
||||
self.assertIn("Events with plugin testdummyhybrid", doc.select("h1")[0].text)
|
||||
self.orga1.refresh_from_db()
|
||||
assert "tests.testdummyhybrid" in self.orga1.get_plugins()
|
||||
|
||||
doc = self.post_doc('/control/organizer/%s/settings/plugins' % self.orga1.slug,
|
||||
{'plugin:tests.testdummyhybrid': 'disable'})
|
||||
self.assertIn("Enable", doc.select("[name=\"plugin:tests.testdummyhybrid\"]")[0].text)
|
||||
|
||||
self.post_doc('/control/organizer/%s/settings/plugins' % self.orga1.slug,
|
||||
{'plugin:tests.testdummy': 'enable'})
|
||||
self.orga1.refresh_from_db()
|
||||
assert "tests.testdummy" not in self.orga1.get_plugins()
|
||||
|
||||
self.post_doc('/control/organizer/%s/settings/plugins' % self.orga1.slug,
|
||||
{'plugin:tests.testdummyorgarestricted': 'enable'})
|
||||
self.orga1.refresh_from_db()
|
||||
assert "testdummyorgarestricted" not in self.orga1.get_plugins()
|
||||
|
||||
self.orga1.settings.allowed_restricted_plugins = ["tests.testdummyorgarestricted"]
|
||||
|
||||
self.post_doc('/control/organizer/%s/settings/plugins' % self.orga1.slug,
|
||||
{'plugin:tests.testdummyorgarestricted': 'enable'})
|
||||
self.orga1.refresh_from_db()
|
||||
assert "tests.testdummyorgarestricted" in self.orga1.get_plugins()
|
||||
|
||||
def test_plugin_events(self):
|
||||
resp = self.client.get('/control/organizer/%s/settings/plugins/tests.testdummyorga/events' % self.orga1.slug)
|
||||
assert resp.status_code == 404
|
||||
assert b"only be enabled for the entire organizer account" in resp.content
|
||||
|
||||
resp = self.client.get(
|
||||
'/control/organizer/%s/settings/plugins/tests.testdummyrestricted/events' % self.orga1.slug)
|
||||
assert resp.status_code == 404
|
||||
assert b"currently not allowed" in resp.content
|
||||
|
||||
resp = self.client.get('/control/organizer/%s/settings/plugins/tests.testdummyhybrid/events' % self.orga1.slug)
|
||||
assert resp.status_code == 404
|
||||
assert b"currently not active on the organizer" in resp.content
|
||||
|
||||
resp = self.client.get('/control/organizer/%s/settings/plugins/pretix.plugins.stripe/events' % self.orga1.slug)
|
||||
assert resp.status_code == 200
|
||||
|
||||
resp = self.client.post('/control/organizer/%s/settings/plugins/pretix.plugins.stripe/events' % self.orga1.slug,
|
||||
{'events': self.event1.pk})
|
||||
assert resp.status_code == 302
|
||||
self.event1.refresh_from_db()
|
||||
assert 'pretix.plugins.stripe' in self.event1.get_plugins()
|
||||
assert 'pretix.plugins.banktransfer' in self.event1.get_plugins()
|
||||
|
||||
resp = self.client.post('/control/organizer/%s/settings/plugins/pretix.plugins.banktransfer/events' % self.orga1.slug,
|
||||
{})
|
||||
assert resp.status_code == 302
|
||||
self.event1.refresh_from_db()
|
||||
assert 'pretix.plugins.banktransfer' not in self.event1.get_plugins()
|
||||
assert 'pretix.plugins.stripe' in self.event1.get_plugins()
|
||||
|
||||
@@ -183,6 +183,8 @@ event_urls = [
|
||||
organizer_urls = [
|
||||
'organizer/abc/edit',
|
||||
'organizer/abc/',
|
||||
'organizer/abc/settings/plugins',
|
||||
'organizer/abc/settings/plugins/pretix.plugins.sendmail/events',
|
||||
'organizer/abc/settings/email',
|
||||
'organizer/abc/settings/email/setup',
|
||||
'organizer/abc/teams',
|
||||
@@ -287,7 +289,7 @@ def test_wrong_event(perf_patch, client, env, url):
|
||||
organizer=env[2], name='Dummy', slug='dummy2',
|
||||
date_from=now(), plugins='pretix.plugins.banktransfer'
|
||||
)
|
||||
t = Team.objects.create(organizer=env[2], can_change_event_settings=True)
|
||||
t = Team.objects.create(pk=2, organizer=env[2], can_change_event_settings=True)
|
||||
t.members.add(env[1])
|
||||
t.limit_events.add(event2)
|
||||
|
||||
@@ -418,7 +420,7 @@ event_permission_urls = [
|
||||
@pytest.mark.parametrize("perm,url,code,http_method", event_permission_urls)
|
||||
def test_wrong_event_permission(perf_patch, client, env, perm, url, code, http_method):
|
||||
t = Team(
|
||||
organizer=env[2], all_events=True
|
||||
pk=2, organizer=env[2], all_events=True
|
||||
)
|
||||
setattr(t, perm, False)
|
||||
t.save()
|
||||
@@ -438,7 +440,7 @@ def test_limited_event_permission_for_other_event(perf_patch, client, env, perm,
|
||||
organizer=env[2], name='Dummy', slug='dummy2',
|
||||
date_from=now(), plugins='pretix.plugins.banktransfer'
|
||||
)
|
||||
t = Team.objects.create(organizer=env[2], can_change_event_settings=True)
|
||||
t = Team.objects.create(pk=2, organizer=env[2], can_change_event_settings=True)
|
||||
t.members.add(env[1])
|
||||
t.limit_events.add(event2)
|
||||
|
||||
@@ -453,7 +455,7 @@ def test_limited_event_permission_for_other_event(perf_patch, client, env, perm,
|
||||
@pytest.mark.django_db
|
||||
def test_current_permission(client, env):
|
||||
t = Team(
|
||||
organizer=env[2], all_events=True
|
||||
pk=2, organizer=env[2], all_events=True
|
||||
)
|
||||
setattr(t, 'can_change_event_settings', True)
|
||||
t.save()
|
||||
@@ -471,7 +473,7 @@ def test_current_permission(client, env):
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize("perm,url,code,http_method", event_permission_urls)
|
||||
def test_correct_event_permission_all_events(perf_patch, client, env, perm, url, code, http_method):
|
||||
t = Team(organizer=env[2], all_events=True)
|
||||
t = Team(pk=2, organizer=env[2], all_events=True)
|
||||
setattr(t, perm, True)
|
||||
t.save()
|
||||
t.members.add(env[1])
|
||||
@@ -489,7 +491,7 @@ def test_correct_event_permission_all_events(perf_patch, client, env, perm, url,
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize("perm,url,code,http_method", event_permission_urls)
|
||||
def test_correct_event_permission_limited(perf_patch, client, env, perm, url, code, http_method):
|
||||
t = Team(organizer=env[2])
|
||||
t = Team(pk=2, organizer=env[2])
|
||||
setattr(t, perm, True)
|
||||
t.save()
|
||||
t.members.add(env[1])
|
||||
@@ -522,6 +524,8 @@ organizer_permission_urls = [
|
||||
("can_change_teams", "organizer/dummy/team/1/edit", 200),
|
||||
("can_change_teams", "organizer/dummy/team/1/delete", 200),
|
||||
("can_change_organizer_settings", "organizer/dummy/edit", 200),
|
||||
("can_change_organizer_settings", "organizer/dummy/settings/plugins", 200),
|
||||
("can_change_organizer_settings", "organizer/dummy/settings/plugins/pretix.plugins.sendmail/events", 200),
|
||||
("can_change_organizer_settings", "organizer/dummy/settings/email", 200),
|
||||
("can_change_organizer_settings", "organizer/dummy/settings/email/setup", 200),
|
||||
("can_change_organizer_settings", "organizer/dummy/devices", 200),
|
||||
@@ -580,7 +584,7 @@ organizer_permission_urls = [
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize("perm,url,code", organizer_permission_urls)
|
||||
def test_wrong_organizer_permission(perf_patch, client, env, perm, url, code):
|
||||
t = Team(organizer=env[2])
|
||||
t = Team(pk=2, organizer=env[2])
|
||||
setattr(t, perm, False)
|
||||
t.save()
|
||||
t.members.add(env[1])
|
||||
@@ -592,7 +596,7 @@ def test_wrong_organizer_permission(perf_patch, client, env, perm, url, code):
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize("perm,url,code", organizer_permission_urls)
|
||||
def test_correct_organizer_permission(perf_patch, client, env, perm, url, code):
|
||||
t = Team(organizer=env[2])
|
||||
t = Team(pk=2, organizer=env[2])
|
||||
setattr(t, perm, True)
|
||||
t.save()
|
||||
t.members.add(env[1])
|
||||
|
||||
@@ -30,6 +30,9 @@ TEMPLATES[0]['DIRS'].append(os.path.join(TEST_DIR, 'templates')) # NOQA
|
||||
INSTALLED_APPS.append('tests.testdummy') # NOQA
|
||||
INSTALLED_APPS.append('tests.testdummyrestricted') # NOQA
|
||||
INSTALLED_APPS.append('tests.testdummyhidden') # NOQA
|
||||
INSTALLED_APPS.append('tests.testdummyorga') # NOQA
|
||||
INSTALLED_APPS.append('tests.testdummyhybrid') # NOQA
|
||||
INSTALLED_APPS.append('tests.testdummyorgarestricted') # NOQA
|
||||
|
||||
PRETIX_AUTH_BACKENDS = [
|
||||
'pretix.base.auth.NativeAuthBackend',
|
||||
|
||||
@@ -19,10 +19,10 @@
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
from django.apps import AppConfig
|
||||
from pretix.base.plugins import PluginConfig
|
||||
|
||||
|
||||
class TestDummyApp(AppConfig):
|
||||
class TestDummyApp(PluginConfig):
|
||||
name = 'tests.testdummy'
|
||||
verbose_name = '.testdummy'
|
||||
|
||||
|
||||
21
src/tests/testdummyhybrid/__init__.py
Normal file
21
src/tests/testdummyhybrid/__init__.py
Normal file
@@ -0,0 +1,21 @@
|
||||
#
|
||||
# This file is part of pretix (Community Edition).
|
||||
#
|
||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||
#
|
||||
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||||
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||||
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||||
# this file, see <https://pretix.eu/about/en/license>.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
34
src/tests/testdummyhybrid/apps.py
Normal file
34
src/tests/testdummyhybrid/apps.py
Normal file
@@ -0,0 +1,34 @@
|
||||
#
|
||||
# This file is part of pretix (Community Edition).
|
||||
#
|
||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||
#
|
||||
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||||
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||||
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||||
# this file, see <https://pretix.eu/about/en/license>.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
from pretix.base.plugins import (
|
||||
PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID, PluginConfig,
|
||||
)
|
||||
|
||||
|
||||
class TestDummyHybridApp(PluginConfig):
|
||||
name = 'tests.testdummyhybrid'
|
||||
verbose_name = 'testdummyhybrid'
|
||||
|
||||
class PretixPluginMeta:
|
||||
name = 'testdummyhybrid'
|
||||
version = '1.0.0'
|
||||
level = PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID
|
||||
21
src/tests/testdummyorga/__init__.py
Normal file
21
src/tests/testdummyorga/__init__.py
Normal file
@@ -0,0 +1,21 @@
|
||||
#
|
||||
# This file is part of pretix (Community Edition).
|
||||
#
|
||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||
#
|
||||
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||||
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||||
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||||
# this file, see <https://pretix.eu/about/en/license>.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
32
src/tests/testdummyorga/apps.py
Normal file
32
src/tests/testdummyorga/apps.py
Normal file
@@ -0,0 +1,32 @@
|
||||
#
|
||||
# This file is part of pretix (Community Edition).
|
||||
#
|
||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||
#
|
||||
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||||
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||||
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||||
# this file, see <https://pretix.eu/about/en/license>.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
from pretix.base.plugins import PLUGIN_LEVEL_ORGANIZER, PluginConfig
|
||||
|
||||
|
||||
class TestDummyOrgaApp(PluginConfig):
|
||||
name = 'tests.testdummyorga'
|
||||
verbose_name = 'testdummyorga'
|
||||
|
||||
class PretixPluginMeta:
|
||||
name = 'testdummyorga'
|
||||
version = '1.0.0'
|
||||
level = PLUGIN_LEVEL_ORGANIZER
|
||||
21
src/tests/testdummyorgarestricted/__init__.py
Normal file
21
src/tests/testdummyorgarestricted/__init__.py
Normal file
@@ -0,0 +1,21 @@
|
||||
#
|
||||
# This file is part of pretix (Community Edition).
|
||||
#
|
||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||
#
|
||||
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||||
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||||
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||||
# this file, see <https://pretix.eu/about/en/license>.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
33
src/tests/testdummyorgarestricted/apps.py
Normal file
33
src/tests/testdummyorgarestricted/apps.py
Normal file
@@ -0,0 +1,33 @@
|
||||
#
|
||||
# This file is part of pretix (Community Edition).
|
||||
#
|
||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||
#
|
||||
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||||
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||||
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||||
# this file, see <https://pretix.eu/about/en/license>.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
from pretix.base.plugins import PLUGIN_LEVEL_ORGANIZER, PluginConfig
|
||||
|
||||
|
||||
class TestDummyOrgaRestrictedApp(PluginConfig):
|
||||
name = 'tests.testdummyorgarestricted'
|
||||
verbose_name = 'testdummyorgarestricted'
|
||||
|
||||
class PretixPluginMeta:
|
||||
name = 'testdummyorgarestricted'
|
||||
version = '1.0.0'
|
||||
level = PLUGIN_LEVEL_ORGANIZER
|
||||
restricted = True
|
||||
Reference in New Issue
Block a user