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 a0aa4f6377..8985fd11ef 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): @@ -522,7 +524,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 9a79b3ec11..d1553224da 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 @@ -323,7 +323,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 @@ -511,8 +511,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): @@ -521,6 +521,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() @@ -551,7 +554,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({ @@ -568,7 +571,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) @@ -580,7 +583,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) @@ -597,7 +600,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 @@ -714,7 +718,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 1562133f9a..ddf9245160 100644 --- a/src/pretix/base/settings.py +++ b/src/pretix/base/settings.py @@ -344,6 +344,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 " @@ -491,6 +492,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, @@ -510,15 +512,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'), ) @@ -528,6 +532,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"), ) @@ -537,6 +542,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"), ) @@ -546,6 +552,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 " @@ -557,6 +564,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, @@ -580,6 +588,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"), @@ -590,6 +599,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"), @@ -602,6 +612,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'}), @@ -612,6 +623,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, @@ -627,6 +639,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, @@ -639,6 +652,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( @@ -654,6 +668,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]), @@ -681,6 +696,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, @@ -693,6 +709,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 " @@ -704,6 +721,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 " @@ -715,6 +733,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."), @@ -726,6 +745,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."), @@ -739,6 +759,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."), @@ -749,6 +770,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 " @@ -776,6 +798,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, " @@ -799,6 +822,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."), @@ -810,6 +834,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."), @@ -820,6 +845,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', @@ -896,6 +922,7 @@ DEFAULTS = { 'type': LazyI18nString, 'form_class': I18nFormField, 'serializer_class': I18nField, + 'write_permission': 'event.settings.payment:write', 'form_kwargs': dict( widget=I18nMarkdownTextarea, widget_kwargs={'attrs': { @@ -917,6 +944,7 @@ DEFAULTS = { ('minutes', _("in minutes")) ), ), + 'write_permission': 'event.settings.payment:write', 'form_kwargs': dict( label=_("Set payment term"), widget=forms.RadioSelect, @@ -934,6 +962,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( @@ -959,6 +988,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 " @@ -976,6 +1006,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. " @@ -1000,6 +1031,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 " @@ -1012,6 +1044,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' " @@ -1024,6 +1057,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 " @@ -1046,6 +1080,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 " @@ -1057,9 +1092,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': { @@ -1068,10 +1105,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': { @@ -1079,6 +1118,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 " @@ -1108,6 +1148,7 @@ DEFAULTS = { ('none', _('Charge no taxes')), ), ), + 'write_permission': 'event.settings.payment:write', 'form_kwargs': dict( label=_("Tax handling on payment fees"), widget=forms.RadioSelect, @@ -1154,6 +1195,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, @@ -1182,6 +1224,7 @@ DEFAULTS = { ('invoice_date', _('Invoice date')), ), ), + 'write_permission': 'event.settings.invoicing:write', 'form_kwargs': dict( label=_("Date of service"), widget=forms.RadioSelect, @@ -1202,6 +1245,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 " @@ -1214,6 +1258,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 " @@ -1223,6 +1268,7 @@ DEFAULTS = { }, 'invoice_generate_sales_channels': { 'default': json.dumps(['web']), + 'write_permission': 'event.settings.invoicing:write', 'type': list }, 'invoice_generate_only_business': { @@ -1239,6 +1285,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={ @@ -1254,6 +1301,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"), @@ -1264,6 +1312,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' @@ -1277,6 +1326,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') @@ -1293,6 +1343,7 @@ DEFAULTS = { 'serializer_kwargs': { 'choices': [('', '')], }, + 'write_permission': 'event.settings.invoicing:write', 'form_kwargs': { "label": pgettext_lazy('address', 'State'), 'choices': [('', '')], @@ -1304,6 +1355,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={ @@ -1317,6 +1369,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, …"), @@ -1328,6 +1381,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, @@ -1338,6 +1392,7 @@ DEFAULTS = { 'type': LazyI18nString, 'form_class': I18nFormField, 'serializer_class': I18nField, + 'write_permission': 'event.settings.invoicing:write', 'form_kwargs': dict( widget=I18nTextarea, widget_kwargs={'attrs': { @@ -1355,6 +1410,7 @@ DEFAULTS = { 'type': LazyI18nString, 'form_class': I18nFormField, 'serializer_class': I18nField, + 'write_permission': 'event.settings.invoicing:write', 'form_kwargs': dict( widget=I18nTextarea, widget_kwargs={'attrs': { @@ -1372,6 +1428,7 @@ DEFAULTS = { 'type': LazyI18nString, 'form_class': I18nFormField, 'serializer_class': I18nField, + 'write_permission': 'event.settings.invoicing:write', 'form_kwargs': dict( widget=I18nTextarea, widget_kwargs={'attrs': { @@ -1386,6 +1443,7 @@ DEFAULTS = { }, 'invoice_language': { 'default': '__user__', + 'write_permission': 'event.settings.invoicing:write', 'type': str }, 'invoice_email_attachment': { @@ -1393,6 +1451,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 " @@ -1406,6 +1465,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 " @@ -3237,7 +3297,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 507993c167..9c2a6a5bc5 100644 --- a/src/pretix/base/signals.py +++ b/src/pretix/base/signals.py @@ -1110,6 +1110,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 2fdb28ba9e..901327c4ac 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 @@ -
{{ display_name }}
{% if pending %}diff --git a/src/pretix/control/templates/pretixcontrol/event/dangerzone.html b/src/pretix/control/templates/pretixcontrol/event/dangerzone.html index 24d7a32599..dc08123fc9 100644 --- a/src/pretix/control/templates/pretixcontrol/event/dangerzone.html +++ b/src/pretix/control/templates/pretixcontrol/event/dangerzone.html @@ -40,12 +40,16 @@ this option. {% endblocktrans %} -