diff --git a/doc/development/api/general.rst b/doc/development/api/general.rst index ac02745b76..f565da5c1a 100644 --- a/doc/development/api/general.rst +++ b/doc/development/api/general.rst @@ -15,7 +15,7 @@ Core item_copy_data, register_sales_channel_types, register_global_settings, quota_availability, global_email_filter, register_ticket_secret_generators, gift_card_transaction_display, register_text_placeholders, register_mail_placeholders, device_info_updated, - register_event_permissions, register_organizer_permissions + register_event_permission_groups, register_organizer_permission_groups Order events """""""""""" diff --git a/doc/development/implementation/permissions.rst b/doc/development/implementation/permissions.rst index 8e668acc28..3732517791 100644 --- a/doc/development/implementation/permissions.rst +++ b/doc/development/implementation/permissions.rst @@ -199,26 +199,49 @@ intended to help compliance with data protection rules as imposed e.g. by GDPR. Adding permissions ------------------ -Plugins can add permissions through the ``register_event_permissions`` and ``register_organizer_permission``. +Plugins can add permissions through the ``register_event_permission_groups`` and ``register_organizer_permission_groups``. We recommend to use this only for very significant permissions, as the system will become less usable with too many permission levels, also because the team page will show all permission options, even those of disabled plugins. -We recommend to prefix the permission string with the plugin name and follow the ``.:`` pattern. + +To register your permissions, you need to register a **permission group** (often representing an area of functionality +or a key model). Below that group, there are **actions**, which represent the actual permissions. Permissions will be +generated as ``:``. Then, you need to define **options** which are the valid combinations of the +actions that should be possible to select for a team. This two-step mechanism exists to provide a better user experience +and avoid useless combinations like "write but not read". Example:: - @receiver(register_event_permissions) - def register_default_event_permissions(sender, **kwargs): + @receiver(register_event_permission_groups) + def register_plugin_event_permissions(sender, **kwargs): return [ - Permission("pretix_myplugin.resource:read", _("Read resources"), - "pretix_myplugin", _("Some helptext")), + PermissionGroup( + name="pretix_myplugin.resource", + label=_("Resources"), + actions=["read", "write"], + options=[ + PermissionOption(actions=tuple(), label=_("No access")), + PermissionOption(actions=("read",), label=_("View")), + PermissionOption(actions=("read", "write"), label=_("View and change")), + ], + help_text=_("Some help text") + ), ] - @receiver(register_organizer_permissions) - def register_default_organizer_permissions(sender, **kwargs): + @receiver(register_organizer_permission_groups) + def register_plugin_organizer_permissions(sender, **kwargs): return [ - Permission("pretix_myplugin.resource:read", _("Read resources"), - "pretix_myplugin", _("Some helptext")), + PermissionGroup( + name="pretix_myplugin.resource", + label=_("Resources"), + actions=["read", "write"], + options=[ + PermissionOption(actions=tuple(), label=_("No access")), + PermissionOption(actions=("read",), label=_("View")), + PermissionOption(actions=("read", "write"), label=_("View and change")), + ], + help_text=_("Some help text") + ), ] .. _configuring teams and permissions: https://docs.pretix.eu/guides/teams/ \ No newline at end of file diff --git a/src/pretix/api/serializers/organizer.py b/src/pretix/api/serializers/organizer.py index 8985fd11ef..289a3c48ca 100644 --- a/src/pretix/api/serializers/organizer.py +++ b/src/pretix/api/serializers/organizer.py @@ -46,7 +46,7 @@ from pretix.base.models import ( ) from pretix.base.models.seating import SeatingPlanLayoutValidator from pretix.base.permissions import ( - get_all_event_permissions, get_all_organizer_permissions, + get_all_event_permission_groups, get_all_organizer_permission_groups, ) from pretix.base.plugins import ( PLUGIN_LEVEL_EVENT, PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID, @@ -355,8 +355,18 @@ class TeamSerializer(serializers.ModelSerializer): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.fields['limit_event_permissions'].choices = [(p.name, p.name) for p in get_all_event_permissions().values()] - self.fields['limit_organizer_permissions'].choices = [(p.name, p.name) for p in get_all_organizer_permissions().values()] + + event_perms_flattened = [] + organizer_perms_flattened = [] + for pg in get_all_event_permission_groups().values(): + for action in pg.actions: + event_perms_flattened.append(f"{pg.name}:{action}") + for pg in get_all_organizer_permission_groups().values(): + for action in pg.actions: + organizer_perms_flattened.append(f"{pg.name}:{action}") + + self.fields['limit_event_permissions'].choices = [(p, p) for p in event_perms_flattened] + self.fields['limit_organizer_permissions'].choices = [(p, p) for p in organizer_perms_flattened] def to_representation(self, instance): r = super().to_representation(instance) @@ -406,6 +416,25 @@ class TeamSerializer(serializers.ModelSerializer): if full_data.get('limit_events') and full_data.get('all_events'): raise ValidationError('Do not set both limit_events and all_events.') + + full_data.update(data) + for pg in get_all_event_permission_groups().values(): + requested = ",".join(sorted( + a for a in pg.actions if self.instance and full_data["limit_event_permissions"].get(f"{pg.name}:{a}") + )) + if requested not in (",".join(sorted(opt.actions)) for opt in pg.options): + raise ValidationError(f"For permission group {pg.name}, the valid combinations of actions are " + f"'{'\' or \''.join(','.join(opt.actions) for opt in pg.options)}' but you tried to " + f"set '{requested}'.") + for pg in get_all_organizer_permission_groups().values(): + requested = ",".join(sorted( + a for a in pg.actions if self.instance and full_data["limit_organizer_permissions"].get(f"{pg.name}:{a}") + )) + if requested not in (",".join(sorted(opt.actions)) for opt in pg.options): + raise ValidationError(f"For permission group {pg.name}, the valid combinations of actions are " + f"'{'\' or \''.join(','.join(opt.actions) for opt in pg.options)}' but you tried to " + f"set '{requested}'.") + return data diff --git a/src/pretix/base/models/organizer.py b/src/pretix/base/models/organizer.py index 449ce38ba1..70e6c7d3ce 100644 --- a/src/pretix/base/models/organizer.py +++ b/src/pretix/base/models/organizer.py @@ -398,12 +398,13 @@ class Team(LoggedModel): } def event_permission_set(self, include_legacy=True) -> set: - from ..permissions import get_all_event_permissions + from ..permissions import get_all_event_permission_groups result = set() - for permission in get_all_event_permissions().keys(): - if self.all_event_permissions or self.limit_event_permissions.get(permission): - result.add(permission) + for pg in get_all_event_permission_groups().values(): + for action in pg.actions: + if self.all_event_permissions or self.limit_event_permissions.get(f"{pg.name}:{action}"): + result.add(f"{pg.name}:{action}") if include_legacy: # Add legacy permissions as well for plugin compatibility @@ -414,12 +415,13 @@ class Team(LoggedModel): return result def organizer_permission_set(self, include_legacy=True) -> set: - from ..permissions import get_all_organizer_permissions + from ..permissions import get_all_organizer_permission_groups result = set() - for permission in get_all_organizer_permissions().keys(): - if self.all_organizer_permissions or self.limit_organizer_permissions.get(permission): - result.add(permission) + for pg in get_all_organizer_permission_groups().values(): + for action in pg.actions: + if self.all_organizer_permissions or self.limit_organizer_permissions.get(f"{pg.name}:{action}"): + result.add(f"{pg.name}:{action}") if include_legacy: # Add legacy permissions as well for plugin compatibility diff --git a/src/pretix/base/permissions.py b/src/pretix/base/permissions.py index d5ab008f25..8e2814c825 100644 --- a/src/pretix/base/permissions.py +++ b/src/pretix/base/permissions.py @@ -21,13 +21,15 @@ # import logging -from collections import OrderedDict, namedtuple +from collections import OrderedDict +from typing import Dict, List, NamedTuple, Tuple from django.dispatch import receiver +from django.utils.functional import Promise from django.utils.translation import gettext_lazy as _, pgettext_lazy from pretix.base.signals import ( - register_event_permissions, register_organizer_permissions, + register_event_permission_groups, register_organizer_permission_groups, ) logger = logging.getLogger(__name__) @@ -35,17 +37,28 @@ _ALL_EVENT_PERMISSIONS = None _ALL_ORGANIZER_PERMISSIONS = None -Permission = namedtuple('Permission', ('name', 'label', 'plugin_name', 'help_text')) +class PermissionOption(NamedTuple): + actions: Tuple[str, ...] + label: str | Promise + help_text: str | Promise = None -def get_all_event_permissions(): +class PermissionGroup(NamedTuple): + name: str + label: str | Promise + actions: List[str] + options: List[PermissionOption] + help_text: str | Promise = None + + +def get_all_event_permission_groups() -> Dict[str, PermissionGroup]: global _ALL_EVENT_PERMISSIONS if _ALL_EVENT_PERMISSIONS: return _ALL_EVENT_PERMISSIONS types = OrderedDict() - for recv, ret in register_event_permissions.send(None): + for recv, ret in register_event_permission_groups.send(None): if isinstance(ret, (list, tuple)): for r in ret: types[r.name] = r @@ -55,14 +68,14 @@ def get_all_event_permissions(): return types -def get_all_organizer_permissions(): +def get_all_organizer_permission_groups() -> Dict[str, PermissionGroup]: global _ALL_ORGANIZER_PERMISSIONS if _ALL_ORGANIZER_PERMISSIONS: return _ALL_ORGANIZER_PERMISSIONS types = OrderedDict() - for recv, ret in register_organizer_permissions.send(None): + for recv, ret in register_organizer_permission_groups.send(None): if isinstance(ret, (list, tuple)): for r in ret: types[r.name] = r @@ -72,46 +85,167 @@ def get_all_organizer_permissions(): return types -@receiver(register_event_permissions, dispatch_uid="base_register_default_event_permissions") +OPTS_ALL_READ = [ + PermissionOption(actions=tuple(), label=pgettext_lazy("permission_level", "View")), + PermissionOption(actions=("write",), label=pgettext_lazy("permission_level", "View and change")), +] +OPTS_ALL_READ_SETTINGS_API = [ + PermissionOption(actions=tuple(), label=pgettext_lazy("permission_level", "View"), + help_text=_("API only")), + PermissionOption(actions=("write",), label=pgettext_lazy("permission_level", "View and change")), +] +OPTS_ALL_READ_SETTINGS_PARENT = [ + PermissionOption(actions=tuple(), label=pgettext_lazy("permission_level", "View"), + help_text=_("Menu item will only show up if the user has permission for general settings.")), + PermissionOption(actions=("write",), label=pgettext_lazy("permission_level", "View and change")), +] +OPTS_READ_WRITE = [ + PermissionOption(actions=tuple(), label=pgettext_lazy("permission_level", "No access")), + PermissionOption(actions=("read",), label=pgettext_lazy("permission_level", "View")), + PermissionOption(actions=("read", "write"), label=pgettext_lazy("permission_level", "View and change")), +] + + +@receiver(register_event_permission_groups, dispatch_uid="base_register_default_event_permissions") def register_default_event_permissions(sender, **kwargs): return [ - Permission("event.settings.general:write", _("Change general settings"), None, - _("This includes access to all settings not listed explicitly below, including plugin settings.")), - Permission("event.settings.payment:write", _("Change payment settings"), None, None), - Permission("event.settings.tax:write", _("Change tax rules"), None, None), - Permission("event.settings.invoicing:write", _("Change invoicing settings"), None, None), - Permission("event.subevents:write", pgettext_lazy("subevent", "Change event series dates"), None, - _("Read access is granted to all teams with access to the event.")), - Permission("event.items:write", _("Change products, quotas, and questions"), None, - _("Also includes related objects like categories or discounts. Read access is granted to all teams with access to the event.")), - Permission("event.orders:read", _("View orders"), None, None), - Permission("event.orders:write", _("Change orders"), None, _("This includes the ability to cancel and refund individual orders.")), - Permission("event.orders:checkin", _("Check-in orders"), None, None), - Permission("event.vouchers:read", _("View vouchers"), None, None), - Permission("event.vouchers:write", _("Change vouchers"), None, None), - Permission("event:cancel", pgettext_lazy("subevent", "Cancel entire event or date"), None, None), + PermissionGroup( + name="event.settings.general", + label=_("General settings"), + actions=["write"], + options=OPTS_ALL_READ_SETTINGS_API, + help_text=_( + "This includes access to all settings not listed explicitly below, including plugin settings." + ), + ), + PermissionGroup( + name="event.settings.payment", + label=_("Payment settings"), + actions=["write"], + options=OPTS_ALL_READ_SETTINGS_PARENT, + ), + PermissionGroup( + name="event.settings.tax", + label=_("Tax settings"), + actions=["write"], + options=OPTS_ALL_READ_SETTINGS_PARENT, + ), + PermissionGroup( + name="event.settings.invoicing", + label=_("Invoicing settings"), + actions=["write"], + options=OPTS_ALL_READ_SETTINGS_PARENT, + ), + PermissionGroup( + name="event.subevents", + label=_("Event series dates"), + actions=["write"], + options=OPTS_ALL_READ, + ), + PermissionGroup( + name="event.items", + label=_("Products, quotas and questions"), + actions=["write"], + options=OPTS_ALL_READ, + help_text=_("Also includes related objects like categories or discounts."), + ), + PermissionGroup( + name="event.orders", + label=_("Orders"), + actions=["read", "write", "checkin"], + options=[ + PermissionOption(actions=tuple(), label=pgettext_lazy("permission_level", "No access")), + PermissionOption(actions=("checkin",), label=pgettext_lazy("permission_level", "Only check-in")), + PermissionOption(actions=("read",), label=pgettext_lazy("permission_level", "View all")), + PermissionOption(actions=("read", "checkin"), label=pgettext_lazy("permission_level", "View all and check-in")), + PermissionOption(actions=("read", "write"), label=pgettext_lazy("permission_level", "View all and change"), + help_text=_("Includes the ability to cancel and refund individual orders.")), + ], + help_text=_("Also includes related objects like the waiting list."), + ), + PermissionGroup( + name="event.vouchers", + label=_("Vouchers"), + actions=["read", "write"], + options=OPTS_READ_WRITE, + ), + PermissionGroup( + name="event", + label=_("Full event or date cancellation"), + actions=["cancel"], + options=[ + # If we ever add more actions, we need a new UI idea here + PermissionOption(actions=tuple(), label=pgettext_lazy("permission_level", "Not allowed")), + PermissionOption(actions=("cancel",), label=pgettext_lazy("permission_level", "Allowed")), + ], + help_text="", + ), ] -@receiver(register_organizer_permissions, dispatch_uid="base_register_default_organizer_permissions") +@receiver(register_organizer_permission_groups, dispatch_uid="base_register_default_organizer_permissions") def register_default_organizer_permissions(sender, **kwargs): return [ - Permission("organizer.events:create", _("Create events"), None, None), - Permission("organizer.settings.general:write", _("Change settings"), None, - _("This includes access to all organizer-level functionality not listed explicitly below, including plugin settings.")), - Permission("organizer.teams:write", _("Change teams"), None, - _("This includes the ability to give someone (including oneself) additional permissions. Read access " - "is implicitly granted to the same team.")), - Permission("organizer.giftcards:read", _("View gift cards"), None, None), - Permission("organizer.giftcards:write", _("Change gift cards"), None, None), - Permission("organizer.customers:read", _("View customer accounts"), None, None), - Permission("organizer.customers:write", _("Change customer accounts"), None, None), - Permission("organizer.reusablemedia:read", _("View reusable media"), None, - _("This includes access to data of tickets connected to reusable media.")), - Permission("organizer.reusablemedia:write", _("Change reusable media"), None, None), - Permission("organizer.devices:read", _("View devices and gates"), None, None), - Permission("organizer.devices:write", _("Change devices and gates"), None, - _("This includes the ability to give access to events and data oneself does not have access to.")), - Permission("organizer.seatingplans:write", _("Change seating plans"), None, - _("Read access is implicitly given to all teams.")), + PermissionGroup( + name="organizer.events", + label=_("Events"), + actions=["create"], + options=[ + PermissionOption(actions=tuple(), label=pgettext_lazy("permission_level", "Only existing events")), + PermissionOption(actions=("create",), label=pgettext_lazy("permission_level", "Create new events")), + ], + help_text="", + ), + PermissionGroup( + name="organizer.settings.general", + label=_("Settings"), + actions=["write"], + options=OPTS_ALL_READ_SETTINGS_API, + help_text=_("This includes access to all organizer-level functionality not listed explicitly below, including plugin settings."), + ), + PermissionGroup( + name="organizer.teams", + label=_("Teams"), + actions=["write"], + options=[ + PermissionOption(actions=tuple(), label=pgettext_lazy("permission_level", "No access")), + PermissionOption(actions=("write",), label=pgettext_lazy("permission_level", "View and change"), + help_text=_("Includes the ability to give someone (including oneself) additional permissions.")), + ], + ), + PermissionGroup( + name="organizer.giftcards", + label=_("Gift cards"), + actions=["read", "write"], + options=OPTS_READ_WRITE, + ), + PermissionGroup( + name="organizer.customers", + label=_("Customers"), + actions=["read", "write"], + options=OPTS_READ_WRITE, + ), + PermissionGroup( + name="organizer.reusablemedia", + label=_("Reusable media"), + actions=["read", "write"], + options=OPTS_READ_WRITE, + ), + PermissionGroup( + name="organizer.devices", + label=_("Devices"), + actions=["read", "write"], + options=[ + PermissionOption(actions=tuple(), label=pgettext_lazy("permission_level", "No access")), + PermissionOption(actions=("read",), label=pgettext_lazy("permission_level", "View")), + PermissionOption(actions=("read", "write"), label=pgettext_lazy("permission_level", "View and change"), + help_text=_("Includes the ability to give access to events and data oneself does not have access to.")), + ], + ), + PermissionGroup( + name="organizer.seatingplans", + label=_("Seating plans"), + actions=["write"], + options=OPTS_ALL_READ, + ), ] diff --git a/src/pretix/base/signals.py b/src/pretix/base/signals.py index 9c2a6a5bc5..e54921dcc9 100644 --- a/src/pretix/base/signals.py +++ b/src/pretix/base/signals.py @@ -561,16 +561,16 @@ however for this signal, the ``sender`` **may also be None** to allow creating t notification settings! """ -register_event_permissions = GlobalSignal() +register_event_permission_groups = GlobalSignal() """ This signal is sent out to get all known permissions. Receivers should return an -instance of pretix.base.permissions.Permission or a list of such instances. +instance of pretix.base.permissions.PermissionGroup or a list of such instances. """ -register_organizer_permissions = GlobalSignal() +register_organizer_permission_groups = GlobalSignal() """ This signal is sent out to get all known permissions. Receivers should return an -instance of pretix.base.permissions.Permission or a list of such instances. +instance of pretix.base.permissions.PermissionGroup or a list of such instances. """ notification = EventPluginSignal() diff --git a/src/pretix/control/forms/organizer.py b/src/pretix/control/forms/organizer.py index af07fac062..987960114e 100644 --- a/src/pretix/control/forms/organizer.py +++ b/src/pretix/control/forms/organizer.py @@ -77,7 +77,7 @@ from pretix.base.models import ( from pretix.base.models.customers import CustomerSSOClient, CustomerSSOProvider from pretix.base.models.organizer import OrganizerFooterLink, TeamQuerySet from pretix.base.permissions import ( - get_all_event_permissions, get_all_organizer_permissions, + get_all_event_permission_groups, get_all_organizer_permission_groups, ) from pretix.base.settings import ( PERSON_NAME_SCHEMES, PERSON_NAME_TITLE_GROUPS, validate_organizer_settings, @@ -335,34 +335,62 @@ class TeamForm(forms.ModelForm): self.fields['limit_events'].queryset = organizer.events.all().order_by( '-has_subevents', '-date_from' ) - self.fields['limit_event_permissions'] = PermissionMultipleChoiceField( - label=self.fields['limit_event_permissions'].label, - choices=[ - (p.name, self._make_label(p)) for p in get_all_event_permissions().values() - ], - widget=forms.CheckboxSelectMultiple(attrs={ - 'data-inverse-dependency': '#id_all_event_permissions', - 'class': 'scrolling-multiple-choice scrolling-multiple-choice-large', - }), - required=False, - ) - self.fields['limit_organizer_permissions'] = PermissionMultipleChoiceField( - label=self.fields['limit_organizer_permissions'].label, - choices=[ - (p.name, self._make_label(p)) for p in get_all_organizer_permissions().values() - ], - widget=forms.CheckboxSelectMultiple(attrs={ - 'data-inverse-dependency': '#id_all_organizer_permissions', - 'class': 'scrolling-multiple-choice scrolling-multiple-choice-large', - }), - required=False, - ) + self.event_field_names = [] + for pg in get_all_event_permission_groups().values(): + initial = ",".join(sorted( + a for a in pg.actions if self.instance and self.instance.limit_event_permissions.get(f"{pg.name}:{a}") + )) or "EMPTY" + self.fields[f'event_{pg.name}'] = forms.ChoiceField( + choices=[ + ( + ",".join(sorted(opt.actions)) or "EMPTY", + format_html( + '{label} ' + '', + label=opt.label, + help_text=opt.help_text, + ) if opt.help_text else opt.label, + ) + for opt in pg.options + ], + label=pg.label, + help_text=pg.help_text, + initial=initial, + widget=forms.RadioSelect, + ) + self.event_field_names.append(f'event_{pg.name}') + self.organizer_field_names = [] + for pg in get_all_organizer_permission_groups().values(): + initial = ",".join(sorted( + a for a in pg.actions if self.instance and self.instance.limit_organizer_permissions.get(f"{pg.name}:{a}") + )) or "EMPTY" + self.fields[f'organizer_{pg.name}'] = forms.ChoiceField( + choices=[ + ( + ",".join(sorted(opt.actions)) or "EMPTY", + format_html( + '{label} ' + '', + label=opt.label, + help_text=opt.help_text, + ) if opt.help_text else opt.label, + ) + for opt in pg.options + ], + label=pg.label, + help_text=pg.help_text, + initial=initial, + widget=forms.RadioSelect, + ) + self.organizer_field_names.append(f'organizer_{pg.name}') class Meta: model = Team fields = ['name', 'require_2fa', 'all_events', 'limit_events', - 'all_event_permissions', 'limit_event_permissions', - 'all_organizer_permissions', 'limit_organizer_permissions'] + 'all_event_permissions', + 'all_organizer_permissions',] widgets = { 'limit_events': forms.CheckboxSelectMultiple(attrs={ 'data-inverse-dependency': '#id_all_events', @@ -375,6 +403,33 @@ class TeamForm(forms.ModelForm): def clean(self): data = super().clean() + + data['limit_event_permissions'] = {} + if not data['all_event_permissions']: + for pg in get_all_event_permission_groups().values(): + selected = data.get(f'event_{pg.name}', 'EMPTY') + if selected == "EMPTY": + selected_actions = [] + else: + selected_actions = selected.split(',') + for action in pg.actions: + if action in selected_actions: + data['limit_event_permissions'][f"{pg.name}:{action}"] = True + self.instance.limit_event_permissions = data['limit_event_permissions'] + + data['limit_organizer_permissions'] = {} + if not data['all_organizer_permissions']: + for pg in get_all_organizer_permission_groups().values(): + selected = data.get(f'organizer_{pg.name}', 'EMPTY') + if selected == "EMPTY": + selected_actions = [] + else: + selected_actions = selected.split(',') + for action in pg.actions: + if action in selected_actions: + data['limit_organizer_permissions'][f"{pg.name}:{action}"] = True + self.instance.limit_organizer_permissions = data['limit_organizer_permissions'] + if self.instance.pk and not data['all_organizer_permissions'] and 'organizer.teams:write' not in data.get('limit_organizer_permissions', []): if not self.instance.organizer.teams.exclude(pk=self.instance.pk).filter( TeamQuerySet.organizer_permission_q("organizer.teams:write"), @@ -385,6 +440,20 @@ class TeamForm(forms.ModelForm): return data + @property + def changed_data_for_log(self): + r = {} + for k in self.changed_data: + if k == "limit_events": + r[k] = [e.id for e in getattr(self.instance, k).all()] + elif k.startswith("event_"): + r["limit_event_permissions"] = self.instance.limit_event_permissions + elif k.startswith("organizer_"): + r["limit_organizer_permissions"] = self.instance.limit_organizer_permissions + else: + r[k] = getattr(self.instance, k) + return r + class GateForm(forms.ModelForm): diff --git a/src/pretix/control/templates/pretixcontrol/organizers/team_edit.html b/src/pretix/control/templates/pretixcontrol/organizers/team_edit.html index 3802a9b0aa..40693af31c 100644 --- a/src/pretix/control/templates/pretixcontrol/organizers/team_edit.html +++ b/src/pretix/control/templates/pretixcontrol/organizers/team_edit.html @@ -1,6 +1,7 @@ {% extends "pretixcontrol/organizers/base.html" %} {% load i18n %} {% load bootstrap3 %} +{% load getitem %} {% block inner %} {% if team %}

{% trans "Team:" %} {{ team.name }}

@@ -23,7 +24,11 @@
{% trans "Organizer permissions" %} {% bootstrap_field form.all_organizer_permissions layout="control" %} - {% bootstrap_field form.limit_organizer_permissions layout="control" %} +
+ {% for f in form.organizer_field_names %} + {% bootstrap_field form|getitem:f layout="control" %} + {% endfor %} +
{% trans "Event permissions" %} @@ -31,7 +36,11 @@ {% bootstrap_field form.all_events layout="control" %} {% bootstrap_field form.limit_events layout="control" %} {% bootstrap_field form.all_event_permissions layout="control" %} - {% bootstrap_field form.limit_event_permissions layout="control" %} +
+ {% for f in form.event_field_names %} + {% bootstrap_field form|getitem:f layout="control" %} + {% endfor %} +