From 8d5bb273c669342c0c43f7f9ec89606cfacd356b Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Mon, 12 Jan 2026 18:30:39 +0100 Subject: [PATCH] Use new permissions, remove inconsistencies --- doc/api/resources/devices.rst | 2 +- doc/api/resources/events.rst | 16 +- doc/api/resources/organizers.rst | 4 - doc/api/resources/reusablemedia.rst | 2 +- doc/api/resources/subevents.rst | 6 - doc/api/resources/teams.rst | 2 - src/pretix/api/serializers/event.py | 3 + src/pretix/api/serializers/media.py | 10 +- src/pretix/api/serializers/organizer.py | 7 +- src/pretix/api/serializers/settings.py | 10 + src/pretix/api/views/event.py | 19 +- src/pretix/api/views/media.py | 2 + src/pretix/api/views/organizer.py | 23 +- src/pretix/base/permissions.py | 20 +- src/pretix/base/settings.py | 65 ++++- src/pretix/base/signals.py | 3 + src/pretix/base/timeline.py | 83 ++++-- src/pretix/control/navigation.py | 239 +++++++++--------- src/pretix/control/permissions.py | 2 +- .../pretixcontrol/checkin/lists.html | 44 ++-- .../datasync/control_order_info.html | 24 +- .../pretixcontrol/event/dangerzone.html | 16 +- .../event/fragment_timeline.html | 2 +- .../templates/pretixcontrol/event/index.html | 32 +-- .../pretixcontrol/event/invoicing.html | 18 +- .../pretixcontrol/event/payment.html | 26 +- .../templates/pretixcontrol/event/tax.html | 50 ++-- .../pretixcontrol/items/categories.html | 42 +-- .../pretixcontrol/items/discounts.html | 60 +++-- .../templates/pretixcontrol/items/index.html | 40 ++- .../pretixcontrol/items/question.html | 80 +++--- .../pretixcontrol/items/questions.html | 32 ++- .../templates/pretixcontrol/items/quotas.html | 30 ++- .../templates/pretixcontrol/order/index.html | 6 +- .../pretixcontrol/orders/refunds.html | 44 ++-- .../pretixcontrol/organizers/customer.html | 100 ++++---- .../pretixcontrol/organizers/customers.html | 16 +- .../pretixcontrol/organizers/devices.html | 72 +++--- .../pretixcontrol/organizers/gates.html | 28 +- .../pretixcontrol/organizers/giftcard.html | 42 +-- .../pretixcontrol/organizers/giftcards.html | 19 +- .../organizers/reusable_media.html | 32 ++- .../organizers/reusable_medium.html | 107 ++++---- .../pretixcontrol/subevents/index.html | 80 +++--- .../pretixcontrol/waitinglist/index.html | 59 +++-- src/pretix/control/views/checkin.py | 12 +- src/pretix/control/views/event.py | 44 +++- src/pretix/control/views/item.py | 2 +- src/pretix/control/views/orders.py | 2 +- src/pretix/control/views/organizer.py | 26 +- src/pretix/control/views/subevents.py | 19 +- src/pretix/control/views/typeahead.py | 24 +- src/pretix/helpers/permission_migration.py | 2 - .../static/pretixbase/js/addressform.js | 11 +- src/pretix/static/pretixcontrol/js/ui/main.js | 8 +- src/tests/api/test_events.py | 32 ++- src/tests/api/test_permissions.py | 43 ++-- src/tests/api/test_reusable_media.py | 55 +++- src/tests/api/test_teams.py | 2 - src/tests/control/test_items.py | 14 +- src/tests/control/test_permissions.py | 79 +++--- 61 files changed, 1214 insertions(+), 780 deletions(-) diff --git a/doc/api/resources/devices.rst b/doc/api/resources/devices.rst index 85d507f223..f133331bc9 100644 --- a/doc/api/resources/devices.rst +++ b/doc/api/resources/devices.rst @@ -30,7 +30,7 @@ software_brand string Device software software_version string Device software version (read-only) created datetime Creation time initialized datetime Time of initialization (or ``null``) -initialization_token string Token for initialization +initialization_token string Token for initialization (field invisible without without write permission) revoked boolean Whether this device no longer has access security_profile string The name of a supported security profile restricting API access ===================================== ========================== ======================================================= diff --git a/doc/api/resources/events.rst b/doc/api/resources/events.rst index 95bd5e2493..72450e5a39 100644 --- a/doc/api/resources/events.rst +++ b/doc/api/resources/events.rst @@ -65,8 +65,6 @@ Endpoints Returns a list of all events within a given organizer the authenticated user/token has access to. - Permission required: "Can change event settings" - **Example request**: .. sourcecode:: http @@ -161,8 +159,6 @@ Endpoints Returns information on one event, identified by its slug. - Permission required: "Can change event settings" - **Example request**: .. sourcecode:: http @@ -234,8 +230,6 @@ Endpoints Please note that events cannot be created as 'live' using this endpoint. Quotas and payment must be added to the event before sales can go live. - Permission required: "Can create events" - **Example request**: .. sourcecode:: http @@ -338,8 +332,6 @@ Endpoints Please note that you can only copy from events under the same organizer this way. Use the ``clone_from`` parameter when creating a new event for this instead. - Permission required: "Can create events" - **Example request**: .. sourcecode:: http @@ -433,8 +425,6 @@ Endpoints Updates an event - Permission required: "Can change event settings" - **Example request**: .. sourcecode:: http @@ -510,8 +500,6 @@ Endpoints Delete an event. Note that events with orders cannot be deleted to ensure data integrity. - Permission required: "Can change event settings" - **Example request**: .. sourcecode:: http @@ -561,8 +549,6 @@ organizer level. Get current values of event settings. - Permission required: "Can change event settings" (Exception: with device auth, *some* settings can always be *read*.) - **Example request**: .. sourcecode:: http @@ -615,6 +601,8 @@ organizer level. Updates event settings. Note that ``PUT`` is not allowed here, only ``PATCH``. + Permission "Can change event settings" is always required. Some keys requrie additional permissions. + .. warning:: Settings can be stored at different levels in pretix. If a value is not set on event level, a default setting diff --git a/doc/api/resources/organizers.rst b/doc/api/resources/organizers.rst index 066657a4de..3fa2c52734 100644 --- a/doc/api/resources/organizers.rst +++ b/doc/api/resources/organizers.rst @@ -110,8 +110,6 @@ Endpoints Updates an organizer. Currently only the ``plugins`` field may be updated. - Permission required: "Can change organizer settings" - **Example request**: .. sourcecode:: http @@ -172,8 +170,6 @@ information about the properties. Get current values of organizer settings. - Permission required: "Can change organizer settings" - **Example request**: .. sourcecode:: http diff --git a/doc/api/resources/reusablemedia.rst b/doc/api/resources/reusablemedia.rst index 101b11b4b1..e0618a5b41 100644 --- a/doc/api/resources/reusablemedia.rst +++ b/doc/api/resources/reusablemedia.rst @@ -154,7 +154,7 @@ Endpoints .. http:post:: /api/v1/organizers/(organizer)/reusablemedia/lookup/ Look up a new reusable medium by its identifier. In some cases, this might lead to the automatic creation of a new - medium behind the scenes. + medium behind the scenes, therefore this endpoint requires write permissions. This endpoint, and this endpoint only, might return media from a different organizer if there is a cross-acceptance agreement. In this case, only linked gift cards will be returned, no order position or customer records, diff --git a/doc/api/resources/subevents.rst b/doc/api/resources/subevents.rst index 0a3f77a193..dd5fea07a9 100644 --- a/doc/api/resources/subevents.rst +++ b/doc/api/resources/subevents.rst @@ -154,8 +154,6 @@ Endpoints Creates a new subevent. - Permission required: "Can create events" - **Example request**: .. sourcecode:: http @@ -300,8 +298,6 @@ Endpoints provide all fields of the resource, other fields will be reset to default. With ``PATCH``, you only need to provide the fields that you want to change. - Permission required: "Can change event settings" - **Example request**: .. sourcecode:: http @@ -373,8 +369,6 @@ Endpoints Delete a sub-event. Note that events with orders cannot be deleted to ensure data integrity. - Permission required: "Can change event settings" - **Example request**: .. sourcecode:: http diff --git a/doc/api/resources/teams.rst b/doc/api/resources/teams.rst index 7af667b3f8..e41796d3bc 100644 --- a/doc/api/resources/teams.rst +++ b/doc/api/resources/teams.rst @@ -63,8 +63,6 @@ Possible values for ``limit_event_permissions`` defined in the core pretix syste event.settings.general:write event.settings.payment:write - event.settings.plugins:write - event.settings.email.sender:write event.settings.tax:write event.settings.invoicing:write event.subevents:write diff --git a/src/pretix/api/serializers/event.py b/src/pretix/api/serializers/event.py index 95159dc78a..4fa4e99a37 100644 --- a/src/pretix/api/serializers/event.py +++ b/src/pretix/api/serializers/event.py @@ -707,7 +707,10 @@ class TaxRuleSerializer(CountryFieldMixin, I18nAwareModelSerializer): class EventSettingsSerializer(SettingsSerializer): + default_write_permission = 'event.settings.general:write' default_fields = [ + # These are readable for all users with access to the events, therefore secrets made in the settings store + # should not be included! 'imprint_url', 'checkout_email_helptext', 'presale_has_ended_text', diff --git a/src/pretix/api/serializers/media.py b/src/pretix/api/serializers/media.py index 8285fade48..894c1bcf0b 100644 --- a/src/pretix/api/serializers/media.py +++ b/src/pretix/api/serializers/media.py @@ -24,7 +24,7 @@ from decimal import Decimal from django.utils.translation import gettext_lazy as _ from rest_framework import serializers -from rest_framework.exceptions import ValidationError +from rest_framework.exceptions import PermissionDenied, ValidationError from pretix.api.serializers.i18n import I18nAwareModelSerializer from pretix.api.serializers.order import OrderPositionSerializer @@ -66,6 +66,9 @@ class ReusableMediaSerializer(I18nAwareModelSerializer): super().__init__(*args, **kwargs) if 'linked_giftcard' in self.context['request'].query_params.getlist('expand'): + if not self.context["can_read_giftcards"]: + raise PermissionDenied("No permission to access gift card details.") + self.fields['linked_giftcard'] = NestedGiftCardSerializer(read_only=True, context=self.context) if 'linked_giftcard.owner_ticket' in self.context['request'].query_params.getlist('expand'): self.fields['linked_giftcard'].fields['owner_ticket'] = NestedOrderPositionSerializer(read_only=True, context=self.context) @@ -77,6 +80,8 @@ class ReusableMediaSerializer(I18nAwareModelSerializer): ) if 'linked_orderposition' in self.context['request'].query_params.getlist('expand'): + # No additional permission check performed, documented limitation of the permission system + # Would get to complex/unusable otherwise since the permission depends on the event self.fields['linked_orderposition'] = NestedOrderPositionSerializer(read_only=True) else: self.fields['linked_orderposition'] = serializers.PrimaryKeyRelatedField( @@ -86,6 +91,9 @@ class ReusableMediaSerializer(I18nAwareModelSerializer): ) if 'customer' in self.context['request'].query_params.getlist('expand'): + if not self.context["can_read_customers"]: + raise PermissionDenied("No permission to access customer details.") + self.fields['customer'] = CustomerSerializer(read_only=True) else: self.fields['customer'] = serializers.SlugRelatedField( diff --git a/src/pretix/api/serializers/organizer.py b/src/pretix/api/serializers/organizer.py index bdead78997..5a1c284138 100644 --- a/src/pretix/api/serializers/organizer.py +++ b/src/pretix/api/serializers/organizer.py @@ -422,7 +422,7 @@ class DeviceSerializer(serializers.ModelSerializer): created = serializers.DateTimeField(read_only=True) revoked = serializers.BooleanField(read_only=True) initialized = serializers.DateTimeField(read_only=True) - initialization_token = serializers.DateTimeField(read_only=True) + initialization_token = serializers.CharField(read_only=True) security_profile = serializers.ChoiceField(choices=[], required=False, default="full") class Meta: @@ -436,6 +436,8 @@ class DeviceSerializer(serializers.ModelSerializer): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.fields['security_profile'].choices = [(k, v.verbose_name) for k, v in get_all_security_profiles().items()] + if not self.context['can_see_tokens']: + del self.fields['initialization_token'] class TeamInviteSerializer(serializers.ModelSerializer): @@ -520,7 +522,10 @@ class TeamMemberSerializer(serializers.ModelSerializer): class OrganizerSettingsSerializer(SettingsSerializer): + default_write_permission = 'organizer.settings.general:write' default_fields = [ + # These are readable for all users with access to the events, therefore secrets made in the settings store + # should not be included! 'customer_accounts', 'customer_accounts_native', 'customer_accounts_link_by_email', diff --git a/src/pretix/api/serializers/settings.py b/src/pretix/api/serializers/settings.py index 435b964db0..ce940a99e0 100644 --- a/src/pretix/api/serializers/settings.py +++ b/src/pretix/api/serializers/settings.py @@ -37,6 +37,8 @@ logger = logging.getLogger(__name__) class SettingsSerializer(serializers.Serializer): default_fields = [] readonly_fields = [] + default_write_permission = 'organizer.settings.general:write' + write_permission_required = {} def __init__(self, *args, **kwargs): self.changed_data = [] @@ -58,9 +60,17 @@ class SettingsSerializer(serializers.Serializer): f._label = str(form_kwargs.get('label', fname)) f._help_text = str(form_kwargs.get('help_text')) f.parent = self + + self.write_permission_required[fname] = DEFAULTS[fname].get('write_permission', self.default_write_permission) + self.fields[fname] = f def validate(self, attrs): + for k in attrs.keys(): + p = self.write_permission_required.get(k, self.default_write_permission) + if p not in self.context["permissions"]: + raise ValidationError({k: f"Setting this field requires permission {p}"}) + return {k: v for k, v in attrs.items() if k not in self.readonly_fields} def update(self, instance: HierarkeyProxy, validated_data): diff --git a/src/pretix/api/views/event.py b/src/pretix/api/views/event.py index 6ec51095e7..55f67e315e 100644 --- a/src/pretix/api/views/event.py +++ b/src/pretix/api/views/event.py @@ -432,7 +432,7 @@ with scopes_disabled(): class SubEventViewSet(ConditionalListView, viewsets.ModelViewSet): serializer_class = SubEventSerializer queryset = SubEvent.objects.none() - write_permission = 'event.settings.general:write' + write_permission = 'event.subevents:write' filter_backends = (DjangoFilterBackend, TotalOrderingFilter) ordering = ('date_from',) ordering_fields = ('id', 'date_from', 'last_modified') @@ -552,7 +552,7 @@ class SubEventViewSet(ConditionalListView, viewsets.ModelViewSet): class TaxRuleViewSet(ConditionalListView, viewsets.ModelViewSet): serializer_class = TaxRuleSerializer queryset = TaxRule.objects.none() - write_permission = 'event.settings.general:write' + write_permission = 'event.settings.tax:write' def get_queryset(self): return self.request.event.tax_rules.all() @@ -647,14 +647,13 @@ class EventSettingsView(views.APIView): def get(self, request, *args, **kwargs): if isinstance(request.auth, Device): s = DeviceEventSettingsSerializer(instance=request.event.settings, event=request.event, context={ - 'request': request - }) - elif 'event.settings.general:write' in request.eventpermset: - s = EventSettingsSerializer(instance=request.event.settings, event=request.event, context={ - 'request': request + 'request': request, 'permissions': request.eventpermset }) else: - raise PermissionDenied() + s = EventSettingsSerializer(instance=request.event.settings, event=request.event, context={ + 'request': request, 'permissions': request.eventpermset, + }) + if 'explain' in request.GET: return Response({ fname: { @@ -668,7 +667,7 @@ class EventSettingsView(views.APIView): def patch(self, request, *wargs, **kwargs): s = EventSettingsSerializer(instance=request.event.settings, data=request.data, partial=True, - event=request.event, context={'request': request}) + event=request.event, context={'request': request, 'permissions': request.eventpermset}) s.is_valid(raise_exception=True) with transaction.atomic(): s.save() @@ -680,7 +679,7 @@ class EventSettingsView(views.APIView): ) s = EventSettingsSerializer( instance=request.event.settings, event=request.event, context={ - 'request': request + 'request': request, 'permissions': request.eventpermset }) return Response(s.data) diff --git a/src/pretix/api/views/media.py b/src/pretix/api/views/media.py index d16bb6a568..7b3be781f6 100644 --- a/src/pretix/api/views/media.py +++ b/src/pretix/api/views/media.py @@ -95,6 +95,8 @@ class ReusableMediaViewSet(viewsets.ModelViewSet): def get_serializer_context(self): ctx = super().get_serializer_context() ctx['organizer'] = self.request.organizer + ctx['can_read_giftcards'] = 'organizer.giftcards:read' in self.request.orgapermset + ctx['can_read_customers'] = 'organizer.customers:read' in self.request.orgapermset return ctx @transaction.atomic() diff --git a/src/pretix/api/views/organizer.py b/src/pretix/api/views/organizer.py index b89881f31c..df7967e7bc 100644 --- a/src/pretix/api/views/organizer.py +++ b/src/pretix/api/views/organizer.py @@ -221,7 +221,7 @@ with scopes_disabled(): class GiftCardViewSet(viewsets.ModelViewSet): serializer_class = GiftCardSerializer queryset = GiftCard.objects.none() - permission = 'organizer.giftcards:write' + permission = 'organizer.giftcards:read' write_permission = 'organizer.giftcards:write' filter_backends = (DjangoFilterBackend,) filterset_class = GiftCardFilter @@ -344,7 +344,7 @@ class GiftCardViewSet(viewsets.ModelViewSet): class GiftCardTransactionViewSet(viewsets.ReadOnlyModelViewSet): serializer_class = GiftCardTransactionSerializer queryset = GiftCardTransaction.objects.none() - permission = 'organizer.giftcards:write' + permission = 'organizer.giftcards:read' write_permission = 'organizer.giftcards:write' @cached_property @@ -532,8 +532,8 @@ class DeviceViewSet(mixins.CreateModelMixin, GenericViewSet): serializer_class = DeviceSerializer queryset = Device.objects.none() - permission = 'organizer.settings.general:write' - write_permission = 'organizer.settings.general:write' + permission = 'organizer.devices:read' + write_permission = 'organizer.devices:write' lookup_field = 'device_id' def get_queryset(self): @@ -542,6 +542,9 @@ class DeviceViewSet(mixins.CreateModelMixin, def get_serializer_context(self): ctx = super().get_serializer_context() ctx['organizer'] = self.request.organizer + ctx['can_see_tokens'] = ( + self.request.user if self.request.user and self.request.user.is_authenticated else self.request.auth + ).has_organizer_permission(self.request.organizer, 'organizer.devices:write', request=self.request) return ctx @transaction.atomic() @@ -572,7 +575,7 @@ class OrganizerSettingsView(views.APIView): def get(self, request, *args, **kwargs): s = OrganizerSettingsSerializer(instance=request.organizer.settings, organizer=request.organizer, context={ - 'request': request + 'request': request, 'permissions': request.orgapermset }) if 'explain' in request.GET: return Response({ @@ -589,7 +592,7 @@ class OrganizerSettingsView(views.APIView): s = OrganizerSettingsSerializer( instance=request.organizer.settings, data=request.data, partial=True, organizer=request.organizer, context={ - 'request': request + 'request': request, 'permissions': request.orgapermset } ) s.is_valid(raise_exception=True) @@ -601,7 +604,7 @@ class OrganizerSettingsView(views.APIView): } ) s = OrganizerSettingsSerializer(instance=request.organizer.settings, organizer=request.organizer, context={ - 'request': request + 'request': request, 'permissions': request.orgapermset }) return Response(s.data) @@ -618,7 +621,8 @@ with scopes_disabled(): class CustomerViewSet(viewsets.ModelViewSet): serializer_class = CustomerSerializer queryset = Customer.objects.none() - permission = 'organizer.customers:write' + permission = 'organizer.customers:read' + write_permission = 'organizer.customers:write' lookup_field = 'identifier' filter_backends = (DjangoFilterBackend,) filterset_class = CustomerFilter @@ -735,7 +739,8 @@ with scopes_disabled(): class MembershipViewSet(viewsets.ModelViewSet): serializer_class = MembershipSerializer queryset = Membership.objects.none() - permission = 'organizer.customers:write' + permission = 'organizer.customers:read' + write_permission = 'organizer.customers:write' filter_backends = (DjangoFilterBackend,) filterset_class = MembershipFilter diff --git a/src/pretix/base/permissions.py b/src/pretix/base/permissions.py index 8ce11dcaf0..4a63597f24 100644 --- a/src/pretix/base/permissions.py +++ b/src/pretix/base/permissions.py @@ -78,18 +78,18 @@ def register_default_event_permissions(sender, **kwargs): 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.plugins:write", _("Change plugin settings"), None, None), - Permission("event.settings.email.sender:write", _("Change email sending 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, None), - Permission("event.items:write", _("Change products and quotas"), None, None), # and questions but that might change? + 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", " entire event or date"), None, None), + Permission("event:cancel", pgettext_lazy("subevent", "Cancel entire event or date"), None, None), ] @@ -100,14 +100,16 @@ def register_default_organizer_permissions(sender, **kwargs): 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.")), + _("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, 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"), None, None), - Permission("organizer.devices:write", _("Change devices"), 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.")), ] diff --git a/src/pretix/base/settings.py b/src/pretix/base/settings.py index 4758f33936..0c801fac75 100644 --- a/src/pretix/base/settings.py +++ b/src/pretix/base/settings.py @@ -345,6 +345,7 @@ DEFAULTS = { 'type': bool, 'form_class': forms.BooleanField, 'serializer_class': serializers.BooleanField, + 'write_permission': 'event.settings.tax:write', 'form_kwargs': dict( label=_("Show net prices instead of gross prices in the product list"), help_text=_("Independent of your choice, the cart will show gross prices as this is the price that needs to be " @@ -492,6 +493,7 @@ DEFAULTS = { 'type': str, 'form_class': forms.ChoiceField, 'serializer_class': serializers.ChoiceField, + 'write_permission': 'event.settings.tax:write', 'form_kwargs': dict( label=_("Rounding of taxes"), widget=forms.RadioSelect, @@ -511,15 +513,17 @@ DEFAULTS = { 'type': bool, 'form_class': forms.BooleanField, 'serializer_class': serializers.BooleanField, + 'write_permission': 'event.settings.invoicing:write', 'form_kwargs': dict( label=_("Ask for invoice address"), - ) + ), }, 'invoice_address_not_asked_free': { 'default': 'False', 'type': bool, 'form_class': forms.BooleanField, 'serializer_class': serializers.BooleanField, + 'write_permission': 'event.settings.invoicing:write', 'form_kwargs': dict( label=_('Do not ask for invoice address if an order is free'), ) @@ -529,6 +533,7 @@ DEFAULTS = { 'type': bool, 'form_class': forms.BooleanField, 'serializer_class': serializers.BooleanField, + 'write_permission': 'event.settings.invoicing:write', 'form_kwargs': dict( label=_("Require customer name"), ) @@ -538,6 +543,7 @@ DEFAULTS = { 'type': bool, 'form_class': forms.BooleanField, 'serializer_class': serializers.BooleanField, + 'write_permission': 'event.settings.invoicing:write', 'form_kwargs': dict( label=_("Show attendee names on invoices"), ) @@ -547,6 +553,7 @@ DEFAULTS = { 'type': bool, 'form_class': forms.BooleanField, 'serializer_class': serializers.BooleanField, + 'write_permission': 'event.settings.invoicing:write', 'form_kwargs': dict( label=_("Show event location on invoices"), help_text=_("The event location will be shown below the list of products if it is the same for all " @@ -558,6 +565,7 @@ DEFAULTS = { 'type': str, 'form_class': forms.ChoiceField, 'serializer_class': serializers.ChoiceField, + 'write_permission': 'event.settings.invoicing:write', 'form_kwargs': dict( label=_("Show exchange rates"), widget=forms.RadioSelect, @@ -581,6 +589,7 @@ DEFAULTS = { 'default': 'False', 'form_class': forms.BooleanField, 'serializer_class': serializers.BooleanField, + 'write_permission': 'event.settings.invoicing:write', 'type': bool, 'form_kwargs': dict( label=_("Require invoice address"), @@ -591,6 +600,7 @@ DEFAULTS = { 'default': 'False', 'form_class': forms.BooleanField, 'serializer_class': serializers.BooleanField, + 'write_permission': 'event.settings.invoicing:write', 'type': bool, 'form_kwargs': dict( label=_("Require a business address"), @@ -603,6 +613,7 @@ DEFAULTS = { 'type': bool, 'form_class': forms.BooleanField, 'serializer_class': serializers.BooleanField, + 'write_permission': 'event.settings.invoicing:write', 'form_kwargs': dict( label=_("Ask for beneficiary"), widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_invoice_address_asked'}), @@ -613,6 +624,7 @@ DEFAULTS = { 'type': LazyI18nString, 'form_class': I18nFormField, 'serializer_class': I18nField, + 'write_permission': 'event.settings.invoicing:write', 'form_kwargs': dict( label=_("Custom recipient field label"), widget=I18nTextInput, @@ -628,6 +640,7 @@ DEFAULTS = { 'type': LazyI18nString, 'form_class': I18nFormField, 'serializer_class': I18nField, + 'write_permission': 'event.settings.invoicing:write', 'form_kwargs': dict( label=_("Custom recipient field help text"), widget=I18nTextInput, @@ -640,6 +653,7 @@ DEFAULTS = { 'type': bool, 'form_class': forms.BooleanField, 'serializer_class': serializers.BooleanField, + 'write_permission': 'event.settings.invoicing:write', 'form_kwargs': dict( label=_("Ask for VAT ID"), help_text=format_lazy( @@ -655,6 +669,7 @@ DEFAULTS = { 'type': list, 'form_class': forms.MultipleChoiceField, 'serializer_class': serializers.MultipleChoiceField, + 'write_permission': 'event.settings.invoicing:write', 'serializer_kwargs': dict( choices=lazy( lambda *args: sorted([(cc, gettext(Country(cc).name)) for cc in VAT_ID_COUNTRIES], key=lambda c: c[1]), @@ -682,6 +697,7 @@ DEFAULTS = { 'type': LazyI18nString, 'form_class': I18nFormField, 'serializer_class': I18nField, + 'write_permission': 'event.settings.invoicing:write', 'form_kwargs': dict( label=_("Invoice address explanation"), widget=I18nMarkdownTextarea, @@ -694,6 +710,7 @@ DEFAULTS = { 'type': bool, 'form_class': forms.BooleanField, 'serializer_class': serializers.BooleanField, + 'write_permission': 'event.settings.invoicing:write', 'form_kwargs': dict( label=_("Show paid amount on partially paid invoices"), help_text=_("If an invoice has already been paid partially, this option will add the paid and pending " @@ -705,6 +722,7 @@ DEFAULTS = { 'type': bool, 'form_class': forms.BooleanField, 'serializer_class': serializers.BooleanField, + 'write_permission': 'event.settings.invoicing:write', 'form_kwargs': dict( label=_("Show free products on invoices"), help_text=_("Note that invoices will never be generated for orders that contain only free " @@ -716,6 +734,7 @@ DEFAULTS = { 'type': bool, 'form_class': forms.BooleanField, 'serializer_class': serializers.BooleanField, + 'write_permission': 'event.settings.invoicing:write', 'form_kwargs': dict( label=_("Show expiration date of order"), help_text=_("The expiration date will not be shown if the invoice is generated after the order is paid."), @@ -727,6 +746,7 @@ DEFAULTS = { 'form_class': forms.IntegerField, 'serializer_class': serializers.IntegerField, 'serializer_kwargs': dict(), + 'write_permission': 'event.settings.invoicing:write', 'form_kwargs': dict( label=_("Minimum length of invoice number after prefix"), help_text=_("The part of your invoice number after your prefix will be filled up with leading zeros up to this length, e.g. INV-001 or INV-00001."), @@ -740,6 +760,7 @@ DEFAULTS = { 'type': bool, 'form_class': forms.BooleanField, 'serializer_class': serializers.BooleanField, + 'write_permission': 'event.settings.invoicing:write', 'form_kwargs': dict( label=_("Generate invoices with consecutive numbers"), help_text=_("If deactivated, the order code will be used in the invoice number."), @@ -750,6 +771,7 @@ DEFAULTS = { 'type': str, 'form_class': forms.CharField, 'serializer_class': serializers.CharField, + 'write_permission': 'event.settings.invoicing:write', 'form_kwargs': dict( label=_("Invoice number prefix"), help_text=_("This will be prepended to invoice numbers. If you leave this field empty, your event slug will " @@ -777,6 +799,7 @@ DEFAULTS = { 'type': str, 'form_class': forms.CharField, 'serializer_class': serializers.CharField, + 'write_permission': 'event.settings.invoicing:write', 'form_kwargs': dict( label=_("Invoice number prefix for cancellations"), help_text=_("This will be prepended to invoice numbers of cancellations. If you leave this field empty, " @@ -800,6 +823,7 @@ DEFAULTS = { 'type': bool, 'form_class': forms.BooleanField, 'serializer_class': serializers.BooleanField, + 'write_permission': 'event.settings.invoicing:write', 'form_kwargs': dict( label=_("Highlight order code to make it stand out visibly"), help_text=_("Only respected by some invoice renderers."), @@ -811,6 +835,7 @@ DEFAULTS = { 'form_class': forms.ChoiceField, 'serializer_class': serializers.ChoiceField, 'serializer_kwargs': lambda: dict(**invoice_font_kwargs()), + 'write_permission': 'event.settings.invoicing:write', 'form_kwargs': lambda: dict( label=_('Font'), help_text=_("Only respected by some invoice renderers."), @@ -821,6 +846,7 @@ DEFAULTS = { 'invoice_renderer': { 'default': 'classic', # default for new events is 'modern1' 'type': str, + 'write_permission': 'event.settings.invoicing:write', }, 'ticket_secret_generator': { 'default': 'random', @@ -897,6 +923,7 @@ DEFAULTS = { 'type': LazyI18nString, 'form_class': I18nFormField, 'serializer_class': I18nField, + 'write_permission': 'event.settings.payment:write', 'form_kwargs': dict( widget=I18nMarkdownTextarea, widget_kwargs={'attrs': { @@ -918,6 +945,7 @@ DEFAULTS = { ('minutes', _("in minutes")) ), ), + 'write_permission': 'event.settings.payment:write', 'form_kwargs': dict( label=_("Set payment term"), widget=forms.RadioSelect, @@ -935,6 +963,7 @@ DEFAULTS = { 'type': int, 'form_class': forms.IntegerField, 'serializer_class': serializers.IntegerField, + 'write_permission': 'event.settings.payment:write', 'form_kwargs': dict( label=_('Payment term in days'), widget=forms.NumberInput( @@ -960,6 +989,7 @@ DEFAULTS = { 'type': bool, 'form_class': forms.BooleanField, 'serializer_class': serializers.BooleanField, + 'write_permission': 'event.settings.payment:write', 'form_kwargs': dict( label=_('Only end payment terms on weekdays'), help_text=_("If this is activated and the payment term of any order ends on a Saturday or Sunday, it will be " @@ -977,6 +1007,7 @@ DEFAULTS = { 'type': int, 'form_class': forms.IntegerField, 'serializer_class': serializers.IntegerField, + 'write_permission': 'event.settings.payment:write', 'form_kwargs': dict( label=_('Payment term in minutes'), help_text=_("The number of minutes after placing an order the user has to pay to preserve their reservation. " @@ -1001,6 +1032,7 @@ DEFAULTS = { 'type': RelativeDateWrapper, 'form_class': RelativeDateField, 'serializer_class': SerializerRelativeDateField, + 'write_permission': 'event.settings.payment:write', 'form_kwargs': dict( label=_('Last date of payments'), help_text=_("The last date any payments are accepted. This has precedence over the terms " @@ -1013,6 +1045,7 @@ DEFAULTS = { 'type': bool, 'form_class': forms.BooleanField, 'serializer_class': serializers.BooleanField, + 'write_permission': 'event.settings.payment:write', 'form_kwargs': dict( label=_('Automatically expire unpaid orders'), help_text=_("If checked, all unpaid orders will automatically go from 'pending' to 'expired' " @@ -1025,6 +1058,7 @@ DEFAULTS = { 'type': int, 'form_class': forms.IntegerField, 'serializer_class': serializers.IntegerField, + 'write_permission': 'event.settings.payment:write', 'form_kwargs': dict( label=_('Expiration delay'), help_text=_("The order will only actually expire this many days after the expiration date communicated " @@ -1047,6 +1081,7 @@ DEFAULTS = { 'type': bool, 'form_class': forms.BooleanField, 'serializer_class': serializers.BooleanField, + 'write_permission': 'event.settings.payment:write', 'form_kwargs': dict( label=_('Hide "payment pending" state on customer-facing pages'), help_text=_("The payment instructions panel will still be shown to the primary customer, but no indication " @@ -1058,9 +1093,11 @@ DEFAULTS = { 'default': 'True', 'type': bool, 'serializer_class': serializers.BooleanField, + 'write_permission': 'event.settings.payment:write', }, 'payment_giftcard_public_name': { 'default': LazyI18nString.from_gettext(gettext_noop('Gift card')), + 'write_permission': 'event.settings.payment:write', 'type': LazyI18nString }, 'payment_giftcard_public_description': { @@ -1069,10 +1106,12 @@ DEFAULTS = { 'enough credit to pay for the full order, you will be shown this page again and you can either ' 'redeem another gift card or select a different payment method for the difference.' )), + 'write_permission': 'event.settings.payment:write', 'type': LazyI18nString }, 'payment_resellers__restrict_to_sales_channels': { 'default': ['resellers'], + 'write_permission': 'event.settings.payment:write', 'type': list }, 'payment_term_accept_late': { @@ -1080,6 +1119,7 @@ DEFAULTS = { 'type': bool, 'form_class': forms.BooleanField, 'serializer_class': serializers.BooleanField, + 'write_permission': 'event.settings.payment:write', 'form_kwargs': dict( label=_('Accept late payments'), help_text=_("Accept payments for orders even when they are in 'expired' state as long as enough " @@ -1109,6 +1149,7 @@ DEFAULTS = { ('none', _('Charge no taxes')), ), ), + 'write_permission': 'event.settings.payment:write', 'form_kwargs': dict( label=_("Tax handling on payment fees"), widget=forms.RadioSelect, @@ -1155,6 +1196,7 @@ DEFAULTS = { ('paid', _('Automatically on payment or when required by payment method')), ), ), + 'write_permission': 'event.settings.invoicing:write', 'form_kwargs': dict( label=_("Generate invoices"), widget=forms.RadioSelect, @@ -1183,6 +1225,7 @@ DEFAULTS = { ('invoice_date', _('Invoice date')), ), ), + 'write_permission': 'event.settings.invoicing:write', 'form_kwargs': dict( label=_("Date of service"), widget=forms.RadioSelect, @@ -1203,6 +1246,7 @@ DEFAULTS = { 'type': bool, 'form_class': forms.BooleanField, 'serializer_class': serializers.BooleanField, + 'write_permission': 'event.settings.invoicing:write', 'form_kwargs': dict( label=_("Automatically cancel and reissue invoice on address changes"), help_text=_("If customers change their invoice address on an existing order, the invoice will " @@ -1215,6 +1259,7 @@ DEFAULTS = { 'type': bool, 'form_class': forms.BooleanField, 'serializer_class': serializers.BooleanField, + 'write_permission': 'event.settings.invoicing:write', 'form_kwargs': dict( label=_("Allow to update existing invoices"), help_text=_("By default, invoices can never again be changed once they are issued. In most countries, we " @@ -1224,6 +1269,7 @@ DEFAULTS = { }, 'invoice_generate_sales_channels': { 'default': json.dumps(['web']), + 'write_permission': 'event.settings.invoicing:write', 'type': list }, 'invoice_generate_only_business': { @@ -1240,6 +1286,7 @@ DEFAULTS = { 'type': str, 'form_class': forms.CharField, 'serializer_class': serializers.CharField, + 'write_permission': 'event.settings.invoicing:write', 'form_kwargs': dict( label=_("Address line"), widget=forms.Textarea(attrs={ @@ -1255,6 +1302,7 @@ DEFAULTS = { 'type': str, 'form_class': forms.CharField, 'serializer_class': serializers.CharField, + 'write_permission': 'event.settings.invoicing:write', 'form_kwargs': dict( max_length=190, label=_("Company name"), @@ -1265,6 +1313,7 @@ DEFAULTS = { 'type': str, 'form_class': forms.CharField, 'serializer_class': serializers.CharField, + 'write_permission': 'event.settings.invoicing:write', 'form_kwargs': dict( widget=forms.TextInput(attrs={ 'placeholder': '12345' @@ -1278,6 +1327,7 @@ DEFAULTS = { 'type': str, 'form_class': forms.CharField, 'serializer_class': serializers.CharField, + 'write_permission': 'event.settings.invoicing:write', 'form_kwargs': dict( widget=forms.TextInput(attrs={ 'placeholder': _('Random City') @@ -1294,6 +1344,7 @@ DEFAULTS = { 'serializer_kwargs': { 'choices': [('', '')], }, + 'write_permission': 'event.settings.invoicing:write', 'form_kwargs': { "label": pgettext_lazy('address', 'State'), 'choices': [('', '')], @@ -1305,6 +1356,7 @@ DEFAULTS = { 'form_class': forms.ChoiceField, 'serializer_class': serializers.ChoiceField, 'serializer_kwargs': lambda: dict(**country_choice_kwargs()), + 'write_permission': 'event.settings.invoicing:write', 'form_kwargs': lambda: dict( label=_('Country'), widget=forms.Select(attrs={ @@ -1318,6 +1370,7 @@ DEFAULTS = { 'type': str, 'form_class': forms.CharField, 'serializer_class': serializers.CharField, + 'write_permission': 'event.settings.invoicing:write', 'form_kwargs': dict( label=_("Domestic tax ID"), help_text=_("e.g. tax number in Germany, ABN in Australia, …"), @@ -1329,6 +1382,7 @@ DEFAULTS = { 'type': str, 'form_class': forms.CharField, 'serializer_class': serializers.CharField, + 'write_permission': 'event.settings.invoicing:write', 'form_kwargs': dict( label=_("EU VAT ID"), max_length=190, @@ -1339,6 +1393,7 @@ DEFAULTS = { 'type': LazyI18nString, 'form_class': I18nFormField, 'serializer_class': I18nField, + 'write_permission': 'event.settings.invoicing:write', 'form_kwargs': dict( widget=I18nTextarea, widget_kwargs={'attrs': { @@ -1356,6 +1411,7 @@ DEFAULTS = { 'type': LazyI18nString, 'form_class': I18nFormField, 'serializer_class': I18nField, + 'write_permission': 'event.settings.invoicing:write', 'form_kwargs': dict( widget=I18nTextarea, widget_kwargs={'attrs': { @@ -1373,6 +1429,7 @@ DEFAULTS = { 'type': LazyI18nString, 'form_class': I18nFormField, 'serializer_class': I18nField, + 'write_permission': 'event.settings.invoicing:write', 'form_kwargs': dict( widget=I18nTextarea, widget_kwargs={'attrs': { @@ -1387,6 +1444,7 @@ DEFAULTS = { }, 'invoice_language': { 'default': '__user__', + 'write_permission': 'event.settings.invoicing:write', 'type': str }, 'invoice_email_attachment': { @@ -1394,6 +1452,7 @@ DEFAULTS = { 'type': bool, 'form_class': forms.BooleanField, 'serializer_class': serializers.BooleanField, + 'write_permission': 'event.settings.invoicing:write', 'form_kwargs': dict( label=_("Attach invoices to emails"), help_text=_("If invoices are automatically generated for all orders, they will be attached to the order " @@ -1407,6 +1466,7 @@ DEFAULTS = { 'type': str, 'form_class': forms.CharField, 'serializer_class': serializers.CharField, + 'write_permission': 'event.settings.invoicing:write', 'form_kwargs': dict( label=_("Email address to receive a copy of each invoice"), help_text=_("Each newly created invoice will be sent to this email address shortly after creation. You can " @@ -3260,7 +3320,8 @@ Your {organizer} team""")) # noqa: W291 'image/png', 'image/jpeg', 'image/gif' ], max_size=settings.FILE_UPLOAD_MAX_SIZE_IMAGE, - ) + ), + 'write_permission': 'event.settings.invoicing:write', }, 'frontpage_text': { 'default': '', diff --git a/src/pretix/base/signals.py b/src/pretix/base/signals.py index aa42950139..594f8c0c5b 100644 --- a/src/pretix/base/signals.py +++ b/src/pretix/base/signals.py @@ -1118,6 +1118,9 @@ api_event_settings_fields = EventPluginSignal() This signal is sent out to collect serializable settings fields for the API. You are expected to return a dictionary mapping names of attributes in the settings store to DRF serializer field instances. +These are readable for all users with access to the events, therefore secrets made in the settings store +should not be included! + As with all event-plugin signals, the ``sender`` keyword argument will contain the event. """ diff --git a/src/pretix/base/timeline.py b/src/pretix/base/timeline.py index 929422f9fe..bf222f35a4 100644 --- a/src/pretix/base/timeline.py +++ b/src/pretix/base/timeline.py @@ -32,7 +32,11 @@ from pretix.base.models import ItemVariation from pretix.base.reldate import RelativeDateWrapper from pretix.base.signals import timeline_events -TimelineEvent = namedtuple('TimelineEvent', ('event', 'subevent', 'datetime', 'description', 'edit_url')) +TimelineEvent = namedtuple( + 'TimelineEvent', + ('event', 'subevent', 'datetime', 'description', 'edit_url', 'edit_permission'), + defaults=(None, None, None, None, None, 'event.settings.general:write') +) def timeline_for_event(event, subevent=None): @@ -46,6 +50,7 @@ def timeline_for_event(event, subevent=None): 'subevent': subevent.pk } ) + ev_edit_permission = 'event.subevents:write' else: ev_edit_url = reverse( 'control:event.settings', kwargs={ @@ -53,12 +58,14 @@ def timeline_for_event(event, subevent=None): 'organizer': event.organizer.slug } ) + ev_edit_permission = 'event.settings.general:write' tl.append(TimelineEvent( event=event, subevent=subevent, datetime=ev.date_from, description=pgettext_lazy('timeline', 'Your event starts'), - edit_url=ev_edit_url + '#id_date_from_0' + edit_url=ev_edit_url + '#id_date_from_0', + edit_permission=ev_edit_permission, )) if ev.date_to: @@ -66,7 +73,8 @@ def timeline_for_event(event, subevent=None): event=event, subevent=subevent, datetime=ev.date_to, description=pgettext_lazy('timeline', 'Your event ends'), - edit_url=ev_edit_url + '#id_date_to_0' + edit_url=ev_edit_url + '#id_date_to_0', + edit_permission=ev_edit_permission, )) if ev.date_admission: @@ -74,7 +82,8 @@ def timeline_for_event(event, subevent=None): event=event, subevent=subevent, datetime=ev.date_admission, description=pgettext_lazy('timeline', 'Admissions for your event start'), - edit_url=ev_edit_url + '#id_date_admission_0' + edit_url=ev_edit_url + '#id_date_admission_0', + edit_permission=ev_edit_permission, )) if ev.presale_start: @@ -82,7 +91,8 @@ def timeline_for_event(event, subevent=None): event=event, subevent=subevent, datetime=ev.presale_start, description=pgettext_lazy('timeline', 'Start of ticket sales'), - edit_url=ev_edit_url + '#id_presale_start_0' + edit_url=ev_edit_url + '#id_presale_start_0', + edit_permission=ev_edit_permission, )) tl.append(TimelineEvent( @@ -97,7 +107,8 @@ def timeline_for_event(event, subevent=None): ) if not ev.presale_end else ( pgettext_lazy('timeline', 'End of ticket sales') ), - edit_url=ev_edit_url + '#id_presale_end_0' + edit_url=ev_edit_url + '#id_presale_end_0', + edit_permission=ev_edit_permission, )) rd = event.settings.get('last_order_modification_date', as_type=RelativeDateWrapper) @@ -106,7 +117,8 @@ def timeline_for_event(event, subevent=None): event=event, subevent=subevent, datetime=rd.datetime(ev), description=pgettext_lazy('timeline', 'Customers can no longer modify their order information'), - edit_url=ev_edit_url + '#id_settings-last_order_modification_date_0_0' + edit_url=ev_edit_url + '#id_settings-last_order_modification_date_0_0', + edit_permission='event.settings.general:write', )) rd = event.settings.get('payment_term_last', as_type=RelativeDateWrapper) @@ -122,7 +134,8 @@ def timeline_for_event(event, subevent=None): edit_url=reverse('control:event.settings.payment', kwargs={ 'event': event.slug, 'organizer': event.organizer.slug - }) + }), + edit_permission='event.settings.payment:write', )) rd = event.settings.get('ticket_download_date', as_type=RelativeDateWrapper) @@ -134,7 +147,8 @@ def timeline_for_event(event, subevent=None): edit_url=reverse('control:event.settings.tickets', kwargs={ 'event': event.slug, 'organizer': event.organizer.slug - }) + }), + edit_permission='event.settings.general:write', )) rd = event.settings.get('cancel_allow_user_until', as_type=RelativeDateWrapper) @@ -146,7 +160,8 @@ def timeline_for_event(event, subevent=None): edit_url=reverse('control:event.settings.cancel', kwargs={ 'event': event.slug, 'organizer': event.organizer.slug - }) + }), + edit_permission='event.settings.general:write', )) rd = event.settings.get('cancel_allow_user_paid_until', as_type=RelativeDateWrapper) @@ -158,7 +173,8 @@ def timeline_for_event(event, subevent=None): edit_url=reverse('control:event.settings.cancel', kwargs={ 'event': event.slug, 'organizer': event.organizer.slug - }) + }), + edit_permission='event.settings.general:write', )) rd = event.settings.get('change_allow_user_until', as_type=RelativeDateWrapper) @@ -170,7 +186,8 @@ def timeline_for_event(event, subevent=None): edit_url=reverse('control:event.settings.cancel', kwargs={ 'event': event.slug, 'organizer': event.organizer.slug - }) + }), + edit_permission='event.settings.general:write', )) rd = event.settings.get('waiting_list_auto_disable', as_type=RelativeDateWrapper) @@ -182,7 +199,8 @@ def timeline_for_event(event, subevent=None): edit_url=reverse('control:event.settings', kwargs={ 'event': event.slug, 'organizer': event.organizer.slug - }) + '#waiting-list-open' + }) + '#waiting-list-open', + edit_permission='event.settings.general:write', )) if not event.has_subevents: @@ -196,7 +214,8 @@ def timeline_for_event(event, subevent=None): edit_url=reverse('control:event.settings.mail', kwargs={ 'event': event.slug, 'organizer': event.organizer.slug - }) + }), + edit_permission='event.settings.general:write', )) if subevent: @@ -210,7 +229,8 @@ def timeline_for_event(event, subevent=None): 'event': event.slug, 'organizer': event.organizer.slug, 'subevent': subevent.pk, - }) + }), + edit_permission='event.subevents:write', )) if sei.available_until: tl.append(TimelineEvent( @@ -221,7 +241,8 @@ def timeline_for_event(event, subevent=None): 'event': event.slug, 'organizer': event.organizer.slug, 'subevent': subevent.pk, - }) + }), + edit_permission='event.subevents:write', )) for sei in subevent.var_overrides.values(): if sei.available_from: @@ -234,7 +255,8 @@ def timeline_for_event(event, subevent=None): 'event': event.slug, 'organizer': event.organizer.slug, 'subevent': subevent.pk, - }) + }), + edit_permission='event.subevents:write', )) if sei.available_until: tl.append(TimelineEvent( @@ -246,7 +268,8 @@ def timeline_for_event(event, subevent=None): 'event': event.slug, 'organizer': event.organizer.slug, 'subevent': subevent.pk, - }) + }), + edit_permission='event.subevents:write', )) for d in event.discounts.filter(Q(available_from__isnull=False) | Q(available_until__isnull=False)): @@ -259,7 +282,8 @@ def timeline_for_event(event, subevent=None): 'event': event.slug, 'organizer': event.organizer.slug, 'discount': d.pk, - }) + }), + edit_permission='event.items:write', )) if d.available_until: tl.append(TimelineEvent( @@ -270,7 +294,8 @@ def timeline_for_event(event, subevent=None): 'event': event.slug, 'organizer': event.organizer.slug, 'discount': d.pk, - }) + }), + edit_permission='event.items:write', )) for p in event.items.filter(Q(available_from__isnull=False) | Q(available_until__isnull=False)): @@ -283,7 +308,8 @@ def timeline_for_event(event, subevent=None): 'event': event.slug, 'organizer': event.organizer.slug, 'item': p.pk, - }) + '#id_available_from_0' + }) + '#id_available_from_0', + edit_permission='event.items:write', )) if p.available_until: tl.append(TimelineEvent( @@ -294,7 +320,8 @@ def timeline_for_event(event, subevent=None): 'event': event.slug, 'organizer': event.organizer.slug, 'item': p.pk, - }) + '#id_available_until_0' + }) + '#id_available_until_0', + edit_permission='event.items:write', )) for v in ItemVariation.objects.filter( @@ -313,7 +340,8 @@ def timeline_for_event(event, subevent=None): 'event': event.slug, 'organizer': event.organizer.slug, 'item': v.item.pk, - }) + '#tab-0-3-open' + }) + '#tab-0-3-open', + edit_permission='event.items:write', )) if v.available_until: tl.append(TimelineEvent( @@ -327,7 +355,8 @@ def timeline_for_event(event, subevent=None): 'event': event.slug, 'organizer': event.organizer.slug, 'item': v.item.pk, - }) + '#tab-0-3-open' + }) + '#tab-0-3-open', + edit_permission='event.items:write', )) pprovs = event.get_payment_providers() @@ -357,7 +386,8 @@ def timeline_for_event(event, subevent=None): 'event': event.slug, 'organizer': event.organizer.slug, 'provider': pprov.identifier, - }) + }), + edit_permission='event.settings.payment:write', )) availability_date = pprov.settings.get('_availability_date', as_type=RelativeDateWrapper) if availability_date: @@ -375,7 +405,8 @@ def timeline_for_event(event, subevent=None): 'event': event.slug, 'organizer': event.organizer.slug, 'provider': pprov.identifier, - }) + }), + edit_permission='event.settings.payment:write', )) for recv, resp in timeline_events.send(sender=event, subevent=subevent): diff --git a/src/pretix/control/navigation.py b/src/pretix/control/navigation.py index 6e5efeb8df..50b203bdf1 100644 --- a/src/pretix/control/navigation.py +++ b/src/pretix/control/navigation.py @@ -43,24 +43,29 @@ def get_event_navigation(request: HttpRequest): 'icon': 'dashboard', } ] - if 'event.settings.general:write' in request.eventpermset: - event_settings = [ - { - 'label': _('General'), - 'url': reverse('control:event.settings', kwargs={ - 'event': request.event.slug, - 'organizer': request.event.organizer.slug, - }), - 'active': url.url_name == 'event.settings', - }, - { - 'label': _('Payment'), - 'url': reverse('control:event.settings.payment', kwargs={ - 'event': request.event.slug, - 'organizer': request.event.organizer.slug, - }), - 'active': url.url_name in ('event.settings.payment', 'event.settings.payment.provider'), - }, + event_settings = [] + if "event.settings.general:write" in request.eventpermset: + event_settings.append({ + 'label': _('General'), + 'url': reverse('control:event.settings', kwargs={ + 'event': request.event.slug, + 'organizer': request.event.organizer.slug, + }), + 'active': url.url_name == 'event.settings', + }) + + if "event.settings.payment:write" in request.eventpermset or "event.settings.general:write" in request.eventpermset: + event_settings.append({ + 'label': _('Payment'), + 'url': reverse('control:event.settings.payment', kwargs={ + 'event': request.event.slug, + 'organizer': request.event.organizer.slug, + }), + 'active': url.url_name in ('event.settings.payment', 'event.settings.payment.provider'), + }) + + if "event.settings.general:write" in request.eventpermset: + event_settings += [ { 'label': _('Plugins'), 'url': reverse('control:event.settings.plugins', kwargs={ @@ -84,23 +89,31 @@ def get_event_navigation(request: HttpRequest): 'organizer': request.event.organizer.slug, }), 'active': url.url_name == 'event.settings.mail', - }, - { - 'label': _('Taxes'), - 'url': reverse('control:event.settings.tax', kwargs={ - 'event': request.event.slug, - 'organizer': request.event.organizer.slug, - }), - 'active': url.url_name.startswith('event.settings.tax'), - }, - { - 'label': _('Invoicing'), - 'url': reverse('control:event.settings.invoice', kwargs={ - 'event': request.event.slug, - 'organizer': request.event.organizer.slug, - }), - 'active': url.url_name == 'event.settings.invoice', - }, + } + ] + + if "event.settings.tax:write" in request.eventpermset or "event.settings.general:write" in request.eventpermset: + event_settings.append({ + 'label': _('Taxes'), + 'url': reverse('control:event.settings.tax', kwargs={ + 'event': request.event.slug, + 'organizer': request.event.organizer.slug, + }), + 'active': url.url_name.startswith('event.settings.tax'), + }) + + if "event.settings.invoicing:write" in request.eventpermset or "event.settings.general:write" in request.eventpermset: + event_settings.append({ + 'label': _('Invoicing'), + 'url': reverse('control:event.settings.invoice', kwargs={ + 'event': request.event.slug, + 'organizer': request.event.organizer.slug, + }), + 'active': url.url_name == 'event.settings.invoice', + }) + + if "event.settings.general:write" in request.eventpermset: + event_settings += [ { 'label': pgettext_lazy('action', 'Cancellation'), 'url': reverse('control:event.settings.cancel', kwargs={ @@ -118,86 +131,85 @@ def get_event_navigation(request: HttpRequest): 'active': url.url_name == 'event.settings.widget', }, ] + + # It would be better to allow plugins to handle the permission themselves, but for backwards compatibility + # we need to have it in the "if" statement event_settings += sorted( sum((list(a[1]) for a in nav_event_settings.send(request.event, request=request)), []), key=lambda r: r['label'] ) + if event_settings: nav.append({ 'label': _('Settings'), - 'url': reverse('control:event.settings', kwargs={ - 'event': request.event.slug, - 'organizer': request.event.organizer.slug, - }), + 'url': event_settings[0]["url"], 'active': False, 'icon': 'wrench', 'children': event_settings }) - if 'event.items:write' in request.eventpermset: - nav.append({ - 'label': _('Products'), - 'url': reverse('control:event.items', kwargs={ - 'event': request.event.slug, - 'organizer': request.event.organizer.slug, - }), - 'active': False, - 'icon': 'ticket', - 'children': [ - { - 'label': _('Products'), - 'url': reverse('control:event.items', kwargs={ - 'event': request.event.slug, - 'organizer': request.event.organizer.slug, - }), - 'active': url.url_name in ( - 'event.item', 'event.items.add', 'event.items') or "event.item." in url.url_name, - }, - { - 'label': _('Quotas'), - 'url': reverse('control:event.items.quotas', kwargs={ - 'event': request.event.slug, - 'organizer': request.event.organizer.slug, - }), - 'active': 'event.items.quota' in url.url_name, - }, - { - 'label': _('Categories'), - 'url': reverse('control:event.items.categories', kwargs={ - 'event': request.event.slug, - 'organizer': request.event.organizer.slug, - }), - 'active': 'event.items.categories' in url.url_name, - }, - { - 'label': _('Questions'), - 'url': reverse('control:event.items.questions', kwargs={ - 'event': request.event.slug, - 'organizer': request.event.organizer.slug, - }), - 'active': 'event.items.questions' in url.url_name, - }, - { - 'label': _('Discounts'), - 'url': reverse('control:event.items.discounts', kwargs={ - 'event': request.event.slug, - 'organizer': request.event.organizer.slug, - }), - 'active': 'event.items.discounts' in url.url_name, - }, - ] - }) - - if 'event.settings.general:write' in request.eventpermset: - if request.event.has_subevents: - nav.append({ - 'label': pgettext_lazy('subevent', 'Dates'), - 'url': reverse('control:event.subevents', kwargs={ + nav.append({ + 'label': _('Products'), + 'url': reverse('control:event.items', kwargs={ + 'event': request.event.slug, + 'organizer': request.event.organizer.slug, + }), + 'active': False, + 'icon': 'ticket', + 'children': [ + { + 'label': _('Products'), + 'url': reverse('control:event.items', kwargs={ 'event': request.event.slug, 'organizer': request.event.organizer.slug, }), - 'active': ('event.subevent' in url.url_name), - 'icon': 'calendar', - }) + 'active': url.url_name in ( + 'event.item', 'event.items.add', 'event.items') or "event.item." in url.url_name, + }, + { + 'label': _('Quotas'), + 'url': reverse('control:event.items.quotas', kwargs={ + 'event': request.event.slug, + 'organizer': request.event.organizer.slug, + }), + 'active': 'event.items.quota' in url.url_name, + }, + { + 'label': _('Categories'), + 'url': reverse('control:event.items.categories', kwargs={ + 'event': request.event.slug, + 'organizer': request.event.organizer.slug, + }), + 'active': 'event.items.categories' in url.url_name, + }, + { + 'label': _('Questions'), + 'url': reverse('control:event.items.questions', kwargs={ + 'event': request.event.slug, + 'organizer': request.event.organizer.slug, + }), + 'active': 'event.items.questions' in url.url_name, + }, + { + 'label': _('Discounts'), + 'url': reverse('control:event.items.discounts', kwargs={ + 'event': request.event.slug, + 'organizer': request.event.organizer.slug, + }), + 'active': 'event.items.discounts' in url.url_name, + }, + ] + }) + + if request.event.has_subevents: + nav.append({ + 'label': pgettext_lazy('subevent', 'Dates'), + 'url': reverse('control:event.subevents', kwargs={ + 'event': request.event.slug, + 'organizer': request.event.organizer.slug, + }), + 'active': ('event.subevent' in url.url_name), + 'icon': 'calendar', + }) if 'event.orders:read' in request.eventpermset: children = [ @@ -291,7 +303,7 @@ def get_event_navigation(request: HttpRequest): ] }) - if 'event.orders:read' in request.eventpermset: + if 'event.orders:read' in request.eventpermset or 'event.settings.general:write' in request.eventpermset: nav.append({ 'label': pgettext_lazy('navigation', 'Check-in'), 'url': reverse('control:event.orders.checkinlists', kwargs={ @@ -544,7 +556,7 @@ def get_organizer_navigation(request): 'icon': 'group', }) - if 'organizer.giftcards:write' in request.orgapermset: + if 'organizer.giftcards:read' in request.orgapermset or 'organizer.giftcards:write' in request.orgapermset: children = [] children.append({ 'label': _('Gift cards'), @@ -575,7 +587,7 @@ def get_organizer_navigation(request): if request.organizer.settings.customer_accounts: children = [] - if 'organizer.customers:write' in request.orgapermset: + if 'organizer.customers:read' in request.orgapermset or 'organizer.customers:write' in request.orgapermset: children.append( { 'label': _('Customers'), @@ -624,16 +636,17 @@ def get_organizer_navigation(request): }) if request.organizer.settings.reusable_media_active: - nav.append({ - 'label': _('Reusable media'), - 'url': reverse('control:organizer.reusable_media', kwargs={ - 'organizer': request.organizer.slug - }), - 'icon': 'key', - 'active': 'organizer.reusable_medi' in url.url_name, - }) + if 'organizer.reusablemedia:read' in request.orgapermset or 'organizer.reusablemedia:write' in request.orgapermset: + nav.append({ + 'label': _('Reusable media'), + 'url': reverse('control:organizer.reusable_media', kwargs={ + 'organizer': request.organizer.slug + }), + 'icon': 'key', + 'active': 'organizer.reusable_medi' in url.url_name, + }) - if 'organizer.settings.general:write' in request.orgapermset: + if 'organizer.devices:read' in request.orgapermset or 'organizer.devices:write' in request.orgapermset: nav.append({ 'label': _('Devices'), 'url': reverse('control:organizer.devices', kwargs={ diff --git a/src/pretix/control/permissions.py b/src/pretix/control/permissions.py index 9a25dfc383..1e3155785c 100644 --- a/src/pretix/control/permissions.py +++ b/src/pretix/control/permissions.py @@ -53,7 +53,7 @@ def event_permission_required(permission): This view decorator rejects all requests with a 403 response which are not from users having the given permission for the event the request is associated with. """ - if permission == 'event.settings.general:write': + if permission == 'can_change_settings': # Legacy support permission = 'event.settings.general:write' diff --git a/src/pretix/control/templates/pretixcontrol/checkin/lists.html b/src/pretix/control/templates/pretixcontrol/checkin/lists.html index 0b08c1bd60..ffd1f689a5 100644 --- a/src/pretix/control/templates/pretixcontrol/checkin/lists.html +++ b/src/pretix/control/templates/pretixcontrol/checkin/lists.html @@ -68,7 +68,7 @@ class="btn btn-primary btn-lg"> {% trans "Create a new check-in list" %} {% endif %} - {% if can_change_organizer_settings %} + {% if link_device_settings %} {% trans "Connected devices" %} {% endif %} @@ -79,11 +79,11 @@ {% trans "Create a new check-in list" %} {% endif %} - {% if can_change_organizer_settings %} + {% if link_device_settings %} {% trans "Connected devices" %} {% endif %} - {% if "event.settings.general:write" in request.eventpermset %} + {% if "event.settings.general:write" in request.eventpermset and "event.orders:write" in request.eventpermset %} @@ -100,7 +100,9 @@ - {% trans "Checked in" %} + {% if "event.orders:read" in request.eventpermset %} + {% trans "Checked in" %} + {% endif %} {% if request.event.has_subevents %} {% trans "Date" context "subevent" %} @@ -119,18 +121,20 @@ {{ cl.name }} - -
- -
- - - {% trans "Cancel event" %} - +
+ {% if "event:cancel" in request.eventpermset %} + + + {% trans "Cancel event" %} + + {% else %} + {% trans "No permission" %} + {% endif %}
diff --git a/src/pretix/control/templates/pretixcontrol/event/fragment_timeline.html b/src/pretix/control/templates/pretixcontrol/event/fragment_timeline.html index 3877578097..71448e13bf 100644 --- a/src/pretix/control/templates/pretixcontrol/event/fragment_timeline.html +++ b/src/pretix/control/templates/pretixcontrol/event/fragment_timeline.html @@ -19,7 +19,7 @@ {{ e.entry.description }} - {% if e.entry.edit_url %} + {% if e.entry.edit_url and e.entry.edit_permission in request.eventpermset %}   diff --git a/src/pretix/control/templates/pretixcontrol/event/index.html b/src/pretix/control/templates/pretixcontrol/event/index.html index 78aeb4f9a4..f1fd140be9 100644 --- a/src/pretix/control/templates/pretixcontrol/event/index.html +++ b/src/pretix/control/templates/pretixcontrol/event/index.html @@ -155,22 +155,24 @@ -
-
-

- {% trans "Event logs" %} -

-
-
- -
+ {% endif %} {% endblock %} diff --git a/src/pretix/control/templates/pretixcontrol/event/invoicing.html b/src/pretix/control/templates/pretixcontrol/event/invoicing.html index 0c51a8e185..f3e843ae44 100644 --- a/src/pretix/control/templates/pretixcontrol/event/invoicing.html +++ b/src/pretix/control/templates/pretixcontrol/event/invoicing.html @@ -165,13 +165,15 @@

-
- - -
+ {% if "event.settings.invoicing:write" in request.eventpermset %} +
+ + +
+ {% endif %} {% endblock %} diff --git a/src/pretix/control/templates/pretixcontrol/event/payment.html b/src/pretix/control/templates/pretixcontrol/event/payment.html index c1746a37d2..12e89a8649 100644 --- a/src/pretix/control/templates/pretixcontrol/event/payment.html +++ b/src/pretix/control/templates/pretixcontrol/event/payment.html @@ -41,14 +41,17 @@ {% endfor %} - - - {% trans "Settings" %} - + {% if "event.settings.payment:write" in request.eventpermset %} + + + {% trans "Settings" %} + + {% endif %} {% endfor %} + {% if "event.settings.general:write" in request.eventpermset %}
@@ -58,6 +61,7 @@ + {% endif %} @@ -83,10 +87,12 @@ {% bootstrap_field form.payment_explanation layout="control" %} -
- -
+ {% if "event.settings.payment:write" in request.eventpermset %} +
+ +
+ {% endif %} {% endblock %} diff --git a/src/pretix/control/templates/pretixcontrol/event/tax.html b/src/pretix/control/templates/pretixcontrol/event/tax.html index 9a58c75405..9700836c98 100644 --- a/src/pretix/control/templates/pretixcontrol/event/tax.html +++ b/src/pretix/control/templates/pretixcontrol/event/tax.html @@ -23,8 +23,10 @@ {% endblocktrans %}

- {% trans "Create a new tax rule" %} + {% if "event.settings.tax:write" in request.eventpermset %} + {% trans "Create a new tax rule" %} + {% endif %} {% else %}
@@ -42,10 +44,14 @@ {% for tr in taxrules %} - - {{ tr.internal_name|default:tr.name }} - + {% if "event.settings.tax:write" in request.eventpermset %} + + {{ tr.internal_name|default:tr.name }} + + {% else %} + {{ tr.internal_name|default:tr.name }} + {% endif %} {% if tr.default %} @@ -53,7 +59,7 @@ {% trans "Default" %} - {% else %} + {% elif "event.settings.tax:write" in request.eventpermset %}
{% csrf_token %} @@ -83,10 +89,12 @@ {% endif %} - - + {% if "event.settings.tax:write" in request.eventpermset %} + + + {% endif %} {% endfor %} @@ -94,9 +102,11 @@ - {% trans "Create a new tax rule" %} - + {% if "event.settings.tax:write" in request.eventpermset %} + {% trans "Create a new tax rule" %} + + {% endif %} @@ -111,10 +121,12 @@ {% bootstrap_field form.tax_rounding layout="control" %} {% bootstrap_field form.display_net_prices layout="control" %} -
- -
+ {% if "event.settings.tax:write" in request.eventpermset %} +
+ +
+ {% endif %}
{% endblock %} diff --git a/src/pretix/control/templates/pretixcontrol/items/categories.html b/src/pretix/control/templates/pretixcontrol/items/categories.html index bb0fccd765..f2dbab638f 100644 --- a/src/pretix/control/templates/pretixcontrol/items/categories.html +++ b/src/pretix/control/templates/pretixcontrol/items/categories.html @@ -16,14 +16,18 @@ {% endblocktrans %}

- {% trans "Create a new category" %} + {% if 'event.items:write' in request.eventpermset %} + {% trans "Create a new category" %} + {% endif %}
{% else %} -

- {% trans "Create a new category" %} - -

+ {% if 'event.items:write' in request.eventpermset %} +

+ {% trans "Create a new category" %} + +

+ {% endif %}
{% csrf_token %}
@@ -39,7 +43,11 @@ {% for c in categories %} - {{ c.internal_name|default:c.name }} + {% if 'event.items:write' in request.eventpermset %} + {{ c.internal_name|default:c.name }} + {% else %} + {{ c.internal_name|default:c.name }} + {% endif %}
#{{ c.pk }} @@ -49,15 +57,17 @@ {{ c.get_category_type_display }} - - - - - - - - + {% if 'event.items:write' in request.eventpermset %} + + + + + + + + + {% endif %} {% endfor %} diff --git a/src/pretix/control/templates/pretixcontrol/items/discounts.html b/src/pretix/control/templates/pretixcontrol/items/discounts.html index 43a2b5e0a2..28a587a077 100644 --- a/src/pretix/control/templates/pretixcontrol/items/discounts.html +++ b/src/pretix/control/templates/pretixcontrol/items/discounts.html @@ -39,15 +39,19 @@ {% endblocktrans %}

- {% trans "Create a new discount" %} + {% if 'event.items:write' in request.eventpermset %} + {% trans "Create a new discount" %} + {% endif %}
{% else %} -

- {% trans "Create a new discount" %} - -

+ {% if 'event.items:write' in request.eventpermset %} +

+ {% trans "Create a new discount" %} + +

+ {% endif %} {% csrf_token %}
@@ -70,8 +74,12 @@ {% else %} {% endif %} - + {% if 'event.items:write' in request.eventpermset %} + {{ d.internal_name }} + {% else %} + {{ d.internal_name }} + {% endif %} {% if d.active %} {% else %} @@ -134,23 +142,25 @@ {% endif %} - - - - - - - - + {% if 'event.items:write' in request.eventpermset %} + + + + + + + + + {% endif %} {% endfor %} diff --git a/src/pretix/control/templates/pretixcontrol/items/index.html b/src/pretix/control/templates/pretixcontrol/items/index.html index a803c64953..08d39ffa5f 100644 --- a/src/pretix/control/templates/pretixcontrol/items/index.html +++ b/src/pretix/control/templates/pretixcontrol/items/index.html @@ -21,14 +21,18 @@ {% endblocktrans %}

- {% trans "Create a new product" %} + {% if 'event.items:write' in request.eventpermset %} + {% trans "Create a new product" %} + {% endif %}
{% else %} -

- {% trans "Create a new product" %} -

+ {% if 'event.items:write' in request.eventpermset %} +

+ {% trans "Create a new product" %} +

+ {% endif %} {% csrf_token %}
@@ -51,7 +55,9 @@ {{ c.internal_name|default:c.name }}{% if c.category_type != "normal" %} ({{ c.get_category_type_display }}){% endif %} - + {% if 'event.items:write' in request.eventpermset %} + + {% endif %} {% endif %} @@ -62,7 +68,11 @@ {% if not i.active %}{% endif %} - {{ i }} + {% if 'event.items:write' in request.eventpermset %} + {{ i }} + {% else %} + {{ i }} + {% endif %} {% if not i.active %}{% endif %}
@@ -158,12 +168,14 @@ {% endif %} - - - - - - + {% if 'event.items:write' in request.eventpermset %} + + + + + + + {% endif %} {% endfor %} diff --git a/src/pretix/control/templates/pretixcontrol/items/question.html b/src/pretix/control/templates/pretixcontrol/items/question.html index 3be5f25a69..35e531c0a0 100644 --- a/src/pretix/control/templates/pretixcontrol/items/question.html +++ b/src/pretix/control/templates/pretixcontrol/items/question.html @@ -7,45 +7,57 @@ {% block inside %}

{% blocktrans with name=question.question %}Question: {{ name }}{% endblocktrans %} - - - {% trans "Edit question" %} - + {% if 'event.items:write' in request.eventpermset %} + + + {% trans "Edit question" %} + + {% endif %}

-
-
-

{% trans "Filter" %}

+ {% if 'event.orders:read' in request.eventpermset %} +
+
+

{% trans "Filter" %}

+
+ +
+
+ {% bootstrap_field form.status %} +
+
+ {% bootstrap_field form.item %} +
+ {% if has_subevents %} +
+ {% bootstrap_field form.subevent %} +
+
+ {% bootstrap_field form.date_range %} +
+ {% endif %} +
+
+ +
+
-
-
-
- {% bootstrap_field form.status %} -
-
- {% bootstrap_field form.item %} -
- {% if has_subevents %} -
- {% bootstrap_field form.subevent %} -
-
- {% bootstrap_field form.date_range %} -
- {% endif %} -
-
- -
-
-
+ {% endif %}
- {% if not stats %} + {% if 'event.orders:read' not in request.eventpermset %} +
+

+ {% blocktrans trimmed %} + No permission to view answers. + {% endblocktrans %} +

+
+ {% elif not stats %}

{% blocktrans trimmed %} diff --git a/src/pretix/control/templates/pretixcontrol/items/questions.html b/src/pretix/control/templates/pretixcontrol/items/questions.html index 4b028657b3..ff5924517a 100644 --- a/src/pretix/control/templates/pretixcontrol/items/questions.html +++ b/src/pretix/control/templates/pretixcontrol/items/questions.html @@ -10,10 +10,12 @@ {% endblocktrans %}

{% csrf_token %} -

- {% trans "Create a new question" %} - -

+ {% if 'event.items:write' in request.eventpermset %} +

+ {% trans "Create a new question" %} + +

+ {% endif %}
@@ -24,7 +26,9 @@ - + {% if 'event.items:write' in request.eventpermset %} + + {% endif %} @@ -79,16 +83,22 @@ {% trans "All personalized products" %} {% endif %} - + {% if 'event.items:write' in request.eventpermset %} + + {% endif %} diff --git a/src/pretix/control/templates/pretixcontrol/items/quotas.html b/src/pretix/control/templates/pretixcontrol/items/quotas.html index c6509296fa..5e2729bfca 100644 --- a/src/pretix/control/templates/pretixcontrol/items/quotas.html +++ b/src/pretix/control/templates/pretixcontrol/items/quotas.html @@ -30,14 +30,18 @@ {% endif %}

- {% trans "Create a new quota" %} + {% if 'event.items:write' in request.eventpermset %} + {% trans "Create a new quota" %} + {% endif %} {% else %} -

- {% trans "Create a new quota" %} - -

+ {% if 'event.items:write' in request.eventpermset %} +

+ {% trans "Create a new quota" %} + +

+ {% endif %}
{% trans "Products" %}
- + {% if q.pk %} - - + {% if 'event.items:write' in request.eventpermset %} + + + {% endif %} {% else %} - + {% if 'event.settings.general:write' in request.eventpermset %} + + {% endif %} {% endif %}
@@ -91,12 +95,14 @@ {% endfor %} diff --git a/src/pretix/control/templates/pretixcontrol/order/index.html b/src/pretix/control/templates/pretixcontrol/order/index.html index dd962fa0bc..1d3fa472df 100644 --- a/src/pretix/control/templates/pretixcontrol/order/index.html +++ b/src/pretix/control/templates/pretixcontrol/order/index.html @@ -319,7 +319,7 @@ {% endif %} - {% if i.transmission_status != "inflight" %} + {% if i.transmission_status != "inflight" and "event.orders:write" in request.eventpermset %} {% csrf_token %} @@ -334,7 +334,7 @@ {% endif %} {% if not i.canceled %} - {% if i.regenerate_allowed %} + {% if i.regenerate_allowed and "event.orders:write" in request.eventpermset %} {% csrf_token %} @@ -344,7 +344,7 @@ {% endif %} - {% if not i.is_cancellation %} + {% if not i.is_cancellation and "event.orders:write" in request.eventpermset %} {% csrf_token %} diff --git a/src/pretix/control/templates/pretixcontrol/orders/refunds.html b/src/pretix/control/templates/pretixcontrol/orders/refunds.html index 94d8af5cef..b90e5b0ce9 100644 --- a/src/pretix/control/templates/pretixcontrol/orders/refunds.html +++ b/src/pretix/control/templates/pretixcontrol/orders/refunds.html @@ -100,28 +100,30 @@ {{ r.amount|money:request.event.currency }} diff --git a/src/pretix/control/templates/pretixcontrol/organizers/customer.html b/src/pretix/control/templates/pretixcontrol/organizers/customer.html index 8a86c8f680..106c7cd1ad 100644 --- a/src/pretix/control/templates/pretixcontrol/organizers/customer.html +++ b/src/pretix/control/templates/pretixcontrol/organizers/customer.html @@ -93,16 +93,18 @@ {% endif %} - + {% if "organizer.customers:write" in request.orgapermset %} + + {% endif %}
@@ -162,35 +164,39 @@
{% endfor %} - - - - - + {% if "organizer.customers:write" in request.orgapermset %} + + + + + + {% endif %}
{% if q.size == None %}Unlimited{% else %}{{ q.size }}{% endif %} {% include "pretixcontrol/items/fragment_quota_availability.html" with availability=q.cached_avail closed=q.closed %} - - - - - + {% if 'event.items:write' in request.eventpermset %} + + + + + + {% endif %}
- {% if r.state == "transit" or r.state == "created" %} - - - {% trans "Cancel" %} - - - - {% trans "Confirm as done" %} - + {% if "event.orders:write" in request.eventpermset %} + {% if r.state == "transit" or r.state == "created" %} + + + {% trans "Cancel" %} + + + + {% trans "Confirm as done" %} + {% elif r.state == "external" %} - - - {% trans "Ignore" %} - - - - {% trans "Process refund" %} - + + + {% trans "Ignore" %} + + + + {% trans "Process refund" %} + + {% endif %} {% endif %}
- - - - {% if m.testmode %} - - + title="{% trans "Edit" %}" + class="btn btn-default"> + + {% if m.testmode %} + + + + {% endif %} {% endif %}
- - - {% trans "Add membership" %} - -
+ + + {% trans "Add membership" %} + +
@@ -300,14 +306,18 @@ {% for gc in gift_cards %} - - {{ gc.secret }} - {% if gc.testmode %} - {% trans "TEST MODE" %} - {% endif %} - {% if gc.expired %} - {% trans "Expired" %} - {% endif %} + {% if "organizer.giftcards:read" in request.orgapermset %} + + {{ gc.secret }} + {% else %} + {{ gc.secret|slice:":3" }}… + {% endif %} + {% if gc.testmode %} + {% trans "TEST MODE" %} + {% endif %} + {% if gc.expired %} + {% trans "Expired" %} + {% endif %} {{ gc.issuance|date:"SHORT_DATETIME_FORMAT" }} {% if gc.expires %}{{ gc.expires|date:"SHORT_DATETIME_FORMAT" }}{% endif %} @@ -316,10 +326,12 @@

{{ gc.value|money:gc.currency }}

- - - + {% if "organizer.giftcards:read" in request.orgapermset %} + + + + {% endif %} {% endfor %} diff --git a/src/pretix/control/templates/pretixcontrol/organizers/customers.html b/src/pretix/control/templates/pretixcontrol/organizers/customers.html index 3bba675fcb..a858ea1a43 100644 --- a/src/pretix/control/templates/pretixcontrol/organizers/customers.html +++ b/src/pretix/control/templates/pretixcontrol/organizers/customers.html @@ -15,8 +15,10 @@ No customer accounts have been created yet. {% endblocktrans %}

- {% trans "Create a new customer" %} + {% if "organizer.customers:write" in request.orgapermset %} + {% trans "Create a new customer" %} + {% endif %}
{% else %}
@@ -43,10 +45,12 @@
-

- {% trans "Create a new customer" %} -

+ {% if "organizer.customers:write" in request.orgapermset %} +

+ {% trans "Create a new customer" %} +

+ {% endif %}
diff --git a/src/pretix/control/templates/pretixcontrol/organizers/devices.html b/src/pretix/control/templates/pretixcontrol/organizers/devices.html index 0274d2e4c8..7a0d96b47d 100644 --- a/src/pretix/control/templates/pretixcontrol/organizers/devices.html +++ b/src/pretix/control/templates/pretixcontrol/organizers/devices.html @@ -51,10 +51,12 @@ -

- {% trans "Connect a device" %} -

+ {% if "organizer.devices:write" in request.orgapermset %} +

+ {% trans "Connect a device" %} +

+ {% endif %} {% csrf_token %} {% for field in filter_form %} @@ -64,10 +66,12 @@
- + {% if "organizer.devices:write" in request.orgapermset %} + + {% endif %} {% for d in devices %} - + {% if "organizer.devices:write" in request.orgapermset %} + + {% endif %} @@ -158,15 +164,17 @@ {% endif %} {% endfor %}
- - + + {% trans "Device ID" %} @@ -105,12 +109,14 @@
- - + + {{ d.device_id }} - {% if not d.initialized %} - - {% trans "Connect" %} - {% endif %} - {% if not d.initialized or d.api_token %} - - {% trans "Revoke access" %} + {% if "organizer.devices:write" in request.orgapermset %} + {% if not d.initialized %} + + {% trans "Connect" %} + {% endif %} + {% if not d.initialized or d.api_token %} + + {% trans "Revoke access" %} + {% endif %} {% endif %} {% if d.initialized %} {% endif %} - + {% if "organizer.devices:write" in request.orgapermset %} + + {% endif %}
-
- -
+ {% if "organizer.devices:write" in request.orgapermset %} +
+ +
+ {% endif %} {% include "pretixcontrol/pagination.html" %} {% endif %} diff --git a/src/pretix/control/templates/pretixcontrol/organizers/gates.html b/src/pretix/control/templates/pretixcontrol/organizers/gates.html index 102cb612f7..8456652624 100644 --- a/src/pretix/control/templates/pretixcontrol/organizers/gates.html +++ b/src/pretix/control/templates/pretixcontrol/organizers/gates.html @@ -6,10 +6,12 @@

{% trans "The list below shows gates that you can use to group check-in devices." %}

- - - {% trans "Create a new gate" %} - + {% if "organizer.devices:write" in request.orgapermset %} + + + {% trans "Create a new gate" %} + + {% endif %} @@ -21,15 +23,21 @@ {% for g in gates %} {% endfor %} diff --git a/src/pretix/control/templates/pretixcontrol/organizers/giftcard.html b/src/pretix/control/templates/pretixcontrol/organizers/giftcard.html index 8a035e490c..2be1cbb22d 100644 --- a/src/pretix/control/templates/pretixcontrol/organizers/giftcard.html +++ b/src/pretix/control/templates/pretixcontrol/organizers/giftcard.html @@ -10,10 +10,12 @@ {% if card.testmode %} {% trans "TEST MODE" %} {% endif %} - - {% trans "Edit" %} - + {% if "organizer.giftcards:write" in request.orgapermset %} + + {% trans "Edit" %} + + {% endif %}
@@ -112,22 +114,24 @@ {% endfor %} -
- - - - + {% if "organizer.giftcards:write" in request.orgapermset %} + + + + + - - + + + {% endif %}
- + {% if "organizer.devices:write" in request.orgapermset %} + + {{ g.name }} + + {% else %} {{ g.name }} - + {% endif %} - - + {% if "organizer.devices:write" in request.orgapermset %} + + + {% endif %}
- - - - -
+ + + + +
diff --git a/src/pretix/control/templates/pretixcontrol/organizers/giftcards.html b/src/pretix/control/templates/pretixcontrol/organizers/giftcards.html index 186c01dc18..26de04f9d1 100644 --- a/src/pretix/control/templates/pretixcontrol/organizers/giftcards.html +++ b/src/pretix/control/templates/pretixcontrol/organizers/giftcards.html @@ -15,10 +15,11 @@ or you can manually issue gift cards. {% endblocktrans %}

- - {% trans "Manually issue a gift card" %} - + {% if "organizer.giftcards:write" in request.orgapermset %} + {% trans "Manually issue a gift card" %} + + {% endif %}
{% else %}
@@ -45,10 +46,12 @@
-

- {% trans "Manually issue a gift card" %} -

+ {% if "organizer.giftcards:write" in request.orgapermset %} +

+ {% trans "Manually issue a gift card" %} +

+ {% endif %}
diff --git a/src/pretix/control/templates/pretixcontrol/organizers/reusable_media.html b/src/pretix/control/templates/pretixcontrol/organizers/reusable_media.html index f481e01cf4..9b96fc8af7 100644 --- a/src/pretix/control/templates/pretixcontrol/organizers/reusable_media.html +++ b/src/pretix/control/templates/pretixcontrol/organizers/reusable_media.html @@ -15,8 +15,10 @@ No media have been created yet. {% endblocktrans %}

- {% trans "Create a new medium" %} + {% if "organizer.reusablemedia:write" in request.orgapermset %} + {% trans "Create a new medium" %} + {% endif %} {% else %}
@@ -40,10 +42,12 @@
-

- {% trans "Create a new medium" %} -

+ {% if "organizer.reusablemedia:write" in request.orgapermset %} +

+ {% trans "Create a new medium" %} +

+ {% endif %}
@@ -77,9 +81,13 @@ {% if m.customer %} - + {% if "organizer.customers:read" in request.orgapermset %} + + {{ m.customer }} + + {% else %} {{ m.customer }} - + {% endif %} {% endif %} {% if m.linked_orderposition %} @@ -92,8 +100,12 @@ {% if m.linked_giftcard %} - - {{ m.linked_giftcard.secret }} + {% if "organizer.giftcards:read" in request.orgapermset %} + + {{ m.linked_giftcard.secret }} + {% else %} + {{ m.linked_giftcard.secret|slice:":3" }}… + {% endif %} {% endif %} diff --git a/src/pretix/control/templates/pretixcontrol/organizers/reusable_medium.html b/src/pretix/control/templates/pretixcontrol/organizers/reusable_medium.html index 66ea2d2804..b32f5dc5bb 100644 --- a/src/pretix/control/templates/pretixcontrol/organizers/reusable_medium.html +++ b/src/pretix/control/templates/pretixcontrol/organizers/reusable_medium.html @@ -22,60 +22,69 @@
-
- {% csrf_token %} -
-
{% trans "Media type" context "reusable_media" %}
-
{{ medium.get_type_display }}
-
{% trans "Identifier" context "reusable_media" %}
-
{{ medium.identifier }}
-
{% trans "Status" %}
-
- {% if not medium.active %} - {% trans "disabled" %} - {% elif medium.is_expired %} - {% trans "expired" %} - {% else %} - {% trans "active" %} - {% endif %} -
-
{% trans "Connections" context "reusable_media" %}
-
- {% if medium.customer %} - - + {% csrf_token %} +
+
{% trans "Media type" context "reusable_media" %}
+
{{ medium.get_type_display }}
+
{% trans "Identifier" context "reusable_media" %}
+
{{ medium.identifier }}
+
{% trans "Status" %}
+
+ {% if not medium.active %} + {% trans "disabled" %} + {% elif medium.is_expired %} + {% trans "expired" %} + {% else %} + {% trans "active" %} + {% endif %} +
+
{% trans "Connections" context "reusable_media" %}
+
+ {% if medium.customer %} + + + {% if "organizer.customers:read" in request.orgapermset %} {{ medium.customer }} - + {% else %} + {{ medium.customer }} {% endif %} - {% if medium.linked_orderposition %} - - - - {{ medium.linked_orderposition.order.code }}-{{ medium.linked_orderposition.positionid }} - - {% endif %} - {% if medium.linked_giftcard %} - - - - {{ medium.linked_giftcard.secret }} - - {% endif %} -
- {% if medium.notes %} -
{% trans "Notes" %}
-
{{ medium.notes }}
+ {% endif %} -
- - + {% if medium.linked_orderposition %} + + + + {{ medium.linked_orderposition.order.code }}-{{ medium.linked_orderposition.positionid }} + + {% endif %} + {% if medium.linked_giftcard %} + + + {% if "organizer.giftcards:read" in request.orgapermset %} + + {{ medium.linked_giftcard.secret }} + + {% else %} + {{ medium.linked_giftcard.secret|slice:":3" }}… + {% endif %} + + {% endif %} +
+ {% if medium.notes %} +
{% trans "Notes" %}
+
{{ medium.notes }}
+ {% endif %} +
+ {% if "organizer.reusablemedia:write" in request.orgapermset %} + + {% endif %}
diff --git a/src/pretix/control/templates/pretixcontrol/subevents/index.html b/src/pretix/control/templates/pretixcontrol/subevents/index.html index dd5d9617aa..52ac83fdf5 100644 --- a/src/pretix/control/templates/pretixcontrol/subevents/index.html +++ b/src/pretix/control/templates/pretixcontrol/subevents/index.html @@ -13,12 +13,14 @@ {% endblocktrans %}

- - {% trans "Create a new date" context "subevent" %} - - {% trans "Create many new dates" context "subevent" %} + {% if "event.subevents:write" in request.eventpermset %} + + {% trans "Create a new date" context "subevent" %} + + {% trans "Create many new dates" context "subevent" %} + {% endif %} {% else %}
@@ -84,11 +86,13 @@
- + {% if "event.subevents:write" in request.eventpermset %} + + {% endif %} @@ -123,11 +127,11 @@ {% for s in subevents %} - + + {% endif %} {% endfor %} diff --git a/src/pretix/control/templates/pretixcontrol/waitinglist/index.html b/src/pretix/control/templates/pretixcontrol/waitinglist/index.html index c816f327f3..04d8c940c7 100644 --- a/src/pretix/control/templates/pretixcontrol/waitinglist/index.html +++ b/src/pretix/control/templates/pretixcontrol/waitinglist/index.html @@ -7,10 +7,12 @@ {% block content %}

{% trans "Waiting list" %} - - - {% trans "Settings" %} - + {% if "event.settings.general:write" in request.eventpermset %} + + + {% trans "Settings" %} + + {% endif %}

{% if not request.event.settings.waiting_list_enabled %}
@@ -259,31 +261,34 @@ {% endif %}
diff --git a/src/pretix/control/views/checkin.py b/src/pretix/control/views/checkin.py index dca7fa6dcf..6b35e18753 100644 --- a/src/pretix/control/views/checkin.py +++ b/src/pretix/control/views/checkin.py @@ -295,7 +295,7 @@ class CheckInListBulkActionView(CheckInListQueryMixin, EventPermissionRequiredMi class CheckinListList(EventPermissionRequiredMixin, PaginationMixin, ListView): model = CheckinList context_object_name = 'checkinlists' - permission = 'event.orders:read' + permission = ('event.orders:read', 'event.settings.general:write') template_name = 'pretixcontrol/checkin/lists.html' ordering = ('subevent__date_from', 'name', 'pk') @@ -317,9 +317,9 @@ class CheckinListList(EventPermissionRequiredMixin, PaginationMixin, ListView): cl.subevent.event = self.request.event # re-use same event object to make sure settings are cached ctx['checkinlists'] = clists - ctx['can_change_organizer_settings'] = self.request.user.has_organizer_permission( + ctx['link_device_settings'] = self.request.user.has_organizer_permission( self.request.organizer, - 'organizer.settings.general:write', + 'organizer.devices:read', self.request ) ctx['filter_form'] = self.filter_form @@ -578,6 +578,12 @@ class CheckInResetView(CheckInListQueryMixin, EventPermissionRequiredMixin, Asyn permission = "event.orders:write" template_name = "pretixcontrol/checkin/reset.html" + def dispatch(self, request, *args, **kwargs): + # Special case, we want two permissions to be set + if not request.user.has_event_permission(request.organizer, request.event, "event.settings.general:write", request=request): + raise PermissionDenied() + return super().dispatch(request, *args, **kwargs) + def get_error_url(self, *args): return reverse( "control:event.orders.checkinlists", diff --git a/src/pretix/control/views/event.py b/src/pretix/control/views/event.py index 29bb26232b..4ff84a45bd 100644 --- a/src/pretix/control/views/event.py +++ b/src/pretix/control/views/event.py @@ -502,7 +502,7 @@ class EventPlugins(EventSettingsViewMixin, EventPermissionRequiredMixin, Templat class PaymentProviderSettings(EventSettingsViewMixin, EventPermissionRequiredMixin, TemplateView, SingleObjectMixin): model = Event context_object_name = 'event' - permission = 'event.settings.general:write' + permission = 'event.settings.payment:write' template_name = 'pretixcontrol/event/payment_provider.html' def get_success_url(self) -> str: @@ -618,10 +618,28 @@ class EventSettingsFormView(EventPermissionRequiredMixin, DecoupleMixin, FormVie return self.render_to_response(self.get_context_data(form=form)) -class PaymentSettings(EventSettingsViewMixin, EventSettingsFormView): +class WritePermissionMixin: + def post(self, request, *args, **kwargs): + # Special case, we want to allow different access for read and write + if not request.user.has_event_permission(request.organizer, request.event, self.write_permission, + request=request): + raise PermissionDenied() + return super().post(request, *args, **kwargs) + + def get_form(self, *args, **kwargs): + form = super().get_form(*args, **kwargs) + if not self.request.user.has_event_permission( + self.request.organizer, self.request.event, self.write_permission, request=self.request): + for f in form.fields.values(): + f.disabled = True + return form + + +class PaymentSettings(WritePermissionMixin, EventSettingsViewMixin, EventSettingsFormView): template_name = 'pretixcontrol/event/payment.html' form_class = PaymentSettingsForm - permission = 'event.settings.general:write' + permission = ('event.settings.payment:write', 'event.settings.general:write') + write_permission = 'event.settings.payment:write' def get_success_url(self) -> str: return reverse('control:event.settings.payment', kwargs={ @@ -647,10 +665,11 @@ class PaymentSettings(EventSettingsViewMixin, EventSettingsFormView): return context -class TaxSettings(EventSettingsViewMixin, EventSettingsFormView): +class TaxSettings(WritePermissionMixin, EventSettingsViewMixin, EventSettingsFormView): template_name = 'pretixcontrol/event/tax.html' form_class = TaxSettingsForm - permission = 'event.settings.general:write' + permission = ('event.settings.tax:write', 'event.settings.general:write') + write_permission = 'event.settings.tax:write' def get_success_url(self) -> str: return reverse('control:event.settings.tax', kwargs={ @@ -666,11 +685,12 @@ class TaxSettings(EventSettingsViewMixin, EventSettingsFormView): return context -class InvoiceSettings(EventSettingsViewMixin, EventSettingsFormView): +class InvoiceSettings(WritePermissionMixin, EventSettingsViewMixin, EventSettingsFormView): model = Event form_class = InvoiceSettingsForm template_name = 'pretixcontrol/event/invoicing.html' - permission = 'event.settings.general:write' + permission = ('event.settings.invoicing:write', 'event.settings.general:write') + write_permission = 'event.settings.invoicing:write' def get_context_data(self, **kwargs): types = get_transmission_types() @@ -738,7 +758,7 @@ class CancelSettings(EventSettingsViewMixin, EventSettingsFormView): class InvoicePreview(EventPermissionRequiredMixin, View): - permission = 'event.settings.general:write' + permission = 'event.settings.invoicing:write' def get(self, request, *args, **kwargs): fname, ftype, fcontent = build_preview_invoice_pdf(request.event) @@ -1297,7 +1317,7 @@ class TaxCreate(EventSettingsViewMixin, EventPermissionRequiredMixin, CreateView model = TaxRule form_class = TaxRuleForm template_name = 'pretixcontrol/event/tax_edit.html' - permission = 'event.settings.general:write' + permission = 'event.settings.tax:write' context_object_name = 'taxrule' def get_success_url(self) -> str: @@ -1358,7 +1378,7 @@ class TaxUpdate(EventSettingsViewMixin, EventPermissionRequiredMixin, UpdateView model = TaxRule form_class = TaxRuleForm template_name = 'pretixcontrol/event/tax_edit.html' - permission = 'event.settings.general:write' + permission = 'event.settings.tax:write' context_object_name = 'rule' def get_object(self, queryset=None) -> TaxRule: @@ -1422,7 +1442,7 @@ class TaxUpdate(EventSettingsViewMixin, EventPermissionRequiredMixin, UpdateView class TaxDefault(EventSettingsViewMixin, EventPermissionRequiredMixin, DetailView): model = TaxRule - permission = 'event.settings.general:write' + permission = 'event.settings.tax:write' def get_object(self, queryset=None) -> TaxRule: try: @@ -1467,7 +1487,7 @@ class TaxDefault(EventSettingsViewMixin, EventPermissionRequiredMixin, DetailVie class TaxDelete(EventSettingsViewMixin, EventPermissionRequiredMixin, CompatDeleteView): model = TaxRule template_name = 'pretixcontrol/event/tax_delete.html' - permission = 'event.settings.general:write' + permission = 'event.settings.tax:write' context_object_name = 'taxrule' def get_object(self, queryset=None) -> TaxRule: diff --git a/src/pretix/control/views/item.py b/src/pretix/control/views/item.py index a74dc7da78..179b8cd095 100644 --- a/src/pretix/control/views/item.py +++ b/src/pretix/control/views/item.py @@ -664,7 +664,7 @@ class QuestionMixin: class QuestionView(EventPermissionRequiredMixin, ChartContainingView, DetailView): model = Question template_name = 'pretixcontrol/items/question.html' - permission = 'event.items:write' + permission = None template_name_field = 'question' @cached_property diff --git a/src/pretix/control/views/orders.py b/src/pretix/control/views/orders.py index 36a972d3ae..e2ca2190db 100644 --- a/src/pretix/control/views/orders.py +++ b/src/pretix/control/views/orders.py @@ -2969,7 +2969,7 @@ class RefundList(EventPermissionRequiredMixin, PaginationMixin, ListView): class EventCancel(EventPermissionRequiredMixin, AsyncAction, FormView): template_name = 'pretixcontrol/orders/cancel.html' - permission = 'event.orders:write' + permission = 'event:cancel' form_class = EventCancelForm task = cancel_event known_errortypes = ['OrderError'] diff --git a/src/pretix/control/views/organizer.py b/src/pretix/control/views/organizer.py index 9a8f066260..2a986a8862 100644 --- a/src/pretix/control/views/organizer.py +++ b/src/pretix/control/views/organizer.py @@ -1232,7 +1232,7 @@ class DeviceQueryMixin: class DeviceListView(DeviceQueryMixin, OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView): model = Device template_name = 'pretixcontrol/organizers/devices.html' - permission = 'organizer.settings.general:write' + permission = 'organizer.devices:read' context_object_name = 'devices' paginate_by = 100 @@ -1245,7 +1245,7 @@ class DeviceListView(DeviceQueryMixin, OrganizerDetailViewMixin, OrganizerPermis class DeviceCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CreateView): model = Device template_name = 'pretixcontrol/organizers/device_edit.html' - permission = 'organizer.settings.general:write' + permission = 'organizer.devices:write' form_class = DeviceForm def get_form_kwargs(self): @@ -1276,7 +1276,7 @@ class DeviceCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixi class DeviceLogView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView): template_name = 'pretixcontrol/organizers/device_logs.html' - permission = 'organizer.settings.general:write' + permission = 'organizer.devices:read' model = LogEntry context_object_name = 'logs' paginate_by = 20 @@ -1304,7 +1304,7 @@ class DeviceLogView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, class DeviceUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, UpdateView): model = Device template_name = 'pretixcontrol/organizers/device_edit.html' - permission = 'organizer.settings.general:write' + permission = 'organizer.devices:write' context_object_name = 'device' form_class = DeviceForm @@ -1347,7 +1347,7 @@ class DeviceUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixi class DeviceBulkUpdateView(DeviceQueryMixin, OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, FormView): template_name = 'pretixcontrol/organizers/device_bulk_edit.html' - permission = 'organizer.settings.general:write' + permission = 'organizer.devices:write' context_object_name = 'device' form_class = DeviceBulkEditForm @@ -1461,7 +1461,7 @@ class DeviceBulkUpdateView(DeviceQueryMixin, OrganizerDetailViewMixin, Organizer class DeviceConnectView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, DetailView): model = Device template_name = 'pretixcontrol/organizers/device_connect.html' - permission = 'organizer.settings.general:write' + permission = 'organizer.devices:write' context_object_name = 'device' def get_object(self, queryset=None): @@ -1493,7 +1493,7 @@ class DeviceConnectView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMix class DeviceRevokeView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, DetailView): model = Device template_name = 'pretixcontrol/organizers/device_revoke.html' - permission = 'organizer.settings.general:write' + permission = 'organizer.devices:write' context_object_name = 'device' def get_object(self, queryset=None): @@ -2308,7 +2308,7 @@ class RunScheduledExportView(OrganizerPermissionRequiredMixin, ExportMixin, View class GateListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView): model = Gate template_name = 'pretixcontrol/organizers/gates.html' - permission = 'organizer.settings.general:write' + permission = 'organizer.devices:read' context_object_name = 'gates' def get_queryset(self): @@ -2318,7 +2318,7 @@ class GateListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, L class GateCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CreateView): model = Gate template_name = 'pretixcontrol/organizers/gate_edit.html' - permission = 'organizer.settings.general:write' + permission = 'organizer.devices:write' form_class = GateForm def get_form_kwargs(self): @@ -2352,7 +2352,7 @@ class GateCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, class GateUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, UpdateView): model = Gate template_name = 'pretixcontrol/organizers/gate_edit.html' - permission = 'organizer.settings.general:write' + permission = 'organizer.devices:write' context_object_name = 'gate' form_class = GateForm @@ -2387,7 +2387,7 @@ class GateUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, class GateDeleteView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CompatDeleteView): model = Gate template_name = 'pretixcontrol/organizers/gate_delete.html' - permission = 'organizer.settings.general:write' + permission = 'organizer.devices:write' context_object_name = 'gate' def get_object(self, queryset=None): @@ -2997,7 +2997,7 @@ class SSOClientDeleteView(OrganizerDetailViewMixin, OrganizerPermissionRequiredM class CustomerListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, PaginationMixin, ListView): model = Customer template_name = 'pretixcontrol/organizers/customers.html' - permission = 'organizer.customers:write' + permission = 'organizer.customers:read' context_object_name = 'customers' def get_queryset(self): @@ -3018,7 +3018,7 @@ class CustomerListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixi class CustomerDetailView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, PaginationMixin, ListView): template_name = 'pretixcontrol/organizers/customer.html' - permission = 'organizer.customers:write' + permission = 'organizer.customers:read' context_object_name = 'orders' def get_queryset(self): diff --git a/src/pretix/control/views/subevents.py b/src/pretix/control/views/subevents.py index f1a08cd12e..4ecf6a8237 100644 --- a/src/pretix/control/views/subevents.py +++ b/src/pretix/control/views/subevents.py @@ -117,7 +117,7 @@ class SubEventList(EventPermissionRequiredMixin, PaginationMixin, SubEventQueryM model = SubEvent context_object_name = 'subevents' template_name = 'pretixcontrol/subevents/index.html' - permission = 'event.settings.general:write' + permission = None def get_queryset(self): return super().get_queryset(True).prefetch_related( @@ -156,7 +156,7 @@ class SubEventList(EventPermissionRequiredMixin, PaginationMixin, SubEventQueryM class SubEventDelete(EventPermissionRequiredMixin, CompatDeleteView): model = SubEvent template_name = 'pretixcontrol/subevents/delete.html' - permission = 'event.settings.general:write' + permission = 'event.subevents:write' context_object_name = 'subevents' def get_object(self, queryset=None) -> SubEvent: @@ -508,7 +508,7 @@ class SubEventEditorMixin(MetaDataEditorMixin): class SubEventUpdate(EventPermissionRequiredMixin, SubEventEditorMixin, UpdateView): model = SubEvent template_name = 'pretixcontrol/subevents/detail.html' - permission = 'event.settings.general:write' + permission = 'event.subevents:write' context_object_name = 'subevent' form_class = SubEventForm @@ -575,7 +575,7 @@ class SubEventUpdate(EventPermissionRequiredMixin, SubEventEditorMixin, UpdateVi class SubEventCreate(SubEventEditorMixin, EventPermissionRequiredMixin, CreateView): model = SubEvent template_name = 'pretixcontrol/subevents/detail.html' - permission = 'event.settings.general:write' + permission = 'event.subevents:write' context_object_name = 'subevent' form_class = SubEventForm @@ -669,7 +669,7 @@ class SubEventCreate(SubEventEditorMixin, EventPermissionRequiredMixin, CreateVi class SubEventBulkAction(SubEventQueryMixin, EventPermissionRequiredMixin, View): - permission = 'event.settings.general:write' + permission = 'event.subevents:write' @transaction.atomic def post(self, request, *args, **kwargs): @@ -740,7 +740,7 @@ class SubEventBulkAction(SubEventQueryMixin, EventPermissionRequiredMixin, View) class SubEventBulkCreate(SubEventEditorMixin, EventPermissionRequiredMixin, AsyncFormView): model = SubEvent template_name = 'pretixcontrol/subevents/bulk.html' - permission = 'event.settings.general:write' + permission = 'event.subevents:write' context_object_name = 'subevent' form_class = SubEventBulkForm itemformclass = BulkSubEventItemForm @@ -1065,7 +1065,7 @@ class SubEventBulkCreate(SubEventEditorMixin, EventPermissionRequiredMixin, Asyn class SubEventBulkEdit(SubEventQueryMixin, EventPermissionRequiredMixin, FormView): - permission = 'event.settings.general:write' + permission = 'event.subevents:write' form_class = SubEventBulkEditForm template_name = 'pretixcontrol/subevents/bulk_edit.html' context_object_name = 'subevent' @@ -1170,7 +1170,10 @@ class SubEventBulkEdit(SubEventQueryMixin, EventPermissionRequiredMixin, FormVie kwargs = {} if self.sampled_quotas is not None: - kwargs['instance'] = self.get_queryset()[0] + try: + kwargs['instance'] = self.get_queryset()[0] + except IndexError: + raise Http404("No matching dates") formsetclass = inlineformset_factory( SubEvent, Quota, diff --git a/src/pretix/control/views/typeahead.py b/src/pretix/control/views/typeahead.py index 55676efa94..73213472bf 100644 --- a/src/pretix/control/views/typeahead.py +++ b/src/pretix/control/views/typeahead.py @@ -1013,10 +1013,16 @@ def item_meta_values(request, organizer, event): }) -@organizer_permission_required(("event.orders:read", "organizer.settings.general:write")) -# This decorator is a bit of a hack since this is not technically an organizer permission, but it does the job here -- -# anyone who can see orders for any event can see the check-in log view where this is used as a filter def devices_select2(request, **kwargs): + allowed = ( + # This check is a bit of a hack since this is not technically an organizer permission, but it does the job here -- + # anyone who can see orders for any event can see the check-in log view where this is used as a filter + request.user.has_organizer_permission(request.organizer, "organizer.devices:read", request=request) or + request.user.get_events_with_permission("event.orders:read").filter(organizer=request.organizer).exists() + ) + if not allowed: + raise PermissionDenied() + query = request.GET.get('query', '') try: page = int(request.GET.get('page', '1')) @@ -1051,10 +1057,16 @@ def devices_select2(request, **kwargs): return JsonResponse(doc) -@organizer_permission_required(("event.orders:read", "event.settings.general:write", "organizer.settings.general:write")) -# This decorator is a bit of a hack since this is not technically an organizer permission, but it does the job here -- -# anyone who can see orders for any event can see the check-in log view where this is used as a filter def gate_select2(request, **kwargs): + allowed = ( + # This check is a bit of a hack since this is not technically an organizer permission, but it does the job here -- + # anyone who can see orders for any event can see the check-in log view where this is used as a filter + request.user.has_organizer_permission(request.organizer, "organizer.devices:read", request=request) or + request.user.get_events_with_permission("event.orders:read").filter(organizer=request.organizer).exists() + ) + if not allowed: + raise PermissionDenied() + query = request.GET.get('query', '') try: page = int(request.GET.get('page', '1')) diff --git a/src/pretix/helpers/permission_migration.py b/src/pretix/helpers/permission_migration.py index f421df0052..f6d88710db 100644 --- a/src/pretix/helpers/permission_migration.py +++ b/src/pretix/helpers/permission_migration.py @@ -28,8 +28,6 @@ OLD_TO_NEW_EVENT_MIGRATION = { "can_change_event_settings": [ "event.settings.general:write", "event.settings.payment:write", - "event.settings.plugins:write", - "event.settings.email.sender:write", "event.settings.tax:write", "event.settings.invoicing:write", "event.subevents:write", diff --git a/src/pretix/static/pretixbase/js/addressform.js b/src/pretix/static/pretixbase/js/addressform.js index c4298b97f3..21604f5f0a 100644 --- a/src/pretix/static/pretixbase/js/addressform.js +++ b/src/pretix/static/pretixbase/js/addressform.js @@ -33,6 +33,13 @@ $(function () { dependents[cleanName($(this).attr("name"))] = $(this) }) + const dependentsDisabled = []; + for (var k in dependents) { + if (dependents[k].prop("disabled")) { + dependentsDisabled.push(k); + } + } + if (!Object.values(dependents).some((el) => el.length)) { // No address fields found, do not create request return; @@ -101,7 +108,7 @@ $(function () { label.append('' + gettext('required') + '') } } - for (var k in dependents) dependents[k].prop("disabled", false); + for (var k in dependents) dependents[k].prop("disabled", dependentsDisabled.includes(k)); loader.hide(); } @@ -158,7 +165,7 @@ $(function () { required = false; dependent.closest(".form-group").toggle(visible).toggleClass('required', required); - dependent.prop("required", required).prop("disabled", false); + dependent.prop("required", required).prop("disabled", dependentsDisabled.includes(k)); } }).finally(function () { loader.hide(); diff --git a/src/pretix/static/pretixcontrol/js/ui/main.js b/src/pretix/static/pretixcontrol/js/ui/main.js index ed24303c2a..77f0cb2215 100644 --- a/src/pretix/static/pretixcontrol/js/ui/main.js +++ b/src/pretix/static/pretixcontrol/js/ui/main.js @@ -340,11 +340,12 @@ var form_handlers = function (el) { } el.find("input[data-checkbox-dependency]").each(function () { + var initially_disabled = $(this).prop("disabled"); var dependent = $(this), dependency = findDependency($(this).attr("data-checkbox-dependency"), this), update = function () { var enabled = dependency.prop('checked'); - dependent.prop('disabled', !enabled).closest('.form-group, .form-field-boundary').toggleClass('disabled', !enabled); + dependent.prop('disabled', !enabled || initially_disabled).closest('.form-group, .form-field-boundary').toggleClass('disabled', !enabled); if (!enabled && !dependent.is('[data-checkbox-dependency-visual]')) { dependent.prop('checked', false); dependent.trigger('change') @@ -366,11 +367,12 @@ var form_handlers = function (el) { }); el.find("div[data-display-dependency], textarea[data-display-dependency], input[data-display-dependency], select[data-display-dependency], button[data-display-dependency]").each(function () { + var initially_disabled = $(this).prop("disabled"); var dependent = $(this), dependency = findDependency($(this).attr("data-display-dependency"), this), update = function (ev) { var enabled = dependency.toArray().some(function(d) { - if (d.disabled) return false; + if (d.disabled && !initially_disabled) return false; if (d.type === 'checkbox' || d.type === 'radio') { return d.checked; } else if (d.type === 'select-one') { @@ -391,7 +393,7 @@ var form_handlers = function (el) { } var $toggling = dependent; if (dependent.is("[data-disable-dependent]")) { - $toggling.attr('disabled', !enabled).trigger("change"); + $toggling.attr('disabled', !enabled || initially_disabled).trigger("change"); } const tagName = dependent.get(0).tagName.toLowerCase() if (tagName !== "div" && tagName !== "button") { diff --git a/src/tests/api/test_events.py b/src/tests/api/test_events.py index 18f63caf35..08d999d2eb 100644 --- a/src/tests/api/test_events.py +++ b/src/tests/api/test_events.py @@ -1387,7 +1387,14 @@ def test_get_event_settings(token_client, organizer, event): @pytest.mark.django_db -def test_patch_event_settings(token_client, organizer, event): +def test_patch_event_settings(token_client, organizer, event, team): + team.all_event_permissions = False + team.limit_event_permissions = { + "event.settings.general:write": True, + "event.settings.tax:write": True, + } + team.save() + organizer.settings.imprint_url = 'https://example.org' resp = token_client.patch( '/api/v1/organizers/{}/events/{}/settings/'.format(organizer.slug, event.slug), @@ -1503,6 +1510,29 @@ def test_patch_event_settings(token_client, organizer, event): event.settings.flush() assert set(event.settings.locales) == set(locales) + resp = token_client.patch( + '/api/v1/organizers/{}/events/{}/settings/'.format(organizer.slug, event.slug), + { + 'display_net_prices': True, + }, + format='json' + ) + assert resp.status_code == 200 + event.settings.flush() + assert event.settings.display_net_prices + + resp = token_client.patch( + '/api/v1/organizers/{}/events/{}/settings/'.format(organizer.slug, event.slug), + { + 'invoice_address_asked': False, + }, + format='json' + ) + assert resp.status_code == 400 + assert resp.data == { + 'invoice_address_asked': ['Setting this field requires permission event.settings.invoicing:write'] + } + @pytest.mark.django_db def test_patch_event_settings_validation(token_client, organizer, event): diff --git a/src/tests/api/test_permissions.py b/src/tests/api/test_permissions.py index 9c73ce0881..b11e97251b 100644 --- a/src/tests/api/test_permissions.py +++ b/src/tests/api/test_permissions.py @@ -59,7 +59,7 @@ event_urls = [ ] event_permission_sub_urls = [ - ('get', 'event.settings.general:write', 'settings/', 200), + ('get', None, 'settings/', 200), ('patch', 'event.settings.general:write', 'settings/', 200), ('get', 'event.orders:read', 'revokedsecrets/', 200), ('get', 'event.orders:read', 'revokedsecrets/1/', 404), @@ -118,12 +118,15 @@ event_permission_sub_urls = [ ('delete', 'event.items:write', 'items/1/addons/1/', 404), ('get', None, 'subevents/', 200), ('get', None, 'subevents/1/', 404), + ('post', 'event.subevents:write', 'subevents/', 400), + ('patch', 'event.subevents:write', 'subevents/1/', 404), + ('put', 'event.subevents:write', 'subevents/1/', 404), ('get', None, 'taxrules/', 200), ('get', None, 'taxrules/1/', 404), - ('post', 'event.settings.general:write', 'taxrules/', 400), - ('put', 'event.settings.general:write', 'taxrules/1/', 404), - ('patch', 'event.settings.general:write', 'taxrules/1/', 404), - ('delete', 'event.settings.general:write', 'taxrules/1/', 404), + ('post', 'event.settings.tax:write', 'taxrules/', 400), + ('put', 'event.settings.tax:write', 'taxrules/1/', 404), + ('patch', 'event.settings.tax:write', 'taxrules/1/', 404), + ('delete', 'event.settings.tax:write', 'taxrules/1/', 404), ('get', 'event.settings.general:write', 'sendmail_rules/', 200), ('get', 'event.settings.general:write', 'sendmail_rules/1/', 404), ('post', 'event.settings.general:write', 'sendmail_rules/', 400), @@ -214,16 +217,16 @@ org_permission_sub_urls = [ ('put', 'organizer.settings.general:write', 'webhooks/1/', 404), ('patch', 'organizer.settings.general:write', 'webhooks/1/', 404), ('delete', 'organizer.settings.general:write', 'webhooks/1/', 404), - ('get', 'organizer.customers:write', 'customers/', 200), + ('get', 'organizer.customers:read', 'customers/', 200), ('post', 'organizer.customers:write', 'customers/', 201), - ('get', 'organizer.customers:write', 'customers/1/', 404), + ('get', 'organizer.customers:read', 'customers/1/', 404), ('patch', 'organizer.customers:write', 'customers/1/', 404), ('post', 'organizer.customers:write', 'customers/1/anonymize/', 404), ('put', 'organizer.customers:write', 'customers/1/', 404), ('delete', 'organizer.customers:write', 'customers/1/', 404), - ('get', 'organizer.customers:write', 'memberships/', 200), + ('get', 'organizer.customers:read', 'memberships/', 200), ('post', 'organizer.customers:write', 'memberships/', 400), - ('get', 'organizer.customers:write', 'memberships/1/', 404), + ('get', 'organizer.customers:read', 'memberships/1/', 404), ('patch', 'organizer.customers:write', 'memberships/1/', 404), ('put', 'organizer.customers:write', 'memberships/1/', 404), ('delete', 'organizer.customers:write', 'memberships/1/', 404), @@ -239,18 +242,18 @@ org_permission_sub_urls = [ ('patch', 'organizer.settings.general:write', 'membershiptypes/1/', 404), ('put', 'organizer.settings.general:write', 'membershiptypes/1/', 404), ('delete', 'organizer.settings.general:write', 'membershiptypes/1/', 404), - ('get', 'organizer.giftcards:write', 'giftcards/', 200), + ('get', 'organizer.giftcards:read', 'giftcards/', 200), ('post', 'organizer.giftcards:write', 'giftcards/', 400), - ('get', 'organizer.giftcards:write', 'giftcards/1/', 404), + ('get', 'organizer.giftcards:read', 'giftcards/1/', 404), ('put', 'organizer.giftcards:write', 'giftcards/1/', 404), ('patch', 'organizer.giftcards:write', 'giftcards/1/', 404), - ('get', 'organizer.giftcards:write', 'giftcards/1/transactions/', 404), - ('get', 'organizer.giftcards:write', 'giftcards/1/transactions/1/', 404), - ('get', 'organizer.settings.general:write', 'devices/', 200), - ('post', 'organizer.settings.general:write', 'devices/', 400), - ('get', 'organizer.settings.general:write', 'devices/1/', 404), - ('put', 'organizer.settings.general:write', 'devices/1/', 404), - ('patch', 'organizer.settings.general:write', 'devices/1/', 404), + ('get', 'organizer.giftcards:read', 'giftcards/1/transactions/', 404), + ('get', 'organizer.giftcards:read', 'giftcards/1/transactions/1/', 404), + ('get', 'organizer.devices:read', 'devices/', 200), + ('post', 'organizer.devices:write', 'devices/', 400), + ('get', 'organizer.devices:read', 'devices/1/', 404), + ('put', 'organizer.devices:write', 'devices/1/', 404), + ('patch', 'organizer.devices:write', 'devices/1/', 404), ('get', 'organizer.teams:write', 'teams/', 200), ('post', 'organizer.teams:write', 'teams/', 400), ('get', 'organizer.teams:write', 'teams/{team_id}/', 200), @@ -266,7 +269,11 @@ org_permission_sub_urls = [ ('get', 'organizer.teams:write', 'teams/{team_id}/tokens/0/', 404), ('delete', 'organizer.teams:write', 'teams/{team_id}/tokens/0/', 404), ('post', 'organizer.teams:write', 'teams/{team_id}/tokens/', 400), + ('get', 'organizer.reusablemedia:read', 'reusablemedia/', 200), ('get', 'organizer.reusablemedia:read', 'reusablemedia/1/', 404), + ('post', 'organizer.reusablemedia:write', 'reusablemedia/', 400), + ('patch', 'organizer.reusablemedia:write', 'reusablemedia/1/', 404), + ('put', 'organizer.reusablemedia:write', 'reusablemedia/1/', 404), ] diff --git a/src/tests/api/test_reusable_media.py b/src/tests/api/test_reusable_media.py index 2938e524a6..30614e67be 100644 --- a/src/tests/api/test_reusable_media.py +++ b/src/tests/api/test_reusable_media.py @@ -119,7 +119,39 @@ def test_medium_list(token_client, organizer, event, medium): @pytest.mark.django_db -def test_medium_detail(token_client, organizer, event, medium, giftcard, customer): +def test_medium_detail_permission_missing(token_client, organizer, event, medium, giftcard, customer, team): + team.all_organizer_permissions = False + team.limit_organizer_permissions = { + "organizer.reusablemedia:read": True, + } + team.save() + resp = token_client.get( + '/api/v1/organizers/{}/reusablemedia/{}/?expand=linked_giftcard'.format( + organizer.slug, medium.pk + ) + ) + assert resp.status_code == 403 + assert "No permission to access gift card details." in str(resp.data) + + resp = token_client.get( + '/api/v1/organizers/{}/reusablemedia/{}/?expand=customer'.format( + organizer.slug, medium.pk + ) + ) + assert resp.status_code == 403 + assert "No permission to access customer details." in str(resp.data) + + +@pytest.mark.django_db +def test_medium_detail(token_client, organizer, event, medium, giftcard, customer, team): + team.all_organizer_permissions = False + team.limit_organizer_permissions = { + "organizer.reusablemedia:read": True, + "organizer.customers:read": True, + "organizer.giftcards:read": True, + } + team.save() + res = dict(TEST_MEDIUM_RES) res["id"] = medium.pk res["created"] = medium.created.isoformat().replace('+00:00', 'Z') @@ -340,7 +372,16 @@ def test_medium_lookup_not_found(token_client, organizer, organizer2, medium): @pytest.mark.django_db -def test_medium_lookup_autocreate(token_client, organizer): +def test_medium_lookup_autocreate(token_client, organizer, team): + team.all_organizer_permissions = False + team.limit_organizer_permissions = { + "organizer.reusablemedia:read": True, + "organizer.reusablemedia:write": True, + "organizer.customers:read": True, + "organizer.giftcards:read": True, + } + team.save() + # Disabled resp = token_client.post( '/api/v1/organizers/{}/reusablemedia/lookup/'.format(organizer.slug), @@ -386,7 +427,15 @@ def test_medium_lookup_autocreate(token_client, organizer): @pytest.mark.django_db -def test_medium_autocreate_giftcard(token_client, organizer): +def test_medium_autocreate_giftcard(token_client, organizer, team): + team.all_organizer_permissions = False + team.limit_organizer_permissions = { + "organizer.reusablemedia:write": True, + "organizer.reusablemedia:read": True, + "organizer.customers:read": True, + "organizer.giftcards:read": True, + } + team.save() organizer.settings.reusable_media_type_nfc_mf0aes_autocreate_giftcard = True organizer.settings.reusable_media_type_nfc_mf0aes_autocreate_giftcard_currency = 'USD' resp = token_client.post( diff --git a/src/tests/api/test_teams.py b/src/tests/api/test_teams.py index 7792e6b1dd..96ab60c2ee 100644 --- a/src/tests/api/test_teams.py +++ b/src/tests/api/test_teams.py @@ -175,8 +175,6 @@ def test_team_update_legacy_add_perm(token_client, organizer, event, second_team assert second_team.limit_event_permissions == { "event.settings.general:write": True, "event.settings.payment:write": True, - "event.settings.plugins:write": True, - "event.settings.email.sender:write": True, "event.settings.tax:write": True, "event.settings.invoicing:write": True, "event.subevents:write": True, diff --git a/src/tests/control/test_items.py b/src/tests/control/test_items.py index 1cf62756ad..d7d37eaaac 100644 --- a/src/tests/control/test_items.py +++ b/src/tests/control/test_items.py @@ -57,9 +57,9 @@ class ItemFormTest(SoupTest): date_from=datetime.datetime(2013, 12, 26, tzinfo=datetime.timezone.utc), ) self.item1 = Item.objects.create(event=self.event1, name="Standard", default_price=0, position=1) - t = Team.objects.create(organizer=self.orga1, all_event_permissions=True) - t.members.add(self.user) - t.limit_events.add(self.event1) + self.team = Team.objects.create(organizer=self.orga1, all_event_permissions=True) + self.team.members.add(self.user) + self.team.limit_events.add(self.event1) self.client.login(email='dummy@dummy.dummy', password='dummy') @@ -270,6 +270,14 @@ class QuestionsTest(ItemFormTest): tbl = doc.select('.container-fluid table.table-bordered tbody')[0] assert tbl.select('tr')[0].select('td')[0].text.strip() == '42' + # Test permission requirement + self.team.all_event_permissions = False + self.team.limit_event_permissions = {} + self.team.save() + doc = self.get_doc('/control/event/%s/%s/questions/%s/' % (self.orga1.slug, self.event1.slug, c.id)) + assert not doc.select('.container-fluid table.table-bordered tbody') + assert doc.select('.empty-collection') + def test_set_dependency(self): with scopes_disabled(): q1 = Question.objects.create(event=self.event1, question="What country are you from?", type="C", required=True) diff --git a/src/tests/control/test_permissions.py b/src/tests/control/test_permissions.py index 84ca45a0cf..223707d524 100644 --- a/src/tests/control/test_permissions.py +++ b/src/tests/control/test_permissions.py @@ -44,7 +44,7 @@ from pretix.base.models import Event, Order, Organizer, Team, User @pytest.fixture def env(): - o = Organizer.objects.create(name='Dummy', slug='dummy') + o = Organizer.objects.create(name='Dummy', slug='dummy', plugins='pretix.plugins.banktransfer') event = Event.objects.create( organizer=o, name='Dummy', slug='dummy', date_from=now(), plugins='pretix.plugins.banktransfer' @@ -311,29 +311,29 @@ event_permission_urls = [ ("event.settings.general:write", "delete/", 200, HTTP_GET), ("event.settings.general:write", "dangerzone/", 200, HTTP_GET), ("event.settings.general:write", "settings/", 200, HTTP_GET), - ("event.settings.general:write", "settings/plugins", 200, HTTP_GET), - ("event.settings.general:write", "settings/payment", 200, HTTP_GET), + # ("event.settings.payment:write", "settings/payment", 200, HTTP_GET), GET allowed also with other permissions + ("event.settings.payment:write", "settings/payment", 200, HTTP_POST), + ("event.settings.payment:write", "settings/payment/banktransfer", 200, HTTP_GET), ("event.settings.general:write", "settings/tickets", 200, HTTP_GET), ("event.settings.general:write", "settings/email", 200, HTTP_GET), ("event.settings.general:write", "settings/email/setup", 200, HTTP_GET), ("event.settings.general:write", "settings/cancel", 200, HTTP_GET), - ("event.settings.general:write", "settings/invoice", 200, HTTP_GET), ("event.settings.general:write", "settings/widget", 200, HTTP_GET), - ("event.settings.general:write", "settings/invoice/preview", 200, HTTP_GET), - ("event.settings.general:write", "settings/tax/", 200, HTTP_GET), - ("event.settings.general:write", "settings/tax/1/", 404, HTTP_GET), - ("event.settings.general:write", "settings/tax/add", 200, HTTP_GET), - ("event.settings.general:write", "settings/tax/1/delete", 404, HTTP_GET), - ("event.settings.general:write", "settings/tax/1/default", 404, HTTP_POST), + ("event.settings.invoicing:write", "settings/invoice", 200, HTTP_GET), + ("event.settings.invoicing:write", "settings/invoice/preview", 200, HTTP_GET), + ("event.settings.tax:write", "settings/tax/", 200, HTTP_GET), + ("event.settings.tax:write", "settings/tax/1/", 404, HTTP_GET), + ("event.settings.tax:write", "settings/tax/add", 200, HTTP_GET), + ("event.settings.tax:write", "settings/tax/1/delete", 404, HTTP_GET), + ("event.settings.tax:write", "settings/tax/1/default", 404, HTTP_POST), ("event.settings.general:write", "comment/", 405, HTTP_GET), - # Lists are currently not access-controlled - # ("event.items:write", "items/", 200), + (None, "items/", 200, HTTP_GET), ("event.items:write", "items/add", 200, HTTP_GET), ("event.items:write", "items/1/up", 404, HTTP_POST), ("event.items:write", "items/1/down", 404, HTTP_POST), ("event.items:write", "items/reorder/2/", 400, HTTP_POST), ("event.items:write", "items/1/delete", 404, HTTP_GET), - # ("event.items:write", "categories/", 200), + (None, "categories/", 200, HTTP_GET), # We don't have to create categories and similar objects # for testing this, it is enough to test that a 404 error # is returned instead of a 403 one. @@ -343,29 +343,30 @@ event_permission_urls = [ ("event.items:write", "categories/2/down", 404, HTTP_POST), ("event.items:write", "categories/reorder", 400, HTTP_POST), ("event.items:write", "categories/add", 200, HTTP_GET), - # ("event.items:write", "questions/", 200, HTTP_GET), - ("event.items:write", "questions/2/", 404, HTTP_GET), + (None, "questions/", 200, HTTP_GET), + (None, "questions/2/", 404, HTTP_GET), ("event.items:write", "questions/2/delete", 404, HTTP_GET), ("event.items:write", "questions/reorder", 400, HTTP_POST), ("event.items:write", "questions/add", 200, HTTP_GET), - # ("event.items:write", "quotas/", 200, HTTP_GET), + (None, "quotas/", 200, HTTP_GET), ("event.items:write", "quotas/2/change", 404, HTTP_GET), ("event.items:write", "quotas/2/delete", 404, HTTP_GET), ("event.items:write", "quotas/add", 200, HTTP_GET), - # ("event.items:write", "discounts/", 200), - # We don't have to create categories and similar objects - # for testing this, it is enough to test that a 404 error - # is returned instead of a 403 one. + (None, "discounts/", 200, HTTP_GET), ("event.items:write", "discounts/2/", 404, HTTP_GET), ("event.items:write", "discounts/2/delete", 404, HTTP_GET), ("event.items:write", "discounts/2/up", 404, HTTP_POST), ("event.items:write", "discounts/2/down", 404, HTTP_POST), ("event.items:write", "discounts/reorder", 400, HTTP_POST), ("event.items:write", "discounts/add", 200, HTTP_GET), - ("event.settings.general:write", "subevents/", 200, HTTP_GET), - ("event.settings.general:write", "subevents/2/", 404, HTTP_GET), - ("event.settings.general:write", "subevents/2/delete", 404, HTTP_GET), - ("event.settings.general:write", "subevents/add", 200, HTTP_GET), + (None, "subevents/", 200, HTTP_GET), + ("event.subevents:write", "subevents/2/", 404, HTTP_GET), + ("event.subevents:write", "subevents/2/", 404, HTTP_POST), + ("event.subevents:write", "subevents/2/delete", 404, HTTP_GET), + ("event.subevents:write", "subevents/add", 200, HTTP_GET), + ("event.subevents:write", "subevents/bulk_add", 200, HTTP_GET), + ("event.subevents:write", "subevents/bulk_action", 302, HTTP_POST), + ("event.subevents:write", "subevents/bulk_edit", 404, HTTP_POST), ("event.orders:read", "orders/overview/", 200, HTTP_GET), ("event.orders:read", "orders/export/", 200, HTTP_GET), ("event.orders:read", "orders/export/do", 302, HTTP_POST), @@ -389,7 +390,7 @@ event_permission_urls = [ ("event.orders:write", "orders/import/", 200, HTTP_GET), ("event.orders:write", "orders/import/0ab7b081-92d3-4480-82de-2f8b056fd32f/", 404, HTTP_GET), ("event.orders:read", "orders/FOO/answer/5/", 404, HTTP_GET), - ("event.orders:write", "cancel/", 200, HTTP_GET), + ("event:cancel", "cancel/", 200, HTTP_GET), ("event.vouchers:write", "vouchers/add", 200, HTTP_GET), ("event.vouchers:write", "vouchers/bulk_add", 200, HTTP_GET), ("event.vouchers:read", "vouchers/", 200, HTTP_GET), @@ -425,6 +426,8 @@ def test_wrong_event_permission(perf_patch, client, env, perm, url, code, http_m t = Team( pk=2, organizer=env[2], all_events=True ) + if not perm: + pytest.skip() t.all_event_permissions = False t.limit_event_permissions.pop(perm, None) t.save() @@ -539,17 +542,17 @@ organizer_permission_urls = [ ("organizer.settings.general:write", "organizer/dummy/outgoingmails", 200), ("organizer.settings.general:write", "organizer/dummy/outgoingmail/1/", 404), ("organizer.settings.general:write", "organizer/dummy/outgoingmail/bulk_action", 405), - ("organizer.settings.general:write", "organizer/dummy/devices", 200), - ("organizer.settings.general:write", "organizer/dummy/devices/select2", 200), - ("organizer.settings.general:write", "organizer/dummy/device/add", 200), - ("organizer.settings.general:write", "organizer/dummy/device/1/edit", 404), - ("organizer.settings.general:write", "organizer/dummy/device/1/connect", 404), - ("organizer.settings.general:write", "organizer/dummy/device/1/revoke", 404), - ("organizer.settings.general:write", "organizer/dummy/gates", 200), - ("organizer.settings.general:write", "organizer/dummy/gates/select2", 200), - ("organizer.settings.general:write", "organizer/dummy/gate/add", 200), - ("organizer.settings.general:write", "organizer/dummy/gate/1/edit", 404), - ("organizer.settings.general:write", "organizer/dummy/gate/1/delete", 404), + ("organizer.devices:read", "organizer/dummy/devices", 200), + ("organizer.devices:read", "organizer/dummy/devices/select2", 200), + ("organizer.devices:write", "organizer/dummy/device/add", 200), + ("organizer.devices:write", "organizer/dummy/device/1/edit", 404), + ("organizer.devices:write", "organizer/dummy/device/1/connect", 404), + ("organizer.devices:write", "organizer/dummy/device/1/revoke", 404), + ("organizer.devices:read", "organizer/dummy/gates", 200), + ("organizer.devices:read", "organizer/dummy/gates/select2", 200), + ("organizer.devices:write", "organizer/dummy/gate/add", 200), + ("organizer.devices:write", "organizer/dummy/gate/1/edit", 404), + ("organizer.devices:write", "organizer/dummy/gate/1/delete", 404), ("organizer.settings.general:write", "organizer/dummy/properties", 200), ("organizer.settings.general:write", "organizer/dummy/property/add", 200), ("organizer.settings.general:write", "organizer/dummy/property/1/edit", 404), @@ -566,12 +569,12 @@ organizer_permission_urls = [ ("organizer.settings.general:write", "organizer/dummy/ssoprovider/add", 200), ("organizer.settings.general:write", "organizer/dummy/ssoprovider/1/edit", 404), ("organizer.settings.general:write", "organizer/dummy/ssoprovider/1/delete", 404), - ("organizer.customers:write", "organizer/dummy/customers", 200), + ("organizer.customers:read", "organizer/dummy/customers", 200), ("organizer.customers:write", "organizer/dummy/customer/ABC/edit", 404), ("organizer.customers:write", "organizer/dummy/customer/ABC/anonymize", 404), ("organizer.customers:write", "organizer/dummy/customer/ABC/membership/add", 404), ("organizer.customers:write", "organizer/dummy/customer/ABC/membership/1/edit", 404), - ("organizer.customers:write", "organizer/dummy/customer/ABC/", 404), + ("organizer.customers:read", "organizer/dummy/customer/ABC/", 404), ("organizer.reusablemedia:read", "organizer/dummy/reusable_media", 200), ("organizer.reusablemedia:write", "organizer/dummy/reusable_media/1/edit", 404), ("organizer.reusablemedia:read", "organizer/dummy/reusable_media/1/", 404),
- {% if "event.subevents:write" in request.eventpermset %} - - {% endif %} - + {% if "event.subevents:write" in request.eventpermset %} + + {% endif %} + {% trans "Name" %}
- {% if "event.subevents:write" in request.eventpermset %} + {% if "event.subevents:write" in request.eventpermset %} + - {% endif %} - {{ s.name }}
@@ -173,28 +177,32 @@ {% endif %}
- + {% if "event.orders:read" in request.eventpermset %} + + {% endif %} - - - + {% if "event.subevents:write" in request.eventpermset %} + + + + {% endif %}
- {% if not e.voucher %} - - + {% if 'event.orders:write' in request.eventpermset %} + {% if not e.voucher %} + + - - - + + + - - {% else %} - - - + + {% else %} + + + + {% endif %} {% endif %}