Pluggable permissions (#5728)

* Data model draft

* Refactor query and assignment usages of old permissions

* Backend UI

* API serializer

* Big string replace

* Docs, tests and fixes for teams api

* Update docs for device auth

* Eliminate old names

* Make tests pass

* Use new permissions, remove inconsistencies

* Add test for translations

* Show plugin permissions

* Add permission for seating plans

* Fix plugin activation

* Fix failing test

* Refactor to permission groups

* Update doc/api/resources/devices.rst

Co-authored-by: luelista <weller@rami.io>

* Update doc/api/resources/events.rst

Co-authored-by: luelista <weller@rami.io>

* Update src/pretix/api/serializers/organizer.py

Co-authored-by: luelista <weller@rami.io>

* Fix typo

* Fix python version compat

* Replacement after rebase

* Add proper permission handling for exports

* Docs for exporters

* Runtime linting of permission names

* Fix typos

* Show export page even without orders permission

* More legacy compat

* Do not strongly validate before plugins are loaded

* Rebase migration

* Add permission for outgoing mails

* Review notes

* Update doc/api/resources/teams.rst

Co-authored-by: Richard Schreiber <schreiber@pretix.eu>

* Clean up logic around exporters

* Review and failures

* Fix migration leading to forbidden combination

* Handle permissions on event copying

* Remove print-statements

* Make test clearer

* Review feedback

* Add AnyPermissionOf

* migration safety

---------

Co-authored-by: luelista <weller@rami.io>
Co-authored-by: Richard Schreiber <schreiber@pretix.eu>
This commit is contained in:
Raphael Michel
2026-03-17 14:43:56 +01:00
committed by GitHub
parent eddde2b6c0
commit df0b580dd6
203 changed files with 5374 additions and 2331 deletions

View File

@@ -36,7 +36,9 @@ from rest_framework.permissions import SAFE_METHODS, BasePermission
from pretix.api.models import OAuthAccessToken
from pretix.base.models import Device, Event, User
from pretix.base.models.auth import SuperuserPermissionSet
from pretix.base.models.auth import (
EventPermissionSet, OrganizerPermissionSet, SuperuserPermissionSet,
)
from pretix.base.models.organizer import TeamAPIToken
from pretix.helpers.security import (
Session2FASetupRequired, SessionInvalid, SessionPasswordChangeRequired,
@@ -85,7 +87,7 @@ class EventPermission(BasePermission):
if isinstance(perm_holder, User) and perm_holder.has_active_staff_session(request.session.session_key):
request.eventpermset = SuperuserPermissionSet()
else:
request.eventpermset = perm_holder.get_event_permission_set(request.organizer, request.event)
request.eventpermset = EventPermissionSet(perm_holder.get_event_permission_set(request.organizer, request.event))
if isinstance(required_permission, (list, tuple)):
if not any(p in request.eventpermset for p in required_permission):
@@ -100,7 +102,7 @@ class EventPermission(BasePermission):
if isinstance(perm_holder, User) and perm_holder.has_active_staff_session(request.session.session_key):
request.orgapermset = SuperuserPermissionSet()
else:
request.orgapermset = perm_holder.get_organizer_permission_set(request.organizer)
request.orgapermset = OrganizerPermissionSet(perm_holder.get_organizer_permission_set(request.organizer))
if isinstance(required_permission, (list, tuple)):
if not any(p in request.eventpermset for p in required_permission):
@@ -124,12 +126,12 @@ class EventCRUDPermission(EventPermission):
def has_permission(self, request, view):
if not super(EventCRUDPermission, self).has_permission(request, view):
return False
elif view.action == 'create' and 'can_create_events' not in request.orgapermset:
elif view.action == 'create' and 'organizer.events:create' not in request.orgapermset:
return False
elif view.action == 'destroy' and 'can_change_event_settings' not in request.eventpermset:
elif view.action == 'destroy' and 'event.settings.general:write' not in request.eventpermset:
return False
elif view.action in ['update', 'partial_update'] \
and 'can_change_event_settings' not in request.eventpermset:
and 'event.settings.general:write' not in request.eventpermset:
return False
return True

View File

@@ -300,7 +300,7 @@ class EventSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
def ignored_meta_properties(self):
perm_holder = (self.context['request'].auth if isinstance(self.context['request'].auth, (Device, TeamAPIToken))
else self.context['request'].user)
if perm_holder.has_organizer_permission(self.context['request'].organizer, 'can_change_organizer_settings', request=self.context['request']):
if perm_holder.has_organizer_permission(self.context['request'].organizer, 'organizer.settings.general:write', request=self.context['request']):
return []
return [k for k, p in self.meta_properties.items() if p.protected]
@@ -445,7 +445,7 @@ class CloneEventSerializer(EventSerializer):
date_admission = validated_data.pop('date_admission', None)
new_event = super().create({**validated_data, 'plugins': None})
event = Event.objects.filter(slug=self.context['event'], organizer=self.context['organizer'].pk).first()
event = self.context['event']
new_event.copy_data_from(event, skip_meta_data='meta_data' in validated_data)
if plugins is not None:
@@ -561,7 +561,7 @@ class SubEventSerializer(I18nAwareModelSerializer):
def ignored_meta_properties(self):
perm_holder = (self.context['request'].auth if isinstance(self.context['request'].auth, (Device, TeamAPIToken))
else self.context['request'].user)
if perm_holder.has_organizer_permission(self.context['request'].organizer, 'can_change_organizer_settings', request=self.context['request']):
if perm_holder.has_organizer_permission(self.context['request'].organizer, 'organizer.settings.general:write', request=self.context['request']):
return []
return [k for k, p in self.meta_properties.items() if p.protected]
@@ -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 stored in the settings store
# should not be included!
'imprint_url',
'checkout_email_helptext',
'presale_has_ended_text',
@@ -1080,16 +1083,16 @@ class SeatSerializer(I18nAwareModelSerializer):
def prefetch_expanded_data(self, items, request, expand_fields):
if 'orderposition' in expand_fields:
if 'can_view_orders' not in request.eventpermset:
raise PermissionDenied('can_view_orders permission required for expand=orderposition')
if 'event.orders:read' not in request.eventpermset:
raise PermissionDenied('event.orders:read permission required for expand=orderposition')
prefetch_by_id(items, OrderPosition.objects.prefetch_related('order'), 'orderposition_id', 'orderposition')
if 'cartposition' in expand_fields:
if 'can_view_orders' not in request.eventpermset:
raise PermissionDenied('can_view_orders permission required for expand=cartposition')
if 'event.orders:read' not in request.eventpermset:
raise PermissionDenied('event.orders:read permission required for expand=cartposition')
prefetch_by_id(items, CartPosition.objects, 'cartposition_id', 'cartposition')
if 'voucher' in expand_fields:
if 'can_view_vouchers' not in request.eventpermset:
raise PermissionDenied('can_view_vouchers permission required for expand=voucher')
if 'event.vouchers:read' not in request.eventpermset:
raise PermissionDenied('event.vouchers:read permission required for expand=voucher')
prefetch_by_id(items, Voucher.objects, 'voucher_id', 'voucher')
def __init__(self, instance, *args, **kwargs):

View File

@@ -27,7 +27,9 @@ from rest_framework.exceptions import ValidationError
from pretix.api.serializers.forms import form_field_to_serializer_field
from pretix.base.exporter import OrganizerLevelExportMixin
from pretix.base.models import ScheduledEventExport, ScheduledOrganizerExport
from pretix.base.models import (
Event, ScheduledEventExport, ScheduledOrganizerExport,
)
from pretix.base.timeframes import SerializerDateFrameField
@@ -54,20 +56,28 @@ class ExporterSerializer(serializers.Serializer):
class JobRunSerializer(serializers.Serializer):
def __init__(self, *args, **kwargs):
ex = kwargs.pop('exporter')
events = kwargs.pop('events', None)
ex = self.ex = kwargs.pop('exporter')
super().__init__(*args, **kwargs)
if events is not None and not isinstance(ex, OrganizerLevelExportMixin):
self.fields["events"] = serializers.SlugRelatedField(
queryset=events,
if ex.is_multievent and not isinstance(ex, OrganizerLevelExportMixin):
self.fields["all_events"] = serializers.BooleanField(
required=False,
allow_empty=False,
)
self.fields["events"] = serializers.SlugRelatedField(
queryset=ex.events,
required=False,
allow_empty=True,
slug_field='slug',
many=True
)
for k, v in ex.export_form_fields.items():
self.fields[k] = form_field_to_serializer_field(v)
def to_representation(self, instance):
# Translate between events as a list of slugs (API) and list of ints (database)
if self.ex.is_multievent and not isinstance(self.ex, OrganizerLevelExportMixin) and "events" in instance and isinstance(instance["events"], list):
instance["events"] = [e.slug for e in self.ex.events.filter(pk__in=instance["events"]).only("slug")]
return instance
def to_internal_value(self, data):
if isinstance(data, QueryDict):
data = data.copy()
@@ -95,6 +105,14 @@ class JobRunSerializer(serializers.Serializer):
data[fk] = f'{d_from.isoformat() if d_from else ""}/{d_to.isoformat() if d_to else ""}'
data = super().to_internal_value(data)
# Translate between events as a list of slugs (API) and list of ints (database)
if self.ex.is_multievent and not isinstance(self.ex, OrganizerLevelExportMixin) and "events" in data and isinstance(data["events"], list):
if data["events"] and isinstance(data["events"][0], Event):
data["events"] = [e.pk for e in data["events"]]
elif data["events"] and isinstance(data["events"][0], str):
data["events"] = [e.pk for e in self.ex.events.filter(slug__in=data["events"]).only("pk")]
return data
def is_valid(self, raise_exception=False):
@@ -131,13 +149,20 @@ class ScheduledExportSerializer(serializers.ModelSerializer):
exporter = self.context['exporters'].get(identifier)
if exporter:
try:
JobRunSerializer(exporter=exporter).to_internal_value(attrs["export_form_data"])
attrs["export_form_data"] = JobRunSerializer(exporter=exporter).to_internal_value(attrs["export_form_data"])
except ValidationError as e:
raise ValidationError({"export_form_data": e.detail})
else:
raise ValidationError({"export_identifier": ["Unknown exporter."]})
return attrs
def to_representation(self, instance):
repr = super().to_representation(instance)
exporter = self.context['exporters'].get(instance.export_identifier)
if exporter:
repr["export_form_data"] = JobRunSerializer(exporter=exporter).to_representation(repr["export_form_data"])
return repr
def validate_mail_additional_recipients(self, value):
d = value.replace(' ', '')
if len(d.split(',')) > 25:

View File

@@ -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(

View File

@@ -615,7 +615,7 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
# /events/…/checkinlists/…/positions/
# We're unable to check this on this level if we're on /checkinrpc/, in which case we rely on the view
# layer to not set pdf_data=true in the first place.
request and hasattr(request, 'eventpermset') and 'can_view_orders' not in request.eventpermset
request and hasattr(request, 'eventpermset') and 'event.orders:read' not in request.eventpermset
)
if ('pdf_data' in self.context and not self.context['pdf_data']) or pdf_data_forbidden:
self.fields.pop('pdf_data', None)

View File

@@ -45,12 +45,19 @@ from pretix.base.models import (
SalesChannel, SeatingPlan, Team, TeamAPIToken, TeamInvite, User,
)
from pretix.base.models.seating import SeatingPlanLayoutValidator
from pretix.base.permissions import (
get_all_event_permission_groups, get_all_organizer_permission_groups,
)
from pretix.base.plugins import (
PLUGIN_LEVEL_EVENT, PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID,
PLUGIN_LEVEL_ORGANIZER,
)
from pretix.base.services.mail import mail
from pretix.base.settings import validate_organizer_settings
from pretix.helpers.permission_migration import (
OLD_TO_NEW_EVENT_COMPAT, OLD_TO_NEW_EVENT_MIGRATION,
OLD_TO_NEW_ORGANIZER_COMPAT, OLD_TO_NEW_ORGANIZER_MIGRATION,
)
from pretix.helpers.urls import build_absolute_uri as build_global_uri
from pretix.multidomain.urlreverse import build_absolute_uri
@@ -306,23 +313,128 @@ class EventSlugField(serializers.SlugRelatedField):
return self.context['organizer'].events.all()
class PermissionMultipleChoiceField(serializers.MultipleChoiceField):
def to_internal_value(self, data):
return {
p: True for p in super().to_internal_value(data)
}
def to_representation(self, value):
return [p for p, v in value.items() if v]
class TeamSerializer(serializers.ModelSerializer):
limit_events = EventSlugField(slug_field='slug', many=True)
limit_event_permissions = PermissionMultipleChoiceField(choices=[], required=False, allow_null=False, allow_empty=True)
limit_organizer_permissions = PermissionMultipleChoiceField(choices=[], required=False, allow_null=False, allow_empty=True)
# Legacy fields, handled in to_representation and validate
can_change_event_settings = serializers.BooleanField(required=False, write_only=True)
can_change_items = serializers.BooleanField(required=False, write_only=True)
can_view_orders = serializers.BooleanField(required=False, write_only=True)
can_change_orders = serializers.BooleanField(required=False, write_only=True)
can_checkin_orders = serializers.BooleanField(required=False, write_only=True)
can_view_vouchers = serializers.BooleanField(required=False, write_only=True)
can_change_vouchers = serializers.BooleanField(required=False, write_only=True)
can_create_events = serializers.BooleanField(required=False, write_only=True)
can_change_organizer_settings = serializers.BooleanField(required=False, write_only=True)
can_change_teams = serializers.BooleanField(required=False, write_only=True)
can_manage_gift_cards = serializers.BooleanField(required=False, write_only=True)
can_manage_customers = serializers.BooleanField(required=False, write_only=True)
can_manage_reusable_media = serializers.BooleanField(required=False, write_only=True)
class Meta:
model = Team
fields = (
'id', 'name', 'require_2fa', 'all_events', 'limit_events', 'can_create_events', 'can_change_teams',
'can_change_organizer_settings', 'can_manage_gift_cards', 'can_change_event_settings',
'can_change_items', 'can_view_orders', 'can_change_orders', 'can_view_vouchers',
'can_change_vouchers', 'can_checkin_orders', 'can_manage_customers', 'can_manage_reusable_media'
'id', 'name', 'require_2fa', 'all_events', 'limit_events', 'all_event_permissions', 'limit_event_permissions',
'all_organizer_permissions', 'limit_organizer_permissions', 'can_change_event_settings',
'can_change_items', 'can_view_orders', 'can_change_orders', 'can_checkin_orders', 'can_view_vouchers',
'can_change_vouchers', 'can_create_events', 'can_change_organizer_settings', 'can_change_teams',
'can_manage_gift_cards', 'can_manage_customers', 'can_manage_reusable_media'
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
event_perms_flattened = []
organizer_perms_flattened = []
for pg in get_all_event_permission_groups().values():
for action in pg.actions:
event_perms_flattened.append(f"{pg.name}:{action}")
for pg in get_all_organizer_permission_groups().values():
for action in pg.actions:
organizer_perms_flattened.append(f"{pg.name}:{action}")
self.fields['limit_event_permissions'].choices = [(p, p) for p in event_perms_flattened]
self.fields['limit_organizer_permissions'].choices = [(p, p) for p in organizer_perms_flattened]
def to_representation(self, instance):
r = super().to_representation(instance)
for old, new in OLD_TO_NEW_EVENT_COMPAT.items():
r[old] = instance.all_event_permissions or all(instance.limit_event_permissions.get(n) for n in new)
for old, new in OLD_TO_NEW_ORGANIZER_COMPAT.items():
r[old] = instance.all_organizer_permissions or all(instance.limit_organizer_permissions.get(n) for n in new)
return r
def validate(self, data):
old_data_set = any(k.startswith("can_") for k in data)
new_data_set = any(k in data for k in [
"all_event_permissions", "limit_event_permissions", "all_organizer_permissions", "limit_organizer_permissions"
])
if old_data_set and new_data_set:
raise ValidationError("You cannot set deprecated and current permission attributes at the same time.")
full_data = self.to_internal_value(self.to_representation(self.instance)) if self.instance else {}
full_data.update(data)
if new_data_set:
if full_data.get('limit_event_permissions') and full_data.get('all_event_permissions'):
raise ValidationError('Do not set both limit_event_permissions and all_event_permissions.')
if full_data.get('limit_organizer_permissions') and full_data.get('all_organizer_permissions'):
raise ValidationError('Do not set both limit_organizer_permissions and all_organizer_permissions.')
if old_data_set:
# Migrate with same logic as in migration 0297_pluggable_permissions
if all(full_data.get(k) is True for k in OLD_TO_NEW_EVENT_MIGRATION.keys() if k != "can_checkin_orders"):
data["all_event_permissions"] = True
data["limit_event_permissions"] = {}
else:
data["all_event_permissions"] = False
data["limit_event_permissions"] = {}
for k, v in OLD_TO_NEW_EVENT_MIGRATION.items():
if full_data.get(k) is True:
data["limit_event_permissions"].update({kk: True for kk in v})
if all(full_data.get(k) is True for k in OLD_TO_NEW_ORGANIZER_MIGRATION.keys() if k != "can_checkin_orders"):
data["all_organizer_permissions"] = True
data["limit_organizer_permissions"] = {}
else:
data["all_organizer_permissions"] = False
data["limit_organizer_permissions"] = {}
for k, v in OLD_TO_NEW_ORGANIZER_MIGRATION.items():
if full_data.get(k) is True:
data["limit_organizer_permissions"].update({kk: True for kk in v})
if full_data.get('limit_events') and full_data.get('all_events'):
raise ValidationError('Do not set both limit_events and all_events.')
full_data.update(data)
for pg in get_all_event_permission_groups().values():
requested = ",".join(sorted(
a for a in pg.actions if self.instance and full_data["limit_event_permissions"].get(f"{pg.name}:{a}")
))
if requested not in (",".join(sorted(opt.actions)) for opt in pg.options):
possible = '\' or \''.join(','.join(opt.actions) for opt in pg.options)
raise ValidationError(f"For permission group {pg.name}, the valid combinations of actions are "
f"'{possible}' but you tried to set '{requested}'.")
for pg in get_all_organizer_permission_groups().values():
requested = ",".join(sorted(
a for a in pg.actions if self.instance and full_data["limit_organizer_permissions"].get(f"{pg.name}:{a}")
))
if requested not in (",".join(sorted(opt.actions)) for opt in pg.options):
possible = '\' or \''.join(','.join(opt.actions) for opt in pg.options)
raise ValidationError(f"For permission group {pg.name}, the valid combinations of actions are "
f"'{possible}' but you tried to set '{requested}'.")
return data
@@ -339,7 +451,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:
@@ -353,6 +465,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):
@@ -437,7 +551,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 stored in the settings store
# should not be included!
'customer_accounts',
'customer_accounts_native',
'customer_accounts_link_by_email',

View File

@@ -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):

View File

@@ -52,8 +52,8 @@ class CartPositionViewSet(CreateModelMixin, DestroyModelMixin, viewsets.ReadOnly
ordering = ('datetime',)
ordering_fields = ('datetime', 'cart_id')
lookup_field = 'id'
permission = 'can_view_orders'
write_permission = 'can_change_orders'
permission = 'event.orders:read'
write_permission = 'event.orders:write'
def get_queryset(self):
return CartPosition.objects.filter(

View File

@@ -67,6 +67,7 @@ from pretix.base.models import (
Question, ReusableMedium, RevokedTicketSecret, TeamAPIToken,
)
from pretix.base.models.orders import PrintLog
from pretix.base.permissions import AnyPermissionOf
from pretix.base.services.checkin import (
CheckInError, RequiredQuestionsError, SQLLogic, perform_checkin,
)
@@ -118,11 +119,11 @@ class CheckinListViewSet(viewsets.ModelViewSet):
def _get_permission_name(self, request):
if request.path.endswith('/failed_checkins/'):
return 'can_checkin_orders', 'can_change_orders'
return 'event.orders:checkin', 'event.orders:write'
elif request.method in SAFE_METHODS:
return 'can_view_orders', 'can_checkin_orders',
return 'event.orders:read', 'event.orders:checkin',
else:
return 'can_change_event_settings'
return 'event.settings.general:write'
def get_queryset(self):
qs = self.request.event.checkin_lists.prefetch_related(
@@ -474,7 +475,7 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
'event': op.order.event,
'pdf_data': pdf_data and (
user if user and user.is_authenticated else auth
).has_event_permission(request.organizer, event, 'can_view_orders', request),
).has_event_permission(request.organizer, event, 'event.orders:read', request),
}
common_checkin_args = dict(
@@ -839,8 +840,8 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
}
filterset_class = CheckinOrderPositionFilter
permission = ('can_view_orders', 'can_checkin_orders')
write_permission = ('can_change_orders', 'can_checkin_orders')
permission = AnyPermissionOf('event.orders:read', 'event.orders:checkin')
write_permission = AnyPermissionOf('event.orders:write', 'event.orders:checkin')
def get_serializer_context(self):
ctx = super().get_serializer_context()
@@ -871,7 +872,7 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
expand=self.request.query_params.getlist('expand'),
)
if 'pk' not in self.request.resolver_match.kwargs and 'can_view_orders' not in self.request.eventpermset \
if 'pk' not in self.request.resolver_match.kwargs and 'event.orders:read' not in self.request.eventpermset \
and len(self.request.query_params.get('search', '')) < 3:
qs = qs.none()
@@ -920,9 +921,9 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
class CheckinRPCRedeemView(views.APIView):
def post(self, request, *args, **kwargs):
if isinstance(self.request.auth, (TeamAPIToken, Device)):
events = self.request.auth.get_events_with_permission(('can_change_orders', 'can_checkin_orders'))
events = self.request.auth.get_events_with_permission(('event.orders:write', 'event.orders:checkin'))
elif self.request.user.is_authenticated:
events = self.request.user.get_events_with_permission(('can_change_orders', 'can_checkin_orders'), self.request).filter(
events = self.request.user.get_events_with_permission(('event.orders:write', 'event.orders:checkin'), self.request).filter(
organizer=self.request.organizer
)
else:
@@ -990,9 +991,9 @@ class CheckinRPCSearchView(ListAPIView):
@cached_property
def lists(self):
if isinstance(self.request.auth, (TeamAPIToken, Device)):
events = self.request.auth.get_events_with_permission(('can_view_orders', 'can_checkin_orders'))
events = self.request.auth.get_events_with_permission(('event.orders:read', 'event.orders:checkin'))
elif self.request.user.is_authenticated:
events = self.request.user.get_events_with_permission(('can_view_orders', 'can_checkin_orders'), self.request).filter(
events = self.request.user.get_events_with_permission(('event.orders:read', 'event.orders:checkin'), self.request).filter(
organizer=self.request.organizer
)
else:
@@ -1009,9 +1010,9 @@ class CheckinRPCSearchView(ListAPIView):
@cached_property
def has_full_access_permission(self):
if isinstance(self.request.auth, (TeamAPIToken, Device)):
events = self.request.auth.get_events_with_permission('can_view_orders')
events = self.request.auth.get_events_with_permission('event.orders:read')
elif self.request.user.is_authenticated:
events = self.request.user.get_events_with_permission('can_view_orders', self.request).filter(
events = self.request.user.get_events_with_permission('event.orders:read', self.request).filter(
organizer=self.request.organizer
)
else:
@@ -1038,9 +1039,9 @@ class CheckinRPCSearchView(ListAPIView):
class CheckinRPCAnnulView(views.APIView):
def post(self, request, *args, **kwargs):
if isinstance(self.request.auth, (TeamAPIToken, Device)):
events = self.request.auth.get_events_with_permission(('can_change_orders', 'can_checkin_orders'))
events = self.request.auth.get_events_with_permission(('event.orders:write', 'event.orders:checkin'))
elif self.request.user.is_authenticated:
events = self.request.user.get_events_with_permission(('can_change_orders', 'can_checkin_orders'), self.request).filter(
events = self.request.user.get_events_with_permission(('event.orders:write', 'event.orders:checkin'), self.request).filter(
organizer=self.request.organizer
)
else:
@@ -1118,7 +1119,7 @@ class CheckinViewSet(viewsets.ReadOnlyModelViewSet):
filterset_class = CheckinFilter
ordering = ('created', 'id')
ordering_fields = ('created', 'datetime', 'id',)
permission = 'can_view_orders'
permission = 'event.orders:read'
def get_queryset(self):
qs = Checkin.all.filter().select_related(

View File

@@ -57,7 +57,7 @@ class DiscountViewSet(ConditionalListView, viewsets.ModelViewSet):
ordering_fields = ('id', 'position')
ordering = ('position', 'id')
permission = None
write_permission = 'can_change_items'
write_permission = 'event.items:write'
def get_queryset(self):
return self.request.event.discounts.prefetch_related(

View File

@@ -281,6 +281,11 @@ class EventViewSet(viewsets.ModelViewSet):
new_event = serializer.save(organizer=self.request.organizer)
if copy_from:
perm_holder = (self.request.auth if isinstance(self.request.auth, (Device, TeamAPIToken))
else self.request.user)
if not copy_from.allow_copy_data(self.request.organizer, perm_holder):
raise PermissionDenied("Not sufficient permission on source event to copy")
new_event.copy_data_from(copy_from, skip_meta_data='meta_data' in serializer.validated_data)
if plugins is not None:
@@ -341,15 +346,24 @@ class CloneEventViewSet(viewsets.ModelViewSet):
lookup_field = 'slug'
lookup_url_kwarg = 'event'
http_method_names = ['post']
write_permission = 'can_create_events'
write_permission = 'event.settings.general:write'
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['event'] = self.kwargs['event']
ctx['event'] = Event.objects.get(slug=self.kwargs['event'], organizer=self.request.organizer)
ctx['organizer'] = self.request.organizer
return ctx
def perform_create(self, serializer):
# Weird edge case: Requires settings permission on the event (to read) but also on the organizer (two write)
perm_holder = (self.request.auth if isinstance(self.request.auth, (Device, TeamAPIToken))
else self.request.user)
if not perm_holder.has_organizer_permission(self.request.organizer, "organizer.events:create", request=self.request):
raise PermissionDenied("No permission to create events")
if not serializer.context['event'].allow_copy_data(self.request.organizer, perm_holder):
raise PermissionDenied("Not sufficient permission on source event to copy")
serializer.save(organizer=self.request.organizer)
serializer.instance.log_action(
@@ -426,7 +440,7 @@ with scopes_disabled():
class SubEventViewSet(ConditionalListView, viewsets.ModelViewSet):
serializer_class = SubEventSerializer
queryset = SubEvent.objects.none()
write_permission = 'can_change_event_settings'
write_permission = 'event.subevents:write'
filter_backends = (DjangoFilterBackend, TotalOrderingFilter)
ordering = ('date_from',)
ordering_fields = ('id', 'date_from', 'last_modified')
@@ -546,7 +560,7 @@ class SubEventViewSet(ConditionalListView, viewsets.ModelViewSet):
class TaxRuleViewSet(ConditionalListView, viewsets.ModelViewSet):
serializer_class = TaxRuleSerializer
queryset = TaxRule.objects.none()
write_permission = 'can_change_event_settings'
write_permission = 'event.settings.tax:write'
def get_queryset(self):
return self.request.event.tax_rules.all()
@@ -589,7 +603,7 @@ class TaxRuleViewSet(ConditionalListView, viewsets.ModelViewSet):
class ItemMetaPropertiesViewSet(viewsets.ModelViewSet):
serializer_class = ItemMetaPropertiesSerializer
queryset = ItemMetaProperty.objects.none()
write_permission = 'can_change_event_settings'
write_permission = 'event.settings.general:write'
def get_queryset(self):
qs = self.request.event.item_meta_properties.all()
@@ -636,19 +650,18 @@ class ItemMetaPropertiesViewSet(viewsets.ModelViewSet):
class EventSettingsView(views.APIView):
permission = None
write_permission = 'can_change_event_settings'
write_permission = 'event.settings.general:write'
def get(self, request, *args, **kwargs):
if isinstance(request.auth, Device):
s = DeviceEventSettingsSerializer(instance=request.event.settings, event=request.event, context={
'request': request
})
elif 'can_change_event_settings' 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: {
@@ -662,7 +675,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()
@@ -674,7 +687,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)
@@ -701,7 +714,7 @@ class SeatFilter(FilterSet):
class SeatViewSet(ConditionalListView, viewsets.ModelViewSet):
serializer_class = SeatSerializer
queryset = Seat.objects.none()
write_permission = 'can_change_event_settings'
write_permission = 'event.settings.general:write'
filter_backends = (DjangoFilterBackend, )
filterset_class = SeatFilter

View File

@@ -40,12 +40,12 @@ from pretix.api.serializers.exporters import (
)
from pretix.base.exporter import OrganizerLevelExportMixin
from pretix.base.models import (
CachedFile, Device, Event, ScheduledEventExport, ScheduledOrganizerExport,
CachedFile, Device, ScheduledEventExport, ScheduledOrganizerExport,
TeamAPIToken,
)
from pretix.base.services.export import export, multiexport
from pretix.base.signals import (
register_data_exporters, register_multievent_data_exporters,
from pretix.base.models.organizer import TeamQuerySet
from pretix.base.services.export import (
export, init_event_exporters, init_organizer_exporters, multiexport,
)
from pretix.helpers.http import ChunkBasedFileResponse
@@ -111,7 +111,7 @@ class ExportersMixin:
@action(detail=True, methods=['POST'])
def run(self, *args, **kwargs):
instance = self.get_object()
serializer = JobRunSerializer(exporter=instance, data=self.request.data, **self.get_serializer_kwargs())
serializer = JobRunSerializer(exporter=instance, data=self.request.data)
serializer.is_valid(raise_exception=True)
cf = CachedFile(web_download=True)
@@ -136,27 +136,34 @@ class ExportersMixin:
class EventExportersViewSet(ExportersMixin, viewsets.ViewSet):
permission = 'can_view_orders'
def get_serializer_kwargs(self):
return {}
permission = None
@cached_property
def exporters(self):
raw_exporters = list(init_event_exporters(
event=self.request.event,
user=self.request.user if self.request.user and self.request.user.is_authenticated else None,
token=self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None,
device=self.request.auth if isinstance(self.request.auth, Device) else None,
request=self.request,
))
exporters = []
responses = register_data_exporters.send(self.request.event)
raw_exporters = [response(self.request.event, self.request.organizer) for r, response in responses if response]
raw_exporters = [
ex for ex in raw_exporters
if ex.available_for_user(self.request.user if self.request.user and self.request.user.is_authenticated else None)
]
for ex in sorted(raw_exporters, key=lambda ex: str(ex.verbose_name)):
ex._serializer = JobRunSerializer(exporter=ex)
exporters.append(ex)
return exporters
def do_export(self, cf, instance, data):
return export.apply_async(args=(self.request.event.id, str(cf.id), instance.identifier, data))
return export.apply_async(args=(
self.request.event.id,
), kwargs={
'user': self.request.user.pk if self.request.user and self.request.user.is_authenticated else None,
'token': self.request.auth.pk if isinstance(self.request.auth, TeamAPIToken) else None,
'device': self.request.auth.pk if isinstance(self.request.auth, Device) else None,
'fileid': str(cf.id),
'provider': instance.identifier,
'form_data': data,
})
class OrganizerExportersViewSet(ExportersMixin, viewsets.ViewSet):
@@ -164,47 +171,23 @@ class OrganizerExportersViewSet(ExportersMixin, viewsets.ViewSet):
@cached_property
def exporters(self):
raw_exporters = list(init_organizer_exporters(
organizer=self.request.organizer,
user=self.request.user if self.request.user and self.request.user.is_authenticated else None,
token=self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None,
device=self.request.auth if isinstance(self.request.auth, Device) else None,
request=self.request,
))
exporters = []
if isinstance(self.request.auth, (Device, TeamAPIToken)):
perm_holder = self.request.auth
else:
perm_holder = self.request.user
events = perm_holder.get_events_with_permission('can_view_orders', request=self.request).filter(
organizer=self.request.organizer
)
responses = register_multievent_data_exporters.send(self.request.organizer)
raw_exporters = [
response(Event.objects.none() if issubclass(response, OrganizerLevelExportMixin) else events, self.request.organizer)
for r, response in responses
if response
]
raw_exporters = [
ex for ex in raw_exporters
if (
not isinstance(ex, OrganizerLevelExportMixin) or
perm_holder.has_organizer_permission(self.request.organizer, ex.organizer_required_permission, self.request)
) and ex.available_for_user(self.request.user if self.request.user and self.request.user.is_authenticated else None)
]
for ex in sorted(raw_exporters, key=lambda ex: str(ex.verbose_name)):
ex._serializer = JobRunSerializer(exporter=ex, events=events)
ex._serializer = JobRunSerializer(exporter=ex)
exporters.append(ex)
return exporters
def get_serializer_kwargs(self):
if isinstance(self.request.auth, (Device, TeamAPIToken)):
perm_holder = self.request.auth
else:
perm_holder = self.request.user
return {
'events': perm_holder.get_events_with_permission('can_view_orders', request=self.request).filter(
organizer=self.request.organizer
)
}
def do_export(self, cf, instance, data):
return multiexport.apply_async(kwargs={
'organizer': self.request.organizer.id,
'user': self.request.user.id if self.request.user.is_authenticated else None,
'user': self.request.user.id if self.request.user and self.request.user.is_authenticated else None,
'token': self.request.auth.pk if isinstance(self.request.auth, TeamAPIToken) else None,
'device': self.request.auth.pk if isinstance(self.request.auth, Device) else None,
'fileid': str(cf.id),
@@ -222,11 +205,11 @@ class ScheduledExportersViewSet(viewsets.ModelViewSet):
class ScheduledEventExportViewSet(ScheduledExportersViewSet):
serializer_class = ScheduledEventExportSerializer
queryset = ScheduledEventExport.objects.none()
permission = 'can_view_orders'
permission = None
def get_queryset(self):
perm_holder = self.request.auth if isinstance(self.request.auth, (TeamAPIToken, Device)) else self.request.user
if not perm_holder.has_event_permission(self.request.organizer, self.request.event, 'can_change_event_settings',
if not perm_holder.has_event_permission(self.request.organizer, self.request.event, 'event.settings.general:write',
request=self.request):
if self.request.user.is_authenticated:
qs = self.request.event.scheduled_exports.filter(owner=self.request.user)
@@ -258,11 +241,28 @@ class ScheduledEventExportViewSet(ScheduledExportersViewSet):
@cached_property
def exporters(self):
responses = register_data_exporters.send(self.request.event)
exporters = [response(self.request.event, self.request.organizer) for r, response in responses if response]
exporters = list(init_event_exporters(
event=self.request.event,
user=self.request.user if self.request.user and self.request.user.is_authenticated else None,
token=self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None,
device=self.request.auth if isinstance(self.request.auth, Device) else None,
request=self.request,
))
return {e.identifier: e for e in exporters}
def perform_update(self, serializer):
if not self.request.user.is_authenticated or self.request.user != serializer.instance.owner:
# This is to prevent a possible privilege escalation where user A creates a scheduled export and
# user B has settings permission (= they can see the export configuration), but not enough permission
# to run the export themselves. Without this check, user B could modify the export and add themselves
# as a recipient. Thereby, user B would gain access to data they can't have.
exporter = self.exporters.get(serializer.instance.export_identifier)
if not exporter:
raise PermissionDenied("No access to exporter.")
perm_holder = self.request.auth if isinstance(self.request.auth, (TeamAPIToken, Device)) else self.request.user
if not perm_holder.has_event_permission(self.request.organizer, self.request.event, exporter.get_required_event_permission()):
raise PermissionDenied("No permission to edit exports you could not run.")
serializer.save(event=self.request.event)
serializer.instance.compute_next_run()
serializer.instance.error_counter = 0
@@ -291,7 +291,7 @@ class ScheduledOrganizerExportViewSet(ScheduledExportersViewSet):
def get_queryset(self):
perm_holder = self.request.auth if isinstance(self.request.auth, (TeamAPIToken, Device)) else self.request.user
if not perm_holder.has_organizer_permission(self.request.organizer, 'can_change_organizer_settings',
if not perm_holder.has_organizer_permission(self.request.organizer, 'organizer.settings.general:write',
request=self.request):
if self.request.user.is_authenticated:
qs = self.request.organizer.scheduled_exports.filter(owner=self.request.user)
@@ -321,26 +321,55 @@ class ScheduledOrganizerExportViewSet(ScheduledExportersViewSet):
ctx['exporters'] = self.exporters
return ctx
@cached_property
def events(self):
if isinstance(self.request.auth, (TeamAPIToken, Device)):
return self.request.auth.get_events_with_permission('can_view_orders')
elif self.request.user.is_authenticated:
return self.request.user.get_events_with_permission('can_view_orders', self.request).filter(
organizer=self.request.organizer
)
@cached_property
def exporters(self):
responses = register_multievent_data_exporters.send(self.request.organizer)
exporters = [
response(Event.objects.none() if issubclass(response, OrganizerLevelExportMixin) else self.events,
self.request.organizer)
for r, response in responses if response
]
exporters = list(init_organizer_exporters(
organizer=self.request.organizer,
user=self.request.user if self.request.user and self.request.user.is_authenticated else None,
token=self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None,
device=self.request.auth if isinstance(self.request.auth, Device) else None,
request=self.request,
))
return {e.identifier: e for e in exporters}
def perform_update(self, serializer):
if not self.request.user.is_authenticated or self.request.user != serializer.instance.owner:
# This is to prevent a possible privilege escalation where user A creates a scheduled export and
# user B has settings permission (= they can see the export configuration), but not enough permission
# to run the export themselves. Without this check, user B could modify the export and add themselves
# as a recipient. Thereby, user B would gain access to data they can't have.
exporter = self.exporters.get(serializer.instance.export_identifier)
if not exporter:
raise PermissionDenied("No access to exporter.")
perm_holder = (self.request.auth if isinstance(self.request.auth, (Device, TeamAPIToken))
else self.request.user)
if isinstance(exporter, OrganizerLevelExportMixin):
if not perm_holder.has_organizer_permission(
self.request.organizer, exporter.get_required_organizer_permission(), request=self.request,
):
raise PermissionDenied("No permission to edit exports you could not run.")
else:
if serializer.instance.export_form_data.get("all_events", False):
if isinstance(self.request.auth, Device):
if not self.request.auth.all_events:
raise PermissionDenied("No permission to edit exports you could not run.")
elif isinstance(self.request.auth, TeamAPIToken):
if not self.request.auth.team.all_events:
raise PermissionDenied("No permission to edit exports you could not run.")
elif self.request.user.is_authenticated:
if not self.request.user.teams.filter(
TeamQuerySet.event_permission_q(exporter.get_required_event_permission()),
all_events=True,
).exists():
raise PermissionDenied("No permission to edit exports you could not run.")
else:
events_selected = serializer.instance.export_form_data.get("events", [])
events_permission = set(perm_holder.get_events_with_permission(
exporter.get_required_event_permission(), request=self.request
).values_list("pk", flat=True))
if not all(e in events_permission for e in events_selected):
raise PermissionDenied("No permission to edit exports you could not run.")
serializer.save(organizer=self.request.organizer)
serializer.instance.compute_next_run()
serializer.instance.error_counter = 0

View File

@@ -99,7 +99,7 @@ class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
ordering = ('position', 'id')
filterset_class = ItemFilter
permission = None
write_permission = 'can_change_items'
write_permission = 'event.items:write'
def get_queryset(self):
return self.request.event.items.select_related('tax_rule').prefetch_related(
@@ -163,7 +163,7 @@ class ItemVariationViewSet(viewsets.ModelViewSet):
ordering_fields = ('id', 'position')
ordering = ('id',)
permission = None
write_permission = 'can_change_items'
write_permission = 'event.items:write'
@cached_property
def item(self):
@@ -234,7 +234,7 @@ class ItemBundleViewSet(viewsets.ModelViewSet):
ordering_fields = ('id',)
ordering = ('id',)
permission = None
write_permission = 'can_change_items'
write_permission = 'event.items:write'
@cached_property
def item(self):
@@ -286,7 +286,7 @@ class ItemProgramTimeViewSet(viewsets.ModelViewSet):
ordering_fields = ('id',)
ordering = ('id',)
permission = None
write_permission = 'can_change_items'
write_permission = 'event.items:write'
@cached_property
def item(self):
@@ -339,7 +339,7 @@ class ItemAddOnViewSet(viewsets.ModelViewSet):
ordering_fields = ('id', 'position')
ordering = ('id',)
permission = None
write_permission = 'can_change_items'
write_permission = 'event.items:write'
@cached_property
def item(self):
@@ -398,7 +398,7 @@ class ItemCategoryViewSet(ConditionalListView, viewsets.ModelViewSet):
ordering_fields = ('id', 'position')
ordering = ('position', 'id')
permission = None
write_permission = 'can_change_items'
write_permission = 'event.items:write'
def get_queryset(self):
return self.request.event.categories.all()
@@ -453,7 +453,7 @@ class QuestionViewSet(ConditionalListView, viewsets.ModelViewSet):
ordering_fields = ('id', 'position')
ordering = ('position', 'id')
permission = None
write_permission = 'can_change_items'
write_permission = 'event.items:write'
def get_queryset(self):
return self.request.event.questions.prefetch_related('options').all()
@@ -497,7 +497,7 @@ class QuestionOptionViewSet(viewsets.ModelViewSet):
ordering_fields = ('id', 'position')
ordering = ('position',)
permission = None
write_permission = 'can_change_items'
write_permission = 'event.items:write'
def get_queryset(self):
q = get_object_or_404(Question, pk=self.kwargs['question'], event=self.request.event)
@@ -564,7 +564,7 @@ class QuotaViewSet(ConditionalListView, viewsets.ModelViewSet):
ordering_fields = ('id', 'size')
ordering = ('id',)
permission = None
write_permission = 'can_change_items'
write_permission = 'event.items:write'
def get_queryset(self):
return self.request.event.quotas.select_related('subevent').prefetch_related('items', 'variations').all()

View File

@@ -62,8 +62,8 @@ with scopes_disabled():
class ReusableMediaViewSet(viewsets.ModelViewSet):
serializer_class = ReusableMediaSerializer
queryset = ReusableMedium.objects.none()
permission = 'can_manage_reusable_media'
write_permission = 'can_manage_reusable_media'
permission = 'organizer.reusablemedia:read'
write_permission = 'organizer.reusablemedia:write'
filter_backends = (DjangoFilterBackend, OrderingFilter)
ordering = ('-updated', '-id')
ordering_fields = ('created', 'updated', 'identifier', 'type', 'id')
@@ -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()

View File

@@ -317,7 +317,7 @@ class OrderViewSetMixin:
class OrganizerOrderViewSet(OrderViewSetMixin, viewsets.ReadOnlyModelViewSet):
def get_base_queryset(self):
perm = "can_view_orders" if self.request.method in SAFE_METHODS else "can_change_orders"
perm = "event.orders:read" if self.request.method in SAFE_METHODS else "event.orders:write"
if isinstance(self.request.auth, (TeamAPIToken, Device)):
return Order.objects.filter(
event__organizer=self.request.organizer,
@@ -338,8 +338,8 @@ class OrganizerOrderViewSet(OrderViewSetMixin, viewsets.ReadOnlyModelViewSet):
class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
permission = 'can_view_orders'
write_permission = 'can_change_orders'
permission = 'event.orders:read'
write_permission = 'event.orders:write'
def get_serializer_context(self):
ctx = super().get_serializer_context()
@@ -1072,8 +1072,6 @@ class OrderPositionViewSetMixin:
ordering = ('order__datetime', 'positionid')
ordering_fields = ('order__code', 'order__datetime', 'positionid', 'attendee_name', 'order__status',)
filterset_class = OrderPositionFilter
permission = 'can_view_orders'
write_permission = 'can_change_orders'
ordering_custom = {
'attendee_name': {
'_order': F('display_name').asc(nulls_first=True),
@@ -1169,11 +1167,13 @@ class OrderPositionViewSetMixin:
class OrganizerOrderPositionViewSet(OrderPositionViewSetMixin, viewsets.ReadOnlyModelViewSet):
serializer_class = OrganizerOrderPositionSerializer
permission = None
write_permission = None
def get_queryset(self):
qs = super().get_queryset()
perm = self.permission if self.request.method in SAFE_METHODS else self.write_permission
perm = "event.orders:read" if self.request.method in SAFE_METHODS else "event.orders:write"
if isinstance(self.request.auth, (TeamAPIToken, Device)):
auth_obj = self.request.auth
@@ -1193,6 +1193,8 @@ class OrganizerOrderPositionViewSet(OrderPositionViewSetMixin, viewsets.ReadOnly
class EventOrderPositionViewSet(OrderPositionViewSetMixin, viewsets.ModelViewSet):
serializer_class = OrderPositionSerializer
permission = 'event.orders:read'
write_permission = 'event.orders:write'
def get_serializer_context(self):
ctx = super().get_serializer_context()
@@ -1611,8 +1613,8 @@ class EventOrderPositionViewSet(OrderPositionViewSetMixin, viewsets.ModelViewSet
class PaymentViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
serializer_class = OrderPaymentSerializer
queryset = OrderPayment.objects.none()
permission = 'can_view_orders'
write_permission = 'can_change_orders'
permission = 'event.orders:read'
write_permission = 'event.orders:write'
lookup_field = 'local_id'
def get_serializer_context(self):
@@ -1784,8 +1786,8 @@ class PaymentViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
class RefundViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
serializer_class = OrderRefundSerializer
queryset = OrderRefund.objects.none()
permission = 'can_view_orders'
write_permission = 'can_change_orders'
permission = 'event.orders:read'
write_permission = 'event.orders:write'
lookup_field = 'local_id'
def get_queryset(self):
@@ -1942,13 +1944,18 @@ class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
ordering = ('nr',)
ordering_fields = ('nr', 'date')
filterset_class = InvoiceFilter
permission = 'can_view_orders'
lookup_url_kwarg = 'number'
lookup_field = 'nr'
write_permission = 'can_change_orders'
def _get_permission_name(self, request):
if 'event' in request.resolver_match.kwargs:
if request.method not in SAFE_METHODS:
return "event.orders:write"
return "event.orders:read"
return None # org-level is handled by event__in check
def get_queryset(self):
perm = "can_view_orders" if self.request.method in SAFE_METHODS else "can_change_orders"
perm = "event.orders:read" if self.request.method in SAFE_METHODS else "event.orders:write"
if getattr(self.request, 'event', None):
qs = self.request.event.invoices
elif isinstance(self.request.auth, (TeamAPIToken, Device)):
@@ -2089,8 +2096,8 @@ class RevokedSecretViewSet(viewsets.ReadOnlyModelViewSet):
ordering = ('-created',)
ordering_fields = ('created', 'secret')
filterset_class = RevokedSecretFilter
permission = 'can_view_orders'
write_permission = 'can_change_orders'
permission = 'event.orders:read'
write_permission = 'event.orders:write'
def get_queryset(self):
return RevokedTicketSecret.objects.filter(event=self.request.event)
@@ -2111,8 +2118,8 @@ class BlockedSecretViewSet(viewsets.ReadOnlyModelViewSet):
filter_backends = (DjangoFilterBackend, TotalOrderingFilter)
ordering = ('-updated', '-pk')
filterset_class = BlockedSecretFilter
permission = 'can_view_orders'
write_permission = 'can_change_orders'
permission = 'event.orders:read'
write_permission = 'event.orders:write'
def get_queryset(self):
return BlockedTicketSecret.objects.filter(event=self.request.event)
@@ -2147,7 +2154,7 @@ class TransactionViewSet(viewsets.ReadOnlyModelViewSet):
ordering = ('datetime', 'pk')
ordering_fields = ('datetime', 'created', 'id',)
filterset_class = TransactionFilter
permission = 'can_view_orders'
permission = 'event.orders:read'
def get_queryset(self):
return Transaction.objects.filter(order__event=self.request.event).select_related("order")
@@ -2164,11 +2171,11 @@ class OrganizerTransactionViewSet(TransactionViewSet):
if isinstance(self.request.auth, (TeamAPIToken, Device)):
qs = qs.filter(
order__event__in=self.request.auth.get_events_with_permission("can_view_orders"),
order__event__in=self.request.auth.get_events_with_permission("event.orders:read"),
)
elif self.request.user.is_authenticated:
qs = qs.filter(
order__event__in=self.request.user.get_events_with_permission("can_view_orders", request=self.request)
order__event__in=self.request.user.get_events_with_permission("event.orders:read", request=self.request)
)
else:
raise PermissionDenied("Unknown authentication scheme")

View File

@@ -70,7 +70,7 @@ class OrganizerViewSet(mixins.UpdateModelMixin, viewsets.ReadOnlyModelViewSet):
filter_backends = (TotalOrderingFilter,)
ordering = ('slug',)
ordering_fields = ('name', 'slug')
write_permission = "can_change_organizer_settings"
write_permission = "organizer.settings.general:write"
def get_queryset(self):
if self.request.user.is_authenticated:
@@ -154,8 +154,8 @@ class OrganizerViewSet(mixins.UpdateModelMixin, viewsets.ReadOnlyModelViewSet):
class SeatingPlanViewSet(viewsets.ModelViewSet):
serializer_class = SeatingPlanSerializer
queryset = SeatingPlan.objects.none()
permission = 'can_change_organizer_settings'
write_permission = 'can_change_organizer_settings'
permission = None
write_permission = 'organizer.seatingplans:write'
def get_queryset(self):
return self.request.organizer.seating_plans.order_by('name')
@@ -221,8 +221,8 @@ with scopes_disabled():
class GiftCardViewSet(viewsets.ModelViewSet):
serializer_class = GiftCardSerializer
queryset = GiftCard.objects.none()
permission = 'can_manage_gift_cards'
write_permission = 'can_manage_gift_cards'
permission = 'organizer.giftcards:read'
write_permission = 'organizer.giftcards:write'
filter_backends = (DjangoFilterBackend,)
filterset_class = GiftCardFilter
@@ -344,8 +344,8 @@ class GiftCardViewSet(viewsets.ModelViewSet):
class GiftCardTransactionViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = GiftCardTransactionSerializer
queryset = GiftCardTransaction.objects.none()
permission = 'can_manage_gift_cards'
write_permission = 'can_manage_gift_cards'
permission = 'organizer.giftcards:read'
write_permission = 'organizer.giftcards:write'
@cached_property
def giftcard(self):
@@ -362,8 +362,8 @@ class GiftCardTransactionViewSet(viewsets.ReadOnlyModelViewSet):
class TeamViewSet(viewsets.ModelViewSet):
serializer_class = TeamSerializer
queryset = Team.objects.none()
permission = 'can_change_teams'
write_permission = 'can_change_teams'
permission = 'organizer.teams:write'
write_permission = 'organizer.teams:write'
def get_queryset(self):
return self.request.organizer.teams.order_by('pk')
@@ -402,8 +402,8 @@ class TeamViewSet(viewsets.ModelViewSet):
class TeamMemberViewSet(DestroyModelMixin, viewsets.ReadOnlyModelViewSet):
serializer_class = TeamMemberSerializer
queryset = User.objects.none()
permission = 'can_change_teams'
write_permission = 'can_change_teams'
permission = 'organizer.teams:write'
write_permission = 'organizer.teams:write'
@cached_property
def team(self):
@@ -431,8 +431,8 @@ class TeamMemberViewSet(DestroyModelMixin, viewsets.ReadOnlyModelViewSet):
class TeamInviteViewSet(CreateModelMixin, DestroyModelMixin, viewsets.ReadOnlyModelViewSet):
serializer_class = TeamInviteSerializer
queryset = TeamInvite.objects.none()
permission = 'can_change_teams'
write_permission = 'can_change_teams'
permission = 'organizer.teams:write'
write_permission = 'organizer.teams:write'
@cached_property
def team(self):
@@ -468,8 +468,8 @@ class TeamInviteViewSet(CreateModelMixin, DestroyModelMixin, viewsets.ReadOnlyMo
class TeamAPITokenViewSet(CreateModelMixin, DestroyModelMixin, viewsets.ReadOnlyModelViewSet):
serializer_class = TeamAPITokenSerializer
queryset = TeamAPIToken.objects.none()
permission = 'can_change_teams'
write_permission = 'can_change_teams'
permission = 'organizer.teams:write'
write_permission = 'organizer.teams:write'
@cached_property
def team(self):
@@ -532,8 +532,8 @@ class DeviceViewSet(mixins.CreateModelMixin,
GenericViewSet):
serializer_class = DeviceSerializer
queryset = Device.objects.none()
permission = 'can_change_organizer_settings'
write_permission = 'can_change_organizer_settings'
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()
@@ -568,11 +571,11 @@ class DeviceViewSet(mixins.CreateModelMixin,
class OrganizerSettingsView(views.APIView):
permission = None
write_permission = 'can_change_organizer_settings'
write_permission = 'organizer.settings.general:write'
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 = 'can_manage_customers'
permission = 'organizer.customers:read'
write_permission = 'organizer.customers:write'
lookup_field = 'identifier'
filter_backends = (DjangoFilterBackend,)
filterset_class = CustomerFilter
@@ -678,7 +682,7 @@ class CustomerViewSet(viewsets.ModelViewSet):
class MembershipTypeViewSet(viewsets.ModelViewSet):
serializer_class = MembershipTypeSerializer
queryset = MembershipType.objects.none()
permission = 'can_change_organizer_settings'
permission = 'organizer.settings.general:write'
def get_queryset(self):
qs = self.request.organizer.membership_types.all()
@@ -735,7 +739,8 @@ with scopes_disabled():
class MembershipViewSet(viewsets.ModelViewSet):
serializer_class = MembershipSerializer
queryset = Membership.objects.none()
permission = 'can_manage_customers'
permission = 'organizer.customers:read'
write_permission = 'organizer.customers:write'
filter_backends = (DjangoFilterBackend,)
filterset_class = MembershipFilter
@@ -785,8 +790,8 @@ with scopes_disabled():
class SalesChannelViewSet(viewsets.ModelViewSet):
serializer_class = SalesChannelSerializer
queryset = SalesChannel.objects.none()
permission = 'can_change_organizer_settings'
write_permission = 'can_change_organizer_settings'
permission = 'organizer.settings.general:write'
write_permission = 'organizer.settings.general:write'
filter_backends = (DjangoFilterBackend,)
filterset_class = SalesChannelFilter
lookup_field = 'identifier'

View File

@@ -204,7 +204,7 @@ class ShreddersMixin:
class EventShreddersViewSet(ShreddersMixin, viewsets.ViewSet):
permission = 'can_change_orders'
permission = 'event.orders:write'
def get_serializer_kwargs(self):
return {}

View File

@@ -62,8 +62,8 @@ class VoucherViewSet(viewsets.ModelViewSet):
ordering = ('id',)
ordering_fields = ('id', 'code', 'max_usages', 'valid_until', 'value')
filterset_class = VoucherFilter
permission = 'can_view_vouchers'
write_permission = 'can_change_vouchers'
permission = 'event.vouchers:read'
write_permission = 'event.vouchers:write'
@scopes_disabled() # we have an event check here, and we can save some performance on subqueries
def get_queryset(self):

View File

@@ -51,8 +51,8 @@ class WaitingListViewSet(viewsets.ModelViewSet):
ordering = ('created', 'pk',)
ordering_fields = ('id', 'created', 'email', 'item')
filterset_class = WaitingListFilter
permission = 'can_view_orders'
write_permission = 'can_change_orders'
permission = 'event.orders:read'
write_permission = 'event.orders:write'
def get_queryset(self):
return self.request.event.waitinglistentries.all()

View File

@@ -35,8 +35,8 @@ class WebhookFilter(FilterSet):
class WebHookViewSet(viewsets.ModelViewSet):
serializer_class = WebHookSerializer
queryset = WebHook.objects.none()
permission = 'can_change_organizer_settings'
write_permission = 'can_change_organizer_settings'
permission = 'organizer.settings.general:write'
write_permission = 'organizer.settings.general:write'
filter_backends = (DjangoFilterBackend,)
filterset_class = WebhookFilter

View File

@@ -224,7 +224,7 @@ class HistoryPasswordValidator:
).delete()
def has_event_access_permission(request, permission='can_change_event_settings'):
def has_event_access_permission(request, permission='event.settings.general:write'):
return (
request.user.is_authenticated and
request.user.has_event_permission(request.organizer, request.event, permission, request=request)

View File

@@ -73,6 +73,9 @@ class BaseExporter:
self.events = Event.objects.filter(pk=event.pk)
self.timezone = event.timezone
if hasattr(self, 'organizer_required_permission'):
raise TypeError("Deprecated attribute organizer_required_permission no longer supported.")
def __str__(self):
return self.identifier
@@ -176,15 +179,30 @@ class BaseExporter:
"""
return True
@classmethod
def get_required_event_permission(cls) -> str:
"""
The permission level required to use this exporter for events. For multi-event-exports, this will be used
to limit the selection of events. Will be ignored if the ``OrganizerLevelExportMixin`` mixin is used.
The default implementation returns ``"event.orders:read"``.
"""
return 'event.orders:read'
class OrganizerLevelExportMixin:
@property
def organizer_required_permission(self) -> str:
@classmethod
def get_required_event_permission(cls):
raise TypeError("required_event_permission may not be called on OrganizerLevelExportMixin")
@classmethod
def get_required_organizer_permission(cls) -> str:
"""
The permission level required to use this exporter. Only useful for organizer-level exports,
not for event-level exports.
The permission level required to use this exporter. Must be set for organizer-level exports. Set to `None` to
allow everyone with any access to the organizer.
``get_required_event_permission`` will be ignored on this class.
"""
return 'can_view_orders'
raise NotImplementedError()
class ListExporter(BaseExporter):

View File

@@ -47,10 +47,13 @@ from ..signals import register_multievent_data_exporters
class CustomerListExporter(OrganizerLevelExportMixin, ListExporter):
identifier = 'customerlist'
verbose_name = gettext_lazy('Customer accounts')
organizer_required_permission = 'can_manage_customers'
category = pgettext_lazy('export_category', 'Customer accounts')
description = gettext_lazy('Download a spreadsheet of all currently registered customer accounts.')
@classmethod
def get_required_organizer_permission(cls) -> str:
return 'organizer.customers:write'
@property
def additional_form_fields(self):
return OrderedDict(

View File

@@ -271,7 +271,7 @@ class OrderListExporter(MultiSheetListExporter):
qs = self._date_filter(qs, form_data, rel='')
if form_data['paid_only']:
if form_data.get('paid_only'):
qs = qs.filter(status=Order.STATUS_PAID)
return qs
@@ -458,7 +458,7 @@ class OrderListExporter(MultiSheetListExporter):
).annotate(
payment_providers=Subquery(p_providers, output_field=CharField()),
).select_related('order', 'order__invoice_address', 'order__customer', 'tax_rule')
if form_data['paid_only']:
if form_data.get('paid_only'):
qs = qs.filter(order__status=Order.STATUS_PAID, canceled=False)
if form_data.get('items'):
@@ -562,7 +562,7 @@ class OrderListExporter(MultiSheetListExporter):
qs = OrderPosition.all.filter(
order__event__in=self.events,
)
if form_data['paid_only']:
if form_data.get('paid_only'):
qs = qs.filter(order__status=Order.STATUS_PAID, canceled=False)
if form_data.get('items'):
@@ -1239,11 +1239,14 @@ class QuotaListExporter(ListExporter):
class GiftcardTransactionListExporter(OrganizerLevelExportMixin, ListExporter):
identifier = 'giftcardtransactionlist'
verbose_name = gettext_lazy('Gift card transactions')
organizer_required_permission = 'can_manage_gift_cards'
category = pgettext_lazy('export_category', 'Gift cards')
description = gettext_lazy('Download a spreadsheet of all gift card transactions.')
repeatable_read = False
@classmethod
def get_required_organizer_permission(cls) -> str:
return 'organizer.giftcards:read'
@property
def additional_form_fields(self):
d = [
@@ -1346,10 +1349,13 @@ class GiftcardRedemptionListExporter(ListExporter):
class GiftcardListExporter(OrganizerLevelExportMixin, ListExporter):
identifier = 'giftcardlist'
verbose_name = gettext_lazy('Gift cards')
organizer_required_permission = 'can_manage_gift_cards'
category = pgettext_lazy('export_category', 'Gift cards')
description = gettext_lazy('Download a spreadsheet of all gift cards including their current value.')
@classmethod
def get_required_organizer_permission(cls) -> str:
return 'organizer.giftcards:read'
@property
def additional_form_fields(self):
return OrderedDict(

View File

@@ -36,6 +36,10 @@ class ReusableMediaExporter(OrganizerLevelExportMixin, ListExporter):
description = _('Download a spread sheet with the data of all reusable medias on your account.')
repeatable_read = False
@classmethod
def get_required_organizer_permission(cls) -> str:
return "organizer.reusablemedia:read"
def iterate_list(self, form_data):
media = ReusableMedium.objects.filter(
organizer=self.organizer,

View File

@@ -0,0 +1,137 @@
from django.db import migrations, models
from pretix.helpers.permission_migration import (
OLD_TO_NEW_EVENT_MIGRATION, OLD_TO_NEW_ORGANIZER_MIGRATION,
)
def migrate_teams_forward(apps, schema_editor):
Team = apps.get_model("pretixbase", "Team")
for team in Team.objects.iterator():
if all(getattr(team, k) for k in OLD_TO_NEW_EVENT_MIGRATION.keys() if k != "can_checkin_orders"):
team.all_event_permissions = True
team.limit_event_permissions = {}
else:
team.all_event_permissions = False
for k, v in OLD_TO_NEW_EVENT_MIGRATION.items():
if getattr(team, k):
team.limit_event_permissions.update({kk: True for kk in v})
# Prevent combinations that were possible previously but no longer make sense
if team.limit_event_permissions.get("event.orders:checkin") and team.limit_event_permissions.get("event.orders:write"):
team.limit_event_permissions.pop("event.orders:checkin")
if team.limit_event_permissions.get("event.orders:write") and not team.limit_event_permissions.get("event.orders:read"):
team.limit_event_permissions.pop("event.orders:write")
if team.limit_event_permissions.get("event.vouchers:write") and not team.limit_event_permissions.get("event.vouchers:read"):
team.limit_event_permissions.pop("event.vouchers:write")
if all(getattr(team, k) for k in OLD_TO_NEW_ORGANIZER_MIGRATION.keys()):
team.all_organizer_permissions = True
team.limit_organizer_permissions = {}
else:
team.all_organizer_permissions = False
for k, v in OLD_TO_NEW_ORGANIZER_MIGRATION.items():
if getattr(team, k):
team.limit_organizer_permissions.update({kk: True for kk in v})
team.save(update_fields=[
"all_event_permissions", "limit_event_permissions", "all_organizer_permissions", "limit_organizer_permissions"
])
def migrate_teams_backward(apps, schema_editor):
Team = apps.get_model("pretixbase", "Team")
for team in Team.objects.iterator():
for k, v in OLD_TO_NEW_EVENT_MIGRATION.items():
setattr(team, k, team.all_event_permissions or all(team.limit_event_permissions.get(kk) for kk in v))
for k, v in OLD_TO_NEW_ORGANIZER_MIGRATION.items():
setattr(team, k, team.all_organizer_permissions or all(team.limit_organizer_permissions.get(kk) for kk in v))
team.save()
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0297_outgoingmail"),
]
operations = [
migrations.AddField(
model_name="team",
name="all_event_permissions",
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name="team",
name="all_organizer_permissions",
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name="team",
name="limit_event_permissions",
field=models.JSONField(default=dict),
),
migrations.AddField(
model_name="team",
name="limit_organizer_permissions",
field=models.JSONField(default=dict),
),
migrations.RunPython(
migrate_teams_forward,
migrate_teams_backward,
),
migrations.RemoveField(
model_name="team",
name="can_change_event_settings",
),
migrations.RemoveField(
model_name="team",
name="can_change_items",
),
migrations.RemoveField(
model_name="team",
name="can_change_orders",
),
migrations.RemoveField(
model_name="team",
name="can_change_organizer_settings",
),
migrations.RemoveField(
model_name="team",
name="can_change_teams",
),
migrations.RemoveField(
model_name="team",
name="can_change_vouchers",
),
migrations.RemoveField(
model_name="team",
name="can_checkin_orders",
),
migrations.RemoveField(
model_name="team",
name="can_create_events",
),
migrations.RemoveField(
model_name="team",
name="can_manage_customers",
),
migrations.RemoveField(
model_name="team",
name="can_manage_gift_cards",
),
migrations.RemoveField(
model_name="team",
name="can_manage_reusable_media",
),
migrations.RemoveField(
model_name="team",
name="can_view_orders",
),
migrations.RemoveField(
model_name="team",
name="can_view_vouchers",
),
]

View File

@@ -213,6 +213,28 @@ class SuperuserPermissionSet:
return True
class EventPermissionSet(set):
def __contains__(self, item):
from pretix.base.permissions import assert_valid_event_permission
if super().__contains__(item):
return True
assert_valid_event_permission(item, allow_tuple=False)
return False
class OrganizerPermissionSet(set):
def __contains__(self, item):
from pretix.base.permissions import assert_valid_organizer_permission
if super().__contains__(item):
return True
assert_valid_organizer_permission(item, allow_tuple=False)
return False
class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
"""
This is the user model used by pretix for authentication.
@@ -473,7 +495,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
:return: set
"""
teams = self._get_teams_for_event(organizer, event)
sets = [t.permission_set() for t in teams]
sets = [t.event_permission_set() for t in teams]
if sets:
return set.union(*sets)
else:
@@ -487,7 +509,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
:return: set
"""
teams = self._get_teams_for_organizer(organizer)
sets = [t.permission_set() for t in teams]
sets = [t.organizer_permission_set() for t in teams]
if sets:
return set.union(*sets)
else:
@@ -502,7 +524,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
:param organizer: The organizer of the event
:param event: The event to check
:param perm_name: The permission, e.g. ``can_change_teams``
:param perm_name: The permission, e.g. ``event.orders:read``
:param request: The current request (optional)
:param session_key: The current session key (optional)
:return: bool
@@ -514,8 +536,8 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
if teams:
self._teamcache['e{}'.format(event.pk)] = teams
if isinstance(perm_name, (tuple, list)):
return any([any(team.has_permission(p) for team in teams) for p in perm_name])
if not perm_name or any([team.has_permission(perm_name) for team in teams]):
return any([any(team.has_event_permission(p) for team in teams) for p in perm_name])
if not perm_name or any([team.has_event_permission(perm_name) for team in teams]):
return True
return False
@@ -525,7 +547,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
to the organizer ``organizer``.
:param organizer: The organizer to check
:param perm_name: The permission, e.g. ``can_change_teams``
:param perm_name: The permission, e.g. ``organizer.events:create``
:param request: The current request (optional). Required to detect staff sessions properly.
:return: bool
"""
@@ -534,8 +556,8 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
teams = self._get_teams_for_organizer(organizer)
if teams:
if isinstance(perm_name, (tuple, list)):
return any([any(team.has_permission(p) for team in teams) for p in perm_name])
if not perm_name or any([team.has_permission(perm_name) for team in teams]):
return any([any(team.has_organizer_permission(p) for team in teams) for p in perm_name])
if not perm_name or any([team.has_organizer_permission(perm_name) for team in teams]):
return True
return False
@@ -566,14 +588,15 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
:return: Iterable of Events
"""
from .event import Event
from .organizer import TeamQuerySet
if request and self.has_active_staff_session(request.session.session_key):
return Event.objects.all()
if isinstance(permission, (tuple, list)):
q = reduce(operator.or_, [Q(**{p: True}) for p in permission])
q = reduce(operator.or_, [TeamQuerySet.event_permission_q(p) for p in permission])
else:
q = Q(**{permission: True})
q = TeamQuerySet.event_permission_q(permission)
return Event.objects.filter(
Q(organizer_id__in=self.teams.filter(q, all_events=True).values_list('organizer', flat=True))
@@ -606,14 +629,13 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
:return: Iterable of Organizers
"""
from .event import Organizer
from .organizer import TeamQuerySet
if request and self.has_active_staff_session(request.session.session_key):
return Organizer.objects.all()
kwargs = {permission: True}
return Organizer.objects.filter(
id__in=self.teams.filter(**kwargs).values_list('organizer', flat=True)
id__in=self.teams.filter(TeamQuerySet.organizer_permission_q(permission)).values_list('organizer', flat=True)
)
def has_active_staff_session(self, session_key=None):

View File

@@ -29,6 +29,9 @@ from django.utils.translation import gettext_lazy as _
from django_scopes import ScopedManager, scopes_disabled
from pretix.base.models import LoggedModel
from pretix.base.permissions import (
AnyPermissionOf, assert_valid_event_permission,
)
@scopes_disabled()
@@ -189,13 +192,19 @@ class Device(LoggedModel):
kwargs['update_fields'] = {'device_id'}.union(kwargs['update_fields'])
super().save(*args, **kwargs)
def permission_set(self) -> set:
def _event_permission_set(self) -> set:
return {
'can_view_orders',
'can_change_orders',
'can_view_vouchers',
'can_manage_gift_cards',
'can_manage_reusable_media',
'event.orders:read',
'event.orders:write',
'event.vouchers:read',
}
def _organizer_permission_set(self) -> set:
return {
'organizer.giftcards:read',
'organizer.giftcards:write',
'organizer.reusablemedia:read',
'organizer.reusablemedia:write',
}
def get_event_permission_set(self, organizer, event) -> set:
@@ -209,7 +218,7 @@ class Device(LoggedModel):
has_event_access = (self.all_events and organizer == self.organizer) or (
event in self.limit_events.all()
)
return self.permission_set() if has_event_access else set()
return self._event_permission_set() if has_event_access else set()
def get_organizer_permission_set(self, organizer) -> set:
"""
@@ -218,7 +227,7 @@ class Device(LoggedModel):
:param organizer: The organizer of the event
:return: set of permissions
"""
return self.permission_set() if self.organizer == organizer else set()
return self._organizer_permission_set() if self.organizer == organizer else set()
def has_event_permission(self, organizer, event, perm_name=None, request=None) -> bool:
"""
@@ -227,7 +236,7 @@ class Device(LoggedModel):
:param organizer: The organizer of the event
:param event: The event to check
:param perm_name: The permission, e.g. ``can_change_teams``
:param perm_name: The permission, e.g. ``event.orders:read``
:param request: This parameter is ignored and only defined for compatibility reasons.
:return: bool
"""
@@ -235,8 +244,8 @@ class Device(LoggedModel):
event in self.limit_events.all()
)
if isinstance(perm_name, (tuple, list)):
return has_event_access and any(p in self.permission_set() for p in perm_name)
return has_event_access and (not perm_name or perm_name in self.permission_set())
return has_event_access and any(p in self._event_permission_set() for p in perm_name)
return has_event_access and (not perm_name or perm_name in self._event_permission_set())
def has_organizer_permission(self, organizer, perm_name=None, request=None):
"""
@@ -244,13 +253,13 @@ class Device(LoggedModel):
to the organizer ``organizer``.
:param organizer: The organizer to check
:param perm_name: The permission, e.g. ``can_change_teams``
:param perm_name: The permission, e.g. ``organizer.events:create``
:param request: This parameter is ignored and only defined for compatibility reasons.
:return: bool
"""
if isinstance(perm_name, (tuple, list)):
return organizer == self.organizer and any(p in self.permission_set() for p in perm_name)
return organizer == self.organizer and (not perm_name or perm_name in self.permission_set())
return organizer == self.organizer and any(p in self._organizer_permission_set() for p in perm_name)
return organizer == self.organizer and (not perm_name or perm_name in self._organizer_permission_set())
def get_events_with_any_permission(self):
"""
@@ -270,9 +279,10 @@ class Device(LoggedModel):
:param request: Ignored, for compatibility with User model
:return: Iterable of Events
"""
assert_valid_event_permission(permission)
if (
isinstance(permission, (list, tuple)) and any(p in self.permission_set() for p in permission)
) or (isinstance(permission, str) and permission in self.permission_set()):
isinstance(permission, (AnyPermissionOf, list, tuple)) and any(p in self._event_permission_set() for p in permission)
) or (isinstance(permission, str) and permission in self._event_permission_set()):
return self.get_events_with_any_permission()
else:
return self.organizer.events.none()

View File

@@ -843,6 +843,33 @@ class Event(EventMixin, LoggedModel):
time(hour=23, minute=59, second=59)
), tz)
def allow_copy_data(self, new_organizer, auth) -> bool:
"""
Returns whether it is allowed to copy the event to the target organizer. Auth can be TeamAPIToken or User.
"""
from ..permissions import get_all_event_permissions
from .auth import User
if self.organizer == new_organizer:
# Copying in the same organizer is always okay with any read access, we just need to ensure it does not
# grant more permissions than I had before, but that is handled by the view logic
return auth.has_event_permission(self.organizer, self, None)
if isinstance(auth, User):
# Cross-organizer copying requires almost full permission of source to prevent settings extraction
required_permissions = get_all_event_permissions() - {
# We do not require these, as this data is not copied
"event.orders:read", "event.orders:write", "event.vouchers:read", "event.vouchers:write",
"event.subevents:write",
}
given_permission = auth.get_event_permission_set(self.organizer, self)
return all(p in given_permission for p in required_permissions if ":" in p)
else:
# Tokens or devices can never copy between organizers, as they are organizer-bound. Kept for future
# compatibility and easier calling
return False
def copy_data_from(self, other, skip_meta_data=False):
from ..signals import event_copy_data
from . import (
@@ -1386,14 +1413,13 @@ class Event(EventMixin, LoggedModel):
from .auth import User
if permission:
kwargs = {permission: True}
qs = Team.objects.with_event_permission(permission)
else:
kwargs = {}
qs = Team.objects.all()
team_with_perm = Team.objects.filter(
team_with_perm = qs.filter(
members__pk=OuterRef('pk'),
organizer=self.organizer,
**kwargs
).filter(
Q(all_events=True) | Q(limit_events__pk=self.pk)
)

View File

@@ -31,9 +31,10 @@
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under the License.
import operator
import string
from datetime import date, datetime, time
from functools import reduce
import pytz_deprecation_shim
from django.conf import settings
@@ -53,6 +54,10 @@ from i18nfield.strings import LazyI18nString
from pretix.base.models.base import LoggedModel
from pretix.base.validators import OrganizerSlugBanlistValidator
from ...helpers.permission_migration import (
OLD_TO_NEW_EVENT_COMPAT, OLD_TO_NEW_ORGANIZER_COMPAT,
LegacyPermissionProperty,
)
from ..settings import settings_hierarkey
from .auth import User
@@ -309,6 +314,38 @@ def generate_api_token():
return get_random_string(length=64, allowed_chars=string.ascii_lowercase + string.digits)
class TeamQuerySet(models.QuerySet):
@classmethod
def event_permission_q(cls, perm_name):
from ..permissions import assert_valid_event_permission
if perm_name.startswith('can_') and perm_name in OLD_TO_NEW_EVENT_COMPAT: # legacy
return reduce(operator.and_, [cls.event_permission_q(p) for p in OLD_TO_NEW_EVENT_COMPAT[perm_name]])
assert_valid_event_permission(perm_name, allow_legacy=False)
return (
Q(all_event_permissions=True) |
Q(**{f'limit_event_permissions__{perm_name}': True})
)
@classmethod
def organizer_permission_q(cls, perm_name):
from ..permissions import assert_valid_organizer_permission
if perm_name.startswith('can_') and perm_name in OLD_TO_NEW_ORGANIZER_COMPAT: # legacy
return reduce(operator.and_, [cls.organizer_permission_q(p) for p in OLD_TO_NEW_ORGANIZER_COMPAT[perm_name]])
assert_valid_organizer_permission(perm_name, allow_legacy=False)
return (
Q(all_organizer_permissions=True) |
Q(**{f'limit_organizer_permissions__{perm_name}': True})
)
def with_event_permission(self, perm_name):
return self.filter(self.event_permission_q(perm_name))
def with_organizer_permission(self, perm_name):
return self.filter(self.organizer_permission_q(perm_name))
class Team(LoggedModel):
"""
A team is a collection of people given certain access rights to one or more events of an organizer.
@@ -321,36 +358,10 @@ class Team(LoggedModel):
:param all_events: Whether this team has access to all events of this organizer
:type all_events: bool
:param limit_events: A set of events this team has access to. Irrelevant if ``all_events`` is ``True``.
:param can_create_events: Whether or not the members can create new events with this organizer account.
:type can_create_events: bool
:param can_change_teams: If ``True``, the members can change the teams of this organizer account.
:type can_change_teams: bool
:param can_manage_customers: If ``True``, the members can view and change organizer-level customer accounts.
:type can_manage_customers: bool
:param can_manage_reusable_media: If ``True``, the members can view and change organizer-level reusable media.
:type can_manage_reusable_media: bool
:param can_change_organizer_settings: If ``True``, the members can change the settings of this organizer account.
:type can_change_organizer_settings: bool
:param can_change_event_settings: If ``True``, the members can change the settings of the associated events.
:type can_change_event_settings: bool
:param can_change_items: If ``True``, the members can change and add items and related objects for the associated events.
:type can_change_items: bool
:param can_view_orders: If ``True``, the members can inspect details of all orders of the associated events.
:type can_view_orders: bool
:param can_change_orders: If ``True``, the members can change details of orders of the associated events.
:type can_change_orders: bool
:param can_checkin_orders: If ``True``, the members can perform check-in related actions.
:type can_checkin_orders: bool
:param can_view_vouchers: If ``True``, the members can inspect details of all vouchers of the associated events.
:type can_view_vouchers: bool
:param can_change_vouchers: If ``True``, the members can change and create vouchers for the associated events.
:type can_change_vouchers: bool
"""
organizer = models.ForeignKey(Organizer, related_name="teams", on_delete=models.CASCADE)
name = models.CharField(max_length=190, verbose_name=_("Team name"))
members = models.ManyToManyField(User, related_name="teams", verbose_name=_("Team members"))
all_events = models.BooleanField(default=False, verbose_name=_("All events (including newly created ones)"))
limit_events = models.ManyToManyField('Event', verbose_name=_("Limit to events"), blank=True)
require_2fa = models.BooleanField(
default=False, verbose_name=_("Require all members of this team to use two-factor authentication"),
help_text=_("If you turn this on, all members of the team will be required to either set up two-factor "
@@ -358,62 +369,33 @@ class Team(LoggedModel):
"all users.")
)
can_create_events = models.BooleanField(
default=False,
verbose_name=_("Can create events"),
)
can_change_teams = models.BooleanField(
default=False,
verbose_name=_("Can change teams and permissions"),
)
can_change_organizer_settings = models.BooleanField(
default=False,
verbose_name=_("Can change organizer settings"),
help_text=_('Someone with this setting can get access to most data of all of your events, i.e. via privacy '
'reports, so be careful who you add to this team!')
)
can_manage_customers = models.BooleanField(
default=False,
verbose_name=_("Can manage customer accounts")
)
can_manage_reusable_media = models.BooleanField(
default=False,
verbose_name=_("Can manage reusable media")
)
can_manage_gift_cards = models.BooleanField(
default=False,
verbose_name=_("Can manage gift cards")
)
can_change_event_settings = models.BooleanField(
default=False,
verbose_name=_("Can change event settings")
)
can_change_items = models.BooleanField(
default=False,
verbose_name=_("Can change product settings")
)
can_view_orders = models.BooleanField(
default=False,
verbose_name=_("Can view orders")
)
can_change_orders = models.BooleanField(
default=False,
verbose_name=_("Can change orders")
)
can_checkin_orders = models.BooleanField(
default=False,
verbose_name=_("Can perform check-ins"),
help_text=_('This includes searching for attendees, which can be used to obtain personal information about '
'attendees. Users with "can change orders" can also perform check-ins.')
)
can_view_vouchers = models.BooleanField(
default=False,
verbose_name=_("Can view vouchers")
)
can_change_vouchers = models.BooleanField(
default=False,
verbose_name=_("Can change vouchers")
)
# Scope
all_events = models.BooleanField(default=False, verbose_name=_("All events (including newly created ones)"))
limit_events = models.ManyToManyField('Event', verbose_name=_("Limit to events"), blank=True)
# Permissions
# We store them as {key: True} instead of [key] because otherwise not all lookups we need are supported on SQLite
all_event_permissions = models.BooleanField(default=False, verbose_name=_("All event permissions"))
limit_event_permissions = models.JSONField(default=dict, verbose_name=_("Event permissions"))
all_organizer_permissions = models.BooleanField(default=False, verbose_name=_("All organizer permissions"))
limit_organizer_permissions = models.JSONField(default=dict, verbose_name=_("Organizer permissions"))
# Legacy lookups for plugin compatibility
can_change_event_settings = LegacyPermissionProperty()
can_change_items = LegacyPermissionProperty()
can_view_orders = LegacyPermissionProperty()
can_change_orders = LegacyPermissionProperty()
can_checkin_orders = LegacyPermissionProperty()
can_view_vouchers = LegacyPermissionProperty()
can_change_vouchers = LegacyPermissionProperty()
can_create_events = LegacyPermissionProperty()
can_change_organizer_settings = LegacyPermissionProperty()
can_change_teams = LegacyPermissionProperty()
can_manage_gift_cards = LegacyPermissionProperty()
can_manage_customers = LegacyPermissionProperty()
can_manage_reusable_media = LegacyPermissionProperty()
objects = TeamQuerySet.as_manager()
def __str__(self) -> str:
return _("%(name)s on %(object)s") % {
@@ -421,21 +403,62 @@ class Team(LoggedModel):
'object': str(self.organizer),
}
def permission_set(self) -> set:
attribs = dir(self)
return {
a for a in attribs if a.startswith('can_') and self.has_permission(a)
}
def event_permission_set(self, include_legacy=True) -> set:
from ..permissions import get_all_event_permission_groups
result = set()
for pg in get_all_event_permission_groups().values():
for action in pg.actions:
if self.all_event_permissions or self.limit_event_permissions.get(f"{pg.name}:{action}"):
result.add(f"{pg.name}:{action}")
if include_legacy:
# Add legacy permissions as well for plugin compatibility
for k, v in OLD_TO_NEW_EVENT_COMPAT.items():
if self.all_event_permissions or all(self.limit_event_permissions.get(kk) for kk in v):
result.add(k)
if "can_change_event_settings" in result:
result.add("can_change_settings")
return result
def organizer_permission_set(self, include_legacy=True) -> set:
from ..permissions import get_all_organizer_permission_groups
result = set()
for pg in get_all_organizer_permission_groups().values():
for action in pg.actions:
if self.all_organizer_permissions or self.limit_organizer_permissions.get(f"{pg.name}:{action}"):
result.add(f"{pg.name}:{action}")
if include_legacy:
# Add legacy permissions as well for plugin compatibility
for k, v in OLD_TO_NEW_ORGANIZER_COMPAT.items():
if self.all_organizer_permissions or all(self.limit_organizer_permissions.get(kk) for kk in v):
result.add(k)
return result
@property
def can_change_settings(self): # Legacy compatiblilty
def can_change_settings(self): # Legacy compatibility
return self.can_change_event_settings
def has_permission(self, perm_name):
try:
def has_event_permission(self, perm_name):
from ..permissions import assert_valid_event_permission
if perm_name.startswith('can_') and hasattr(self, perm_name): # legacy
return getattr(self, perm_name)
except AttributeError:
raise ValueError('Invalid required permission: %s' % perm_name)
assert_valid_event_permission(perm_name, allow_legacy=False)
return self.all_event_permissions or self.limit_event_permissions.get(perm_name, False)
def has_organizer_permission(self, perm_name):
from ..permissions import assert_valid_organizer_permission
if perm_name.startswith('can_') and hasattr(self, perm_name): # legacy
return getattr(self, perm_name)
assert_valid_organizer_permission(perm_name, allow_legacy=False)
return self.all_organizer_permissions or self.limit_organizer_permissions.get(perm_name, False)
def permission_for_event(self, event):
if self.all_events:
@@ -447,6 +470,19 @@ class Team(LoggedModel):
def active_tokens(self):
return self.tokens.filter(active=True)
def save(self, **kwargs):
if not isinstance(self.limit_event_permissions, dict):
raise TypeError("Permissions must be a dictionary")
if not isinstance(self.limit_organizer_permissions, dict):
raise TypeError("Permissions must be a dictionary")
for k in self.limit_event_permissions.values():
if k is not True:
raise TypeError("Permissions must only contain True values")
for k in self.limit_organizer_permissions.values():
if k is not True:
raise TypeError("Permissions must only contain True values")
return super().save(**kwargs)
class Meta:
verbose_name = _("Team")
verbose_name_plural = _("Teams")
@@ -503,7 +539,7 @@ class TeamAPIToken(models.Model):
has_event_access = (self.team.all_events and organizer == self.team.organizer) or (
event in self.team.limit_events.all()
)
return self.team.permission_set() if has_event_access else set()
return self.team.event_permission_set() if has_event_access else set()
def get_organizer_permission_set(self, organizer) -> set:
"""
@@ -512,7 +548,7 @@ class TeamAPIToken(models.Model):
:param organizer: The organizer of the event
:return: set of permissions
"""
return self.team.permission_set() if self.team.organizer == organizer else set()
return self.team.organizer_permission_set() if self.team.organizer == organizer else set()
def has_event_permission(self, organizer, event, perm_name=None, request=None) -> bool:
"""
@@ -521,7 +557,7 @@ class TeamAPIToken(models.Model):
:param organizer: The organizer of the event
:param event: The event to check
:param perm_name: The permission, e.g. ``can_change_teams``
:param perm_name: The permission, e.g. ``event.orders:read``
:param request: This parameter is ignored and only defined for compatibility reasons.
:return: bool
"""
@@ -529,8 +565,8 @@ class TeamAPIToken(models.Model):
event in self.team.limit_events.all()
)
if isinstance(perm_name, (tuple, list)):
return has_event_access and any(self.team.has_permission(p) for p in perm_name)
return has_event_access and (not perm_name or self.team.has_permission(perm_name))
return has_event_access and any(self.team.has_event_permission(p) for p in perm_name)
return has_event_access and (not perm_name or self.team.has_event_permission(perm_name))
def has_organizer_permission(self, organizer, perm_name=None, request=None):
"""
@@ -538,13 +574,13 @@ class TeamAPIToken(models.Model):
to the organizer ``organizer``.
:param organizer: The organizer to check
:param perm_name: The permission, e.g. ``can_change_teams``
:param perm_name: The permission, e.g. ``organizer.events:create``
:param request: This parameter is ignored and only defined for compatibility reasons.
:return: bool
"""
if isinstance(perm_name, (tuple, list)):
return organizer == self.team.organizer and any(self.team.has_permission(p) for p in perm_name)
return organizer == self.team.organizer and (not perm_name or self.team.has_permission(perm_name))
return organizer == self.team.organizer and any(self.team.has_organizer_permission(p) for p in perm_name)
return organizer == self.team.organizer and (not perm_name or self.team.has_organizer_permission(perm_name))
def get_events_with_any_permission(self):
"""
@@ -564,9 +600,11 @@ class TeamAPIToken(models.Model):
:param request: Ignored, for compatibility with User model
:return: Iterable of Events
"""
from pretix.base.permissions import AnyPermissionOf
if (
isinstance(permission, (list, tuple)) and any(getattr(self.team, p, False) for p in permission)
) or (isinstance(permission, str) and getattr(self.team, permission, False)):
isinstance(permission, (AnyPermissionOf, list, tuple)) and any(self.team.has_event_permission(p) for p in permission)
) or (isinstance(permission, str) and self.team.has_event_permission(permission)):
return self.get_events_with_any_permission()
else:
return self.team.organizer.events.none()

View File

@@ -151,7 +151,7 @@ def get_all_notification_types(event=None):
class ParametrizedOrderNotificationType(NotificationType):
required_permission = "can_view_orders"
required_permission = "event.orders:read"
def __init__(self, event, action_type, verbose_name, title):
self._action_type = action_type

View File

@@ -0,0 +1,334 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
import functools
import logging
import warnings
from collections import OrderedDict
from typing import Callable, Dict, List, NamedTuple, Set, Tuple
from django.apps import apps
from django.dispatch import receiver
from django.utils.functional import Promise
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from pretix.base.signals import (
register_event_permission_groups, register_organizer_permission_groups,
)
logger = logging.getLogger(__name__)
def cache_until_change(input_value: Callable):
def decorator(func):
old_input_value = None
cached_result = None
@functools.wraps(func)
def wrapper():
nonlocal cached_result, old_input_value
if cached_result is None or old_input_value != input_value():
cached_result = func()
old_input_value = input_value()
return cached_result
return wrapper
return decorator
class PermissionOption(NamedTuple):
actions: Tuple[str, ...]
label: str | Promise
help_text: str | Promise = None
class PermissionGroup(NamedTuple):
name: str
label: str | Promise
actions: List[str]
options: List[PermissionOption]
help_text: str | Promise = None
@cache_until_change(input_value=lambda: apps.ready)
def get_all_event_permission_groups() -> Dict[str, PermissionGroup]:
types = OrderedDict()
for recv, ret in register_event_permission_groups.send(None):
if isinstance(ret, (list, tuple)):
for r in ret:
types[r.name] = r
else:
types[ret.name] = ret
return types
@cache_until_change(input_value=lambda: apps.ready)
def get_all_organizer_permission_groups() -> Dict[str, PermissionGroup]:
types = OrderedDict()
for recv, ret in register_organizer_permission_groups.send(None):
if isinstance(ret, (list, tuple)):
for r in ret:
types[r.name] = r
else:
types[ret.name] = ret
return types
@cache_until_change(input_value=lambda: apps.ready)
def get_all_event_permissions() -> Set[str]:
from pretix.helpers.permission_migration import OLD_TO_NEW_EVENT_COMPAT
res = set(OLD_TO_NEW_EVENT_COMPAT.keys())
for pg in get_all_event_permission_groups().values():
for a in pg.actions:
res.add(f"{pg.name}:{a}")
return res
@cache_until_change(input_value=lambda: apps.ready)
def get_all_organizer_permissions() -> Set[str]:
from pretix.helpers.permission_migration import OLD_TO_NEW_ORGANIZER_COMPAT
res = set(OLD_TO_NEW_ORGANIZER_COMPAT.keys())
for pg in get_all_organizer_permission_groups().values():
for a in pg.actions:
res.add(f"{pg.name}:{a}")
return res
def assert_valid_event_permission(permission, allow_legacy=True, allow_tuple=True):
if not apps.ready:
# can't really check yet
return
if allow_legacy and permission == "can_change_settings":
permission = "can_change_event_settings"
if permission is None:
return
if isinstance(permission, (AnyPermissionOf, list, tuple)) and allow_tuple:
for p in permission:
assert_valid_event_permission(p)
return
if not allow_legacy and ':' not in permission:
raise ValueError(f"Not allowed to use legacy permission '{permission}'")
all_permissions = get_all_event_permissions()
if permission not in all_permissions:
# Warning *and* exception because warning is silently caught when used in if statements in Django templates
warnings.warn(f"Use of undefined permission '{permission}'")
raise Exception(f"Undefined permission '{permission}'")
def assert_valid_organizer_permission(permission, allow_legacy=True, allow_tuple=True):
if not apps.ready:
# can't really check yet
return
if permission is None:
return
if isinstance(permission, (AnyPermissionOf, list, tuple)) and allow_tuple:
for p in permission:
assert_valid_organizer_permission(p)
return
if not allow_legacy and ':' not in permission:
raise ValueError(f"Not allowed to use legacy permission '{permission}'")
all_permissions = get_all_organizer_permissions()
if permission not in all_permissions:
# Warning *and* exception because warning is silently caught when used in if statements in Django templates
warnings.warn(f"Use of undefined permission '{permission}'")
raise Exception(f"Undefined permission '{permission}'")
class AnyPermissionOf(list):
def __init__(self, *items):
super().__init__(items)
OPTS_ALL_READ = [
PermissionOption(actions=tuple(), label=pgettext_lazy("permission_level", "View")),
PermissionOption(actions=("write",), label=pgettext_lazy("permission_level", "View and change")),
]
OPTS_ALL_READ_SETTINGS_API = [
PermissionOption(actions=tuple(), label=pgettext_lazy("permission_level", "View"),
help_text=_("API only")),
PermissionOption(actions=("write",), label=pgettext_lazy("permission_level", "View and change")),
]
OPTS_ALL_READ_SETTINGS_PARENT = [
PermissionOption(actions=tuple(), label=pgettext_lazy("permission_level", "View"),
help_text=_("Menu item will only show up if the user has permission for general settings.")),
PermissionOption(actions=("write",), label=pgettext_lazy("permission_level", "View and change")),
]
OPTS_READ_WRITE = [
PermissionOption(actions=tuple(), label=pgettext_lazy("permission_level", "No access")),
PermissionOption(actions=("read",), label=pgettext_lazy("permission_level", "View")),
PermissionOption(actions=("read", "write"), label=pgettext_lazy("permission_level", "View and change")),
]
@receiver(register_event_permission_groups, dispatch_uid="base_register_default_event_permissions")
def register_default_event_permissions(sender, **kwargs):
return [
PermissionGroup(
name="event.settings.general",
label=_("General settings"),
actions=["write"],
options=OPTS_ALL_READ_SETTINGS_API,
help_text=_(
"This includes access to all settings not listed explicitly below, including plugin settings."
),
),
PermissionGroup(
name="event.settings.payment",
label=_("Payment settings"),
actions=["write"],
options=OPTS_ALL_READ_SETTINGS_PARENT,
),
PermissionGroup(
name="event.settings.tax",
label=_("Tax settings"),
actions=["write"],
options=OPTS_ALL_READ_SETTINGS_PARENT,
),
PermissionGroup(
name="event.settings.invoicing",
label=_("Invoicing settings"),
actions=["write"],
options=OPTS_ALL_READ_SETTINGS_PARENT,
),
PermissionGroup(
name="event.subevents",
label=_("Event series dates"),
actions=["write"],
options=OPTS_ALL_READ,
),
PermissionGroup(
name="event.items",
label=_("Products, quotas and questions"),
actions=["write"],
options=OPTS_ALL_READ,
help_text=_("Also includes related objects like categories or discounts."),
),
PermissionGroup(
name="event.orders",
label=_("Orders"),
actions=["read", "write", "checkin"],
options=[
PermissionOption(actions=tuple(), label=pgettext_lazy("permission_level", "No access")),
PermissionOption(actions=("checkin",), label=pgettext_lazy("permission_level", "Only check-in")),
PermissionOption(actions=("read",), label=pgettext_lazy("permission_level", "View all")),
PermissionOption(actions=("read", "checkin"), label=pgettext_lazy("permission_level", "View all and check-in")),
PermissionOption(actions=("read", "write"), label=pgettext_lazy("permission_level", "View all and change"),
help_text=_("Includes the ability to cancel and refund individual orders.")),
],
help_text=_("Also includes related objects like the waiting list."),
),
PermissionGroup(
name="event.vouchers",
label=_("Vouchers"),
actions=["read", "write"],
options=OPTS_READ_WRITE,
),
PermissionGroup(
name="event",
label=_("Full event or date cancellation"),
actions=["cancel"],
options=[
# If we ever add more actions, we need a new UI idea here
PermissionOption(actions=tuple(), label=pgettext_lazy("permission_level", "Not allowed")),
PermissionOption(actions=("cancel",), label=pgettext_lazy("permission_level", "Allowed")),
],
help_text="",
),
]
@receiver(register_organizer_permission_groups, dispatch_uid="base_register_default_organizer_permissions")
def register_default_organizer_permissions(sender, **kwargs):
return [
PermissionGroup(
name="organizer.events",
label=_("Events"),
actions=["create"],
options=[
PermissionOption(actions=tuple(), label=pgettext_lazy("permission_level", "Access existing events")),
PermissionOption(actions=("create",), label=pgettext_lazy("permission_level", "Access existing and create new events")),
],
help_text=_("The level of access to events is determined in detail by the settings below."),
),
PermissionGroup(
name="organizer.settings.general",
label=_("Settings"),
actions=["write"],
options=OPTS_ALL_READ_SETTINGS_API,
help_text=_("This includes access to all organizer-level functionality not listed explicitly below, including plugin settings."),
),
PermissionGroup(
name="organizer.teams",
label=_("Teams"),
actions=["write"],
options=[
PermissionOption(actions=tuple(), label=pgettext_lazy("permission_level", "No access")),
PermissionOption(actions=("write",), label=pgettext_lazy("permission_level", "View and change"),
help_text=_("Includes the ability to give someone (including oneself) additional permissions.")),
],
),
PermissionGroup(
name="organizer.giftcards",
label=_("Gift cards"),
actions=["read", "write"],
options=OPTS_READ_WRITE,
),
PermissionGroup(
name="organizer.customers",
label=_("Customers"),
actions=["read", "write"],
options=OPTS_READ_WRITE,
),
PermissionGroup(
name="organizer.reusablemedia",
label=_("Reusable media"),
actions=["read", "write"],
options=OPTS_READ_WRITE,
),
PermissionGroup(
name="organizer.devices",
label=_("Devices"),
actions=["read", "write"],
options=[
PermissionOption(actions=tuple(), label=pgettext_lazy("permission_level", "No access")),
PermissionOption(actions=("read",), label=pgettext_lazy("permission_level", "View")),
PermissionOption(actions=("read", "write"), label=pgettext_lazy("permission_level", "View and change"),
help_text=_("Includes the ability to give access to events and data oneself does not have access to.")),
],
),
PermissionGroup(
name="organizer.seatingplans",
label=_("Seating plans"),
actions=["write"],
options=OPTS_ALL_READ,
),
PermissionGroup(
name="organizer.outgoingmails",
label=_("Outgoing emails"),
actions=["read"],
options=[
PermissionOption(actions=tuple(), label=pgettext_lazy("permission_level", "No access")),
PermissionOption(actions=("read",), label=pgettext_lazy("permission_level", "View")),
],
),
]

View File

@@ -34,7 +34,7 @@ from django_scopes import scopes_disabled
from i18nfield.strings import LazyI18nString
from pretix.base.email import get_email_context
from pretix.base.exporter import OrganizerLevelExportMixin
from pretix.base.exporter import BaseExporter, OrganizerLevelExportMixin
from pretix.base.i18n import LazyLocaleException, language
from pretix.base.models import (
CachedFile, Device, Event, Organizer, ScheduledEventExport, TeamAPIToken,
@@ -64,7 +64,15 @@ class ExportEmptyError(ExportError):
@app.task(base=ProfiledEventTask, throws=(ExportError, ExportEmptyError), bind=True)
def export(self, event: Event, fileid: str, provider: str, form_data: Dict[str, Any]) -> None:
def export(self, event: Event, user: User, device: int, token: int, fileid: str, provider: str,
form_data: Dict[str, Any], staff_session=False) -> None:
if user:
user = User.objects.get(pk=user)
if device:
device = Device.objects.get(pk=device)
if token:
device = TeamAPIToken.objects.get(pk=token)
def set_progress(val):
if not self.request.called_directly:
self.update_state(
@@ -72,30 +80,38 @@ def export(self, event: Event, fileid: str, provider: str, form_data: Dict[str,
meta={'value': val}
)
ex = init_event_exporter(
identifier=provider,
event=event,
user=user,
token=token,
device=device,
staff_session=staff_session,
progress_callback=set_progress,
)
if not ex:
raise ExportError(
gettext('Export not found or you do not have sufficient permission to perform this export.')
)
file = CachedFile.objects.get(id=fileid)
with language(event.settings.locale, event.settings.region), override(event.settings.timezone):
responses = register_data_exporters.send(event)
for recv, response in responses:
if not response:
continue
ex = response(event, event.organizer, set_progress)
if ex.identifier == provider:
if ex.repeatable_read:
with repeatable_reads_transaction():
d = ex.render(form_data)
else:
d = ex.render(form_data)
if ex.repeatable_read:
with repeatable_reads_transaction():
d = ex.render(form_data)
else:
d = ex.render(form_data)
if d is None:
raise ExportError(
gettext('Your export did not contain any data.')
)
file.filename, file.type, data = d
if d is None:
raise ExportError(
gettext('Your export did not contain any data.')
)
file.filename, file.type, data = d
close_old_connections() # This task can run very long, we might need a new DB connection
close_old_connections() # This task can run very long, we might need a new DB connection
f = ContentFile(data)
file.file.save(cachedfile_name(file, file.filename), f)
f = ContentFile(data)
file.file.save(cachedfile_name(file, file.filename), f)
return str(file.pk)
@@ -105,10 +121,7 @@ def multiexport(self, organizer: Organizer, user: User, device: int, token: int,
if device:
device = Device.objects.get(pk=device)
if token:
device = TeamAPIToken.objects.get(pk=token)
allowed_events = (device or token or user).get_events_with_permission('can_view_orders')
if user and staff_session:
allowed_events = organizer.events.all()
token = TeamAPIToken.objects.get(pk=token)
def set_progress(val):
if not self.request.called_directly:
@@ -118,12 +131,35 @@ def multiexport(self, organizer: Organizer, user: User, device: int, token: int,
)
file = CachedFile.objects.get(id=fileid)
event_qs = organizer.events.all()
if form_data.get('events') is not None and not form_data.get('all_events'):
if form_data['events'] and isinstance(form_data['events'][0], str): # legacy API-created schedules
event_qs = event_qs.filter(slug__in=form_data.get('events'))
else:
event_qs = event_qs.filter(pk__in=form_data.get('events'))
ex = init_organizer_exporter(
identifier=provider,
organizer=organizer,
user=user,
token=token,
device=device,
staff_session=staff_session,
progress_callback=set_progress,
event_qs=event_qs,
)
if not ex:
raise ExportError(
gettext('Export not found or you do not have sufficient permission to perform this export.')
)
if user:
locale = user.locale
timezone = user.timezone
region = None # todo: add to user?
else:
e = allowed_events.first()
e = ex.events.first()
if e:
locale = e.settings.locale
timezone = e.settings.timezone
@@ -133,47 +169,140 @@ def multiexport(self, organizer: Organizer, user: User, device: int, token: int,
timezone = organizer.settings.timezone or settings.TIME_ZONE
region = organizer.settings.region
with language(locale, region), override(timezone):
if form_data.get('events') is not None and not form_data.get('all_events'):
if isinstance(form_data['events'][0], str):
events = allowed_events.filter(slug__in=form_data.get('events'), organizer=organizer)
else:
events = allowed_events.filter(pk__in=form_data.get('events'), organizer=organizer)
if ex.repeatable_read:
with repeatable_reads_transaction():
d = ex.render(form_data)
else:
events = allowed_events.filter(organizer=organizer)
responses = register_multievent_data_exporters.send(organizer)
d = ex.render(form_data)
if d is None:
raise ExportError(
gettext('Your export did not contain any data.')
)
file.filename, file.type, data = d
for recv, response in responses:
if not response:
continue
ex = response(events, organizer, set_progress)
if ex.identifier == provider:
if (
isinstance(ex, OrganizerLevelExportMixin) and
not staff_session and
not (device or token or user).has_organizer_permission(organizer, ex.organizer_required_permission)
):
raise ExportError(
gettext('You do not have sufficient permission to perform this export.')
)
close_old_connections() # This task can run very long, we might need a new DB connection
if ex.repeatable_read:
with repeatable_reads_transaction():
d = ex.render(form_data)
else:
d = ex.render(form_data)
if d is None:
raise ExportError(
gettext('Your export did not contain any data.')
)
file.filename, file.type, data = d
close_old_connections() # This task can run very long, we might need a new DB connection
f = ContentFile(data)
file.file.save(cachedfile_name(file, file.filename), f)
f = ContentFile(data)
file.file.save(cachedfile_name(file, file.filename), f)
return str(file.pk)
def init_event_exporter(identifier, **kwargs):
for ex in init_event_exporters(**kwargs):
if ex.identifier == identifier:
return ex
return None
def init_event_exporters(event, user=None, token=None, device=None, request=None, staff_session=False, **kwargs):
if not user and not token and not device:
raise ValueError("No auth source given.")
perm_holder = device or token or user
responses = register_data_exporters.send(event)
for r, response in responses:
if not response:
continue
if issubclass(response, OrganizerLevelExportMixin):
raise TypeError("Cannot user organizer-level exporter on event level")
permission_name = response.get_required_event_permission()
if not perm_holder.has_event_permission(event.organizer, event, permission_name, request) and not staff_session:
continue
exporter: BaseExporter = response(event=event, organizer=event.organizer, **kwargs)
if not exporter.available_for_user(user if user and user.is_authenticated else None):
continue
yield exporter
def init_organizer_exporter(identifier, **kwargs):
for ex in init_organizer_exporters(**kwargs):
if ex.identifier == identifier:
return ex
return None
def init_organizer_exporters(
organizer, user=None, token=None, device=None, request=None, staff_session=False, event_qs=None, **kwargs
):
if not user and not token and not device:
raise ValueError("No auth source given.")
perm_holder = device or token or user
_event_list_cache = {}
_has_permission_on_any_team_cache = {}
_team_cache = None
responses = register_multievent_data_exporters.send(organizer)
for r, response in responses:
if not response:
continue
if issubclass(response, OrganizerLevelExportMixin):
exporter: BaseExporter = response(event=Event.objects.none(), organizer=organizer, **kwargs)
try:
if not perm_holder.has_organizer_permission(organizer, response.get_required_organizer_permission(), request) and not staff_session:
continue
except NotImplementedError:
logger.error(f"Not showing export {response} because get_required_organizer_permission() is not implemented.")
continue
else:
permission_name = response.get_required_event_permission()
if permission_name not in _event_list_cache:
if staff_session:
events = event_qs.all()
elif event_qs is not None:
events = event_qs.filter(
pk__in=perm_holder.get_events_with_permission(
permission_name, request=request
).filter(
organizer=organizer
).values("id")
)
else:
events = perm_holder.get_events_with_permission(
permission_name, request=request
).filter(
organizer=organizer
)
_event_list_cache[permission_name] = events
if permission_name not in _has_permission_on_any_team_cache:
# Check if the user has this event permission on any teams they are part of to decide whether to show
# the export at all.
# This is different from _event_list_cache[permission_name].exists() for the case of an organizer with
# zero events in total, or a team with zero events. In these cases, we still want people to be able
# to see waht exports they'll get once they have events.
if user:
if _team_cache is None:
_team_cache = list(user.teams.filter(organizer=organizer))
_has_permission_on_any_team_cache[permission_name] = staff_session or any(
t.has_event_permission(permission_name) for t in _team_cache
)
elif token:
_has_permission_on_any_team_cache[permission_name] = token.team.has_event_permission(permission_name)
elif device:
_has_permission_on_any_team_cache[permission_name] = device.has_event_permission(permission_name)
if not _has_permission_on_any_team_cache[permission_name]:
continue
exporter: BaseExporter = response(event=_event_list_cache[permission_name], organizer=organizer, **kwargs)
if not exporter.available_for_user(user if user and user.is_authenticated else None):
continue
yield exporter
def _run_scheduled_export(schedule, context: Union[Event, Organizer], exporter, config_url, retry_func, has_permission):
with language(schedule.locale, context.settings.region), override(schedule.tz):
file = CachedFile(web_download=False)
@@ -217,7 +346,7 @@ def _run_scheduled_export(schedule, context: Union[Event, Organizer], exporter,
try:
if not exporter:
raise ExportError("Export type not found.")
raise ExportError("Export type not found or permission denied.")
if exporter.repeatable_read:
with repeatable_reads_transaction():
d = exporter.render(schedule.export_form_data)
@@ -291,31 +420,20 @@ def _run_scheduled_export(schedule, context: Union[Event, Organizer], exporter,
def scheduled_organizer_export(self, organizer: Organizer, schedule: int) -> None:
schedule = organizer.scheduled_exports.get(pk=schedule)
allowed_events = schedule.owner.get_events_with_permission('can_view_orders')
event_qs = organizer.events.all()
if schedule.export_form_data.get('events') is not None and not schedule.export_form_data.get('all_events'):
if isinstance(schedule.export_form_data['events'][0], str):
events = allowed_events.filter(slug__in=schedule.export_form_data.get('events'), organizer=organizer)
event_qs = event_qs.filter(slug__in=schedule.export_form_data.get('events'))
else:
events = allowed_events.filter(pk__in=schedule.export_form_data.get('events'), organizer=organizer)
else:
events = allowed_events.filter(organizer=organizer)
responses = register_multievent_data_exporters.send(organizer)
exporter = None
for recv, response in responses:
if not response:
continue
ex = response(events, organizer)
if ex.identifier == schedule.export_identifier:
exporter = ex
break
event_qs = event_qs.filter(pk__in=schedule.export_form_data.get('events'))
exporter = init_organizer_exporter(
identifier=schedule.export_identifier,
organizer=organizer,
user=schedule.owner,
event_qs=event_qs,
)
has_permission = schedule.owner.is_active
if isinstance(exporter, OrganizerLevelExportMixin):
if not schedule.owner.has_organizer_permission(organizer, exporter.organizer_required_permission):
has_permission = False
if exporter and not exporter.available_for_user(schedule.owner):
has_permission = False
_run_scheduled_export(
schedule,
@@ -336,17 +454,12 @@ def scheduled_organizer_export(self, organizer: Organizer, schedule: int) -> Non
def scheduled_event_export(self, event: Event, schedule: int) -> None:
schedule = event.scheduled_exports.get(pk=schedule)
responses = register_data_exporters.send(event)
exporter = None
for recv, response in responses:
if not response:
continue
ex = response(event, event.organizer)
if ex.identifier == schedule.export_identifier:
exporter = ex
break
has_permission = schedule.owner.is_active and schedule.owner.has_event_permission(event.organizer, event, 'can_view_orders')
exporter = init_event_exporter(
identifier=schedule.export_identifier,
event=event,
user=schedule.owner,
)
has_permission = schedule.owner.is_active
_run_scheduled_export(
schedule,

View File

@@ -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': '',

View File

@@ -561,6 +561,18 @@ however for this signal, the ``sender`` **may also be None** to allow creating t
notification settings!
"""
register_event_permission_groups = GlobalSignal()
"""
This signal is sent out to get all known permissions. Receivers should return an
instance of pretix.base.permissions.PermissionGroup or a list of such instances.
"""
register_organizer_permission_groups = GlobalSignal()
"""
This signal is sent out to get all known permissions. Receivers should return an
instance of pretix.base.permissions.PermissionGroup or a list of such instances.
"""
notification = EventPluginSignal()
"""
Arguments: ``logentry_id``, ``notification_type``
@@ -1106,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 stored in the settings store
should not be included!
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
"""

View File

@@ -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):

View File

@@ -102,7 +102,7 @@ def _default_context(request):
complain_testmode_orders = request.event.orders.filter(testmode=True).exists()
request.event.cache.set('complain_testmode_orders', complain_testmode_orders, 30)
ctx['complain_testmode_orders'] = complain_testmode_orders and request.user.has_event_permission(
request.organizer, request.event, 'can_view_orders', request=request
request.organizer, request.event, 'event.orders:read', request=request
)
else:
ctx['complain_testmode_orders'] = False

View File

@@ -62,6 +62,7 @@ from pretix.base.forms import (
)
from pretix.base.models import Event, Organizer, TaxRule, Team
from pretix.base.models.event import EventFooterLink, EventMetaValue, SubEvent
from pretix.base.models.organizer import TeamQuerySet
from pretix.base.models.tax import TAX_CODE_LISTS
from pretix.base.reldate import RelativeDateField, RelativeDateTimeField
from pretix.base.services.placeholders import FormPlaceholderMixin
@@ -100,11 +101,12 @@ class EventWizardFoundationForm(forms.Form):
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user')
self.session = kwargs.pop('session')
self.clone_from = kwargs.pop('clone_from')
super().__init__(*args, **kwargs)
qs = Organizer.objects.all()
if not self.user.has_active_staff_session(self.session.session_key):
qs = qs.filter(
id__in=self.user.teams.filter(can_create_events=True).values_list('organizer', flat=True)
id__in=self.user.teams.filter(TeamQuerySet.organizer_permission_q("organizer.events:create")).values_list('organizer', flat=True)
)
self.fields['organizer'] = forms.ModelChoiceField(
label=_("Organizer"),
@@ -125,6 +127,16 @@ class EventWizardFoundationForm(forms.Form):
self.fields['organizer'].initial = organizer
self.fields['locales'].initial = organizer.settings.locales
def clean(self):
d = super().clean()
if d.get('organizer') and self.clone_from and not self.user.has_active_staff_session(self.session.session_key):
if not self.clone_from.allow_copy_data(d['organizer'], self.user):
raise ValidationError({
"organizer": _("You do not have a sufficient level of access on the event you selected "
"to copy it to the desired organizer.")
})
return d
class EventWizardBasicsForm(I18nModelForm):
error_messages = {
@@ -198,6 +210,7 @@ class EventWizardBasicsForm(I18nModelForm):
self.has_subevents = kwargs.pop('has_subevents')
self.user = kwargs.pop('user')
self.session = kwargs.pop('session')
self.clone_from = kwargs.pop('clone_from')
super().__init__(*args, **kwargs)
if 'timezone' not in self.initial:
self.initial['timezone'] = get_current_timezone_name()
@@ -238,6 +251,16 @@ class EventWizardBasicsForm(I18nModelForm):
'check "{field}" above.').format(field=self.fields["no_taxes"].label)
})
if self.clone_from and not self.user.has_active_staff_session(self.session.session_key):
if data.get("team"):
source_event_perms = self.user.get_event_permission_set(self.organizer, self.clone_from)
team_perms = data["team"].event_permission_set(include_legacy=False)
if any(t not in source_event_perms for t in team_perms):
raise ValidationError({
"team": _("You cannot choose a team that would give you more access than you have on "
"the event you are copying.")
})
# change timezone
zone = ZoneInfo(data.get('timezone'))
data['date_from'] = self.reset_timezone(zone, data.get('date_from'))
@@ -261,9 +284,12 @@ class EventWizardBasicsForm(I18nModelForm):
@staticmethod
def has_control_rights(user, organizer, session):
# It's mostly pointless to let a user create an event where they can't event change the name or create products,
# so we detect if the user has sufficient access for that on a new event.
return user.teams.filter(
organizer=organizer, all_events=True, can_change_event_settings=True, can_change_items=True,
can_change_orders=True, can_change_vouchers=True
TeamQuerySet.event_permission_q("event.settings.general:write"),
organizer=organizer,
all_events=True,
).exists() or user.has_active_staff_session(session.session_key)
@@ -293,18 +319,24 @@ class EventWizardCopyForm(forms.Form):
if user.has_active_staff_session(session.session_key):
return Event.objects.all()
return Event.objects.filter(
# It is generally pointless to let users copy events when they would not even be able to change the
# date of the event they have just created. Therefore, even if it looks wrong, we're checking a write
# permission for read access.
Q(organizer_id__in=user.teams.filter(
all_events=True, can_change_event_settings=True, can_change_items=True
TeamQuerySet.event_permission_q("event.settings.general:write"),
all_events=True,
).values_list('organizer', flat=True)) | Q(id__in=user.teams.filter(
can_change_event_settings=True, can_change_items=True
TeamQuerySet.event_permission_q("event.settings.general:write"),
).values_list('limit_events__id', flat=True))
)
def __init__(self, *args, **kwargs):
kwargs.pop('organizer')
self.organizer = kwargs.pop('organizer')
kwargs.pop('locales')
self.session = kwargs.pop('session')
self.team = kwargs.pop('team')
kwargs.pop('has_subevents')
kwargs.pop('clone_from')
self.user = kwargs.pop('user')
super().__init__(*args, **kwargs)
@@ -323,6 +355,24 @@ class EventWizardCopyForm(forms.Form):
)
self.fields['copy_from_event'].widget.choices = self.fields['copy_from_event'].choices
def clean(self):
d = super().clean()
if d.get('copy_from_event') and not self.user.has_active_staff_session(self.session.session_key):
if not d['copy_from_event'].allow_copy_data(self.organizer, self.user):
raise ValidationError({
"copy_from_event": _("You do not have a sufficient level of access on the event you selected "
"to copy it to the desired organizer.")
})
if self.team:
source_event_perms = self.user.get_event_permission_set(self.organizer, d['copy_from_event'])
team_perms = self.team.event_permission_set(include_legacy=False)
if any(t not in source_event_perms for t in team_perms):
raise ValidationError({
"copy_from_event": _("You cannot choose an event on which you have less access than the "
"team you selected in the previous step.")
})
return d
class EventMetaValueForm(forms.ModelForm):

View File

@@ -1111,7 +1111,7 @@ class OrderPaymentSearchFilterForm(forms.Form):
self.fields['organizer'].queryset = Organizer.objects.filter(
pk__in=self.request.user.teams.values_list('organizer', flat=True)
)
self.fields['event'].queryset = self.request.user.get_events_with_permission('can_view_orders')
self.fields['event'].queryset = self.request.user.get_events_with_permission('event.orders:read')
self.fields['provider'].choices += get_all_payment_providers()

View File

@@ -75,7 +75,10 @@ from pretix.base.models import (
ReusableMedium, SalesChannel, Team,
)
from pretix.base.models.customers import CustomerSSOClient, CustomerSSOProvider
from pretix.base.models.organizer import OrganizerFooterLink
from pretix.base.models.organizer import OrganizerFooterLink, TeamQuerySet
from pretix.base.permissions import (
get_all_event_permission_groups, get_all_organizer_permission_groups,
)
from pretix.base.settings import (
PERSON_NAME_SCHEMES, PERSON_NAME_TITLE_GROUPS, validate_organizer_settings,
)
@@ -297,7 +300,34 @@ class MembershipTypeForm(I18nModelForm):
fields = ['name', 'transferable', 'allow_parallel_usage', 'max_usages']
class PermissionMultipleChoiceField(forms.MultipleChoiceField):
def to_python(self, value):
return {
k: True for k in super().to_python(value) if k
}
def prepare_value(self, value):
if isinstance(value, dict):
return [k for k, v in value.items() if v is True]
return super().prepare_value(value)
class TeamForm(forms.ModelForm):
def _make_label(self, p):
source = '{}'
params = [p.label]
if p.plugin_name:
source = '<span class="fa fa-puzzle-piece text-muted" data-toggle="tooltip" title="{}"></span> ' + source
params.insert(0, _("Provided by a plugin"))
if p.help_text:
source += ' <span class="fa fa-info-circle text-muted" data-toggle="tooltip" title="{}"></span>'
params.append(p.help_text)
source += ' (<code>{}</code>)'
params.append(p.name)
return format_html(source, *params)
def __init__(self, *args, **kwargs):
organizer = kwargs.pop('organizer')
@@ -305,16 +335,62 @@ class TeamForm(forms.ModelForm):
self.fields['limit_events'].queryset = organizer.events.all().order_by(
'-has_subevents', '-date_from'
)
self.event_field_names = []
for pg in get_all_event_permission_groups().values():
initial = ",".join(sorted(
a for a in pg.actions if self.instance and self.instance.limit_event_permissions.get(f"{pg.name}:{a}")
)) or "EMPTY"
self.fields[f'event_{pg.name}'] = forms.ChoiceField(
choices=[
(
",".join(sorted(opt.actions)) or "EMPTY",
format_html(
'{label} '
'<span class="fa fa-question-circle fa-fw text-muted" data-toggle="tooltip"'
' data-placement="right" title="{help_text}"></span>',
label=opt.label,
help_text=opt.help_text,
) if opt.help_text else opt.label,
)
for opt in pg.options
],
label=pg.label,
help_text=pg.help_text,
initial=initial,
widget=forms.RadioSelect,
)
self.event_field_names.append(f'event_{pg.name}')
self.organizer_field_names = []
for pg in get_all_organizer_permission_groups().values():
initial = ",".join(sorted(
a for a in pg.actions if self.instance and self.instance.limit_organizer_permissions.get(f"{pg.name}:{a}")
)) or "EMPTY"
self.fields[f'organizer_{pg.name}'] = forms.ChoiceField(
choices=[
(
",".join(sorted(opt.actions)) or "EMPTY",
format_html(
'{label} '
'<span class="fa fa-question-circle fa-fw text-muted" data-toggle="tooltip"'
' data-placement="right" title="{help_text}"></span>',
label=opt.label,
help_text=opt.help_text,
) if opt.help_text else opt.label,
)
for opt in pg.options
],
label=pg.label,
help_text=pg.help_text,
initial=initial,
widget=forms.RadioSelect,
)
self.organizer_field_names.append(f'organizer_{pg.name}')
class Meta:
model = Team
fields = ['name', 'require_2fa', 'all_events', 'limit_events', 'can_create_events',
'can_change_teams', 'can_change_organizer_settings',
'can_manage_gift_cards', 'can_manage_customers',
'can_manage_reusable_media',
'can_change_event_settings', 'can_change_items',
'can_view_orders', 'can_change_orders', 'can_checkin_orders',
'can_view_vouchers', 'can_change_vouchers']
fields = ['name', 'require_2fa', 'all_events', 'limit_events',
'all_event_permissions',
'all_organizer_permissions',]
widgets = {
'limit_events': forms.CheckboxSelectMultiple(attrs={
'data-inverse-dependency': '#id_all_events',
@@ -327,15 +403,57 @@ class TeamForm(forms.ModelForm):
def clean(self):
data = super().clean()
if self.instance.pk and not data['can_change_teams']:
data['limit_event_permissions'] = {}
if not data['all_event_permissions']:
for pg in get_all_event_permission_groups().values():
selected = data.get(f'event_{pg.name}', 'EMPTY')
if selected == "EMPTY":
selected_actions = []
else:
selected_actions = selected.split(',')
for action in pg.actions:
if action in selected_actions:
data['limit_event_permissions'][f"{pg.name}:{action}"] = True
self.instance.limit_event_permissions = data['limit_event_permissions']
data['limit_organizer_permissions'] = {}
if not data['all_organizer_permissions']:
for pg in get_all_organizer_permission_groups().values():
selected = data.get(f'organizer_{pg.name}', 'EMPTY')
if selected == "EMPTY":
selected_actions = []
else:
selected_actions = selected.split(',')
for action in pg.actions:
if action in selected_actions:
data['limit_organizer_permissions'][f"{pg.name}:{action}"] = True
self.instance.limit_organizer_permissions = data['limit_organizer_permissions']
if self.instance.pk and not data['all_organizer_permissions'] and 'organizer.teams:write' not in data.get('limit_organizer_permissions', []):
if not self.instance.organizer.teams.exclude(pk=self.instance.pk).filter(
can_change_teams=True, members__isnull=False
TeamQuerySet.organizer_permission_q("organizer.teams:write"),
members__isnull=False
).exists():
raise ValidationError(_('The changes could not be saved because there would be no remaining team with '
'the permission to change teams and permissions.'))
return data
@property
def changed_data_for_log(self):
r = {}
for k in self.changed_data:
if k == "limit_events":
r[k] = [e.id for e in getattr(self.instance, k).all()]
elif k.startswith("event_"):
r["limit_event_permissions"] = self.instance.limit_event_permissions
elif k.startswith("organizer_"):
r["limit_organizer_permissions"] = self.instance.limit_organizer_permissions
else:
r[k] = getattr(self.instance, k)
return r
class GateForm(forms.ModelForm):

View File

@@ -45,7 +45,9 @@ from django.utils.translation import gettext as _
from django_scopes import scope
from pretix.base.models import Event, Organizer
from pretix.base.models.auth import SuperuserPermissionSet, User
from pretix.base.models.auth import (
EventPermissionSet, OrganizerPermissionSet, SuperuserPermissionSet, User,
)
from pretix.helpers.http import redirect_to_url
from pretix.helpers.security import (
Session2FASetupRequired, SessionInvalid, SessionPasswordChangeRequired,
@@ -170,7 +172,7 @@ class PermissionMiddleware:
if request.user.has_active_staff_session(request.session.session_key):
request.eventpermset = SuperuserPermissionSet()
else:
request.eventpermset = request.user.get_event_permission_set(request.organizer, request.event)
request.eventpermset = EventPermissionSet(request.user.get_event_permission_set(request.organizer, request.event))
elif 'organizer' in url.kwargs:
if url.kwargs['organizer'] == '-':
# This is a hack that just takes the user to ANY organizer. It's useful to link to features in support
@@ -192,7 +194,7 @@ class PermissionMiddleware:
if request.user.has_active_staff_session(request.session.session_key):
request.orgapermset = SuperuserPermissionSet()
else:
request.orgapermset = request.user.get_organizer_permission_set(request.organizer)
request.orgapermset = OrganizerPermissionSet(request.user.get_organizer_permission_set(request.organizer))
with scope(organizer=getattr(request, 'organizer', None)):
r = self.get_response(request)

View File

@@ -43,24 +43,29 @@ def get_event_navigation(request: HttpRequest):
'icon': 'dashboard',
}
]
if 'can_change_event_settings' 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,88 +131,87 @@ 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 'can_change_items' 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 'can_change_event_settings' 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 'can_view_orders' in request.eventpermset:
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 = [
{
'label': _('All orders'),
@@ -242,7 +254,7 @@ def get_event_navigation(request: HttpRequest):
'active': 'event.orders.waitinglist' in url.url_name,
},
]
if 'can_change_orders' in request.eventpermset:
if 'event.orders:write' in request.eventpermset:
children.append({
'label': _('Import'),
'url': reverse('control:event.orders.import', kwargs={
@@ -261,8 +273,18 @@ def get_event_navigation(request: HttpRequest):
'icon': 'shopping-cart',
'children': children
})
else:
nav.append({
'label': _('Export'),
'url': reverse('control:event.orders.export', kwargs={
'event': request.event.slug,
'organizer': request.event.organizer.slug,
}),
'active': 'event.orders.export' in url.url_name,
'icon': 'download',
})
if 'can_view_vouchers' in request.eventpermset:
if 'event.vouchers:read' in request.eventpermset:
nav.append({
'label': _('Vouchers'),
'url': reverse('control:event.vouchers', kwargs={
@@ -291,7 +313,7 @@ def get_event_navigation(request: HttpRequest):
]
})
if 'can_view_orders' 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={
@@ -485,7 +507,7 @@ def get_organizer_navigation(request):
'icon': 'calendar',
},
]
if 'can_change_organizer_settings' in request.orgapermset:
if 'organizer.settings.general:write' in request.orgapermset:
nav.append({
'label': _('Settings'),
'url': reverse('control:organizer.edit', kwargs={
@@ -539,7 +561,7 @@ def get_organizer_navigation(request):
]
})
if 'can_change_teams' in request.orgapermset:
if 'organizer.teams:write' in request.orgapermset:
nav.append({
'label': _('Teams'),
'url': reverse('control:organizer.teams', kwargs={
@@ -549,7 +571,7 @@ def get_organizer_navigation(request):
'icon': 'group',
})
if 'can_manage_gift_cards' in request.orgapermset:
if 'organizer.giftcards:read' in request.orgapermset or 'organizer.giftcards:write' in request.orgapermset:
children = []
children.append({
'label': _('Gift cards'),
@@ -559,7 +581,7 @@ def get_organizer_navigation(request):
'active': 'organizer.giftcard' in url.url_name and 'acceptance' not in url.url_name,
'children': children,
})
if 'can_change_organizer_settings' in request.orgapermset:
if 'organizer.settings.general:write' in request.orgapermset:
children.append(
{
'label': _('Acceptance'),
@@ -580,7 +602,7 @@ def get_organizer_navigation(request):
if request.organizer.settings.customer_accounts:
children = []
if 'can_manage_customers' in request.orgapermset:
if 'organizer.customers:read' in request.orgapermset or 'organizer.customers:write' in request.orgapermset:
children.append(
{
'label': _('Customers'),
@@ -590,7 +612,7 @@ def get_organizer_navigation(request):
'active': 'organizer.customer' in url.url_name,
}
)
if 'can_change_organizer_settings' in request.orgapermset:
if 'organizer.settings.general:write' in request.orgapermset:
children.append(
{
'label': _('Membership types'),
@@ -629,16 +651,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 'can_change_organizer_settings' 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={
@@ -672,7 +695,7 @@ def get_organizer_navigation(request):
'icon': 'download',
})
if 'can_change_organizer_settings' in request.orgapermset:
if 'organizer.settings.general:write' in request.orgapermset:
merge_in(nav, [{
'parent': reverse('control:organizer.export', kwargs={
'organizer': request.organizer.slug,
@@ -684,6 +707,7 @@ def get_organizer_navigation(request):
'active': (url.url_name == 'organizer.datasync.failedjobs'),
}])
if 'organizer.outgoingmails:read' in request.orgapermset:
nav.append({
'label': _('Outgoing emails'),
'url': reverse('control:organizer.outgoingmails', kwargs={

View File

@@ -38,6 +38,9 @@ from django.core.exceptions import PermissionDenied
from django.urls import reverse
from django.utils.translation import gettext as _
from pretix.base.permissions import (
assert_valid_event_permission, assert_valid_organizer_permission,
)
from pretix.helpers.http import redirect_to_url
@@ -55,7 +58,9 @@ def event_permission_required(permission):
"""
if permission == 'can_change_settings':
# Legacy support
permission = 'can_change_event_settings'
permission = 'event.settings.general:write'
assert_valid_event_permission(permission)
def decorator(function):
def wrapper(request, *args, **kw):
@@ -79,7 +84,7 @@ class EventPermissionRequiredMixin:
This mixin is equivalent to the event_permission_required view decorator but
is in a form suitable for class-based views.
"""
permission = ''
permission = None # None means "any permission"
@classmethod
def as_view(cls, **initkwargs):
@@ -92,9 +97,11 @@ def organizer_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 == 'can_change_settings':
if permission in ('event.settings.general:write', 'can_change_settings', 'can_change_event_settings'):
# Legacy support
permission = 'can_change_organizer_settings'
permission = 'organizer.settings.general:write'
assert_valid_organizer_permission(permission)
def decorator(function):
def wrapper(request, *args, **kw):
@@ -116,7 +123,7 @@ class OrganizerPermissionRequiredMixin:
This mixin is equivalent to the organizer_permission_required view decorator but
is in a form suitable for class-based views.
"""
permission = ''
permission = None # None means "any permission"
@classmethod
def as_view(cls, **initkwargs):

View File

@@ -9,7 +9,7 @@
{% block content %}
<h1>
{% blocktrans with name=checkinlist.name %}Check-in list: {{ name }}{% endblocktrans %}
{% if 'can_change_event_settings' in request.eventpermset %}
{% if 'event.settings.general:write' in request.eventpermset %}
<a href="{% url "control:event.orders.checkinlists.edit" event=request.event.slug organizer=request.event.organizer.slug list=checkinlist.pk %}"
class="btn btn-default">
<span class="fa fa-wrench"></span>
@@ -87,7 +87,7 @@
<thead>
<tr>
<th>
{% if "can_change_orders" in request.eventpermset or "can_checkin_orders" in request.eventpermset %}
{% if "event.orders:write" in request.eventpermset or "event.orders:checkin" in request.eventpermset %}
<label aria-label="{% trans "select all rows for batch-operation" %}"
class="batch-select-label"><input type="checkbox" data-toggle-table/></label>
{% endif %}
@@ -132,7 +132,7 @@
{% for e in entries %}
<tr>
<td>
{% if "can_change_orders" in request.eventpermset or "can_checkin_orders" in request.eventpermset %}
{% if "event.orders:write" in request.eventpermset or "event.orders:checkin" in request.eventpermset %}
<input type="checkbox" name="checkin" id="id_checkin" class="" value="{{ e.pk }}"/>
{% endif %}
</td>
@@ -207,7 +207,7 @@
</table>
</div>
<div class="batch-select-actions">
{% if "can_change_orders" in request.eventpermset or "can_checkin_orders" in request.eventpermset %}
{% if "event.orders:write" in request.eventpermset or "event.orders:checkin" in request.eventpermset %}
<button type="submit" class="btn btn-primary btn-save">
<span class="fa fa-sign-in" aria-hidden="true"></span>
{% trans "Check-In selected attendees" %}
@@ -217,7 +217,7 @@
{% trans "Check-Out selected attendees" %}
</button>
{% endif %}
{% if "can_change_orders" in request.eventpermset %}
{% if "event.orders:write" in request.eventpermset %}
<button type="submit" class="btn btn-danger btn-save" name="revert"
formaction="{% url "control:event.orders.checkinlists.bulk_revert" event=request.event.slug organizer=request.event.organizer.slug list=checkinlist.pk %}"
data-no-asynctask

View File

@@ -63,27 +63,27 @@
{% endif %}
</p>
{% if "can_change_event_settings" in request.eventpermset %}
{% if "event.settings.general:write" in request.eventpermset %}
<a href="{% url "control:event.orders.checkinlists.add" organizer=request.event.organizer.slug event=request.event.slug %}"
class="btn btn-primary btn-lg"><i class="fa fa-plus"></i> {% trans "Create a new check-in list" %}
</a>
{% endif %}
{% if can_change_organizer_settings %}
{% if link_device_settings %}
<a href="{% url "control:organizer.devices" organizer=request.organizer.slug %}"
class="btn btn-default btn-lg"><i class="fa fa-tablet"></i> {% trans "Connected devices" %}</a>
{% endif %}
</div>
{% else %}
<p>
{% if "can_change_event_settings" in request.eventpermset %}
{% if "event.settings.general:write" in request.eventpermset %}
<a href="{% url "control:event.orders.checkinlists.add" organizer=request.event.organizer.slug event=request.event.slug %}"
class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create a new check-in list" %}</a>
{% endif %}
{% if can_change_organizer_settings %}
{% if link_device_settings %}
<a href="{% url "control:organizer.devices" organizer=request.organizer.slug %}"
class="btn btn-default"><i class="fa fa-tablet"></i> {% trans "Connected devices" %}</a>
{% endif %}
{% if "can_change_orders" in request.eventpermset %}
{% if "event.settings.general:write" in request.eventpermset and "event.orders:write" in request.eventpermset %}
<a href="{% url "control:event.orders.checkinlists.reset" organizer=request.event.organizer.slug event=request.event.slug %}"
class="btn btn-default">
<span class="fa fa-repeat"></span>
@@ -100,7 +100,9 @@
<a href="?{% url_replace request 'ordering' '-name' %}"><i class="fa fa-caret-down"></i></a>
<a href="?{% url_replace request 'ordering' 'name' %}"><i class="fa fa-caret-up"></i></a>
</th>
<th>{% trans "Checked in" %}</th>
{% if "event.orders:read" in request.eventpermset %}
<th>{% trans "Checked in" %}</th>
{% endif %}
{% if request.event.has_subevents %}
<th>
{% trans "Date" context "subevent" %}
@@ -119,18 +121,20 @@
<strong><a
href="{% url "control:event.orders.checkinlists.show" organizer=request.event.organizer.slug event=request.event.slug list=cl.id %}">{{ cl.name }}</a></strong>
</td>
<td>
<div class="quotabox availability">
<div class="progress">
<div class="progress-bar progress-bar-success progress-bar-{{ cl.percent }}">
{% if "event.orders:read" in request.eventpermset %}
<td>
<div class="quotabox availability">
<div class="progress">
<div class="progress-bar progress-bar-success progress-bar-{{ cl.percent }}">
</div>
</div>
<div class="numbers">
{{ cl.checkin_count|default_if_none:"0" }} /
{{ cl.position_count|default_if_none:"0" }}
</div>
</div>
<div class="numbers">
{{ cl.checkin_count|default_if_none:"0" }} /
{{ cl.position_count|default_if_none:"0" }}
</div>
</div>
</td>
</td>
{% endif %}
{% if request.event.has_subevents %}
{% if cl.subevent %}
<td>
@@ -156,16 +160,18 @@
{% endif %}
</td>
<td class="text-right flip">
<a href="{% url "control:event.orders.checkinlists.show" organizer=request.event.organizer.slug event=request.event.slug list=cl.id %}"
class="btn btn-default btn-sm"><i class="fa fa-eye"></i></a>
{% if "can_change_event_settings" in request.eventpermset %}
{% if "event.orders:read" in request.eventpermset %}
<a href="{% url "control:event.orders.checkinlists.show" organizer=request.event.organizer.slug event=request.event.slug list=cl.id %}"
class="btn btn-default btn-sm"><i class="fa fa-eye"></i></a>
<a href="{% url "control:event.orders.checkinlists.simulator" organizer=request.event.organizer.slug event=request.event.slug list=cl.id %}"
title="{% trans "Check-in simulator" %}" data-toggle="tooltip"
class="btn btn-default btn-sm"><i class="fa fa-flask"></i></a>
{% endif %}
{% if "event.settings.general:write" in request.eventpermset %}
<a href="{% url "control:event.orders.checkinlists.add" organizer=request.event.organizer.slug event=request.event.slug %}?copy_from={{ cl.id }}"
class="btn btn-sm btn-default" title="{% trans "Clone" %}" data-toggle="tooltip">
<span class="fa fa-copy"></span>
</a>
<a href="{% url "control:event.orders.checkinlists.simulator" organizer=request.event.organizer.slug event=request.event.slug list=cl.id %}"
title="{% trans "Check-in simulator" %}" data-toggle="tooltip"
class="btn btn-default btn-sm"><i class="fa fa-flask"></i></a>
<a href="{% url "control:event.orders.checkinlists.edit" organizer=request.event.organizer.slug event=request.event.slug list=cl.id %}"
class="btn btn-default btn-sm"><i class="fa fa-wrench"></i></a>
<a href="{% url "control:event.orders.checkinlists.delete" organizer=request.event.organizer.slug event=request.event.slug list=cl.id %}"

View File

@@ -9,7 +9,7 @@
{% block inside %}
<h1>
{% blocktrans with name=checkinlist.name %}Check-in list: {{ name }}{% endblocktrans %}
{% if 'can_change_event_settings' in request.eventpermset %}
{% if 'event.settings.general:write' in request.eventpermset %}
<a href="{% url "control:event.orders.checkinlists.edit" event=request.event.slug organizer=request.event.organizer.slug list=checkinlist.pk %}"
class="btn btn-default">
<span class="fa fa-wrench"></span>

View File

@@ -11,18 +11,20 @@
<ul class="list-group">
{% for identifier, display_name, pending, objects in providers %}
<li class="list-group-item">
<form action="{% url "control:event.order.sync_job" organizer=event.organizer.slug event=event.slug code=order.code provider=identifier %}" method="post" class="form-inline pull-right">
{% csrf_token %}
{% if pending %}
{% if pending.not_before > now or pending.need_manual_retry %}
<button type="submit" name="run_job_now" value="{{ pending.pk }}" class="btn btn-default"><i class="fa fa-refresh"></i> {% trans "Retry now" %}</button>
{% if "event.orders:write" in request.eventpermset %}
<form action="{% url "control:event.order.sync_job" organizer=event.organizer.slug event=event.slug code=order.code provider=identifier %}" method="post" class="form-inline pull-right">
{% csrf_token %}
{% if pending %}
{% if pending.not_before > now or pending.need_manual_retry %}
<button type="submit" name="run_job_now" value="{{ pending.pk }}" class="btn btn-default"><i class="fa fa-refresh"></i> {% trans "Retry now" %}</button>
{% endif %}
<button type="submit" name="cancel_job" value="{{ pending.pk }}" class="btn btn-danger"><i class="fa fa-times"></i> {% trans "Cancel" %}</button>
{% else %}
<button type="submit" class="btn btn-default"><i class="fa fa-refresh"></i> {% trans "Sync now" %}</button>
<input type="hidden" name="queue_sync" value="true">
{% endif %}
<button type="submit" name="cancel_job" value="{{ pending.pk }}" class="btn btn-danger"><i class="fa fa-times"></i> {% trans "Cancel" %}</button>
{% else %}
<button type="submit" class="btn btn-default"><i class="fa fa-refresh"></i> {% trans "Sync now" %}</button>
<input type="hidden" name="queue_sync" value="true">
{% endif %}
</form>
</form>
{% endif %}
<p><b>{{ display_name }}</b></p>
{% if pending %}
<p>

View File

@@ -40,12 +40,16 @@
this option.
{% endblocktrans %}
</div>
<div class="col-sm-12 col-md-3">
<a href="{% url "control:event.cancel" organizer=request.organizer.slug event=request.event.slug %}"
class="btn btn-danger btn-block btn-lg">
<span class="fa fa-ban"></span>
{% trans "Cancel event" %}
</a>
<div class="col-sm-12 col-md-3 text-center">
{% if "event:cancel" in request.eventpermset %}
<a href="{% url "control:event.cancel" organizer=request.organizer.slug event=request.event.slug %}"
class="btn btn-danger btn-block btn-lg">
<span class="fa fa-ban"></span>
{% trans "Cancel event" %}
</a>
{% else %}
{% trans "No permission" %}
{% endif %}
</div>
</div>
</div>

View File

@@ -19,7 +19,7 @@
<span class="{% if e.time < nearly_now %}text-muted{% endif %}">
{{ e.entry.description }}
</span>
{% if e.entry.edit_url %}
{% if e.entry.edit_url and e.entry.edit_permission in request.eventpermset %}
&nbsp;
<a href="{{ e.entry.edit_url }}" class="text-muted">
<span class="fa fa-edit"></span>

View File

@@ -155,22 +155,24 @@
</form>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">
{% trans "Event logs" %}
</h3>
</div>
<ul class="list-group" id="logs_target">
<div class="logs-lazy-loading">
<span class="fa fa-cog fa-4x"></span>
{% if "event.orders:read" in request.eventpermset or "event.orders:write" in request.eventpermset or "event.settings.general:write" in request.eventpermset or "event.items:write" in request.eventpermset %}
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">
{% trans "Event logs" %}
</h3>
</div>
<ul class="list-group" id="logs_target">
<div class="logs-lazy-loading">
<span class="fa fa-cog fa-4x"></span>
</div>
</ul>
<div class="panel-footer">
<a href="{% url "control:event.log" event=request.event.slug organizer=request.event.organizer.slug %}">
{% trans "Show more logs" %}
</a>
</div>
</ul>
<div class="panel-footer">
<a href="{% url "control:event.log" event=request.event.slug organizer=request.event.organizer.slug %}">
{% trans "Show more logs" %}
</a>
</div>
</div>
{% endif %}
{% endblock %}

View File

@@ -165,13 +165,15 @@
</p>
</fieldset>
</div>
<div class="form-group submit-group">
<button type="submit" class="btn btn-default btn-lg" name="preview" value="preview" formtarget="_blank">
{% trans "Save and show preview" %}
</button>
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
</div>
{% if "event.settings.invoicing:write" in request.eventpermset %}
<div class="form-group submit-group">
<button type="submit" class="btn btn-default btn-lg" name="preview" value="preview" formtarget="_blank">
{% trans "Save and show preview" %}
</button>
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
</div>
{% endif %}
</form>
{% endblock %}

View File

@@ -41,14 +41,17 @@
{% endfor %}
</td>
<td class="text-right flip">
<a href="{% url 'control:event.settings.payment.provider' event=request.event.slug organizer=request.organizer.slug provider=provider.identifier %}"
class="btn btn-default">
<span class="fa fa-cog"></span>
{% trans "Settings" %}
</a>
{% if "event.settings.payment:write" in request.eventpermset %}
<a href="{% url 'control:event.settings.payment.provider' event=request.event.slug organizer=request.organizer.slug provider=provider.identifier %}"
class="btn btn-default">
<span class="fa fa-cog"></span>
{% trans "Settings" %}
</a>
{% endif %}
</td>
</tr>
{% endfor %}
{% if "event.settings.general:write" in request.eventpermset %}
<tr>
<td colspan="4">
<br>
@@ -58,6 +61,7 @@
</a>
</td>
</tr>
{% endif %}
</tbody>
</table>
@@ -83,10 +87,12 @@
{% bootstrap_field form.payment_explanation layout="control" %}
</fieldset>
</div>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
</div>
{% if "event.settings.payment:write" in request.eventpermset %}
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
</div>
{% endif %}
</form>
{% endblock %}

View File

@@ -23,8 +23,10 @@
{% endblocktrans %}
</p>
<a href="{% url "control:event.settings.tax.add" organizer=request.event.organizer.slug event=request.event.slug %}"
class="btn btn-primary btn-lg"><i class="fa fa-plus"></i> {% trans "Create a new tax rule" %}</a>
{% if "event.settings.tax:write" in request.eventpermset %}
<a href="{% url "control:event.settings.tax.add" organizer=request.event.organizer.slug event=request.event.slug %}"
class="btn btn-primary btn-lg"><i class="fa fa-plus"></i> {% trans "Create a new tax rule" %}</a>
{% endif %}
</div>
{% else %}
<div class="table-responsive">
@@ -42,10 +44,14 @@
{% for tr in taxrules %}
<tr>
<td>
<strong><a
href="{% url "control:event.settings.tax.edit" organizer=request.event.organizer.slug event=request.event.slug rule=tr.id %}">
{{ tr.internal_name|default:tr.name }}
</a></strong>
{% if "event.settings.tax:write" in request.eventpermset %}
<strong><a
href="{% url "control:event.settings.tax.edit" organizer=request.event.organizer.slug event=request.event.slug rule=tr.id %}">
{{ tr.internal_name|default:tr.name }}
</a></strong>
{% else %}
<strong>{{ tr.internal_name|default:tr.name }}</strong>
{% endif %}
</td>
<td>
{% if tr.default %}
@@ -53,7 +59,7 @@
<span class="fa fa-check"></span>
{% trans "Default" %}
</span>
{% else %}
{% elif "event.settings.tax:write" in request.eventpermset %}
<form class="form-inline" method="post"
action="{% url "control:event.settings.tax.default" organizer=request.event.organizer.slug event=request.event.slug rule=tr.id %}">
{% csrf_token %}
@@ -83,10 +89,12 @@
{% endif %}
</td>
<td class="text-right flip">
<a href="{% url "control:event.settings.tax.edit" organizer=request.event.organizer.slug event=request.event.slug rule=tr.id %}"
class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
<a href="{% url "control:event.settings.tax.delete" organizer=request.event.organizer.slug event=request.event.slug rule=tr.id %}"
class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
{% if "event.settings.tax:write" in request.eventpermset %}
<a href="{% url "control:event.settings.tax.edit" organizer=request.event.organizer.slug event=request.event.slug rule=tr.id %}"
class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
<a href="{% url "control:event.settings.tax.delete" organizer=request.event.organizer.slug event=request.event.slug rule=tr.id %}"
class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
{% endif %}
</td>
</tr>
{% endfor %}
@@ -94,9 +102,11 @@
<tfoot>
<tr>
<td colspan="5">
<a href="{% url "control:event.settings.tax.add" organizer=request.event.organizer.slug event=request.event.slug %}"
class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create a new tax rule" %}
</a>
{% if "event.settings.tax:write" in request.eventpermset %}
<a href="{% url "control:event.settings.tax.add" organizer=request.event.organizer.slug event=request.event.slug %}"
class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create a new tax rule" %}
</a>
{% endif %}
</td>
</tr>
</tfoot>
@@ -111,10 +121,12 @@
{% bootstrap_field form.tax_rounding layout="control" %}
{% bootstrap_field form.display_net_prices layout="control" %}
</fieldset>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
</div>
{% if "event.settings.tax:write" in request.eventpermset %}
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
</div>
{% endif %}
</form>
{% endblock %}

View File

@@ -16,14 +16,18 @@
{% endblocktrans %}
</p>
<a href="{% url "control:event.items.categories.add" organizer=request.event.organizer.slug event=request.event.slug %}"
class="btn btn-primary btn-lg"><i class="fa fa-plus"></i> {% trans "Create a new category" %}</a>
{% if 'event.items:write' in request.eventpermset %}
<a href="{% url "control:event.items.categories.add" organizer=request.event.organizer.slug event=request.event.slug %}"
class="btn btn-primary btn-lg"><i class="fa fa-plus"></i> {% trans "Create a new category" %}</a>
{% endif %}
</div>
{% else %}
<p>
<a href="{% url "control:event.items.categories.add" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create a new category" %}
</a>
</p>
{% if 'event.items:write' in request.eventpermset %}
<p>
<a href="{% url "control:event.items.categories.add" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create a new category" %}
</a>
</p>
{% endif %}
<form method="post">
{% csrf_token %}
<div class="table-responsive">
@@ -39,7 +43,11 @@
{% for c in categories %}
<tr data-dnd-id="{{ c.id }}">
<td>
<strong><a href="{% url "control:event.items.categories.edit" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}">{{ c.internal_name|default:c.name }}</a></strong>
{% if 'event.items:write' in request.eventpermset %}
<strong><a href="{% url "control:event.items.categories.edit" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}">{{ c.internal_name|default:c.name }}</a></strong>
{% else %}
<strong>{{ c.internal_name|default:c.name }}</strong>
{% endif %}
<br>
<small class="text-muted">
#{{ c.pk }}
@@ -49,15 +57,17 @@
{{ c.get_category_type_display }}
</td>
<td class="text-right flip">
<button title="{% trans "Move up" %}" formaction="{% url "control:event.items.categories.up" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}" class="btn btn-default btn-sm sortable-up"{% if forloop.counter0 == 0 and not page_obj.has_previous %} disabled{% endif %}><i class="fa fa-arrow-up"></i></button>
<button title="{% trans "Move down" %}" formaction="{% url "control:event.items.categories.down" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}" class="btn btn-default btn-sm sortable-down"{% if forloop.revcounter0 == 0 and not page_obj.has_next %} disabled{% endif %}><i class="fa fa-arrow-down"></i></button>
<span class="dnd-container" title="{% trans "Click and drag this button to reorder. Double click to show buttons for reordering." %}"></span>
<a title="{% trans "Edit" %}" href="{% url "control:event.items.categories.edit" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}" class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
<a href="{% url "control:event.items.categories.add" organizer=request.event.organizer.slug event=request.event.slug %}?copy_from={{ c.id }}"
class="btn btn-sm btn-default" title="{% trans "Clone" %}" data-toggle="tooltip">
<span class="fa fa-copy"></span>
</a>
<a title="{% trans "Delete" %}" href="{% url "control:event.items.categories.delete" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
{% if 'event.items:write' in request.eventpermset %}
<button title="{% trans "Move up" %}" formaction="{% url "control:event.items.categories.up" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}" class="btn btn-default btn-sm sortable-up"{% if forloop.counter0 == 0 and not page_obj.has_previous %} disabled{% endif %}><i class="fa fa-arrow-up"></i></button>
<button title="{% trans "Move down" %}" formaction="{% url "control:event.items.categories.down" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}" class="btn btn-default btn-sm sortable-down"{% if forloop.revcounter0 == 0 and not page_obj.has_next %} disabled{% endif %}><i class="fa fa-arrow-down"></i></button>
<span class="dnd-container" title="{% trans "Click and drag this button to reorder. Double click to show buttons for reordering." %}"></span>
<a title="{% trans "Edit" %}" href="{% url "control:event.items.categories.edit" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}" class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
<a href="{% url "control:event.items.categories.add" organizer=request.event.organizer.slug event=request.event.slug %}?copy_from={{ c.id }}"
class="btn btn-sm btn-default" title="{% trans "Clone" %}" data-toggle="tooltip">
<span class="fa fa-copy"></span>
</a>
<a title="{% trans "Delete" %}" href="{% url "control:event.items.categories.delete" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
{% endif %}
</td>
</tr>
{% endfor %}

View File

@@ -39,15 +39,19 @@
{% endblocktrans %}
</p>
<a href="{% url "control:event.items.discounts.add" organizer=request.event.organizer.slug event=request.event.slug %}"
class="btn btn-primary btn-lg"><i class="fa fa-plus"></i> {% trans "Create a new discount" %}</a>
{% if 'event.items:write' in request.eventpermset %}
<a href="{% url "control:event.items.discounts.add" organizer=request.event.organizer.slug event=request.event.slug %}"
class="btn btn-primary btn-lg"><i class="fa fa-plus"></i> {% trans "Create a new discount" %}</a>
{% endif %}
</div>
{% else %}
<p>
<a href="{% url "control:event.items.discounts.add" organizer=request.event.organizer.slug event=request.event.slug %}"
class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create a new discount" %}
</a>
</p>
{% if 'event.items:write' in request.eventpermset %}
<p>
<a href="{% url "control:event.items.discounts.add" organizer=request.event.organizer.slug event=request.event.slug %}"
class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create a new discount" %}
</a>
</p>
{% endif %}
<form method="post">
{% csrf_token %}
<div class="table-responsive">
@@ -70,8 +74,12 @@
{% else %}
<del>
{% endif %}
<a href="{% url "control:event.items.discounts.edit" organizer=request.event.organizer.slug event=request.event.slug discount=d.id %}">
{% if 'event.items:write' in request.eventpermset %}
<a href="{% url "control:event.items.discounts.edit" organizer=request.event.organizer.slug event=request.event.slug discount=d.id %}">
{{ d.internal_name }}</a>
{% else %}
{{ d.internal_name }}
{% endif %}
{% if d.active %}
</strong>
{% else %}
@@ -134,23 +142,25 @@
</td>
{% endif %}
<td class="text-right flip">
<button formaction="{% url "control:event.items.discounts.up" organizer=request.event.organizer.slug event=request.event.slug discount=d.id %}"
class="btn btn-default btn-sm sortable-up" title="{% trans "Move up" %}"
{% if forloop.counter0 == 0 and not page_obj.has_previous %}
disabled{% endif %}><i class="fa fa-arrow-up"></i></button>
<button formaction="{% url "control:event.items.discounts.down" organizer=request.event.organizer.slug event=request.event.slug discount=d.id %}"
class="btn btn-default btn-sm sortable-down" title="{% trans "Move down" %}"
{% if forloop.revcounter0 == 0 and not page_obj.has_next %} disabled{% endif %}>
<i class="fa fa-arrow-down"></i></button>
<span class="dnd-container" title="{% trans "Click and drag this button to reorder. Double click to show buttons for reordering." %}"></span>
<a href="{% url "control:event.items.discounts.edit" organizer=request.event.organizer.slug event=request.event.slug discount=d.id %}"
class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
<a href="{% url "control:event.items.discounts.add" organizer=request.event.organizer.slug event=request.event.slug %}?copy_from={{ d.id }}"
class="btn btn-sm btn-default" title="{% trans "Clone" %}" data-toggle="tooltip">
<span class="fa fa-copy"></span>
</a>
<a href="{% url "control:event.items.discounts.delete" organizer=request.event.organizer.slug event=request.event.slug discount=d.id %}"
class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
{% if 'event.items:write' in request.eventpermset %}
<button formaction="{% url "control:event.items.discounts.up" organizer=request.event.organizer.slug event=request.event.slug discount=d.id %}"
class="btn btn-default btn-sm sortable-up" title="{% trans "Move up" %}"
{% if forloop.counter0 == 0 and not page_obj.has_previous %}
disabled{% endif %}><i class="fa fa-arrow-up"></i></button>
<button formaction="{% url "control:event.items.discounts.down" organizer=request.event.organizer.slug event=request.event.slug discount=d.id %}"
class="btn btn-default btn-sm sortable-down" title="{% trans "Move down" %}"
{% if forloop.revcounter0 == 0 and not page_obj.has_next %} disabled{% endif %}>
<i class="fa fa-arrow-down"></i></button>
<span class="dnd-container" title="{% trans "Click and drag this button to reorder. Double click to show buttons for reordering." %}"></span>
<a href="{% url "control:event.items.discounts.edit" organizer=request.event.organizer.slug event=request.event.slug discount=d.id %}"
class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
<a href="{% url "control:event.items.discounts.add" organizer=request.event.organizer.slug event=request.event.slug %}?copy_from={{ d.id }}"
class="btn btn-sm btn-default" title="{% trans "Clone" %}" data-toggle="tooltip">
<span class="fa fa-copy"></span>
</a>
<a href="{% url "control:event.items.discounts.delete" organizer=request.event.organizer.slug event=request.event.slug discount=d.id %}"
class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
{% endif %}
</td>
</tr>
{% endfor %}

View File

@@ -21,14 +21,18 @@
{% endblocktrans %}
</p>
<a href="{% url "control:event.items.add" organizer=request.event.organizer.slug event=request.event.slug %}"
class="btn btn-primary btn-lg"><i class="fa fa-plus"></i> {% trans "Create a new product" %}</a>
{% if 'event.items:write' in request.eventpermset %}
<a href="{% url "control:event.items.add" organizer=request.event.organizer.slug event=request.event.slug %}"
class="btn btn-primary btn-lg"><i class="fa fa-plus"></i> {% trans "Create a new product" %}</a>
{% endif %}
</div>
{% else %}
<p>
<a href="{% url "control:event.items.add" organizer=request.event.organizer.slug event=request.event.slug %}"
class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create a new product" %}</a>
</p>
{% if 'event.items:write' in request.eventpermset %}
<p>
<a href="{% url "control:event.items.add" organizer=request.event.organizer.slug event=request.event.slug %}"
class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create a new product" %}</a>
</p>
{% endif %}
<form method="post">
{% csrf_token %}
<div class="table-responsive">
@@ -51,7 +55,9 @@
<tbody>
<tr class="sortable-disabled"><th colspan="9" scope="colgroup" class="text-muted">
{{ c.internal_name|default:c.name }}{% if c.category_type != "normal" %} <span class="font-normal">({{ c.get_category_type_display }})</span>{% endif %}
<a href="{% url "control:event.items.categories.edit" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}" title="{% trans "Edit" %}"><span class="fa fa-edit fa-fw"></span></a>
{% if 'event.items:write' in request.eventpermset %}
<a href="{% url "control:event.items.categories.edit" organizer=request.event.organizer.slug event=request.event.slug category=c.id %}" title="{% trans "Edit" %}"><span class="fa fa-edit fa-fw"></span></a>
{% endif %}
</th></tr>
</tbody>
{% endif %}
@@ -62,7 +68,11 @@
<tr data-dnd-id="{{ i.id }}" {% if not i.active %}class="row-muted"{% endif %}>
<td><strong>
{% if not i.active %}<strike>{% endif %}
<a href="{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=i.id %}">{{ i }}</a>
{% if 'event.items:write' in request.eventpermset %}
<a href="{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=i.id %}">{{ i }}</a>
{% else %}
{{ i }}
{% endif %}
{% if not i.active %}</strike>{% endif %}
</strong>
<br>
@@ -158,12 +168,14 @@
{% endif %}
</td>
<td class="text-right flip col-actions">
<button title="{% trans "Move up" %}" formaction="{% url "control:event.items.up" organizer=request.event.organizer.slug event=request.event.slug item=i.id %}" class="btn btn-default btn-sm sortable-up"{% if forloop.counter0 == 0 %} disabled{% endif %}><i class="fa fa-arrow-up"></i></button>
<button title="{% trans "Move down" %}" formaction="{% url "control:event.items.down" organizer=request.event.organizer.slug event=request.event.slug item=i.id %}" class="btn btn-default btn-sm sortable-down"{% if forloop.revcounter0 == 0 %} disabled{% endif %}><i class="fa fa-arrow-down"></i></button>
<span class="dnd-container" title="{% trans "Click and drag this button to reorder. Double click to show buttons for reordering." %}"></span>
<a href="{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=i.id %}" class="btn btn-default btn-sm" title="{% trans "Edit" %}"><i class="fa fa-edit"></i></a>
<a href="{% url "control:event.items.add" organizer=request.event.organizer.slug event=request.event.slug %}?copy_from={{ i.id }}" class="btn btn-default btn-sm" title="{% trans "Clone" %}" data-toggle="tooltip"><i class="fa fa-copy"></i></a>
<a href="{% url "control:event.items.delete" organizer=request.event.organizer.slug event=request.event.slug item=i.id %}" class="btn btn-danger btn-sm" title="{% trans "Delete" %}"><i class="fa fa-trash"></i></a>
{% if 'event.items:write' in request.eventpermset %}
<button title="{% trans "Move up" %}" formaction="{% url "control:event.items.up" organizer=request.event.organizer.slug event=request.event.slug item=i.id %}" class="btn btn-default btn-sm sortable-up"{% if forloop.counter0 == 0 %} disabled{% endif %}><i class="fa fa-arrow-up"></i></button>
<button title="{% trans "Move down" %}" formaction="{% url "control:event.items.down" organizer=request.event.organizer.slug event=request.event.slug item=i.id %}" class="btn btn-default btn-sm sortable-down"{% if forloop.revcounter0 == 0 %} disabled{% endif %}><i class="fa fa-arrow-down"></i></button>
<span class="dnd-container" title="{% trans "Click and drag this button to reorder. Double click to show buttons for reordering." %}"></span>
<a href="{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=i.id %}" class="btn btn-default btn-sm" title="{% trans "Edit" %}"><i class="fa fa-edit"></i></a>
<a href="{% url "control:event.items.add" organizer=request.event.organizer.slug event=request.event.slug %}?copy_from={{ i.id }}" class="btn btn-default btn-sm" title="{% trans "Clone" %}" data-toggle="tooltip"><i class="fa fa-copy"></i></a>
<a href="{% url "control:event.items.delete" organizer=request.event.organizer.slug event=request.event.slug item=i.id %}" class="btn btn-danger btn-sm" title="{% trans "Delete" %}"><i class="fa fa-trash"></i></a>
{% endif %}
</td>
</tr>
{% endfor %}

View File

@@ -7,45 +7,57 @@
{% block inside %}
<h1>
{% blocktrans with name=question.question %}Question: {{ name }}{% endblocktrans %}
<a href="{% url "control:event.items.questions.edit" event=request.event.slug organizer=request.event.organizer.slug question=question.pk %}"
class="btn btn-default">
<span class="fa fa-edit"></span>
{% trans "Edit question" %}
</a>
{% if 'event.items:write' in request.eventpermset %}
<a href="{% url "control:event.items.questions.edit" event=request.event.slug organizer=request.event.organizer.slug question=question.pk %}"
class="btn btn-default">
<span class="fa fa-edit"></span>
{% trans "Edit question" %}
</a>
{% endif %}
</h1>
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{% trans "Filter" %}</h3>
{% if 'event.orders:read' in request.eventpermset %}
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{% trans "Filter" %}</h3>
</div>
<form class="panel-body filter-form" action="" method="get">
<div class="row">
<div class="col-md-2 col-xs-6">
{% bootstrap_field form.status %}
</div>
<div class="col-md-3 col-xs-6">
{% bootstrap_field form.item %}
</div>
{% if has_subevents %}
<div class="col-md-3 col-xs-6">
{% bootstrap_field form.subevent %}
</div>
<div class="col-md-4 col-xs-6">
{% bootstrap_field form.date_range %}
</div>
{% endif %}
</div>
<div class="text-right">
<button class="btn btn-primary btn-lg" type="submit">
<span class="fa fa-filter"></span>
{% trans "Filter" %}
</button>
</div>
</form>
</div>
<form class="panel-body filter-form" action="" method="get">
<div class="row">
<div class="col-md-2 col-xs-6">
{% bootstrap_field form.status %}
</div>
<div class="col-md-3 col-xs-6">
{% bootstrap_field form.item %}
</div>
{% if has_subevents %}
<div class="col-md-3 col-xs-6">
{% bootstrap_field form.subevent %}
</div>
<div class="col-md-4 col-xs-6">
{% bootstrap_field form.date_range %}
</div>
{% endif %}
</div>
<div class="text-right">
<button class="btn btn-primary btn-lg" type="submit">
<span class="fa fa-filter"></span>
{% trans "Filter" %}
</button>
</div>
</form>
</div>
{% endif %}
<div class="row">
{% if not stats %}
{% if 'event.orders:read' not in request.eventpermset %}
<div class="empty-collection col-md-10 col-xs-12">
<p>
{% blocktrans trimmed %}
No permission to view answers.
{% endblocktrans %}
</p>
</div>
{% elif not stats %}
<div class="empty-collection col-md-10 col-xs-12">
<p>
{% blocktrans trimmed %}

View File

@@ -10,10 +10,12 @@
{% endblocktrans %}
</p>
{% csrf_token %}
<p>
<a href="{% url "control:event.items.questions.add" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create a new question" %}
</a>
</p>
{% if 'event.items:write' in request.eventpermset %}
<p>
<a href="{% url "control:event.items.questions.add" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create a new question" %}
</a>
</p>
{% endif %}
<div class="table-responsive">
<table class="table table-hover table-quotas">
<thead>
@@ -24,7 +26,9 @@
<th class="iconcol"></th>
<th class="iconcol"></th>
<th>{% trans "Products" %}</th>
<th class="action-col-2"></th>
{% if 'event.items:write' in request.eventpermset %}
<th class="action-col-2"></th>
{% endif %}
<th class="action-col-2"></th>
</tr>
</thead>
@@ -79,16 +83,22 @@
<small>{% trans "All personalized products" %}</small>
{% endif %}
</td>
<td class="dnd-container">
</td>
{% if 'event.items:write' in request.eventpermset %}
<td class="dnd-container">
</td>
{% endif %}
<td class="text-right flip">
{% if q.pk %}
<a href="{% url "control:event.items.questions.show" organizer=request.event.organizer.slug event=request.event.slug question=q.id %}" class="btn btn-default btn-sm"><i class="fa fa-bar-chart"></i></a>
<a href="{% url "control:event.items.questions.edit" organizer=request.event.organizer.slug event=request.event.slug question=q.id %}" class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
<a href="{% url "control:event.items.questions.delete" organizer=request.event.organizer.slug event=request.event.slug question=q.id %}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
{% if 'event.items:write' in request.eventpermset %}
<a href="{% url "control:event.items.questions.edit" organizer=request.event.organizer.slug event=request.event.slug question=q.id %}" class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
<a href="{% url "control:event.items.questions.delete" organizer=request.event.organizer.slug event=request.event.slug question=q.id %}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
{% endif %}
{% else %}
<a href="{% url "control:event.settings" organizer=request.event.organizer.slug event=request.event.slug %}#tab-0-2-open"
class="btn btn-default btn-sm"><i class="fa fa-wrench"></i></a>
{% if 'event.settings.general:write' in request.eventpermset %}
<a href="{% url "control:event.settings" organizer=request.event.organizer.slug event=request.event.slug %}#tab-0-2-open"
class="btn btn-default btn-sm"><i class="fa fa-wrench"></i></a>
{% endif %}
{% endif %}
</td>
</tr>

View File

@@ -7,7 +7,7 @@
{% block inside %}
<h1>
{% blocktrans with name=quota.name %}Quota: {{ name }}{% endblocktrans %}
{% if 'can_change_items' in request.eventpermset %}
{% if 'event.items:write' in request.eventpermset %}
<a href="{% url "control:event.items.quotas.edit" event=request.event.slug organizer=request.event.organizer.slug quota=quota.pk %}"
class="btn btn-default">
<span class="fa fa-edit"></span>

View File

@@ -30,14 +30,18 @@
{% endif %}
</p>
<a href="{% url "control:event.items.quotas.add" organizer=request.event.organizer.slug event=request.event.slug %}"
class="btn btn-primary btn-lg"><i class="fa fa-plus"></i> {% trans "Create a new quota" %}</a>
{% if 'event.items:write' in request.eventpermset %}
<a href="{% url "control:event.items.quotas.add" organizer=request.event.organizer.slug event=request.event.slug %}"
class="btn btn-primary btn-lg"><i class="fa fa-plus"></i> {% trans "Create a new quota" %}</a>
{% endif %}
</div>
{% else %}
<p>
<a href="{% url "control:event.items.quotas.add" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create a new quota" %}
</a>
</p>
{% if 'event.items:write' in request.eventpermset %}
<p>
<a href="{% url "control:event.items.quotas.add" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create a new quota" %}
</a>
</p>
{% endif %}
<div class="table-responsive">
<table class="table table-hover table-quotas">
<thead>
@@ -91,12 +95,14 @@
<td>{% if q.size == None %}Unlimited{% else %}{{ q.size }}{% endif %}</td>
<td>{% include "pretixcontrol/items/fragment_quota_availability.html" with availability=q.cached_avail closed=q.closed %}</td>
<td class="text-right flip">
<a href="{% url "control:event.items.quotas.edit" organizer=request.event.organizer.slug event=request.event.slug quota=q.id %}" class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
<a href="{% url "control:event.items.quotas.add" organizer=request.event.organizer.slug event=request.event.slug %}?copy_from={{ q.id }}"
class="btn btn-sm btn-default" title="{% trans "Clone" %}" data-toggle="tooltip">
<span class="fa fa-copy"></span>
</a>
<a href="{% url "control:event.items.quotas.delete" organizer=request.event.organizer.slug event=request.event.slug quota=q.id %}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
{% if 'event.items:write' in request.eventpermset %}
<a href="{% url "control:event.items.quotas.edit" organizer=request.event.organizer.slug event=request.event.slug quota=q.id %}" class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
<a href="{% url "control:event.items.quotas.add" organizer=request.event.organizer.slug event=request.event.slug %}?copy_from={{ q.id }}"
class="btn btn-sm btn-default" title="{% trans "Clone" %}" data-toggle="tooltip">
<span class="fa fa-copy"></span>
</a>
<a href="{% url "control:event.items.quotas.delete" organizer=request.event.organizer.slug event=request.event.slug quota=q.id %}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
{% endif %}
</td>
</tr>
{% endfor %}

View File

@@ -26,7 +26,7 @@
{% endif %}
{% include "pretixcontrol/orders/fragment_order_status.html" with order=order class="pull-right flip" %}
</h1>
{% if 'can_change_orders' in request.eventpermset %}
{% if 'event.orders:write' in request.eventpermset %}
<form action="{% url "control:event.order.transition" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}"
method="post">
{% csrf_token %}
@@ -193,7 +193,7 @@
<dt>{% trans "Order locale" %}</dt>
<dd>
{{ display_locale }}
{% if "can_change_orders" in request.eventpermset %}
{% if "event.orders:write" in request.eventpermset %}
<a href="{% url "control:event.order.locale" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}" class="btn btn-default btn-xs">
<span class="fa fa-edit"></span>
</a>
@@ -220,7 +220,7 @@
{{ order.customer.identifier }} {{ order.customer.email }}
</a>
{% endif %}
{% if "can_change_orders" in request.eventpermset %}
{% if "event.orders:write" in request.eventpermset %}
<a href="{% url "control:event.order.contact" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}" class="btn btn-default btn-xs">
<span class="fa fa-edit"></span>
</a>
@@ -233,7 +233,7 @@
{% if order.email and order.email_known_to_work %}
<span class="fa fa-check-circle text-success" data-toggle="tooltip" title="{% trans "We know that this email address works because the user clicked a link we sent them." %}"></span>
{% endif %}
{% if "can_change_orders" in request.eventpermset %}
{% if "event.orders:write" in request.eventpermset %}
<a href="{% url "control:event.order.contact" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}" class="btn btn-default btn-xs">
<span class="fa fa-edit"></span>
</a>
@@ -257,7 +257,7 @@
<dt>{% trans "Phone number" %}</dt>
<dd>
{{ order.phone|default_if_none:""|phone_format }}
{% if "can_change_orders" in request.eventpermset %}
{% if "event.orders:write" in request.eventpermset %}
<a href="{% url "control:event.order.contact" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}" class="btn btn-default btn-xs">
<span class="fa fa-edit"></span>
</a>
@@ -319,7 +319,7 @@
<span class="fa fa-check text-success fa-stack-1x fa-stack-shifted"></span>
</span>
{% endif %}
{% if i.transmission_status != "inflight" %}
{% if i.transmission_status != "inflight" and "event.orders:write" in request.eventpermset %}
<form class="form-inline helper-display-inline" method="post"
action="{% url "control:event.order.retransmitinvoice" event=request.event.slug organizer=request.event.organizer.slug code=order.code id=i.pk %}">
{% csrf_token %}
@@ -334,7 +334,7 @@
</form>
{% endif %}
{% if not i.canceled %}
{% if i.regenerate_allowed %}
{% if i.regenerate_allowed and "event.orders:write" in request.eventpermset %}
<form class="form-inline helper-display-inline" method="post"
action="{% url "control:event.order.regeninvoice" event=request.event.slug organizer=request.event.organizer.slug code=order.code id=i.pk %}">
{% csrf_token %}
@@ -344,7 +344,7 @@
</button>
</form>
{% endif %}
{% if not i.is_cancellation %}
{% if not i.is_cancellation and "event.orders:write" in request.eventpermset %}
<form class="form-inline helper-display-inline" method="post"
action="{% url "control:event.order.reissueinvoice" event=request.event.slug organizer=request.event.organizer.slug code=order.code id=i.pk %}">
{% csrf_token %}
@@ -371,7 +371,7 @@
<br/>
{% endif %}
{% endfor %}
{% if can_generate_invoice and 'can_change_orders' in request.eventpermset %}
{% if can_generate_invoice and 'event.orders:write' in request.eventpermset %}
<br/>
<form class="form-inline helper-display-inline" method="post"
action="{% url "control:event.order.geninvoice" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}">
@@ -382,7 +382,7 @@
</form>
{% endif %}
</dd>
{% elif can_generate_invoice and 'can_change_orders' in request.eventpermset %}
{% elif can_generate_invoice and 'event.orders:write' in request.eventpermset %}
<dt>{% trans "Invoices" %}</dt>
<dd>
<form class="form-inline helper-display-inline" method="post"
@@ -400,7 +400,7 @@
<div class="panel panel-default items">
<div class="panel-heading">
<div class="pull-right flip">
{% if 'can_change_orders' in request.eventpermset %}
{% if 'event.orders:write' in request.eventpermset %}
<a href="{% url "control:event.order.info" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}">
<span class="fa fa-edit"></span>
{% trans "Change answers" %}
@@ -893,7 +893,7 @@
{% endfor %}
</tbody>
</table>
{% if order.payment_refund_sum > 0 and "can_change_orders" in request.eventpermset %}
{% if order.payment_refund_sum > 0 and "event.orders:write" in request.eventpermset %}
<a href="{% url "control:event.order.refunds.start" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}" class="btn btn-default">
{% trans "Create a refund" %}
</a>
@@ -1012,7 +1012,7 @@
<div class="panel panel-default">
<div class="panel-heading">
<div class="pull-right flip">
{% if 'can_change_orders' in request.eventpermset %}
{% if 'event.orders:write' in request.eventpermset %}
<a href="{% url "control:event.order.info" event=request.event.slug organizer=request.event.organizer.slug code=order.code %}">
<span class="fa fa-edit"></span>
{% trans "Change" %}
@@ -1088,7 +1088,7 @@
{% bootstrap_field comment_form.custom_followup_at %}
{% bootstrap_field comment_form.checkin_attention show_help=True show_label=False %}
{% bootstrap_field comment_form.checkin_text show_help=True show_label=False %}
{% if "can_change_orders" in request.eventpermset %}
{% if "event.orders:write" in request.eventpermset %}
<button class="btn btn-default">
{% trans "Update comment" %}
</button>

View File

@@ -34,7 +34,7 @@
{% if s.export_verbose_name == "?" %}
<strong class="text-danger">
<span class="fa fa-warning fa-fw"></span>
{% trans "Exporter not found" %}
{% trans "Exporter not found or no permission" %}
</strong>
{% elif s.error_counter >= 5 %}
<strong class="text-danger">
@@ -115,5 +115,9 @@
</a>
{% endfor %}
</div>
{% empty %}
<p class="empty-collection">
{% trans "There are no exporters available for you." %}
</p>
{% endfor %}
{% endblock %}

View File

@@ -39,16 +39,18 @@
</fieldset>
{% if schedule_form %}
{% include "pretixcontrol/orders/fragment_export_schedule_form.html" %}
<div class="form-group submit-group">
<button formaction="{{ request.get_full_path }}" name="schedule" value="save" type="submit"
class="btn btn-primary btn-save" data-no-asynctask>
{% if scheduled_copy_from %}
{% trans "Save copy" %}
{% else %}
{% trans "Save" %}
{% endif %}
</button>
</div>
{% if not no_save %}
<div class="form-group submit-group">
<button formaction="{{ request.get_full_path }}" name="schedule" value="save" type="submit"
class="btn btn-primary btn-save" data-no-asynctask>
{% if scheduled_copy_from %}
{% trans "Save copy" %}
{% else %}
{% trans "Save" %}
{% endif %}
</button>
</div>
{% endif %}
{% else %}
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">

View File

@@ -122,7 +122,7 @@
<table class="table table-condensed table-hover table-orders">
<thead>
<tr>
{% if "can_change_orders" in request.eventpermset %}
{% if "event.orders:write" in request.eventpermset %}
<th>
<label aria-label="{% trans "select all rows for batch-operation" %}"
class="batch-select-label"><input type="checkbox" data-toggle-table/></label>
@@ -154,7 +154,7 @@
<a href="?{% url_replace request 'ordering' 'status' %}"><i class="fa fa-caret-up"></i></a>
</th>
</tr>
{% if page_obj.paginator.num_pages > 1 and "can_change_orders" in request.eventpermset %}
{% if page_obj.paginator.num_pages > 1 and "event.orders:write" in request.eventpermset %}
<tr class="table-select-all warning hidden">
<td>
<input type="checkbox" name="__ALL" id="__all"
@@ -171,7 +171,7 @@
<tbody>
{% for o in orders %}
<tr>
{% if "can_change_orders" in request.eventpermset %}
{% if "event.orders:write" in request.eventpermset %}
<td>
<label aria-label="{% trans "select row for batch-operation" %}"
class="batch-select-label"><input type="checkbox" name="order"
@@ -281,7 +281,7 @@
{% endif %}
</table>
</div>
{% if "can_change_orders" in request.eventpermset %}
{% if "event.orders:write" in request.eventpermset %}
<div class="batch-select-actions">
<div class="btn-group dropup">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown"

View File

@@ -100,28 +100,30 @@
{{ r.amount|money:request.event.currency }}
</td>
<td class="text-right flip">
{% if r.state == "transit" or r.state == "created" %}
<a href="{% url "control:event.order.refunds.cancel" event=request.event.slug organizer=request.event.organizer.slug code=r.order.code refund=r.pk %}?next={{ request.get_full_path|urlencode }}"
class="btn btn-danger btn-xs" data-toggle="tooltip">
<span class="fa fa-times"></span>
{% trans "Cancel" %}
</a>
<a href="{% url "control:event.order.refunds.done" event=request.event.slug organizer=request.event.organizer.slug code=r.order.code refund=r.pk %}?next={{ request.get_full_path|urlencode }}"
class="btn btn-primary btn-xs" data-toggle="tooltip">
<span class="fa fa-check"></span>
{% trans "Confirm as done" %}
</a>
{% if "event.orders:write" in request.eventpermset %}
{% if r.state == "transit" or r.state == "created" %}
<a href="{% url "control:event.order.refunds.cancel" event=request.event.slug organizer=request.event.organizer.slug code=r.order.code refund=r.pk %}?next={{ request.get_full_path|urlencode }}"
class="btn btn-danger btn-xs" data-toggle="tooltip">
<span class="fa fa-times"></span>
{% trans "Cancel" %}
</a>
<a href="{% url "control:event.order.refunds.done" event=request.event.slug organizer=request.event.organizer.slug code=r.order.code refund=r.pk %}?next={{ request.get_full_path|urlencode }}"
class="btn btn-primary btn-xs" data-toggle="tooltip">
<span class="fa fa-check"></span>
{% trans "Confirm as done" %}
</a>
{% elif r.state == "external" %}
<a href="{% url "control:event.order.refunds.cancel" event=request.event.slug organizer=request.event.organizer.slug code=r.order.code refund=r.pk %}?next={{ request.get_full_path|urlencode }}"
class="btn btn-default btn-xs" data-toggle="tooltip">
<span class="fa fa-times"></span>
{% trans "Ignore" %}
</a>
<a href="{% url "control:event.order.refunds.process" event=request.event.slug organizer=request.event.organizer.slug code=r.order.code refund=r.pk %}?next={{ request.get_full_path|urlencode }}"
class="btn btn-primary btn-xs" data-toggle="tooltip">
<span class="fa fa-check"></span>
{% trans "Process refund" %}
</a>
<a href="{% url "control:event.order.refunds.cancel" event=request.event.slug organizer=request.event.organizer.slug code=r.order.code refund=r.pk %}?next={{ request.get_full_path|urlencode }}"
class="btn btn-default btn-xs" data-toggle="tooltip">
<span class="fa fa-times"></span>
{% trans "Ignore" %}
</a>
<a href="{% url "control:event.order.refunds.process" event=request.event.slug organizer=request.event.organizer.slug code=r.order.code refund=r.pk %}?next={{ request.get_full_path|urlencode }}"
class="btn btn-primary btn-xs" data-toggle="tooltip">
<span class="fa fa-check"></span>
{% trans "Process refund" %}
</a>
{% endif %}
{% endif %}
</td>
</tr>

View File

@@ -93,16 +93,18 @@
{% endif %}
</dl>
</form>
<div class="text-right">
<a href="{% url "control:organizer.customer.edit" organizer=request.organizer.slug customer=customer.identifier %}"
class="btn btn-default">
<i class="fa fa-edit"></i> {% trans "Edit" %}
</a>
<a href="{% url "control:organizer.customer.anonymize" organizer=request.organizer.slug customer=customer.identifier %}"
class="btn btn-danger">
<i class="fa fa-trash"></i> {% trans "Anonymize" %}
</a>
</div>
{% if "organizer.customers:write" in request.orgapermset %}
<div class="text-right">
<a href="{% url "control:organizer.customer.edit" organizer=request.organizer.slug customer=customer.identifier %}"
class="btn btn-default">
<i class="fa fa-edit"></i> {% trans "Edit" %}
</a>
<a href="{% url "control:organizer.customer.anonymize" organizer=request.organizer.slug customer=customer.identifier %}"
class="btn btn-danger">
<i class="fa fa-trash"></i> {% trans "Anonymize" %}
</a>
</div>
{% endif %}
</div>
</div>
<div class="panel panel-default items">
@@ -162,35 +164,39 @@
</div>
</td>
<td class="text-right flip">
<a href="{% url "control:organizer.customer.membership.edit" organizer=request.organizer.slug customer=customer.identifier id=m.pk %}"
data-toggle="tooltip"
title="{% trans "Edit" %}"
class="btn btn-default">
<i class="fa fa-edit"></i>
</a>
{% if m.testmode %}
<a href="{% url "control:organizer.customer.membership.delete" organizer=request.organizer.slug customer=customer.identifier id=m.pk %}"
{% if "organizer.customers:write" in request.orgapermset %}
<a href="{% url "control:organizer.customer.membership.edit" organizer=request.organizer.slug customer=customer.identifier id=m.pk %}"
data-toggle="tooltip"
title="{% trans "Delete" %}"
class="btn btn-danger">
<i class="fa fa-trash"></i>
title="{% trans "Edit" %}"
class="btn btn-default">
<i class="fa fa-edit"></i>
</a>
{% if m.testmode %}
<a href="{% url "control:organizer.customer.membership.delete" organizer=request.organizer.slug customer=customer.identifier id=m.pk %}"
data-toggle="tooltip"
title="{% trans "Delete" %}"
class="btn btn-danger">
<i class="fa fa-trash"></i>
</a>
{% endif %}
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
<tfoot>
<tr>
<td colspan="7">
<a href="{% url "control:organizer.customer.membership.add" organizer=request.organizer.slug customer=customer.identifier %}"
class="btn btn-default">
<i class="fa fa-plus"></i>
{% trans "Add membership" %}
</a>
</td>
</tr>
</tfoot>
{% if "organizer.customers:write" in request.orgapermset %}
<tfoot>
<tr>
<td colspan="7">
<a href="{% url "control:organizer.customer.membership.add" organizer=request.organizer.slug customer=customer.identifier %}"
class="btn btn-default">
<i class="fa fa-plus"></i>
{% trans "Add membership" %}
</a>
</td>
</tr>
</tfoot>
{% endif %}
</table>
</div>
<div class="panel panel-default items">
@@ -300,14 +306,18 @@
{% for gc in gift_cards %}
<tr>
<td>
<a href="{% url "control:organizer.giftcard" organizer=organizer.slug giftcard=gc.id %}">
<strong>{{ gc.secret }}</strong></a>
{% if gc.testmode %}
<span class="label label-warning">{% trans "TEST MODE" %}</span>
{% endif %}
{% if gc.expired %}
<span class="label label-danger">{% trans "Expired" %}</span>
{% endif %}
{% if "organizer.giftcards:read" in request.orgapermset %}
<a href="{% url "control:organizer.giftcard" organizer=organizer.slug giftcard=gc.id %}">
<strong>{{ gc.secret }}</strong></a>
{% else %}
<strong>{{ gc.secret|slice:":3" }}…</strong>
{% endif %}
{% if gc.testmode %}
<span class="label label-warning">{% trans "TEST MODE" %}</span>
{% endif %}
{% if gc.expired %}
<span class="label label-danger">{% trans "Expired" %}</span>
{% endif %}
</td>
<td>{{ gc.issuance|date:"SHORT_DATETIME_FORMAT" }}</td>
<td>{% if gc.expires %}{{ gc.expires|date:"SHORT_DATETIME_FORMAT" }}{% endif %}</td>
@@ -316,10 +326,12 @@
<p class="text-right">{{ gc.value|money:gc.currency }}</p>
</td>
<td class="text-right">
<a href="{% url "control:organizer.giftcard" organizer=organizer.slug giftcard=gc.id %}"
class="btn btn-default btn-sm" data-toggle="tooltip" title="{% trans "Details" %}">
<i class="fa fa-eye"></i>
</a>
{% if "organizer.giftcards:read" in request.orgapermset %}
<a href="{% url "control:organizer.giftcard" organizer=organizer.slug giftcard=gc.id %}"
class="btn btn-default btn-sm" data-toggle="tooltip" title="{% trans "Details" %}">
<i class="fa fa-eye"></i>
</a>
{% endif %}
</td>
</tr>
{% endfor %}

View File

@@ -15,8 +15,10 @@
No customer accounts have been created yet.
{% endblocktrans %}
</p>
<a href="{% url "control:organizer.customer.create" organizer=request.organizer.slug %}"
class="btn btn-primary btn-lg"><i class="fa fa-plus"></i> {% trans "Create a new customer" %}</a>
{% if "organizer.customers:write" in request.orgapermset %}
<a href="{% url "control:organizer.customer.create" organizer=request.organizer.slug %}"
class="btn btn-primary btn-lg"><i class="fa fa-plus"></i> {% trans "Create a new customer" %}</a>
{% endif %}
</div>
{% else %}
<div class="panel panel-default">
@@ -43,10 +45,12 @@
</div>
</form>
</div>
<p>
<a href="{% url "control:organizer.customer.create" organizer=request.organizer.slug %}"
class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create a new customer" %}</a>
</p>
{% if "organizer.customers:write" in request.orgapermset %}
<p>
<a href="{% url "control:organizer.customer.create" organizer=request.organizer.slug %}"
class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create a new customer" %}</a>
</p>
{% endif %}
<div class="table-responsive">
<table class="table table-condensed table-hover">
<thead>

View File

@@ -7,7 +7,7 @@
{% blocktrans with name=request.organizer.name %}Organizer: {{ name }}{% endblocktrans %}
</h1>
{% if events|length == 0 and not filter_form.filtered %}
{% if "can_create_events" in request.orgapermset %}
{% if "organizer.events:create" in request.orgapermset %}
<p>
<a href="{% url "control:events.add" %}?organizer={{ request.organizer.slug }}" class="btn btn-primary">
<span class="fa fa-plus"></span>
@@ -51,7 +51,7 @@
</div>
</form>
</div>
{% if "can_create_events" in request.orgapermset %}
{% if "organizer.events:create" in request.orgapermset %}
<p>
<a href="{% url "control:events.add" %}?organizer={{ request.organizer.slug }}" class="btn btn-primary">
<span class="fa fa-plus"></span>
@@ -147,7 +147,7 @@
data-toggle="tooltip">
<span class="fa fa-eye"></span>
</a>
{% if "can_create_events" in request.orgapermset %}
{% if "organizer.events:create" in request.orgapermset %}
<a href="{% url "control:events.add" %}?clone={{ e.pk }}" class="btn btn-sm btn-default"
title="{% trans "Clone event" %}" data-toggle="tooltip">
<span class="fa fa-copy"></span>

View File

@@ -51,10 +51,12 @@
</div>
</form>
</div>
<p>
<a href="{% url "control:organizer.device.add" organizer=request.organizer.slug %}"
class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Connect a device" %}</a>
</p>
{% if "organizer.devices:write" in request.orgapermset %}
<p>
<a href="{% url "control:organizer.device.add" organizer=request.organizer.slug %}"
class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Connect a device" %}</a>
</p>
{% endif %}
<form action="{% url "control:organizer.device.bulk_edit" organizer=request.organizer.slug %}" method="post">
{% csrf_token %}
{% for field in filter_form %}
@@ -64,10 +66,12 @@
<table class="table table-condensed table-hover table-quotas">
<thead>
<tr>
<th>
<label aria-label="{% trans "select all rows for batch-operation" %}"
class="batch-select-label"><input type="checkbox" data-toggle-table/></label>
</th>
{% if "organizer.devices:write" in request.orgapermset %}
<th>
<label aria-label="{% trans "select all rows for batch-operation" %}"
class="batch-select-label"><input type="checkbox" data-toggle-table/></label>
</th>
{% endif %}
<th>{% trans "Device ID" %}
<a href="?{% url_replace request 'ordering' '-device_id' %}"><i
class="fa fa-caret-down"></i></a>
@@ -105,12 +109,14 @@
<tbody>
{% for d in devices %}
<tr {% if d.revoked %}class="text-muted"{% endif %}>
<td>
<label aria-label="{% trans "select row for batch-operation" %}"
class="batch-select-label"><input type="checkbox" name="device"
class="batch-select-checkbox"
value="{{ d.pk }}"/></label>
</td>
{% if "organizer.devices:write" in request.orgapermset %}
<td>
<label aria-label="{% trans "select row for batch-operation" %}"
class="batch-select-label"><input type="checkbox" name="device"
class="batch-select-checkbox"
value="{{ d.pk }}"/></label>
</td>
{% endif %}
<td>
{{ d.device_id }}
</td>
@@ -158,15 +164,17 @@
{% endif %}
</td>
<td class="text-right flip">
{% if not d.initialized %}
<a href="{% url "control:organizer.device.connect" organizer=request.organizer.slug device=d.id %}"
class="btn btn-primary btn-sm"><i class="fa fa-link"></i>
{% trans "Connect" %}</a>
{% endif %}
{% if not d.initialized or d.api_token %}
<a href="{% url "control:organizer.device.revoke" organizer=request.organizer.slug device=d.id %}"
class="btn btn-default btn-sm">
{% trans "Revoke access" %}</a>
{% if "organizer.devices:write" in request.orgapermset %}
{% if not d.initialized %}
<a href="{% url "control:organizer.device.connect" organizer=request.organizer.slug device=d.id %}"
class="btn btn-primary btn-sm"><i class="fa fa-link"></i>
{% trans "Connect" %}</a>
{% endif %}
{% if not d.initialized or d.api_token %}
<a href="{% url "control:organizer.device.revoke" organizer=request.organizer.slug device=d.id %}"
class="btn btn-default btn-sm">
{% trans "Revoke access" %}</a>
{% endif %}
{% endif %}
{% if d.initialized %}
<a href="{% url "control:organizer.device.logs" organizer=request.organizer.slug device=d.id %}"
@@ -175,19 +183,23 @@
{% trans "Logs" %}
</a>
{% endif %}
<a href="{% url "control:organizer.device.edit" organizer=request.organizer.slug device=d.id %}"
class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
{% if "organizer.devices:write" in request.orgapermset %}
<a href="{% url "control:organizer.device.edit" organizer=request.organizer.slug device=d.id %}"
class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="batch-select-actions">
<button type="submit" class="btn btn-primary btn-save" name="action" value="edit">
<i class="fa fa-edit"></i>{% trans "Edit selected" %}
</button>
</div>
{% if "organizer.devices:write" in request.orgapermset %}
<div class="batch-select-actions">
<button type="submit" class="btn btn-primary btn-save" name="action" value="edit">
<i class="fa fa-edit"></i>{% trans "Edit selected" %}
</button>
</div>
{% endif %}
</form>
{% include "pretixcontrol/pagination.html" %}
{% endif %}

View File

@@ -34,7 +34,7 @@
{% if s.export_verbose_name == "?" %}
<strong class="text-danger">
<span class="fa fa-warning fa-fw"></span>
{% trans "Exporter not found" %}
{% trans "Exporter not found or no permission" %}
</strong>
{% elif s.error_counter >= 5 %}
<strong class="text-danger">
@@ -115,5 +115,9 @@
</a>
{% endfor %}
</div>
{% empty %}
<p class="empty-collection">
{% trans "There are no exporters available for you." %}
</p>
{% endfor %}
{% endblock %}

View File

@@ -40,16 +40,18 @@
</fieldset>
{% if schedule_form %}
{% include "pretixcontrol/orders/fragment_export_schedule_form.html" %}
<div class="form-group submit-group">
<button formaction="{{ request.get_full_path }}" name="schedule" value="save" type="submit"
class="btn btn-primary btn-save" data-no-asynctask>
{% if scheduled_copy_from %}
{% trans "Save copy" %}
{% else %}
{% trans "Save" %}
{% endif %}
</button>
</div>
{% if not no_save %}
<div class="form-group submit-group">
<button formaction="{{ request.get_full_path }}" name="schedule" value="save" type="submit"
class="btn btn-primary btn-save" data-no-asynctask>
{% if scheduled_copy_from %}
{% trans "Save copy" %}
{% else %}
{% trans "Save" %}
{% endif %}
</button>
</div>
{% endif %}
{% else %}
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">

View File

@@ -6,10 +6,12 @@
<p>
{% trans "The list below shows gates that you can use to group check-in devices." %}
</p>
<a href="{% url "control:organizer.gate.add" organizer=request.organizer.slug %}" class="btn btn-default">
<span class="fa fa-plus"></span>
{% trans "Create a new gate" %}
</a>
{% if "organizer.devices:write" in request.orgapermset %}
<a href="{% url "control:organizer.gate.add" organizer=request.organizer.slug %}" class="btn btn-default">
<span class="fa fa-plus"></span>
{% trans "Create a new gate" %}
</a>
{% endif %}
<table class="table table-condensed table-hover">
<thead>
<tr>
@@ -21,15 +23,21 @@
{% for g in gates %}
<tr>
<td><strong>
<a href="{% url "control:organizer.gate.edit" organizer=request.organizer.slug gate=g.id %}">
{% if "organizer.devices:write" in request.orgapermset %}
<a href="{% url "control:organizer.gate.edit" organizer=request.organizer.slug gate=g.id %}">
{{ g.name }}
</a>
{% else %}
{{ g.name }}
</a>
{% endif %}
</strong></td>
<td class="text-right flip">
<a href="{% url "control:organizer.gate.edit" organizer=request.organizer.slug gate=g.id %}"
class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
<a href="{% url "control:organizer.gate.delete" organizer=request.organizer.slug gate=g.id %}"
class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
{% if "organizer.devices:write" in request.orgapermset %}
<a href="{% url "control:organizer.gate.edit" organizer=request.organizer.slug gate=g.id %}"
class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
<a href="{% url "control:organizer.gate.delete" organizer=request.organizer.slug gate=g.id %}"
class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
{% endif %}
</td>
</tr>
{% endfor %}

View File

@@ -10,10 +10,12 @@
{% if card.testmode %}
<span class="label label-warning">{% trans "TEST MODE" %}</span>
{% endif %}
<a href="{% url "control:organizer.giftcard.edit" organizer=request.organizer.slug giftcard=card.id %}"
class="btn btn-default">
<i class="fa fa-edit"></i> {% trans "Edit" %}
</a>
{% if "organizer.giftcards:write" in request.orgapermset %}
<a href="{% url "control:organizer.giftcard.edit" organizer=request.organizer.slug giftcard=card.id %}"
class="btn btn-default">
<i class="fa fa-edit"></i> {% trans "Edit" %}
</a>
{% endif %}
</h1>
<div class="row">
<div class="col-md-10 col-xs-12">
@@ -112,22 +114,24 @@
</tr>
{% endfor %}
</tbody>
<tfoot>
<tr>
<td></td>
<td>
<input type="text" class="form-control helper-display-block" placeholder="{% trans "Text" %}"
name="text">
</td>
<td class="text-right form-inline">
<input type="text" class="form-control input-sm" placeholder="{% trans "Value" %}" name="value">
<button type="submit" class="btn btn-primary">
<span class="fa fa-plus"></span>
</button>
</td>
{% if "organizer.giftcards:write" in request.orgapermset %}
<tfoot>
<tr>
<td></td>
<td>
<input type="text" class="form-control helper-display-block" placeholder="{% trans "Text" %}"
name="text">
</td>
<td class="text-right form-inline">
<input type="text" class="form-control input-sm" placeholder="{% trans "Value" %}" name="value">
<button type="submit" class="btn btn-primary">
<span class="fa fa-plus"></span>
</button>
</td>
</tr>
</tfoot>
</tr>
</tfoot>
{% endif %}
</table>
</form>
</div>

View File

@@ -15,10 +15,11 @@
or you can manually issue gift cards.
{% endblocktrans %}
</p>
<a href="{% url "control:organizer.giftcard.add" organizer=request.organizer.slug %}"
class="btn btn-default btn-lg"><i class="fa fa-plus"></i> {% trans "Manually issue a gift card" %}
</a>
{% if "organizer.giftcards:write" in request.orgapermset %}
<a href="{% url "control:organizer.giftcard.add" organizer=request.organizer.slug %}"
class="btn btn-default btn-lg"><i class="fa fa-plus"></i> {% trans "Manually issue a gift card" %}
</a>
{% endif %}
</div>
{% else %}
<div class="panel panel-default">
@@ -45,10 +46,12 @@
</div>
</form>
</div>
<p>
<a href="{% url "control:organizer.giftcard.add" organizer=request.organizer.slug %}"
class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Manually issue a gift card" %}</a>
</p>
{% if "organizer.giftcards:write" in request.orgapermset %}
<p>
<a href="{% url "control:organizer.giftcard.add" organizer=request.organizer.slug %}"
class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Manually issue a gift card" %}</a>
</p>
{% endif %}
<div class="table-responsive">
<table class="table table-condensed table-hover">
<thead>

View File

@@ -15,8 +15,10 @@
No media have been created yet.
{% endblocktrans %}
</p>
<a href="{% url "control:organizer.reusable_medium.create" organizer=request.organizer.slug %}"
class="btn btn-primary btn-lg"><i class="fa fa-plus"></i> {% trans "Create a new medium" %}</a>
{% if "organizer.reusablemedia:write" in request.orgapermset %}
<a href="{% url "control:organizer.reusable_medium.create" organizer=request.organizer.slug %}"
class="btn btn-primary btn-lg"><i class="fa fa-plus"></i> {% trans "Create a new medium" %}</a>
{% endif %}
</div>
{% else %}
<div class="panel panel-default">
@@ -40,10 +42,12 @@
</div>
</form>
</div>
<p>
<a href="{% url "control:organizer.reusable_medium.create" organizer=request.organizer.slug %}"
class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create a new medium" %}</a>
</p>
{% if "organizer.reusablemedia:write" in request.orgapermset %}
<p>
<a href="{% url "control:organizer.reusable_medium.create" organizer=request.organizer.slug %}"
class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create a new medium" %}</a>
</p>
{% endif %}
<div class="table-responsive">
<table class="table table-condensed table-hover">
<thead>
@@ -77,9 +81,13 @@
{% if m.customer %}
<span class="helper-display-block">
<span class="fa fa-user fa-fw"></span>
<a href="{% url "control:organizer.customer" organizer=request.organizer.slug customer=m.customer.identifier %}">
{% if "organizer.customers:read" in request.orgapermset %}
<a href="{% url "control:organizer.customer" organizer=request.organizer.slug customer=m.customer.identifier %}">
{{ m.customer }}
</a>
{% else %}
{{ m.customer }}
</a>
{% endif %}
</span>
{% endif %}
{% if m.linked_orderposition %}
@@ -92,8 +100,12 @@
{% if m.linked_giftcard %}
<span class="helper-display-block">
<span class="fa fa-credit-card fa-fw"></span>
<a href="{% url "control:organizer.giftcard" organizer=request.organizer.slug giftcard=m.linked_giftcard.id %}">
{{ m.linked_giftcard.secret }}</a>
{% if "organizer.giftcards:read" in request.orgapermset %}
<a href="{% url "control:organizer.giftcard" organizer=request.organizer.slug giftcard=m.linked_giftcard.id %}">
{{ m.linked_giftcard.secret }}</a>
{% else %}
{{ m.linked_giftcard.secret|slice:":3" }}…
{% endif %}
</span>
{% endif %}
</td>

View File

@@ -22,60 +22,68 @@
</h3>
</div>
<div class="panel-body">
<form action="" method="post">
{% csrf_token %}
<dl class="dl-horizontal">
<dt>{% trans "Media type" context "reusable_media" %}</dt>
<dd>{{ medium.get_type_display }}</dd>
<dt>{% trans "Identifier" context "reusable_media" %}</dt>
<dd><code>{{ medium.identifier }}</code></dd>
<dt>{% trans "Status" %}</dt>
<dd>
{% if not medium.active %}
{% trans "disabled" %}
{% elif medium.is_expired %}
{% trans "expired" %}
{% else %}
{% trans "active" %}
{% endif %}
</dd>
<dt>{% trans "Connections" context "reusable_media" %}</dt>
<dd>
{% if medium.customer %}
<span class="helper-display-block">
<span class="fa fa-user fa-fw"></span>
<dl class="dl-horizontal">
<dt>{% trans "Media type" context "reusable_media" %}</dt>
<dd>{{ medium.get_type_display }}</dd>
<dt>{% trans "Identifier" context "reusable_media" %}</dt>
<dd><code>{{ medium.identifier }}</code></dd>
<dt>{% trans "Status" %}</dt>
<dd>
{% if not medium.active %}
{% trans "disabled" %}
{% elif medium.is_expired %}
{% trans "expired" %}
{% else %}
{% trans "active" %}
{% endif %}
</dd>
<dt>{% trans "Connections" context "reusable_media" %}</dt>
<dd>
{% if medium.customer %}
<span class="helper-display-block">
<span class="fa fa-user fa-fw"></span>
{% if "organizer.customers:read" in request.orgapermset %}
<a href="{% url "control:organizer.customer" organizer=request.organizer.slug customer=medium.customer.identifier %}">
{{ medium.customer }}
</a>
</span>
{% else %}
{{ medium.customer }}
{% endif %}
{% if medium.linked_orderposition %}
<span class="helper-display-block">
<span class="fa fa-ticket fa-fw"></span>
<a href="{% url "control:event.order" event=medium.linked_orderposition.order.event.slug organizer=request.organizer.slug code=medium.linked_orderposition.order.code %}">
{{ medium.linked_orderposition.order.code }}</a>-{{ medium.linked_orderposition.positionid }}
</span>
{% endif %}
{% if medium.linked_giftcard %}
<span class="helper-display-block">
<span class="fa fa-credit-card fa-fw"></span>
<a href="{% url "control:organizer.giftcard" organizer=request.organizer.slug giftcard=medium.linked_giftcard.id %}">
{{ medium.linked_giftcard.secret }}</a>
</span>
{% endif %}
</dd>
{% if medium.notes %}
<dt>{% trans "Notes" %}</dt>
<dd>{{ medium.notes }}</dd>
</span>
{% endif %}
</dl>
</form>
<div class="text-right">
<a href="{% url "control:organizer.reusable_medium.edit" organizer=request.organizer.slug pk=medium.pk %}"
class="btn btn-default">
<i class="fa fa-edit"></i> {% trans "Edit" %}
</a>
</div>
{% if medium.linked_orderposition %}
<span class="helper-display-block">
<span class="fa fa-ticket fa-fw"></span>
<a href="{% url "control:event.order" event=medium.linked_orderposition.order.event.slug organizer=request.organizer.slug code=medium.linked_orderposition.order.code %}">
{{ medium.linked_orderposition.order.code }}</a>-{{ medium.linked_orderposition.positionid }}
</span>
{% endif %}
{% if medium.linked_giftcard %}
<span class="helper-display-block">
<span class="fa fa-credit-card fa-fw"></span>
{% if "organizer.giftcards:read" in request.orgapermset %}
<a href="{% url "control:organizer.giftcard" organizer=request.organizer.slug giftcard=medium.linked_giftcard.id %}">
{{ medium.linked_giftcard.secret }}
</a>
{% else %}
{{ medium.linked_giftcard.secret|slice:":3" }}…
{% endif %}
</span>
{% endif %}
</dd>
{% if medium.notes %}
<dt>{% trans "Notes" %}</dt>
<dd>{{ medium.notes }}</dd>
{% endif %}
</dl>
{% if "organizer.reusablemedia:write" in request.orgapermset %}
<div class="text-right">
<a href="{% url "control:organizer.reusable_medium.edit" organizer=request.organizer.slug pk=medium.pk %}"
class="btn btn-default">
<i class="fa fa-edit"></i> {% trans "Edit" %}
</a>
</div>
{% endif %}
</div>
</div>
</div>

View File

@@ -1,6 +1,7 @@
{% extends "pretixcontrol/organizers/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% load getitem %}
{% block inner %}
{% if team %}
<h1>{% trans "Team:" %} {{ team.name }}</h1>
@@ -22,25 +23,24 @@
</fieldset>
<fieldset>
<legend>{% trans "Organizer permissions" %}</legend>
{% bootstrap_field form.can_create_events layout="control" %}
{% bootstrap_field form.can_manage_gift_cards layout="control" %}
{% bootstrap_field form.can_manage_customers layout="control" %}
{% bootstrap_field form.can_manage_reusable_media layout="control" %}
{% bootstrap_field form.can_change_teams layout="control" %}
{% bootstrap_field form.can_change_organizer_settings layout="control" %}
{% bootstrap_field form.all_organizer_permissions layout="control" %}
<div class="team-permission-groups col-md-9 col-md-offset-3" data-display-dependency="#id_all_organizer_permissions" data-inverse>
{% for f in form.organizer_field_names %}
{% bootstrap_field form|getitem:f layout="control" %}
{% endfor %}
</div>
</fieldset>
<fieldset>
<legend>{% trans "Event permissions" %}</legend>
{% bootstrap_field form.all_events layout="control" %}
{% bootstrap_field form.limit_events layout="control" %}
{% bootstrap_field form.can_change_event_settings layout="control" %}
{% bootstrap_field form.can_change_items layout="control" %}
{% bootstrap_field form.can_view_orders layout="control" %}
{% bootstrap_field form.can_change_orders layout="control" %}
{% bootstrap_field form.can_checkin_orders layout="control" %}
{% bootstrap_field form.can_view_vouchers layout="control" %}
{% bootstrap_field form.can_change_vouchers layout="control" %}
{% bootstrap_field form.all_event_permissions layout="control" %}
<div class="team-permission-groups col-md-9 col-md-offset-3" data-display-dependency="#id_all_event_permissions" data-inverse>
{% for f in form.event_field_names %}
{% bootstrap_field form|getitem:f layout="control" %}
{% endfor %}
</div>
</fieldset>
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">

View File

@@ -13,12 +13,14 @@
{% endblocktrans %}
</p>
<a href="{% url "control:event.subevents.add" organizer=request.event.organizer.slug event=request.event.slug %}"
class="btn btn-primary btn-lg"><i class="fa fa-plus"></i>
{% trans "Create a new date" context "subevent" %}</a>
<a href="{% url "control:event.subevents.bulk" organizer=request.event.organizer.slug event=request.event.slug %}"
class="btn btn-primary btn-lg"><i class="fa fa-plus"></i>
{% trans "Create many new dates" context "subevent" %}</a>
{% if "event.subevents:write" in request.eventpermset %}
<a href="{% url "control:event.subevents.add" organizer=request.event.organizer.slug event=request.event.slug %}"
class="btn btn-primary btn-lg"><i class="fa fa-plus"></i>
{% trans "Create a new date" context "subevent" %}</a>
<a href="{% url "control:event.subevents.bulk" organizer=request.event.organizer.slug event=request.event.slug %}"
class="btn btn-primary btn-lg"><i class="fa fa-plus"></i>
{% trans "Create many new dates" context "subevent" %}</a>
{% endif %}
</div>
{% else %}
<div class="panel panel-default">
@@ -65,7 +67,7 @@
</div>
</form>
</div>
{% if "can_change_event_settings" in request.eventpermset %}
{% if "event.subevents:write" in request.eventpermset %}
<p>
<a href="{% url "control:event.subevents.add" organizer=request.event.organizer.slug event=request.event.slug %}"
class="btn btn-default"><i class="fa fa-plus"></i>
@@ -84,11 +86,13 @@
<table class="table table-hover table-quotas">
<thead>
<tr>
<th>
{% if "can_change_event_settings" in request.eventpermset %}
<label aria-label="{% trans "select all rows for batch-operation" %}" class="batch-select-label"><input type="checkbox" data-toggle-table/></label>
{% endif %}
</th>
{% if "event.subevents:write" in request.eventpermset %}
<th>
{% if "event.subevents:write" in request.eventpermset %}
<label aria-label="{% trans "select all rows for batch-operation" %}" class="batch-select-label"><input type="checkbox" data-toggle-table/></label>
{% endif %}
</th>
{% endif %}
<th>
{% trans "Name" %}
</th>
@@ -107,7 +111,7 @@
</th>
<th></th>
</tr>
{% if "can_change_event_settings" in request.eventpermset and page_obj.paginator.num_pages > 1 %}
{% if "event.subevents:write" in request.eventpermset and page_obj.paginator.num_pages > 1 %}
<tr class="table-select-all warning hidden">
<td>
<input type="checkbox" name="__ALL" id="__all" data-results-total="{{ page_obj.paginator.count }}">
@@ -123,11 +127,11 @@
<tbody>
{% for s in subevents %}
<tr>
<td>
{% if "can_change_event_settings" in request.eventpermset %}
{% if "event.subevents:write" in request.eventpermset %}
<td>
<label aria-label="{% trans "select row for batch-operation" %}" class="batch-select-label"><input type="checkbox" name="subevent" class="batch-select-checkbox" value="{{ s.pk }}"/></label>
{% endif %}
</td>
</td>
{% endif %}
<td>
<strong><a href="{% url "control:event.subevent" organizer=request.event.organizer.slug event=request.event.slug subevent=s.id %}?returnto={{ request.GET.urlencode|urlencode }}">
{{ s.name }}</a></strong><br>
@@ -173,35 +177,39 @@
{% endif %}
</td>
<td class="text-right flip">
<a href="{% url "control:event.orders" organizer=request.event.organizer.slug event=request.event.slug %}?subevent={{ s.id }}" class="btn btn-default btn-sm" title="{% trans "Show orders" %}"><i class="fa fa-shopping-cart" aria-hidden="true"></i></a>
{% if "event.orders:read" in request.eventpermset %}
<a href="{% url "control:event.orders" organizer=request.event.organizer.slug event=request.event.slug %}?subevent={{ s.id }}" class="btn btn-default btn-sm" title="{% trans "Show orders" %}"><i class="fa fa-shopping-cart" aria-hidden="true"></i></a>
{% endif %}
<a href="{% url "control:event.subevent" organizer=request.event.organizer.slug event=request.event.slug subevent=s.id %}?returnto={{ request.GET.urlencode|urlencode }}" class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
<div class="btn-group {% if forloop.revcounter0 < 2 %}dropup{% endif %}">
<button type="button" class="btn btn-default btn-sm dropdown-toggle"
data-toggle="dropdown">
<span class="fa fa-copy"></span>
</button>
<ul class="dropdown-menu dropdown-menu-right">
<li>
<a href="{% url "control:event.subevents.add" organizer=request.event.organizer.slug event=request.event.slug %}?copy_from={{ s.id }}">
{% trans "Use as a template for a new date" context "subevent" %}
</a>
</li>
<li>
<a href="{% url "control:event.subevents.bulk" organizer=request.event.organizer.slug event=request.event.slug %}?copy_from={{ s.id }}">
{% trans "Use as a template for many new dates" context "subevent" %}
</a>
</li>
</ul>
</div>
<a href="{% url "control:event.subevent.delete" organizer=request.event.organizer.slug event=request.event.slug subevent=s.id %}?returnto={{ request.GET.urlencode|urlencode }}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
{% if "event.subevents:write" in request.eventpermset %}
<a href="{% url "control:event.subevent" organizer=request.event.organizer.slug event=request.event.slug subevent=s.id %}?returnto={{ request.GET.urlencode|urlencode }}" class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
<div class="btn-group {% if forloop.revcounter0 < 2 %}dropup{% endif %}">
<button type="button" class="btn btn-default btn-sm dropdown-toggle"
data-toggle="dropdown">
<span class="fa fa-copy"></span>
</button>
<ul class="dropdown-menu dropdown-menu-right">
<li>
<a href="{% url "control:event.subevents.add" organizer=request.event.organizer.slug event=request.event.slug %}?copy_from={{ s.id }}">
{% trans "Use as a template for a new date" context "subevent" %}
</a>
</li>
<li>
<a href="{% url "control:event.subevents.bulk" organizer=request.event.organizer.slug event=request.event.slug %}?copy_from={{ s.id }}">
{% trans "Use as a template for many new dates" context "subevent" %}
</a>
</li>
</ul>
</div>
<a href="{% url "control:event.subevent.delete" organizer=request.event.organizer.slug event=request.event.slug subevent=s.id %}?returnto={{ request.GET.urlencode|urlencode }}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if "can_change_event_settings" in request.eventpermset %}
{% if "event.subevents:write" in request.eventpermset %}
<div class="batch-select-actions">
<button type="submit" class="btn btn-danger btn-save" name="action" value="delete">
<i class="fa fa-trash"></i>{% trans "Delete selected" %}

View File

@@ -120,7 +120,7 @@
</div>
</div>
</div>
{% if "can_change_vouchers" in request.eventpermset %}
{% if "event.vouchers:write" in request.eventpermset %}
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}

View File

@@ -72,7 +72,7 @@
{% endif %}
</p>
{% if "can_change_vouchers" in request.eventpermset %}
{% if "event.vouchers:write" in request.eventpermset %}
<a href="{% url "control:event.vouchers.add" organizer=request.event.organizer.slug event=request.event.slug %}"
class="btn btn-primary btn-lg"><i class="fa fa-plus"></i> {% trans "Create a new voucher" %}</a>
<a href="{% url "control:event.vouchers.bulk" organizer=request.event.organizer.slug event=request.event.slug %}"
@@ -83,7 +83,7 @@
</div>
{% else %}
<p>
{% if "can_change_vouchers" in request.eventpermset %}
{% if "event.vouchers:write" in request.eventpermset %}
<a href="{% url "control:event.vouchers.add" organizer=request.event.organizer.slug event=request.event.slug %}"
class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create a new voucher" %}</a>
<a href="{% url "control:event.vouchers.bulk" organizer=request.event.organizer.slug event=request.event.slug %}"
@@ -103,7 +103,7 @@
<table class="table table-hover table-quotas">
<thead>
<tr>
{% if "can_change_vouchers" in request.eventpermset %}
{% if "event.vouchers:write" in request.eventpermset %}
<th>
<label aria-label="{% trans "select all rows for batch-operation" %}" class="batch-select-label">
<input type="checkbox" data-toggle-table />
@@ -148,7 +148,7 @@
<tbody>
{% for v in vouchers %}
<tr>
{% if "can_change_vouchers" in request.eventpermset %}
{% if "event.vouchers:write" in request.eventpermset %}
<td>
<label aria-label="{% trans "select row for batch-operation" %}" class="batch-select-label">
<input type="checkbox" name="voucher" class="batch-select-checkbox" value="{{ v.pk }}"/>
@@ -192,7 +192,7 @@
</td>
{% endif %}
<td class="text-right flip">
{% if "can_change_vouchers" in request.eventpermset %}
{% if "event.vouchers:write" in request.eventpermset %}
<a href="{% url "control:event.vouchers.bulk" organizer=request.event.organizer.slug event=request.event.slug %}?copy_from={{ v.id }}"
class="btn btn-sm btn-default" title="{% trans "Use as a template for new vouchers" %}" data-toggle="tooltip">
<span class="fa fa-copy"></span>
@@ -205,7 +205,7 @@
</tbody>
</table>
</div>
{% if "can_change_vouchers" in request.eventpermset %}
{% if "event.vouchers:write" in request.eventpermset %}
<div class="batch-select-actions">
<button type="submit" class="btn btn-danger" name="action" value="delete">
<i class="fa fa-trash" aria-hidden="true"></i>

View File

@@ -7,10 +7,12 @@
{% block content %}
<h1>
{% trans "Waiting list" %}
<a href="{% url "control:event.settings" event=request.event.slug organizer=request.organizer.slug %}#waiting-list-open" class="btn btn-default">
<span class="fa fa-cog"></span>
{% trans "Settings" %}
</a>
{% if "event.settings.general:write" in request.eventpermset %}
<a href="{% url "control:event.settings" event=request.event.slug organizer=request.organizer.slug %}#waiting-list-open" class="btn btn-default">
<span class="fa fa-cog"></span>
{% trans "Settings" %}
</a>
{% endif %}
</h1>
{% if not request.event.settings.waiting_list_enabled %}
<div class="alert alert-danger">
@@ -27,7 +29,7 @@
</div>
{% endif %}
<div class="row">
{% if 'can_change_orders' in request.eventpermset %}
{% if 'event.orders:write' in request.eventpermset %}
<form method="post" class="col-md-6"
action="{% url "control:event.orders.waitinglist.auto" event=request.event.slug organizer=request.organizer.slug %}"
data-asynctask>
@@ -80,7 +82,7 @@
</div>
</form>
{% endif %}
<div class="{% if 'can_change_orders' in request.eventpermset %}col-md-6{% else %}col-md-12{% endif %}">
<div class="{% if 'event.orders:write' in request.eventpermset %}col-md-6{% else %}col-md-12{% endif %}">
<div class="panel panel-default">
<div class="panel-heading">
{% trans "Sales estimate" %}
@@ -152,7 +154,7 @@
<thead>
<tr>
<th>
{% if "can_change_orders" in request.eventpermset %}
{% if "event.orders:write" in request.eventpermset %}
<label aria-label="{% trans "select all rows for batch-operation" %}" class="batch-select-label"><input type="checkbox" data-toggle-table/></label>
{% endif %}
</th>
@@ -172,7 +174,7 @@
<th>{% trans "Voucher" %}</th>
<th></th>
</tr>
{% if "can_change_orders" in request.eventpermset and page_obj.paginator.num_pages > 1 %}
{% if "event.orders:write" in request.eventpermset and page_obj.paginator.num_pages > 1 %}
<tr class="table-select-all warning hidden">
<td>
<input type="checkbox" name="__ALL" id="__all" data-results-total="{{ page_obj.paginator.count }}">
@@ -189,7 +191,7 @@
{% for e in entries %}
<tr>
<td>
{% if "can_change_orders" in request.eventpermset %}
{% if "event.orders:write" in request.eventpermset %}
<label aria-label="{% trans "select row for batch-operation" %}" class="batch-select-label"><input type="checkbox" name="entry" class="batch-select-checkbox" value="{{ e.pk }}"/></label>
{% endif %}
</td>
@@ -259,31 +261,34 @@
{% endif %}
</td>
<td class="text-right flip">
{% if not e.voucher %}
<button name="move_top" value="{{ e.pk }}" class="btn btn-default btn-sm"
data-toggle="tooltip" title="{% trans "Move to the top of the list" %}">
<span class="fa fa-thumbs-up"></span>
</button>
<button name="move_end" value="{{ e.pk }}" class="btn btn-default btn-sm"
data-toggle="tooltip" title="{% trans "Move to the end of the list" %}">
<span class="fa fa-thumbs-down"></span>
</button>
{% if 'event.orders:write' in request.eventpermset %}
{% if not e.voucher %}
<button name="move_top" value="{{ e.pk }}" class="btn btn-default btn-sm"
data-toggle="tooltip" title="{% trans "Move to the top of the list" %}">
<span class="fa fa-thumbs-up"></span>
</button>
<button name="move_end" value="{{ e.pk }}" class="btn btn-default btn-sm"
data-toggle="tooltip" title="{% trans "Move to the end of the list" %}">
<span class="fa fa-thumbs-down"></span>
</button>
<a href="{% url "control:event.orders.waitinglist.edit" organizer=request.event.organizer.slug event=request.event.slug entry=e.id %}"
class="btn btn-default btn-sm" title="{% trans "Edit entry" %}"
data-toggle="tooltip">
<i class="fa fa-edit" aria-hidden="true"></i>
</a>
<a href="{% url "control:event.orders.waitinglist.edit" organizer=request.event.organizer.slug event=request.event.slug entry=e.id %}"
class="btn btn-default btn-sm" title="{% trans "Edit entry" %}"
data-toggle="tooltip">
<i class="fa fa-edit" aria-hidden="true"></i>
</a>
<a href="{% url "control:event.orders.waitinglist.delete" organizer=request.event.organizer.slug event=request.event.slug entry=e.id %}?next={{ request.get_full_path|urlencode }}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
{% else %}
<button class="btn btn-default btn-sm disabled">
<span class="fa fa-thumbs-up"></span>
</button>
<button class="btn btn-default btn-sm disabled">
<span class="fa fa-thumbs-down"></span>
</button>
<span class="btn btn-danger btn-sm disabled"><i class="fa fa-trash"></i></span>
<a href="{% url "control:event.orders.waitinglist.delete" organizer=request.event.organizer.slug event=request.event.slug entry=e.id %}?next={{ request.get_full_path|urlencode }}"
class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
{% else %}
<button class="btn btn-default btn-sm disabled">
<span class="fa fa-thumbs-up"></span>
</button>
<button class="btn btn-default btn-sm disabled">
<span class="fa fa-thumbs-down"></span>
</button>
<span class="btn btn-danger btn-sm disabled"><i class="fa fa-trash"></i></span>
{% endif %}
{% endif %}
</td>
</tr>
@@ -291,7 +296,7 @@
</tbody>
</table>
</div>
{% if "can_change_orders" in request.eventpermset %}
{% if "event.orders:write" in request.eventpermset %}
<div class="batch-select-actions">
<button type="submit" class="btn btn-danger btn-save" name="action" value="delete">
<i class="fa fa-trash"></i>

View File

@@ -53,6 +53,7 @@ from pretix.base.media import MEDIA_TYPES
from pretix.base.models import Checkin, LogEntry, Order, OrderPosition
from pretix.base.models.checkin import CheckinList
from pretix.base.models.orders import PrintLog
from pretix.base.permissions import AnyPermissionOf
from pretix.base.services.checkin import (
LazyRuleVars, _logic_annotate_for_graphic_explain,
)
@@ -150,7 +151,7 @@ class CheckInListShow(EventPermissionRequiredMixin, PaginationMixin, CheckInList
model = Checkin
context_object_name = 'entries'
template_name = 'pretixcontrol/checkin/index.html'
permission = 'can_view_orders'
permission = 'event.orders:read'
def dispatch(self, request, *args, **kwargs):
self.list = get_object_or_404(self.request.event.checkin_lists.all(), pk=kwargs.get("list"))
@@ -211,7 +212,7 @@ class CheckInListBulkRevertConfirmView(CheckInListQueryMixin, EventPermissionReq
class CheckInListBulkActionView(CheckInListQueryMixin, EventPermissionRequiredMixin, AsyncPostView):
permission = ('can_change_orders', 'can_checkin_orders')
permission = AnyPermissionOf('event.orders:write', 'event.orders:checkin')
def dispatch(self, request, *args, **kwargs):
self.list = get_object_or_404(self.request.event.checkin_lists.all(), pk=kwargs.get("list"))
@@ -228,7 +229,7 @@ class CheckInListBulkActionView(CheckInListQueryMixin, EventPermissionRequiredMi
self.list = get_object_or_404(request.event.checkin_lists.all(), pk=kwargs.get("list"))
positions = self.get_queryset()
if request.POST.get('revert') == 'true':
if not request.user.has_event_permission(request.organizer, request.event, 'can_change_orders', request=request):
if not request.user.has_event_permission(request.organizer, request.event, 'event.orders:write', request=request):
raise PermissionDenied()
for op in positions:
if op.order.status == Order.STATUS_PAID or (
@@ -295,7 +296,7 @@ class CheckInListBulkActionView(CheckInListQueryMixin, EventPermissionRequiredMi
class CheckinListList(EventPermissionRequiredMixin, PaginationMixin, ListView):
model = CheckinList
context_object_name = 'checkinlists'
permission = 'can_view_orders'
permission = AnyPermissionOf('event.orders:read', 'event.settings.general:write')
template_name = 'pretixcontrol/checkin/lists.html'
ordering = ('subevent__date_from', 'name', 'pk')
@@ -317,9 +318,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,
'can_change_organizer_settings',
'organizer.devices:read',
self.request
)
ctx['filter_form'] = self.filter_form
@@ -335,7 +336,7 @@ class CheckinListCreate(EventPermissionRequiredMixin, CreateView):
model = CheckinList
form_class = CheckinListForm
template_name = 'pretixcontrol/checkin/list_edit.html'
permission = 'can_change_event_settings'
permission = 'event.settings.general:write'
context_object_name = 'checkinlist'
def dispatch(self, request, *args, **kwargs):
@@ -386,7 +387,7 @@ class CheckinListUpdate(EventPermissionRequiredMixin, UpdateView):
model = CheckinList
form_class = CheckinListForm
template_name = 'pretixcontrol/checkin/list_edit.html'
permission = 'can_change_event_settings'
permission = 'event.settings.general:write'
context_object_name = 'checkinlist'
def dispatch(self, request, *args, **kwargs):
@@ -445,7 +446,7 @@ class CheckinListUpdate(EventPermissionRequiredMixin, UpdateView):
class CheckinListDelete(EventPermissionRequiredMixin, CompatDeleteView):
model = CheckinList
template_name = 'pretixcontrol/checkin/list_delete.html'
permission = 'can_change_event_settings'
permission = 'event.settings.general:write'
context_object_name = 'checkinlist'
def get_object(self, queryset=None) -> CheckinList:
@@ -476,7 +477,7 @@ class CheckinListDelete(EventPermissionRequiredMixin, CompatDeleteView):
class CheckinListView(EventPermissionRequiredMixin, PaginationMixin, ListView):
model = Checkin
context_object_name = 'checkins'
permission = 'can_view_orders'
permission = 'event.orders:read'
template_name = 'pretixcontrol/checkin/checkins.html'
ordering = ('-datetime', '-pk')
@@ -505,7 +506,7 @@ class CheckinListView(EventPermissionRequiredMixin, PaginationMixin, ListView):
class CheckInListSimulator(EventPermissionRequiredMixin, FormView):
template_name = 'pretixcontrol/checkin/simulator.html'
permission = 'can_view_orders'
permission = 'event.orders:read'
form_class = CheckinListSimulatorForm
def dispatch(self, request, *args, **kwargs):
@@ -575,9 +576,15 @@ class CheckInListSimulator(EventPermissionRequiredMixin, FormView):
class CheckInResetView(CheckInListQueryMixin, EventPermissionRequiredMixin, AsyncFormView):
form_class = CheckinResetForm
permission = "can_change_orders"
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",

View File

@@ -66,6 +66,7 @@ from pretix.control.signals import (
from pretix.helpers.daterange import daterange
from ...base.models.orders import CancellationRequest
from ...base.models.organizer import TeamQuerySet
from ...base.templatetags.money import money_filter
from ..logdisplay import OVERVIEW_BANLIST
@@ -350,10 +351,10 @@ def event_index(request, organizer, event):
except SubEvent.DoesNotExist:
pass
can_view_orders = request.user.has_event_permission(request.organizer, request.event, 'can_view_orders',
can_view_orders = request.user.has_event_permission(request.organizer, request.event, 'event.orders:read',
request=request)
can_change_event_settings = request.user.has_event_permission(request.organizer, request.event,
'can_change_event_settings', request=request)
'event.settings.general:write', request=request)
widgets = []
if can_view_orders:
@@ -425,11 +426,11 @@ def event_index_log_lazy(request, organizer, event):
'device').order_by('-datetime')
qs = qs.exclude(action_type__in=OVERVIEW_BANLIST)
can_view_orders = request.user.has_event_permission(request.organizer, request.event, 'can_view_orders',
can_view_orders = request.user.has_event_permission(request.organizer, request.event, 'event.orders:read',
request=request)
can_change_event_settings = request.user.has_event_permission(request.organizer, request.event,
'can_change_event_settings', request=request)
can_view_vouchers = request.user.has_event_permission(request.organizer, request.event, 'can_view_vouchers',
'event.settings.general:write', request=request)
can_view_vouchers = request.user.has_event_permission(request.organizer, request.event, 'event.vouchers:read',
request=request)
if not can_view_orders:
@@ -441,7 +442,7 @@ def event_index_log_lazy(request, organizer, event):
ContentType.objects.get_for_model(Voucher),
ContentType.objects.get_for_model(Order)
]
if request.user.has_event_permission(request.organizer, request.event, 'can_change_items', request=request):
if request.user.has_event_permission(request.organizer, request.event, 'event.items:write', request=request):
allowed_types += [
ContentType.objects.get_for_model(Item),
ContentType.objects.get_for_model(ItemCategory),
@@ -491,8 +492,13 @@ def widgets_for_event_qs(request, qs, user, nmax, lazy=False):
# Get set of events where we have the permission to show the # of orders
if not lazy:
events_with_orders = set(qs.filter(
Q(organizer_id__in=user.teams.filter(all_events=True, can_view_orders=True).values_list('organizer', flat=True))
| Q(id__in=user.teams.filter(can_view_orders=True).values_list('limit_events__id', flat=True))
Q(organizer_id__in=user.teams.filter(
TeamQuerySet.event_permission_q("event.orders:read"),
all_events=True,
).values_list('organizer', flat=True))
| Q(id__in=user.teams.filter(
TeamQuerySet.event_permission_q("event.orders:read"),
).values_list('limit_events__id', flat=True))
).values_list('id', flat=True))
tpl = """
@@ -635,7 +641,7 @@ def user_index(request):
ctx = {
'widgets': rearrange(widgets),
'can_create_event': request.user.teams.filter(can_create_events=True).exists(),
'can_create_event': request.user.teams.with_organizer_permission("organizer.events:create").exists(),
'upcoming': widgets_for_event_qs(
request,
annotated_event_query(request, lazy=True).filter(

View File

@@ -72,7 +72,7 @@ def on_control_order_info(sender: Event, request, order: Order, **kwargs):
class ControlSyncJob(OrderView):
permission = 'can_change_orders'
permission = 'event.orders:write'
def post(self, request, provider, *args, **kwargs):
prov, meta = datasync_providers.get(active_in=self.request.event, identifier=provider)
@@ -154,7 +154,7 @@ class GlobalFailedSyncJobsView(AdministratorPermissionRequiredMixin, FailedSyncJ
class OrganizerFailedSyncJobsView(OrganizerPermissionRequiredMixin, FailedSyncJobsView):
permission = "can_change_organizer_settings"
permission = "organizer.settings.general:write"
def get_queryset(self):
return super().get_queryset().filter(
@@ -163,7 +163,7 @@ class OrganizerFailedSyncJobsView(OrganizerPermissionRequiredMixin, FailedSyncJo
class EventFailedSyncJobsView(EventPermissionRequiredMixin, FailedSyncJobsView):
permission = "can_change_event_settings"
permission = "event.settings.general:write"
def get_queryset(self):
return super().get_queryset().filter(

View File

@@ -50,7 +50,7 @@ from . import CreateView, PaginationMixin, UpdateView
class DiscountDelete(EventPermissionRequiredMixin, CompatDeleteView):
model = Discount
template_name = 'pretixcontrol/items/discount_delete.html'
permission = 'can_change_items'
permission = 'event.items:write'
context_object_name = 'discount'
def get_context_data(self, *args, **kwargs) -> dict:
@@ -96,7 +96,7 @@ class DiscountUpdate(EventPermissionRequiredMixin, UpdateView):
model = Discount
form_class = DiscountForm
template_name = 'pretixcontrol/items/discount.html'
permission = 'can_change_items'
permission = 'event.items:write'
context_object_name = 'discount'
def get_object(self, queryset=None) -> Discount:
@@ -139,7 +139,7 @@ class DiscountCreate(EventPermissionRequiredMixin, CreateView):
model = Discount
form_class = DiscountForm
template_name = 'pretixcontrol/items/discount.html'
permission = 'can_change_items'
permission = 'event.items:write'
context_object_name = 'discount'
def get_success_url(self) -> str:
@@ -227,7 +227,7 @@ def discount_move(request, discount, up=True):
messages.success(request, _('The order of discounts has been updated.'))
@event_permission_required("can_change_items")
@event_permission_required("event.items:write")
@require_http_methods(["POST"])
def discount_move_up(request, organizer, event, discount):
discount_move(request, discount, up=True)
@@ -236,7 +236,7 @@ def discount_move_up(request, organizer, event, discount):
event=request.event.slug)
@event_permission_required("can_change_items")
@event_permission_required("event.items:write")
@require_http_methods(["POST"])
def discount_move_down(request, organizer, event, discount):
discount_move(request, discount, up=False)
@@ -246,7 +246,7 @@ def discount_move_down(request, organizer, event, discount):
@transaction.atomic
@event_permission_required("can_change_items")
@event_permission_required("event.items:write")
@require_http_methods(["POST"])
def reorder_discounts(request, organizer, event):
try:

View File

@@ -106,6 +106,7 @@ from ...base.i18n import language
from ...base.models.items import (
Item, ItemCategory, ItemMetaProperty, Question, Quota,
)
from ...base.permissions import AnyPermissionOf
from ...base.plugins import (
PLUGIN_LEVEL_EVENT, PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID,
PLUGIN_LEVEL_ORGANIZER,
@@ -155,7 +156,7 @@ class MetaDataEditorMixin:
property=p,
disabled=(
p.protected and
not self.request.user.has_organizer_permission(self.request.organizer, 'can_change_organizer_settings', request=self.request)
not self.request.user.has_organizer_permission(self.request.organizer, 'organizer.settings.general:write', request=self.request)
),
instance=val_instances.get(p.pk, self.meta_model(property=p, event=self.object)),
data=(self.request.POST if self.request.method == "POST" else None)
@@ -187,7 +188,7 @@ class EventUpdate(DecoupleMixin, EventSettingsViewMixin, EventPermissionRequired
model = Event
form_class = EventUpdateForm
template_name = 'pretixcontrol/event/settings.html'
permission = 'can_change_event_settings'
permission = 'event.settings.general:write'
@cached_property
def object(self) -> Event:
@@ -346,7 +347,7 @@ class EventUpdate(DecoupleMixin, EventSettingsViewMixin, EventPermissionRequired
class EventPlugins(EventSettingsViewMixin, EventPermissionRequiredMixin, TemplateView, SingleObjectMixin):
model = Event
context_object_name = 'event'
permission = 'can_change_event_settings'
permission = 'event.settings.general:write'
template_name = 'pretixcontrol/event/plugins.html'
def get_object(self, queryset=None) -> Event:
@@ -447,15 +448,14 @@ class EventPlugins(EventSettingsViewMixin, EventPermissionRequiredMixin, Templat
continue
if getattr(pluginmeta, 'level', PLUGIN_LEVEL_EVENT) == PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID:
if not request.user.has_organizer_permission(request.organizer, "can_change_organizer_settings", request):
messages.error(
request,
_("You do not have sufficient permission to enable plugins that need to be enabled "
"for the entire organizer account.")
)
continue
if module not in self.object.organizer.get_plugins():
if not request.user.has_organizer_permission(request.organizer, "organizer.settings.general:write", request):
messages.error(
request,
_("You do not have sufficient permission to enable plugins that need to be enabled "
"for the entire organizer account.")
)
continue
self.object.organizer.log_action('pretix.organizer.plugins.enabled', user=self.request.user,
data={'plugin': module})
self.object.organizer.enable_plugin(module, allow_restricted=request.event.settings.allowed_restricted_plugins)
@@ -502,7 +502,7 @@ class EventPlugins(EventSettingsViewMixin, EventPermissionRequiredMixin, Templat
class PaymentProviderSettings(EventSettingsViewMixin, EventPermissionRequiredMixin, TemplateView, SingleObjectMixin):
model = Event
context_object_name = 'event'
permission = 'can_change_event_settings'
permission = 'event.settings.payment:write'
template_name = 'pretixcontrol/event/payment_provider.html'
def get_success_url(self) -> str:
@@ -581,7 +581,7 @@ class PaymentProviderSettings(EventSettingsViewMixin, EventPermissionRequiredMix
class EventSettingsFormView(EventPermissionRequiredMixin, DecoupleMixin, FormView):
model = Event
permission = 'can_change_event_settings'
permission = 'event.settings.general:write'
def get_context_data(self, *args, **kwargs) -> dict:
context = super().get_context_data(*args, **kwargs)
@@ -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 = 'can_change_event_settings'
permission = AnyPermissionOf('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 = 'can_change_event_settings'
permission = AnyPermissionOf('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 = 'can_change_event_settings'
permission = AnyPermissionOf('event.settings.invoicing:write', 'event.settings.general:write')
write_permission = 'event.settings.invoicing:write'
def get_context_data(self, **kwargs):
types = get_transmission_types()
@@ -704,7 +724,7 @@ class CancelSettings(EventSettingsViewMixin, EventSettingsFormView):
model = Event
form_class = CancelSettingsForm
template_name = 'pretixcontrol/event/cancel.html'
permission = 'can_change_event_settings'
permission = 'event.settings.general:write'
def get_success_url(self) -> str:
return reverse('control:event.settings.cancel', kwargs={
@@ -738,7 +758,7 @@ class CancelSettings(EventSettingsViewMixin, EventSettingsFormView):
class InvoicePreview(EventPermissionRequiredMixin, View):
permission = 'can_change_event_settings'
permission = 'event.settings.invoicing:write'
def get(self, request, *args, **kwargs):
fname, ftype, fcontent = build_preview_invoice_pdf(request.event)
@@ -753,7 +773,7 @@ class InvoicePreview(EventPermissionRequiredMixin, View):
class DangerZone(EventPermissionRequiredMixin, TemplateView):
permission = 'can_change_event_settings'
permission = 'event.settings.general:write'
template_name = 'pretixcontrol/event/dangerzone.html'
@@ -769,7 +789,7 @@ class MailSettings(EventSettingsViewMixin, EventSettingsFormView):
model = Event
form_class = MailSettingsForm
template_name = 'pretixcontrol/event/mail.html'
permission = 'can_change_event_settings'
permission = 'event.settings.general:write'
def get_success_url(self) -> str:
return reverse('control:event.settings.mail', kwargs={
@@ -801,7 +821,7 @@ class MailSettings(EventSettingsViewMixin, EventSettingsFormView):
class MailSettingsSetup(EventPermissionRequiredMixin, MailSettingsSetupView):
permission = 'can_change_event_settings'
permission = 'event.settings.general:write'
basetpl = 'pretixcontrol/event/base.html'
def get_success_url(self) -> str:
@@ -817,7 +837,7 @@ class MailSettingsSetup(EventPermissionRequiredMixin, MailSettingsSetupView):
class MailSettingsPreview(EventPermissionRequiredMixin, View):
permission = 'can_change_event_settings'
permission = 'event.settings.general:write'
# create index-language mapping
@cached_property
@@ -887,7 +907,7 @@ class MailSettingsPreview(EventPermissionRequiredMixin, View):
class MailSettingsRendererPreview(MailSettingsPreview):
permission = 'can_change_event_settings'
permission = 'event.settings.general:write'
def post(self, request, *args, **kwargs):
return HttpResponse(status=405)
@@ -935,7 +955,7 @@ class MailSettingsRendererPreview(MailSettingsPreview):
class TicketSettingsPreview(EventPermissionRequiredMixin, View):
permission = 'can_change_event_settings'
permission = 'event.settings.general:write'
@cached_property
def output(self):
@@ -967,7 +987,7 @@ class TicketSettings(EventSettingsViewMixin, EventPermissionRequiredMixin, FormV
model = Event
form_class = TicketSettingsForm
template_name = 'pretixcontrol/event/tickets.html'
permission = 'can_change_event_settings'
permission = 'event.settings.general:write'
def get_context_data(self, *args, **kwargs) -> dict:
context = super().get_context_data(*args, **kwargs)
@@ -1078,7 +1098,7 @@ class EventPermissions(EventSettingsViewMixin, EventPermissionRequiredMixin, Tem
class EventLive(EventPermissionRequiredMixin, TemplateView):
permission = 'can_change_event_settings'
permission = 'event.settings.general:write'
template_name = 'pretixcontrol/event/live.html'
def get_context_data(self, **kwargs):
@@ -1145,12 +1165,12 @@ class EventLive(EventPermissionRequiredMixin, TemplateView):
class EventTransferSession(EventPermissionRequiredMixin, TemplateView):
permission = 'can_change_event_settings'
permission = 'event.settings.general:write'
template_name = 'pretixcontrol/event/transfer_session.html'
class EventDelete(RecentAuthenticationRequiredMixin, EventPermissionRequiredMixin, FormView):
permission = 'can_change_event_settings'
permission = 'event.settings.general:write'
template_name = 'pretixcontrol/event/delete.html'
form_class = EventDeleteForm
@@ -1218,20 +1238,20 @@ class EventLog(EventPermissionRequiredMixin, PaginationMixin, ListView):
'user', 'content_type', 'api_token', 'oauth_application', 'device'
).order_by('-datetime', '-pk')
qs = qs.exclude(action_type__in=OVERVIEW_BANLIST)
if not self.request.user.has_event_permission(self.request.organizer, self.request.event, 'can_view_orders',
if not self.request.user.has_event_permission(self.request.organizer, self.request.event, 'event.orders:read',
request=self.request):
qs = qs.exclude(content_type=ContentType.objects.get_for_model(Order))
if not self.request.user.has_event_permission(self.request.organizer, self.request.event, 'can_view_vouchers',
if not self.request.user.has_event_permission(self.request.organizer, self.request.event, 'event.vouchers:read',
request=self.request):
qs = qs.exclude(content_type=ContentType.objects.get_for_model(Voucher))
if not self.request.user.has_event_permission(self.request.organizer, self.request.event,
'can_change_event_settings', request=self.request):
'event.settings.general:write', request=self.request):
allowed_types = [
ContentType.objects.get_for_model(Voucher),
ContentType.objects.get_for_model(Order)
]
if self.request.user.has_event_permission(self.request.organizer, self.request.event,
'can_change_items', request=self.request):
'event.items:write', request=self.request):
allowed_types += [
ContentType.objects.get_for_model(Item),
ContentType.objects.get_for_model(ItemCategory),
@@ -1268,7 +1288,7 @@ class EventLog(EventPermissionRequiredMixin, PaginationMixin, ListView):
class EventComment(EventPermissionRequiredMixin, View):
permission = 'can_change_event_settings'
permission = 'event.settings.general:write'
def post(self, *args, **kwargs):
form = CommentForm(self.request.POST)
@@ -1297,7 +1317,7 @@ class TaxCreate(EventSettingsViewMixin, EventPermissionRequiredMixin, CreateView
model = TaxRule
form_class = TaxRuleForm
template_name = 'pretixcontrol/event/tax_edit.html'
permission = 'can_change_event_settings'
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 = 'can_change_event_settings'
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 = 'can_change_event_settings'
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 = 'can_change_event_settings'
permission = 'event.settings.tax:write'
context_object_name = 'taxrule'
def get_object(self, queryset=None) -> TaxRule:
@@ -1504,7 +1524,7 @@ class TaxDelete(EventSettingsViewMixin, EventPermissionRequiredMixin, CompatDele
class WidgetSettings(EventSettingsViewMixin, EventPermissionRequiredMixin, FormView):
template_name = 'pretixcontrol/event/widget.html'
permission = 'can_change_event_settings'
permission = 'event.settings.general:write'
form_class = WidgetCodeForm
def get_form_kwargs(self):
@@ -1533,7 +1553,7 @@ class WidgetSettings(EventSettingsViewMixin, EventPermissionRequiredMixin, FormV
class QuickSetupView(FormView):
template_name = 'pretixcontrol/event/quick_setup.html'
permission = 'can_change_event_settings'
permission = 'event.settings.general:write'
form_class = QuickSetupForm
def dispatch(self, request, *args, **kwargs):

View File

@@ -159,7 +159,7 @@ def item_move(request, item, up=True):
messages.success(request, _('The order of items has been updated.'))
@event_permission_required("can_change_items")
@event_permission_required("event.items:write")
@require_http_methods(["POST"])
def item_move_up(request, organizer, event, item):
item_move(request, item, up=True)
@@ -168,7 +168,7 @@ def item_move_up(request, organizer, event, item):
event=request.event.slug)
@event_permission_required("can_change_items")
@event_permission_required("event.items:write")
@require_http_methods(["POST"])
def item_move_down(request, organizer, event, item):
item_move(request, item, up=False)
@@ -178,7 +178,7 @@ def item_move_down(request, organizer, event, item):
@transaction.atomic
@event_permission_required("can_change_items")
@event_permission_required("event.items:write")
@require_http_methods(["POST"])
def reorder_items(request, organizer, event, category):
try:
@@ -215,7 +215,7 @@ def reorder_items(request, organizer, event, category):
class CategoryDelete(EventPermissionRequiredMixin, CompatDeleteView):
model = ItemCategory
template_name = 'pretixcontrol/items/category_delete.html'
permission = 'can_change_items'
permission = 'event.items:write'
context_object_name = 'category'
def get_object(self, queryset=None) -> ItemCategory:
@@ -249,7 +249,7 @@ class CategoryUpdate(EventPermissionRequiredMixin, UpdateView):
model = ItemCategory
form_class = CategoryForm
template_name = 'pretixcontrol/items/category.html'
permission = 'can_change_items'
permission = 'event.items:write'
context_object_name = 'category'
def get_object(self, queryset=None) -> ItemCategory:
@@ -287,7 +287,7 @@ class CategoryCreate(EventPermissionRequiredMixin, CreateView):
model = ItemCategory
form_class = CategoryForm
template_name = 'pretixcontrol/items/category.html'
permission = 'can_change_items'
permission = 'event.items:write'
context_object_name = 'category'
def get_success_url(self) -> str:
@@ -371,7 +371,7 @@ def category_move(request, category, up=True):
messages.success(request, _('The order of categories has been updated.'))
@event_permission_required("can_change_items")
@event_permission_required("event.items:write")
@require_http_methods(["POST"])
def category_move_up(request, organizer, event, category):
category_move(request, category, up=True)
@@ -380,7 +380,7 @@ def category_move_up(request, organizer, event, category):
event=request.event.slug)
@event_permission_required("can_change_items")
@event_permission_required("event.items:write")
@require_http_methods(["POST"])
def category_move_down(request, organizer, event, category):
category_move(request, category, up=False)
@@ -390,7 +390,7 @@ def category_move_down(request, organizer, event, category):
@transaction.atomic
@event_permission_required("can_change_items")
@event_permission_required("event.items:write")
@require_http_methods(["POST"])
def reorder_categories(request, organizer, event):
try:
@@ -522,7 +522,7 @@ class QuestionList(ListView):
@transaction.atomic
@event_permission_required("can_change_items")
@event_permission_required("event.items:write")
@require_http_methods(["POST"])
def reorder_questions(request, organizer, event):
try:
@@ -570,7 +570,7 @@ def reorder_questions(request, organizer, event):
class QuestionDelete(EventPermissionRequiredMixin, CompatDeleteView):
model = Question
template_name = 'pretixcontrol/items/question_delete.html'
permission = 'can_change_items'
permission = 'event.items:write'
context_object_name = 'question'
def get_object(self, queryset=None) -> Question:
@@ -664,7 +664,7 @@ class QuestionMixin:
class QuestionView(EventPermissionRequiredMixin, ChartContainingView, DetailView):
model = Question
template_name = 'pretixcontrol/items/question.html'
permission = 'can_change_items'
permission = None
template_name_field = 'question'
@cached_property
@@ -753,7 +753,7 @@ class QuestionUpdate(EventPermissionRequiredMixin, QuestionMixin, UpdateView):
model = Question
form_class = QuestionForm
template_name = 'pretixcontrol/items/question_edit.html'
permission = 'can_change_items'
permission = 'event.items:write'
context_object_name = 'question'
def get_object(self, queryset=None) -> Question:
@@ -794,7 +794,7 @@ class QuestionCreate(EventPermissionRequiredMixin, QuestionMixin, CreateView):
model = Question
form_class = QuestionForm
template_name = 'pretixcontrol/items/question_edit.html'
permission = 'can_change_items'
permission = 'event.items:write'
context_object_name = 'question'
def get_form_kwargs(self):
@@ -888,7 +888,7 @@ class QuotaCreate(EventPermissionRequiredMixin, CreateView):
model = Quota
form_class = QuotaForm
template_name = 'pretixcontrol/items/quota_edit.html'
permission = 'can_change_items'
permission = 'event.items:write'
context_object_name = 'quota'
def get_success_url(self) -> str:
@@ -1055,7 +1055,7 @@ class QuotaView(ChartContainingView, DetailView):
raise Http404(_("The requested quota does not exist."))
def post(self, request, *args, **kwargs):
if not request.user.has_event_permission(request.organizer, request.event, 'can_change_items', request):
if not request.user.has_event_permission(request.organizer, request.event, 'event.items:write', request):
raise PermissionDenied()
quota = self.get_object()
if 'reopen' in request.POST:
@@ -1085,7 +1085,7 @@ class QuotaUpdate(EventPermissionRequiredMixin, UpdateView):
model = Quota
form_class = QuotaForm
template_name = 'pretixcontrol/items/quota_edit.html'
permission = 'can_change_items'
permission = 'event.items:write'
context_object_name = 'quota'
def get_context_data(self, *args, **kwargs):
@@ -1143,7 +1143,7 @@ class QuotaUpdate(EventPermissionRequiredMixin, UpdateView):
class QuotaDelete(EventPermissionRequiredMixin, CompatDeleteView):
model = Quota
template_name = 'pretixcontrol/items/quota_delete.html'
permission = 'can_change_items'
permission = 'event.items:write'
context_object_name = 'quota'
def get_object(self, queryset=None) -> Quota:
@@ -1246,7 +1246,7 @@ class MetaDataEditorMixin:
class ItemCreate(EventPermissionRequiredMixin, MetaDataEditorMixin, CreateView):
form_class = ItemCreateForm
template_name = 'pretixcontrol/item/create.html'
permission = 'can_change_items'
permission = 'event.items:write'
def get_success_url(self) -> str:
return reverse('control:event.item', kwargs={
@@ -1322,7 +1322,7 @@ class ItemCreate(EventPermissionRequiredMixin, MetaDataEditorMixin, CreateView):
class ItemUpdateGeneral(ItemDetailMixin, EventPermissionRequiredMixin, MetaDataEditorMixin, UpdateView):
form_class = ItemUpdateForm
template_name = 'pretixcontrol/item/index.html'
permission = 'can_change_items'
permission = 'event.items:write'
@cached_property
def plugin_forms(self):
@@ -1584,7 +1584,7 @@ class ItemUpdateGeneral(ItemDetailMixin, EventPermissionRequiredMixin, MetaDataE
class ItemDelete(EventPermissionRequiredMixin, CompatDeleteView):
model = Item
template_name = 'pretixcontrol/item/delete.html'
permission = 'can_change_items'
permission = 'event.items:write'
context_object_name = 'item'
def get_context_data(self, *args, **kwargs) -> dict:

View File

@@ -86,7 +86,7 @@ class OutgoingMailListView(OutgoingMailQueryMixin, OrganizerDetailViewMixin, Org
template_name = 'pretixcontrol/organizers/outgoing_mails.html'
# Assume "the highest" permission level for now because emails could belog to any event, order, or customer.
# We plan to add a special permissoin in the future
permission = 'can_change_organizer_settings'
permission = 'organizer.outgoingmails:read'
context_object_name = 'mails'
paginate_by = 100
@@ -100,7 +100,7 @@ class OutgoingMailListView(OutgoingMailQueryMixin, OrganizerDetailViewMixin, Org
class OutgoingMailDetailView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, DetailView):
model = OutgoingMail
template_name = 'pretixcontrol/organizers/outgoing_mail.html'
permission = 'can_change_organizer_settings'
permission = 'organizer.outgoingmails:read'
context_object_name = 'mail'
def get_object(self, queryset=None):
@@ -136,7 +136,7 @@ class OutgoingMailDetailView(OrganizerDetailViewMixin, OrganizerPermissionRequir
class OutgoingMailBulkAction(OutgoingMailQueryMixin, OrganizerPermissionRequiredMixin, OrganizerDetailViewMixin, View):
permission = 'can_change_organizer_settings'
permission = 'organizer.outgoingmails:read'
@transaction.atomic
def post(self, request, *args, **kwargs):

View File

@@ -51,6 +51,7 @@ from i18nfield.strings import LazyI18nString
from pretix.base.forms import SafeSessionWizardView
from pretix.base.i18n import language
from pretix.base.models import Event, EventMetaValue, Organizer, Quota, Team
from pretix.base.models.organizer import TeamQuerySet
from pretix.base.services.quotas import QuotaAvailability
from pretix.control.forms.event import (
EventWizardBasicsForm, EventWizardCopyForm, EventWizardFoundationForm,
@@ -195,7 +196,9 @@ class EventWizard(SafeSessionWizardView):
qs = Organizer.objects.all()
if not self.request.user.has_active_staff_session(self.request.session.session_key):
qs = qs.filter(
id__in=self.request.user.teams.filter(can_create_events=True).values_list('organizer', flat=True)
id__in=self.request.user.teams.filter(
TeamQuerySet.organizer_permission_q("organizer.events:create"),
).values_list('organizer', flat=True)
)
organizer = qs.get(slug=self.request.GET.get('organizer'))
initial['organizer'] = organizer
@@ -213,12 +216,7 @@ class EventWizard(SafeSessionWizardView):
except Event.DoesNotExist:
allow = False
else:
allow = (
request.user.has_event_permission(clone_from.organizer, clone_from,
'can_change_event_settings', request)
and request.user.has_event_permission(clone_from.organizer, clone_from,
'can_change_items', request)
)
allow = request.user.has_event_permission(clone_from.organizer, clone_from, None, request)
if not allow:
messages.error(self.request, _('You do not have permission to clone this event.'))
else:
@@ -227,7 +225,7 @@ class EventWizard(SafeSessionWizardView):
def get_context_data(self, form, **kwargs):
ctx = super().get_context_data(form, **kwargs)
ctx['has_organizer'] = self.request.user.teams.filter(can_create_events=True).exists()
ctx['has_organizer'] = self.request.user.teams.filter(TeamQuerySet.organizer_permission_q("organizer.events:create")).exists()
if self.steps.current == 'basics':
ctx['organizer'] = self.get_cleaned_data_for_step('foundation').get('organizer')
return ctx
@@ -244,6 +242,7 @@ class EventWizard(SafeSessionWizardView):
kwargs = {
'user': self.request.user,
'session': self.request.session,
'clone_from': self.clone_from,
}
if step != 'foundation':
fdata = self.get_cleaned_data_for_step('foundation')
@@ -255,6 +254,13 @@ class EventWizard(SafeSessionWizardView):
}
# The show must go on, we catch this error in render()
kwargs.update(fdata)
if step == 'copy':
bdata = self.get_cleaned_data_for_step('basics')
if bdata:
bdata = {
'team': bdata.get('team'),
}
kwargs.update(bdata)
return kwargs
def get_template_names(self):
@@ -279,31 +285,50 @@ class EventWizard(SafeSessionWizardView):
user=self.request.user,
)
if not EventWizardBasicsForm.has_control_rights(self.request.user, event.organizer, self.request.session):
if copy_data and copy_data['copy_from_event']:
copy_from_event = copy_data['copy_from_event']
elif self.clone_from:
copy_from_event = self.clone_from
else:
copy_from_event = None
if not EventWizardBasicsForm.has_control_rights(
self.request.user, event.organizer, self.request.session
):
if basics_data["team"] is not None:
t = basics_data["team"]
t.limit_events.add(event)
elif event.organizer.settings.event_team_provisioning:
# Create a new team for new events with full access, but for copied events with the same access
# as the source
limit_event_permissions = {}
if copy_from_event and copy_from_event.organizer == event.organizer:
source_teams = self.request.user._get_teams_for_event(copy_from_event.organizer, copy_from_event)
all_event_permissions = any(t.all_event_permissions for t in source_teams)
if not all_event_permissions:
for t in source_teams:
limit_event_permissions.update(t.limit_event_permissions)
else:
# The cross-organizer case is protected through allow_copy_data
all_event_permissions = True
t = Team.objects.create(
organizer=event.organizer,
name=_('Team {event}').format(
event=str(event.name)[:100] + "" if len(str(event.name)) > 100 else str(event.name)
),
can_change_event_settings=True, can_change_items=True,
can_view_orders=True, can_change_orders=True, can_view_vouchers=True,
can_change_vouchers=True
all_organizer_permissions=False,
all_event_permissions=all_event_permissions,
limit_event_permissions=limit_event_permissions,
)
t.members.add(self.request.user)
t.limit_events.add(event)
t.log_action('pretix.team.created', user=self.request.user, data={
'_created_by_event_wizard': True,
'name': t.name,
'can_change_event_settings': True,
'can_change_items': True,
'can_view_orders': True,
'can_change_orders': True,
'can_view_vouchers': True,
'can_change_vouchers': True,
'all_organizer_permissions': False,
'all_event_permissions': all_event_permissions,
'limit_event_permissions': limit_event_permissions,
'limit_events': [event.pk],
})
@@ -314,11 +339,8 @@ class EventWizard(SafeSessionWizardView):
})
event.log_action('pretix.event.settings', user=self.request.user, data=logdata)
if copy_data and copy_data['copy_from_event']:
from_event = copy_data['copy_from_event']
event.copy_data_from(from_event)
elif self.clone_from:
event.copy_data_from(self.clone_from)
if copy_from_event:
event.copy_data_from(copy_from_event)
else:
event.set_active_plugins(settings.PRETIX_PLUGINS_DEFAULT.split(","),
allow_restricted=settings.PRETIX_PLUGINS_DEFAULT.split(","))
@@ -331,10 +353,8 @@ class EventWizard(SafeSessionWizardView):
event.set_defaults()
if basics_data['tax_rate'] is not None:
if self.clone_from:
default_tax_rule = self.clone_from.cached_default_tax_rule
elif copy_data and copy_data['copy_from_event']:
default_tax_rule = from_event.cached_default_tax_rule
if copy_from_event:
default_tax_rule = copy_from_event.cached_default_tax_rule
else:
default_tax_rule = None
if not default_tax_rule or default_tax_rule.rate != basics_data['tax_rate']:
@@ -348,7 +368,7 @@ class EventWizard(SafeSessionWizardView):
event.settings.set('locale', basics_data['locale'])
event.settings.set('locales', foundation_data['locales'])
if (copy_data and copy_data['copy_from_event']) or self.clone_from or event.has_subevents:
if copy_from_event or event.has_subevents:
return redirect(reverse('control:event.settings', kwargs={
'organizer': event.organizer.slug,
'event': event.slug,

View File

@@ -214,7 +214,7 @@ class BaseProcessView(AsyncAction, FormView):
class OrderImportView(EventPermissionRequiredMixin, BaseImportView):
template_name = 'pretixcontrol/orders/import_start.html'
permission = 'can_change_orders'
permission = 'event.orders:write'
def get_process_url(self, request, cf, charset):
return reverse('control:event.orders.import.process', kwargs={
@@ -225,7 +225,7 @@ class OrderImportView(EventPermissionRequiredMixin, BaseImportView):
class OrderProcessView(EventPermissionRequiredMixin, BaseProcessView):
permission = 'can_change_orders'
permission = 'event.orders:write'
template_name = 'pretixcontrol/orders/import_process.html'
form_class = OrdersProcessForm
task = import_orders
@@ -257,7 +257,7 @@ class OrderProcessView(EventPermissionRequiredMixin, BaseProcessView):
class VoucherImportView(EventPermissionRequiredMixin, BaseImportView):
template_name = 'pretixcontrol/vouchers/import_start.html'
permission = 'can_change_vouchers'
permission = 'event.vouchers:write'
def get_process_url(self, request, cf, charset):
return reverse('control:event.vouchers.import.process', kwargs={
@@ -268,7 +268,7 @@ class VoucherImportView(EventPermissionRequiredMixin, BaseImportView):
class VoucherProcessView(EventPermissionRequiredMixin, BaseProcessView):
permission = 'can_change_vouchers'
permission = 'event.vouchers:write'
template_name = 'pretixcontrol/vouchers/import_process.html'
form_class = VouchersProcessForm
task = import_vouchers

View File

@@ -92,7 +92,9 @@ from pretix.base.payment import PaymentException
from pretix.base.secrets import assign_ticket_secret
from pretix.base.services import tickets
from pretix.base.services.cancelevent import cancel_event
from pretix.base.services.export import export, scheduled_event_export
from pretix.base.services.export import (
export, init_event_exporters, scheduled_event_export,
)
from pretix.base.services.invoices import (
generate_cancellation, generate_invoice, invoice_pdf, invoice_pdf_task,
invoice_qualified, regenerate_invoice, transmit_invoice,
@@ -109,9 +111,7 @@ from pretix.base.services.tax import (
VATIDFinalError, VATIDTemporaryError, validate_vat_id,
)
from pretix.base.services.tickets import generate
from pretix.base.signals import (
order_modified, register_data_exporters, register_ticket_outputs,
)
from pretix.base.signals import order_modified, register_ticket_outputs
from pretix.base.templatetags.money import money_filter
from pretix.base.templatetags.rich_text import markdown_compile_email
from pretix.base.views.mixins import OrderQuestionsViewMixin
@@ -169,7 +169,7 @@ class OrderSearchMixin:
class OrderSearch(OrderSearchMixin, EventPermissionRequiredMixin, TemplateView):
template_name = 'pretixcontrol/orders/search.html'
permission = 'can_view_orders'
permission = 'event.orders:read'
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
@@ -199,7 +199,7 @@ class OrderSearch(OrderSearchMixin, EventPermissionRequiredMixin, TemplateView):
class BaseOrderBulkActionView(OrderSearchMixin, EventPermissionRequiredMixin, AsyncFormView):
template_name = 'pretixcontrol/orders/bulk_action.html'
permission = 'can_change_orders'
permission = 'event.orders:write'
form_class = forms.Form
def get_queryset(self):
@@ -402,7 +402,7 @@ class OrderList(OrderSearchMixin, EventPermissionRequiredMixin, PaginationMixin,
model = Order
context_object_name = 'orders'
template_name = 'pretixcontrol/orders/index.html'
permission = 'can_view_orders'
permission = 'event.orders:read'
def get_queryset(self):
qs = Order.objects.filter(
@@ -526,7 +526,7 @@ class OrderView(EventPermissionRequiredMixin, DetailView):
class OrderDetail(OrderView):
template_name = 'pretixcontrol/order/index.html'
permission = 'can_view_orders'
permission = 'event.orders:read'
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
@@ -626,7 +626,7 @@ class OrderDetail(OrderView):
class OrderTransactions(OrderView):
template_name = 'pretixcontrol/order/transactions.html'
permission = 'can_view_orders'
permission = 'event.orders:read'
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
@@ -645,7 +645,7 @@ class OrderTransactions(OrderView):
class OrderDownload(AsyncAction, OrderView):
task = generate
permission = 'can_view_orders'
permission = 'event.orders:read'
def get_success_url(self, value):
return self.get_self_url()
@@ -744,7 +744,7 @@ class OrderDownload(AsyncAction, OrderView):
class OrderComment(OrderView):
permission = 'can_change_orders'
permission = 'event.orders:write'
def post(self, *args, **kwargs):
form = CommentForm(self.request.POST)
@@ -784,7 +784,7 @@ class OrderComment(OrderView):
class OrderApprove(OrderView):
permission = 'can_change_orders'
permission = 'event.orders:write'
def post(self, *args, **kwargs):
if self.order.require_approval:
@@ -803,7 +803,7 @@ class OrderApprove(OrderView):
class OrderDelete(OrderView):
permission = 'can_change_orders'
permission = 'event.orders:write'
def post(self, *args, **kwargs):
if self.order.testmode:
@@ -833,7 +833,7 @@ class OrderDelete(OrderView):
class OrderDeny(OrderView):
permission = 'can_change_orders'
permission = 'event.orders:write'
def post(self, request, *args, **kwargs):
if self.order.require_approval:
@@ -859,7 +859,7 @@ class OrderDeny(OrderView):
class OrderPaymentCancel(OrderView):
permission = 'can_change_orders'
permission = 'event.orders:write'
@cached_property
def payment(self):
@@ -898,7 +898,7 @@ class OrderPaymentCancel(OrderView):
class OrderRefundCancel(OrderView):
permission = 'can_change_orders'
permission = 'event.orders:write'
@cached_property
def refund(self):
@@ -928,7 +928,7 @@ class OrderRefundCancel(OrderView):
class OrderRefundProcess(OrderView):
permission = 'can_change_orders'
permission = 'event.orders:write'
@cached_property
def refund(self):
@@ -967,7 +967,7 @@ class OrderRefundProcess(OrderView):
class OrderRefundDone(OrderView):
permission = 'can_change_orders'
permission = 'event.orders:write'
@cached_property
def refund(self):
@@ -990,7 +990,7 @@ class OrderRefundDone(OrderView):
class OrderCancellationRequestDelete(OrderView):
permission = 'can_change_orders'
permission = 'event.orders:write'
@cached_property
def req(self):
@@ -1024,7 +1024,7 @@ class OrderCancellationRequestDelete(OrderView):
class OrderPaymentConfirm(OrderView):
permission = 'can_change_orders'
permission = 'event.orders:write'
@cached_property
def payment(self):
@@ -1078,7 +1078,7 @@ class OrderPaymentConfirm(OrderView):
class OrderRefundView(OrderView):
permission = 'can_change_orders'
permission = 'event.orders:write'
@cached_property
def start_form(self):
@@ -1427,7 +1427,7 @@ class OrderRefundView(OrderView):
class OrderTransition(OrderView):
permission = 'can_change_orders'
permission = 'event.orders:write'
@cached_property
def req(self):
@@ -1592,7 +1592,7 @@ class OrderTransition(OrderView):
class OrderInvoiceCreate(OrderView):
permission = 'can_change_orders'
permission = 'event.orders:write'
def post(self, *args, **kwargs):
with transaction.atomic():
@@ -1618,7 +1618,7 @@ class OrderInvoiceCreate(OrderView):
class OrderCheckVATID(OrderView):
permission = 'can_change_orders'
permission = 'event.orders:write'
def post(self, *args, **kwargs):
try:
@@ -1666,7 +1666,7 @@ class OrderCheckVATID(OrderView):
class OrderInvoiceRegenerate(OrderView):
permission = 'can_change_orders'
permission = 'event.orders:write'
def post(self, *args, **kwargs):
try:
@@ -1699,7 +1699,7 @@ class OrderInvoiceRegenerate(OrderView):
class OrderInvoiceRetransmit(OrderView):
permission = 'can_change_orders'
permission = 'event.orders:write'
def post(self, *args, **kwargs):
with transaction.atomic(durable=True):
@@ -1730,7 +1730,7 @@ class OrderInvoiceRetransmit(OrderView):
class OrderInvoiceReissue(OrderView):
permission = 'can_change_orders'
permission = 'event.orders:write'
def post(self, *args, **kwargs):
with transaction.atomic():
@@ -1781,7 +1781,7 @@ class OrderInvoiceInspect(AdministratorPermissionRequiredMixin, OrderView):
class OrderResendLink(OrderView):
permission = 'can_change_orders'
permission = 'event.orders:write'
def post(self, *args, **kwargs):
if 'position' in kwargs:
@@ -1798,7 +1798,7 @@ class OrderResendLink(OrderView):
class InvoiceDownload(EventPermissionRequiredMixin, View):
permission = 'can_view_orders'
permission = 'event.orders:read'
def get_order_url(self):
return reverse('control:event.order', kwargs={
@@ -1842,7 +1842,7 @@ class InvoiceDownload(EventPermissionRequiredMixin, View):
class OrderExtend(OrderView):
permission = 'can_change_orders'
permission = 'event.orders:write'
def post(self, *args, **kwargs):
if self.form.is_valid():
@@ -1890,7 +1890,7 @@ class OrderExtend(OrderView):
class OrderReactivate(OrderView):
permission = 'can_change_orders'
permission = 'event.orders:write'
@cached_property
def reactivate_form(self):
@@ -1940,7 +1940,7 @@ class OrderReactivate(OrderView):
class OrderChange(OrderView):
permission = 'can_change_orders'
permission = 'event.orders:write'
template_name = 'pretixcontrol/order/change.html'
@cached_property
@@ -2197,7 +2197,7 @@ class OrderChange(OrderView):
class OrderModifyInformation(OrderQuestionsViewMixin, OrderView):
permission = 'can_change_orders'
permission = 'event.orders:write'
template_name = 'pretixcontrol/order/change_questions.html'
only_user_visible = False
all_optional = True
@@ -2250,7 +2250,7 @@ class OrderModifyInformation(OrderQuestionsViewMixin, OrderView):
class OrderContactChange(OrderView):
permission = 'can_change_orders'
permission = 'event.orders:write'
template_name = 'pretixcontrol/order/change_contact.html'
def get_context_data(self, **kwargs):
@@ -2265,7 +2265,7 @@ class OrderContactChange(OrderView):
data=self.request.POST if self.request.method == "POST" else None,
customers=self.request.organizer.settings.customer_accounts and (
self.request.user.has_organizer_permission(
self.request.organizer, 'can_manage_customers', request=self.request
self.request.organizer, 'organizer.customers:write', request=self.request
)
)
)
@@ -2334,7 +2334,7 @@ class OrderContactChange(OrderView):
class OrderLocaleChange(OrderView):
permission = 'can_change_orders'
permission = 'event.orders:write'
template_name = 'pretixcontrol/order/change_locale.html'
def get_context_data(self, **kwargs):
@@ -2390,7 +2390,7 @@ class OrderViewMixin:
class OrderSendMail(EventPermissionRequiredMixin, OrderViewMixin, FormView):
template_name = 'pretixcontrol/order/sendmail.html'
permission = 'can_change_orders'
permission = 'event.orders:write'
form_class = OrderMailForm
def get_form_kwargs(self):
@@ -2514,7 +2514,7 @@ class OrderPositionSendMail(OrderSendMail):
class OrderEmailHistory(EventPermissionRequiredMixin, OrderViewMixin, ListView):
template_name = 'pretixcontrol/order/mail_history.html'
permission = 'can_view_orders'
permission = 'event.orders:read'
model = LogEntry
context_object_name = 'logs'
paginate_by = 10
@@ -2551,7 +2551,7 @@ class OrderEmailHistory(EventPermissionRequiredMixin, OrderViewMixin, ListView):
class AnswerDownload(EventPermissionRequiredMixin, OrderViewMixin, ListView):
permission = 'can_view_orders'
permission = 'event.orders:read'
def get(self, request, *args, **kwargs):
answid = kwargs.get('answer')
@@ -2575,7 +2575,7 @@ class AnswerDownload(EventPermissionRequiredMixin, OrderViewMixin, ListView):
class OverView(EventPermissionRequiredMixin, TemplateView):
template_name = 'pretixcontrol/orders/overview.html'
permission = 'can_view_orders'
permission = 'event.orders:read'
@cached_property
def filter_form(self):
@@ -2614,7 +2614,7 @@ class OverView(EventPermissionRequiredMixin, TemplateView):
class OrderGo(EventPermissionRequiredMixin, View):
permission = 'can_view_orders'
permission = 'event.orders:read'
def get_order(self, code):
try:
@@ -2649,12 +2649,7 @@ class OrderGo(EventPermissionRequiredMixin, View):
class ExportMixin:
@cached_property
def exporters(self):
responses = register_data_exporters.send(self.request.event)
raw_exporters = [response(self.request.event, self.request.organizer) for r, response in responses if response]
raw_exporters = [
ex for ex in raw_exporters
if ex.available_for_user(self.request.user if self.request.user and self.request.user.is_authenticated else None)
]
raw_exporters = list(init_event_exporters(self.request.event, user=self.request.user, request=self.request))
return sorted(
raw_exporters,
key=lambda ex: (0 if ex.category else 1, ex.category or "", 0 if ex.featured else 1, str(ex.verbose_name).lower())
@@ -2699,7 +2694,7 @@ class ExportMixin:
return ex
def get_scheduled_queryset(self):
if not self.request.user.has_event_permission(self.request.organizer, self.request.event, 'can_change_event_settings',
if not self.request.user.has_event_permission(self.request.organizer, self.request.event, 'event.settings.general:write',
request=self.request):
qs = self.request.event.scheduled_exports.filter(owner=self.request.user)
else:
@@ -2726,7 +2721,7 @@ class ExportMixin:
class ExportDoView(EventPermissionRequiredMixin, ExportMixin, AsyncAction, TemplateView):
permission = 'can_view_orders'
permission = None
known_errortypes = ['ExportError', 'ExportEmptyError']
task = export
template_name = 'pretixcontrol/orders/export_form.html'
@@ -2771,11 +2766,20 @@ class ExportDoView(EventPermissionRequiredMixin, ExportMixin, AsyncAction, Templ
cf.date = now()
cf.expires = now() + timedelta(hours=24)
cf.save()
return self.do(self.request.event.id, str(cf.id), self.exporter.identifier, data)
return self.do(
self.request.event.id,
user=self.request.user.id,
fileid=str(cf.id),
provider=self.exporter.identifier,
device=None,
token=None,
form_data=data,
staff_session=self.request.user.has_active_staff_session(self.request.session.session_key)
)
class ExportView(EventPermissionRequiredMixin, ExportMixin, ListView):
permission = 'can_view_orders'
permission = None
paginate_by = 25
context_object_name = 'scheduled'
@@ -2787,7 +2791,16 @@ class ExportView(EventPermissionRequiredMixin, ExportMixin, ListView):
@transaction.atomic()
def post(self, request, *args, **kwargs):
if request.POST.get("schedule") == "save":
if not self.has_permission():
if self.scheduled and self.scheduled.pk and not self.has_permission_to_edit_scheduled():
messages.error(
self.request,
_(
"Your user account does not have sufficient permission to run this report, therefore "
"you cannot change it."
)
)
return super().get(request, *args, **kwargs)
elif (not self.scheduled or not self.scheduled.pk) and not self.has_permission_to_create_scheduled():
messages.error(
self.request,
_(
@@ -2875,8 +2888,32 @@ class ExportView(EventPermissionRequiredMixin, ExportMixin, ListView):
def get_queryset(self):
return self.get_scheduled_queryset()
def has_permission(self):
return self.request.user.has_event_permission(self.request.organizer, self.request.event, "can_view_orders")
def has_permission_to_edit_scheduled(self):
# Exports can be edited by
# - their owner
# - any staff session user
# - any user with permission for organizer settings *and* the permissions required to run the rport
# This is to prevent a possible privilege escalation where user A creates a scheduled export and
# user B has settings permission (= they can see the export configuration), but not enough permission
# to run the export themselves. Without this check, user B could modify the export and add themselves
# as a recipient. Thereby, user B would gain access to data they can't have.
if not self.exporter:
return False
if self.scheduled.owner == self.request.user:
return True
if self.request.user.has_active_staff_session(self.request.session.session_key):
return True
if not self.exporter.available_for_user(self.request.user):
return False
if self.request.user.has_event_permission(self.request.organizer, self.request.event,
"event.settings.general:write", request=self.request):
return self.request.user.has_event_permission(self.request.organizer, self.request.event,
self.exporter.get_required_event_permission())
def has_permission_to_create_scheduled(self):
# Exports can only be created if the user has the correct permissions. We *ignore* staff sessions, because
# the export is not *run* during a staff session and then would fail at the scheduled time.
return self.request.user.has_event_permission(self.request.organizer, self.request.event, self.exporter.get_required_event_permission())
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
@@ -2885,6 +2922,15 @@ class ExportView(EventPermissionRequiredMixin, ExportMixin, ListView):
ctx['schedule_form'] = self.schedule_form
ctx['rrule_form'] = self.rrule_form
ctx['scheduled_copy_from'] = self.scheduled_copy_from
if self.scheduled and self.scheduled.pk and not self.has_permission_to_edit_scheduled() and self.exporter:
ctx['no_save'] = True
for f in self.exporter.form.fields.values():
f.disabled = True
for f in self.rrule_form.fields.values():
f.disabled = True
for f in self.schedule_form.fields.values():
f.disabled = True
elif not self.exporter:
for s in ctx['scheduled']:
try:
@@ -2895,7 +2941,7 @@ class ExportView(EventPermissionRequiredMixin, ExportMixin, ListView):
class DeleteScheduledExportView(EventPermissionRequiredMixin, ExportMixin, CompatDeleteView):
permission = 'can_view_orders'
permission = None
template_name = 'pretixcontrol/orders/export_delete.html'
context_object_name = 'export'
@@ -2944,7 +2990,7 @@ class RefundList(EventPermissionRequiredMixin, PaginationMixin, ListView):
model = OrderRefund
context_object_name = 'refunds'
template_name = 'pretixcontrol/orders/refunds.html'
permission = 'can_view_orders'
permission = 'event.orders:read'
def get_queryset(self):
qs = OrderRefund.objects.filter(
@@ -2969,7 +3015,7 @@ class RefundList(EventPermissionRequiredMixin, PaginationMixin, ListView):
class EventCancel(EventPermissionRequiredMixin, AsyncAction, FormView):
template_name = 'pretixcontrol/orders/cancel.html'
permission = 'can_change_orders'
permission = 'event:cancel'
form_class = EventCancelForm
task = cancel_event
known_errortypes = ['OrderError']
@@ -3054,7 +3100,7 @@ class EventCancel(EventPermissionRequiredMixin, AsyncAction, FormView):
class EventCancelConfirm(EventPermissionRequiredMixin, AsyncAction, FormView):
template_name = 'pretixcontrol/orders/cancel_confirm.html'
permission = 'can_change_orders'
permission = 'event.orders:write'
form_class = EventCancelConfirmForm
task = cancel_event
known_errortypes = ['OrderError']

View File

@@ -96,15 +96,18 @@ from pretix.base.models.giftcards import (
GiftCardAcceptance, GiftCardTransaction, gen_giftcard_secret,
)
from pretix.base.models.orders import CancellationRequest
from pretix.base.models.organizer import SalesChannel, TeamAPIToken
from pretix.base.models.organizer import (
SalesChannel, TeamAPIToken, TeamQuerySet,
)
from pretix.base.payment import PaymentException
from pretix.base.plugins import (
PLUGIN_LEVEL_EVENT, PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID,
PLUGIN_LEVEL_ORGANIZER,
)
from pretix.base.services.export import multiexport, scheduled_organizer_export
from pretix.base.services.export import (
init_organizer_exporters, multiexport, scheduled_organizer_export,
)
from pretix.base.services.mail import mail, prefix_subject
from pretix.base.signals import register_multievent_data_exporters
from pretix.base.templatetags.rich_text import markdown_compile_email
from pretix.base.views.tasks import AsyncAction
from pretix.control.forms.exports import ScheduledOrganizerExportForm
@@ -245,13 +248,13 @@ class OrganizerDetail(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin
class OrganizerTeamView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, DetailView):
model = Organizer
template_name = 'pretixcontrol/organizers/teams.html'
permission = 'can_change_permissions'
permission = 'organizer.teams:write'
context_object_name = 'organizer'
class OrganizerSettingsFormView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, FormView):
model = Organizer
permission = 'can_change_organizer_settings'
permission = 'organizer.settings.general:write'
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
@@ -282,7 +285,7 @@ class OrganizerSettingsFormView(OrganizerDetailViewMixin, OrganizerPermissionReq
class OrganizerMailSettings(OrganizerSettingsFormView):
form_class = MailSettingsForm
template_name = 'pretixcontrol/organizers/mail.html'
permission = 'can_change_organizer_settings'
permission = 'organizer.settings.general:write'
def get_success_url(self):
return reverse('control:organizer.settings.mail', kwargs={
@@ -308,7 +311,7 @@ class OrganizerMailSettings(OrganizerSettingsFormView):
class MailSettingsSetup(OrganizerPermissionRequiredMixin, MailSettingsSetupView):
permission = 'can_change_organizer_settings'
permission = 'organizer.settings.general:write'
basetpl = 'pretixcontrol/base.html'
def get_success_url(self):
@@ -323,7 +326,7 @@ class MailSettingsSetup(OrganizerPermissionRequiredMixin, MailSettingsSetupView)
class MailSettingsPreview(OrganizerPermissionRequiredMixin, View):
permission = 'can_change_organizer_settings'
permission = 'organizer.settings.general:write'
# return the origin text if key is missing in dict
class SafeDict(dict):
@@ -455,7 +458,7 @@ class OrganizerUpdate(OrganizerPermissionRequiredMixin, UpdateView):
model = Organizer
form_class = OrganizerUpdateForm
template_name = 'pretixcontrol/organizers/edit.html'
permission = 'can_change_organizer_settings'
permission = 'organizer.settings.general:write'
context_object_name = 'organizer'
@cached_property
@@ -583,10 +586,7 @@ class OrganizerCreate(CreateView):
ret = super().form_valid(form)
t = Team.objects.create(
organizer=form.instance, name=_('Administrators'),
all_events=True, can_create_events=True, can_change_teams=True, can_manage_gift_cards=True,
can_change_organizer_settings=True, can_change_event_settings=True, can_change_items=True,
can_manage_customers=True, can_manage_reusable_media=True,
can_view_orders=True, can_change_orders=True, can_view_vouchers=True, can_change_vouchers=True
all_events=True, all_event_permissions=True, all_organizer_permissions=True,
)
t.members.add(self.request.user)
return ret
@@ -600,7 +600,7 @@ class OrganizerCreate(CreateView):
class OrganizerPlugins(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, TemplateView, SingleObjectMixin):
model = Organizer
context_object_name = 'organizer'
permission = 'can_change_organizer_settings'
permission = 'organizer.settings.general:write'
template_name = 'pretixcontrol/organizers/plugins.html'
def get_object(self, queryset=None) -> Organizer:
@@ -772,14 +772,14 @@ class OrganizerPlugins(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixi
class OrganizerPluginEvents(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, FormView):
model = Organizer
context_object_name = 'organizer'
permission = 'can_change_organizer_settings'
permission = 'organizer.settings.general:write'
template_name = 'pretixcontrol/organizers/plugin_events.html'
form_class = OrganizerPluginEventsForm
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs["events"] = self.request.user.get_events_with_permission(
"can_change_event_settings", request=self.request
"event.settings.general:write", request=self.request
).filter(organizer=self.request.organizer)
kwargs["initial"] = {
"events": self.request.organizer.events.filter(plugins__regex='(^|,)' + self.plugin.module + '(,|$)')
@@ -857,7 +857,7 @@ class OrganizerPluginEvents(OrganizerDetailViewMixin, OrganizerPermissionRequire
class TeamListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, PaginationMixin, ListView):
model = Team
template_name = 'pretixcontrol/organizers/teams.html'
permission = 'can_change_teams'
permission = 'organizer.teams:write'
context_object_name = 'teams'
def get_queryset(self):
@@ -883,7 +883,7 @@ class TeamListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, P
class TeamCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CreateView):
model = Team
template_name = 'pretixcontrol/organizers/team_edit.html'
permission = 'can_change_teams'
permission = 'organizer.teams:write'
form_class = TeamForm
def get_form_kwargs(self):
@@ -906,10 +906,7 @@ class TeamCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin,
form.instance.organizer = self.request.organizer
ret = super().form_valid(form)
form.instance.members.add(self.request.user)
form.instance.log_action('pretix.team.created', user=self.request.user, data={
k: getattr(self.object, k) if k != 'limit_events' else [e.id for e in getattr(self.object, k).all()]
for k in form.changed_data
})
form.instance.log_action('pretix.team.created', user=self.request.user, data=form.changed_data_for_log)
return ret
def form_invalid(self, form):
@@ -920,7 +917,7 @@ class TeamCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin,
class TeamUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, UpdateView):
model = Team
template_name = 'pretixcontrol/organizers/team_edit.html'
permission = 'can_change_teams'
permission = 'organizer.teams:write'
context_object_name = 'team'
form_class = TeamForm
@@ -941,10 +938,7 @@ class TeamUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin,
@transaction.atomic
def form_valid(self, form):
if form.has_changed():
self.object.log_action('pretix.team.changed', user=self.request.user, data={
k: getattr(self.object, k) if k != 'limit_events' else [e.id for e in getattr(self.object, k).all()]
for k in form.changed_data
})
self.object.log_action('pretix.team.changed', user=self.request.user, data=form.changed_data_for_log)
messages.success(self.request, _('Your changes have been saved.'))
return super().form_valid(form)
@@ -956,7 +950,7 @@ class TeamUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin,
class TeamDeleteView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CompatDeleteView):
model = Team
template_name = 'pretixcontrol/organizers/team_delete.html'
permission = 'can_change_teams'
permission = 'organizer.teams:write'
context_object_name = 'team'
def get_object(self, queryset=None):
@@ -974,7 +968,8 @@ class TeamDeleteView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin,
def is_allowed(self) -> bool:
return self.request.organizer.teams.exclude(pk=self.kwargs.get('team')).filter(
can_change_teams=True, members__isnull=False
TeamQuerySet.organizer_permission_q("organizer.teams:write"),
members__isnull=False
).exists() or self.request.user.has_active_staff_session(self.request.session.session_key)
@transaction.atomic
@@ -1015,7 +1010,7 @@ class TeamDeleteView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin,
class TeamMemberView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, DetailView):
template_name = 'pretixcontrol/organizers/team_members.html'
context_object_name = 'team'
permission = 'can_change_teams'
permission = 'organizer.teams:write'
model = Team
def get_object(self, queryset=None):
@@ -1067,9 +1062,10 @@ class TeamMemberView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin,
pass
else:
other_admin_teams = self.request.organizer.teams.exclude(pk=self.object.pk).filter(
can_change_teams=True, members__isnull=False
TeamQuerySet.organizer_permission_q("organizer.teams:write"),
members__isnull=False
).exists() or self.request.user.has_active_staff_session(self.request.session.session_key)
if not other_admin_teams and self.object.can_change_teams and self.object.members.count() == 1:
if not other_admin_teams and self.object.has_organizer_permission("organizer.teams:write") and self.object.members.count() == 1:
messages.error(self.request, _('You cannot remove the last member from this team as no one would '
'be left with the permission to change teams.'))
return redirect(self.get_success_url())
@@ -1233,7 +1229,7 @@ class DeviceQueryMixin:
class DeviceListView(DeviceQueryMixin, OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
model = Device
template_name = 'pretixcontrol/organizers/devices.html'
permission = 'can_change_organizer_settings'
permission = 'organizer.devices:read'
context_object_name = 'devices'
paginate_by = 100
@@ -1246,7 +1242,7 @@ class DeviceListView(DeviceQueryMixin, OrganizerDetailViewMixin, OrganizerPermis
class DeviceCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CreateView):
model = Device
template_name = 'pretixcontrol/organizers/device_edit.html'
permission = 'can_change_organizer_settings'
permission = 'organizer.devices:write'
form_class = DeviceForm
def get_form_kwargs(self):
@@ -1277,7 +1273,7 @@ class DeviceCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixi
class DeviceLogView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
template_name = 'pretixcontrol/organizers/device_logs.html'
permission = 'can_change_organizer_settings'
permission = 'organizer.devices:read'
model = LogEntry
context_object_name = 'logs'
paginate_by = 20
@@ -1305,7 +1301,7 @@ class DeviceLogView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin,
class DeviceUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, UpdateView):
model = Device
template_name = 'pretixcontrol/organizers/device_edit.html'
permission = 'can_change_organizer_settings'
permission = 'organizer.devices:write'
context_object_name = 'device'
form_class = DeviceForm
@@ -1348,7 +1344,7 @@ class DeviceUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixi
class DeviceBulkUpdateView(DeviceQueryMixin, OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, FormView):
template_name = 'pretixcontrol/organizers/device_bulk_edit.html'
permission = 'can_change_organizer_settings'
permission = 'organizer.devices:write'
context_object_name = 'device'
form_class = DeviceBulkEditForm
@@ -1462,7 +1458,7 @@ class DeviceBulkUpdateView(DeviceQueryMixin, OrganizerDetailViewMixin, Organizer
class DeviceConnectView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, DetailView):
model = Device
template_name = 'pretixcontrol/organizers/device_connect.html'
permission = 'can_change_organizer_settings'
permission = 'organizer.devices:write'
context_object_name = 'device'
def get_object(self, queryset=None):
@@ -1494,7 +1490,7 @@ class DeviceConnectView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMix
class DeviceRevokeView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, DetailView):
model = Device
template_name = 'pretixcontrol/organizers/device_revoke.html'
permission = 'can_change_organizer_settings'
permission = 'organizer.devices:write'
context_object_name = 'device'
def get_object(self, queryset=None):
@@ -1524,7 +1520,7 @@ class DeviceRevokeView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixi
class WebHookListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
model = WebHook
template_name = 'pretixcontrol/organizers/webhooks.html'
permission = 'can_change_organizer_settings'
permission = 'organizer.settings.general:write'
context_object_name = 'webhooks'
def get_queryset(self):
@@ -1534,7 +1530,7 @@ class WebHookListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin
class WebHookCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CreateView):
model = WebHook
template_name = 'pretixcontrol/organizers/webhook_edit.html'
permission = 'can_change_organizer_settings'
permission = 'organizer.settings.general:write'
form_class = WebHookForm
def get_form_kwargs(self):
@@ -1568,7 +1564,7 @@ class WebHookCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMix
class WebHookUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, UpdateView):
model = WebHook
template_name = 'pretixcontrol/organizers/webhook_edit.html'
permission = 'can_change_organizer_settings'
permission = 'organizer.settings.general:write'
context_object_name = 'webhook'
form_class = WebHookForm
@@ -1611,7 +1607,7 @@ class WebHookUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMix
class WebHookLogsView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
model = WebHook
template_name = 'pretixcontrol/organizers/webhook_logs.html'
permission = 'can_change_organizer_settings'
permission = 'organizer.settings.general:write'
context_object_name = 'calls'
paginate_by = 50
@@ -1653,7 +1649,7 @@ class WebHookLogsView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin
class GiftCardAcceptanceInviteView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, FormView):
model = GiftCardAcceptance
template_name = 'pretixcontrol/organizers/giftcard_acceptance_invite.html'
permission = 'can_change_organizer_settings'
permission = 'organizer.settings.general:write'
form_class = GiftCardAcceptanceInviteForm
def get_form_kwargs(self):
@@ -1686,7 +1682,7 @@ class GiftCardAcceptanceInviteView(OrganizerDetailViewMixin, OrganizerPermission
class GiftCardAcceptanceListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
model = GiftCardAcceptance
template_name = 'pretixcontrol/organizers/giftcard_acceptance_list.html'
permission = 'can_change_organizer_settings'
permission = 'organizer.settings.general:write'
context_object_name = 'acceptor_acceptance'
paginate_by = 50
@@ -1755,7 +1751,7 @@ class GiftCardAcceptanceListView(OrganizerDetailViewMixin, OrganizerPermissionRe
class GiftCardListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
model = GiftCard
template_name = 'pretixcontrol/organizers/giftcards.html'
permission = 'can_manage_gift_cards'
permission = 'organizer.giftcards:read'
context_object_name = 'giftcards'
paginate_by = 50
@@ -1778,7 +1774,7 @@ class GiftCardListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixi
ctx = super().get_context_data(**kwargs)
ctx['filter_form'] = self.filter_form
ctx['other_organizers'] = self.request.user.get_organizers_with_permission(
'can_manage_gift_cards', self.request
'organizer.giftcards:write', self.request
).exclude(pk=self.request.organizer.pk)
return ctx
@@ -1789,7 +1785,7 @@ class GiftCardListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixi
class GiftCardDetailView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, DetailView):
template_name = 'pretixcontrol/organizers/giftcard.html'
permission = 'can_manage_gift_cards'
permission = 'organizer.giftcards:read'
context_object_name = 'card'
def get_object(self, queryset=None) -> Organizer:
@@ -1800,6 +1796,8 @@ class GiftCardDetailView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMi
@transaction.atomic()
def post(self, request, *args, **kwargs):
if not request.user.has_organizer_permission(request.organizer, "organizer.giftcards:write", request=request):
raise PermissionDenied()
self.object = GiftCard.objects.select_for_update(of=OF_SELF).get(pk=self.get_object().pk)
if 'revert' in request.POST:
t = get_object_or_404(self.object.transactions.all(), pk=request.POST.get('revert'), order__isnull=False)
@@ -1881,7 +1879,7 @@ class GiftCardDetailView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMi
class GiftCardCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CreateView):
template_name = 'pretixcontrol/organizers/giftcard_create.html'
permission = 'can_manage_gift_cards'
permission = 'organizer.giftcards:write'
form_class = GiftCardCreateForm
success_url = 'invalid'
@@ -1932,7 +1930,7 @@ class GiftCardCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMi
class GiftCardUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, UpdateView):
template_name = 'pretixcontrol/organizers/giftcard_edit.html'
permission = 'can_manage_gift_cards'
permission = 'organizer.giftcards:write'
form_class = GiftCardUpdateForm
success_url = 'invalid'
context_object_name = 'card'
@@ -2012,7 +2010,7 @@ class ExportMixin:
)),
('events',
forms.ModelMultipleChoiceField(
queryset=self.events,
queryset=ex.events,
widget=forms.CheckboxSelectMultiple(
attrs={
'class': 'scrolling-multiple-choice',
@@ -2025,29 +2023,9 @@ class ExportMixin:
])
return ex
@cached_property
def events(self):
return self.request.user.get_events_with_permission('can_view_orders', request=self.request).filter(
organizer=self.request.organizer
)
@cached_property
def exporters(self):
responses = register_multievent_data_exporters.send(self.request.organizer)
raw_exporters = [
response(Event.objects.none() if issubclass(response, OrganizerLevelExportMixin) else self.events,
self.request.organizer)
for r, response in responses
if response
]
raw_exporters = [
ex for ex in raw_exporters
if (
not isinstance(ex, OrganizerLevelExportMixin) or
self.request.user.has_organizer_permission(self.request.organizer, ex.organizer_required_permission,
self.request)
) and ex.available_for_user(self.request.user if self.request.user and self.request.user.is_authenticated else None)
]
raw_exporters = list(init_organizer_exporters(self.request.organizer, user=self.request.user, request=self.request))
return sorted(
raw_exporters,
key=lambda ex: (
@@ -2061,7 +2039,7 @@ class ExportMixin:
return ctx
def get_scheduled_queryset(self):
if not self.request.user.has_organizer_permission(self.request.organizer, 'can_change_organizer_settings',
if not self.request.user.has_organizer_permission(self.request.organizer, 'organizer.settings.general:write',
request=self.request):
qs = self.request.organizer.scheduled_exports.filter(owner=self.request.user)
else:
@@ -2146,7 +2124,16 @@ class ExportView(OrganizerPermissionRequiredMixin, ExportMixin, ListView):
@transaction.atomic()
def post(self, request, *args, **kwargs):
if request.POST.get("schedule") == "save":
if not self.has_permission():
if self.scheduled and self.scheduled.pk and not self.has_permission_to_edit_scheduled():
messages.error(
self.request,
_(
"Your user account does not have sufficient permission to run this report, therefore "
"you cannot change it."
)
)
return super().get(request, *args, **kwargs)
elif (not self.scheduled or not self.scheduled.pk) and not self.has_permission_to_create_scheduled():
messages.error(
self.request,
_(
@@ -2237,12 +2224,58 @@ class ExportView(OrganizerPermissionRequiredMixin, ExportMixin, ListView):
def get_queryset(self):
return self.get_scheduled_queryset()
def has_permission(self):
if isinstance(self.exporter, OrganizerLevelExportMixin):
if not self.request.user.has_organizer_permission(self.request.organizer, self.exporter.organizer_required_permission):
return False
if self.exporter and not self.exporter.available_for_user(self.request.user):
def has_permission_to_edit_scheduled(self):
# Exports can be edited by
# - their owner
# - any staff session user
# - any user with permission for organizer settings *and* the permissions required to run the report
# This is to prevent a possible privilege escalation where user A creates a scheduled export and
# user B has settings permission (= they can see the export configuration), but not enough permission
# to run the export themselves. Without this check, user B could modify the export and add themselves
# as a recipient. Thereby, user B would gain access to data they can't have.
if not self.exporter:
# Triggered in scenario 5 in test_organizer_edit_restrictions
return False
if self.scheduled.owner == self.request.user:
return True
if self.request.user.has_active_staff_session(self.request.session.session_key):
return True
if not self.exporter.available_for_user(self.request.user):
return False
if self.request.user.has_organizer_permission(self.request.organizer, "organizer.settings.general:write", request=self.request):
if isinstance(self.exporter, OrganizerLevelExportMixin):
# Test scenario 5/6 in test_organizer_edit_restrictions
return self.request.user.has_organizer_permission(
self.request.organizer, self.exporter.get_required_organizer_permission(), request=self.request
)
else:
if self.scheduled.export_form_data.get("all_events", False):
# Test scenario 1/2 in test_organizer_edit_restrictions
return self.request.user.teams.filter(
TeamQuerySet.event_permission_q(self.exporter.get_required_event_permission()),
all_events=True,
).exists()
else:
# Test scenario 3/4 in test_organizer_edit_restrictions
events_selected = self.scheduled.export_form_data.get("events", [])
events_permission = set(self.request.user.get_events_with_permission(
self.exporter.get_required_event_permission(), request=self.request,
).values_list("pk", flat=True))
return all(e in events_permission for e in events_selected)
def has_permission_to_create_scheduled(self):
# Exports can only be created if the user has the correct permissions. We *ignore* staff sessions, because
# the export is not *run* during a staff session and then would fail at the scheduled time.
if self.exporter:
if isinstance(self.exporter, OrganizerLevelExportMixin):
if not self.request.user.has_organizer_permission(self.request.organizer, self.exporter.get_required_organizer_permission()):
return False
else:
permission_name = self.exporter.get_required_event_permission()
if not any(t.has_event_permission(permission_name) for t in self.request.user.teams.filter(organizer=self.request.organizer)):
return False
if not self.exporter.available_for_user(self.request.user):
return False
return True
def get_context_data(self, **kwargs):
@@ -2251,6 +2284,17 @@ class ExportView(OrganizerPermissionRequiredMixin, ExportMixin, ListView):
ctx['schedule_form'] = self.schedule_form
ctx['rrule_form'] = self.rrule_form
ctx['scheduled_copy_from'] = self.scheduled_copy_from
if self.scheduled and self.scheduled.pk and not self.has_permission_to_edit_scheduled() and self.exporter:
ctx['no_save'] = True
for f in self.exporter.form.fields.values():
f.disabled = True
f.widget.attrs.pop("data-inverse-dependency", None)
for f in self.rrule_form.fields.values():
f.disabled = True
for f in self.schedule_form.fields.values():
f.disabled = True
elif not self.exporter:
for s in ctx['scheduled']:
try:
@@ -2307,7 +2351,7 @@ class RunScheduledExportView(OrganizerPermissionRequiredMixin, ExportMixin, View
class GateListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
model = Gate
template_name = 'pretixcontrol/organizers/gates.html'
permission = 'can_change_organizer_settings'
permission = 'organizer.devices:read'
context_object_name = 'gates'
def get_queryset(self):
@@ -2317,7 +2361,7 @@ class GateListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, L
class GateCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CreateView):
model = Gate
template_name = 'pretixcontrol/organizers/gate_edit.html'
permission = 'can_change_organizer_settings'
permission = 'organizer.devices:write'
form_class = GateForm
def get_form_kwargs(self):
@@ -2351,7 +2395,7 @@ class GateCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin,
class GateUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, UpdateView):
model = Gate
template_name = 'pretixcontrol/organizers/gate_edit.html'
permission = 'can_change_organizer_settings'
permission = 'organizer.devices:write'
context_object_name = 'gate'
form_class = GateForm
@@ -2386,7 +2430,7 @@ class GateUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin,
class GateDeleteView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CompatDeleteView):
model = Gate
template_name = 'pretixcontrol/organizers/gate_delete.html'
permission = 'can_change_organizer_settings'
permission = 'organizer.devices:write'
context_object_name = 'gate'
def get_object(self, queryset=None):
@@ -2410,7 +2454,7 @@ class GateDeleteView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin,
class EventMetaPropertyListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
model = EventMetaProperty
template_name = 'pretixcontrol/organizers/properties.html'
permission = 'can_change_organizer_settings'
permission = 'organizer.settings.general:write'
context_object_name = 'properties'
def get_queryset(self):
@@ -2461,7 +2505,7 @@ class EventMetaPropertyEditorMixin:
class EventMetaPropertyCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, EventMetaPropertyEditorMixin, CreateView):
model = EventMetaProperty
permission = 'can_change_organizer_settings'
permission = 'organizer.settings.general:write'
def get_object(self, queryset=None):
return EventMetaProperty()
@@ -2491,7 +2535,7 @@ class EventMetaPropertyCreateView(OrganizerDetailViewMixin, OrganizerPermissionR
class EventMetaPropertyUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, EventMetaPropertyEditorMixin, UpdateView):
model = EventMetaProperty
permission = 'can_change_organizer_settings'
permission = 'organizer.settings.general:write'
context_object_name = 'property'
def get_object(self, queryset=None):
@@ -2523,7 +2567,7 @@ class EventMetaPropertyUpdateView(OrganizerDetailViewMixin, OrganizerPermissionR
class EventMetaPropertyDeleteView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CompatDeleteView):
model = EventMetaProperty
template_name = 'pretixcontrol/organizers/property_delete.html'
permission = 'can_change_organizer_settings'
permission = 'organizer.settings.general:write'
context_object_name = 'property'
def get_object(self, queryset=None):
@@ -2567,7 +2611,7 @@ def meta_property_move(request, property, up=True):
messages.success(request, _('The order of properties has been updated.'))
@organizer_permission_required("can_change_organizer_settings")
@organizer_permission_required("organizer.settings.general:write")
@require_http_methods(["POST"])
def meta_property_move_up(request, organizer, property):
meta_property_move(request, property, up=True)
@@ -2575,7 +2619,7 @@ def meta_property_move_up(request, organizer, property):
organizer=request.organizer.slug)
@organizer_permission_required("can_change_organizer_settings")
@organizer_permission_required("organizer.settings.general:write")
@require_http_methods(["POST"])
def meta_property_move_down(request, organizer, property):
meta_property_move(request, property, up=False)
@@ -2584,7 +2628,7 @@ def meta_property_move_down(request, organizer, property):
@transaction.atomic
@organizer_permission_required("can_change_organizer_settings")
@organizer_permission_required("organizer.settings.general:write")
@require_http_methods(["POST"])
def reorder_meta_properties(request, organizer):
try:
@@ -2616,7 +2660,7 @@ def reorder_meta_properties(request, organizer):
class LogView(OrganizerPermissionRequiredMixin, PaginationMixin, ListView):
template_name = 'pretixcontrol/organizers/logs.html'
permission = 'can_change_organizer_settings'
permission = 'organizer.settings.general:write'
model = LogEntry
context_object_name = 'logs'
@@ -2641,7 +2685,7 @@ class LogView(OrganizerPermissionRequiredMixin, PaginationMixin, ListView):
class MembershipTypeListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
model = MembershipType
template_name = 'pretixcontrol/organizers/membershiptypes.html'
permission = 'can_change_organizer_settings'
permission = 'organizer.settings.general:write'
context_object_name = 'types'
def get_queryset(self):
@@ -2651,7 +2695,7 @@ class MembershipTypeListView(OrganizerDetailViewMixin, OrganizerPermissionRequir
class MembershipTypeCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CreateView):
model = MembershipType
template_name = 'pretixcontrol/organizers/membershiptype_edit.html'
permission = 'can_change_organizer_settings'
permission = 'organizer.settings.general:write'
form_class = MembershipTypeForm
def get_object(self, queryset=None):
@@ -2685,7 +2729,7 @@ class MembershipTypeCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequ
class MembershipTypeUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, UpdateView):
model = MembershipType
template_name = 'pretixcontrol/organizers/membershiptype_edit.html'
permission = 'can_change_organizer_settings'
permission = 'organizer.settings.general:write'
context_object_name = 'type'
form_class = MembershipTypeForm
@@ -2720,7 +2764,7 @@ class MembershipTypeUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequ
class MembershipTypeDeleteView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CompatDeleteView):
model = MembershipType
template_name = 'pretixcontrol/organizers/membershiptype_delete.html'
permission = 'can_change_organizer_settings'
permission = 'organizer.settings.general:write'
context_object_name = 'type'
def get_object(self, queryset=None):
@@ -2750,7 +2794,7 @@ class MembershipTypeDeleteView(OrganizerDetailViewMixin, OrganizerPermissionRequ
class SSOProviderListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
model = CustomerSSOProvider
template_name = 'pretixcontrol/organizers/ssoproviders.html'
permission = 'can_change_organizer_settings'
permission = 'organizer.settings.general:write'
context_object_name = 'providers'
def get_queryset(self):
@@ -2760,7 +2804,7 @@ class SSOProviderListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredM
class SSOProviderCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CreateView):
model = CustomerSSOProvider
template_name = 'pretixcontrol/organizers/ssoprovider_edit.html'
permission = 'can_change_organizer_settings'
permission = 'organizer.settings.general:write'
form_class = SSOProviderForm
def get_object(self, queryset=None):
@@ -2794,7 +2838,7 @@ class SSOProviderCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequire
class SSOProviderUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, UpdateView):
model = CustomerSSOProvider
template_name = 'pretixcontrol/organizers/ssoprovider_edit.html'
permission = 'can_change_organizer_settings'
permission = 'organizer.settings.general:write'
context_object_name = 'provider'
form_class = SSOProviderForm
@@ -2836,7 +2880,7 @@ class SSOProviderUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequire
class SSOProviderDeleteView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CompatDeleteView):
model = CustomerSSOProvider
template_name = 'pretixcontrol/organizers/ssoprovider_delete.html'
permission = 'can_change_organizer_settings'
permission = 'organizer.settings.general:write'
context_object_name = 'provider'
def get_object(self, queryset=None):
@@ -2866,7 +2910,7 @@ class SSOProviderDeleteView(OrganizerDetailViewMixin, OrganizerPermissionRequire
class SSOClientListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
model = CustomerSSOClient
template_name = 'pretixcontrol/organizers/ssoclients.html'
permission = 'can_change_organizer_settings'
permission = 'organizer.settings.general:write'
context_object_name = 'clients'
def get_queryset(self):
@@ -2876,7 +2920,7 @@ class SSOClientListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMix
class SSOClientCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CreateView):
model = CustomerSSOClient
template_name = 'pretixcontrol/organizers/ssoclient_edit.html'
permission = 'can_change_organizer_settings'
permission = 'organizer.settings.general:write'
form_class = SSOClientForm
def get_object(self, queryset=None):
@@ -2916,7 +2960,7 @@ class SSOClientCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredM
class SSOClientUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, UpdateView):
model = CustomerSSOClient
template_name = 'pretixcontrol/organizers/ssoclient_edit.html'
permission = 'can_change_organizer_settings'
permission = 'organizer.settings.general:write'
context_object_name = 'client'
form_class = SSOClientForm
@@ -2966,7 +3010,7 @@ class SSOClientUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredM
class SSOClientDeleteView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CompatDeleteView):
model = CustomerSSOClient
template_name = 'pretixcontrol/organizers/ssoclient_delete.html'
permission = 'can_change_organizer_settings'
permission = 'organizer.settings.general:write'
context_object_name = 'client'
def get_object(self, queryset=None):
@@ -2996,7 +3040,7 @@ class SSOClientDeleteView(OrganizerDetailViewMixin, OrganizerPermissionRequiredM
class CustomerListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, PaginationMixin, ListView):
model = Customer
template_name = 'pretixcontrol/organizers/customers.html'
permission = 'can_manage_customers'
permission = 'organizer.customers:read'
context_object_name = 'customers'
def get_queryset(self):
@@ -3017,7 +3061,7 @@ class CustomerListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixi
class CustomerDetailView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, PaginationMixin, ListView):
template_name = 'pretixcontrol/organizers/customer.html'
permission = 'can_manage_customers'
permission = 'organizer.customers:read'
context_object_name = 'orders'
def get_queryset(self):
@@ -3133,7 +3177,7 @@ class CustomerDetailView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMi
class CustomerCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CreateView):
template_name = 'pretixcontrol/organizers/customer_edit.html'
permission = 'can_manage_customers'
permission = 'organizer.customers:write'
context_object_name = 'customer'
form_class = CustomerCreateForm
@@ -3163,7 +3207,7 @@ class CustomerCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMi
class CustomerUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, UpdateView):
template_name = 'pretixcontrol/organizers/customer_edit.html'
permission = 'can_manage_customers'
permission = 'organizer.customers:write'
context_object_name = 'customer'
form_class = CustomerUpdateForm
@@ -3192,7 +3236,7 @@ class CustomerUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMi
class MembershipUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, UpdateView):
template_name = 'pretixcontrol/organizers/customer_membership.html'
permission = 'can_manage_customers'
permission = 'organizer.customers:write'
context_object_name = 'membership'
form_class = MembershipUpdateForm
@@ -3232,7 +3276,7 @@ class MembershipUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequired
class MembershipDeleteView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CompatDeleteView):
template_name = 'pretixcontrol/organizers/customer_membership_delete.html'
permission = 'can_manage_customers'
permission = 'organizer.customers:write'
context_object_name = 'membership'
def get_object(self, queryset=None):
@@ -3270,7 +3314,7 @@ class MembershipDeleteView(OrganizerDetailViewMixin, OrganizerPermissionRequired
class MembershipCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CreateView):
template_name = 'pretixcontrol/organizers/customer_membership.html'
permission = 'can_manage_customers'
permission = 'organizer.customers:write'
context_object_name = 'membership'
form_class = MembershipUpdateForm
@@ -3309,7 +3353,7 @@ class MembershipCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequired
class CustomerAnonymizeView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, DetailView):
template_name = 'pretixcontrol/organizers/customer_anonymize.html'
permission = 'can_manage_customers'
permission = 'organizer.customers:write'
context_object_name = 'customer'
def get_object(self, queryset=None):
@@ -3336,7 +3380,7 @@ class CustomerAnonymizeView(OrganizerDetailViewMixin, OrganizerPermissionRequire
class ReusableMediaListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, PaginationMixin, ListView):
model = ReusableMedium
template_name = 'pretixcontrol/organizers/reusable_media.html'
permission = 'can_manage_reusable_media'
permission = 'organizer.reusablemedia:read'
context_object_name = 'media'
def get_queryset(self):
@@ -3360,7 +3404,7 @@ class ReusableMediaListView(OrganizerDetailViewMixin, OrganizerPermissionRequire
class ReusableMediumDetailView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, TemplateView):
template_name = 'pretixcontrol/organizers/reusable_medium.html'
permission = 'can_manage_reusable_media'
permission = 'organizer.reusablemedia:read'
@cached_property
def medium(self):
@@ -3377,7 +3421,7 @@ class ReusableMediumDetailView(OrganizerDetailViewMixin, OrganizerPermissionRequ
class ReusableMediumCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CreateView):
template_name = 'pretixcontrol/organizers/reusable_medium_edit.html'
permission = 'can_manage_reusable_media'
permission = 'organizer.reusablemedia:write'
context_object_name = 'medium'
form_class = ReusableMediumCreateForm
@@ -3406,7 +3450,7 @@ class ReusableMediumCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequ
class ReusableMediumUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, UpdateView):
template_name = 'pretixcontrol/organizers/reusable_medium_edit.html'
permission = 'can_manage_reusable_media'
permission = 'organizer.reusablemedia:write'
context_object_name = 'medium'
form_class = ReusableMediumUpdateForm
@@ -3436,7 +3480,7 @@ class ReusableMediumUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequ
class ChannelListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
model = SalesChannel
template_name = 'pretixcontrol/organizers/channels.html'
permission = 'can_change_organizer_settings'
permission = 'organizer.settings.general:write'
context_object_name = 'channels'
def get_queryset(self):
@@ -3455,7 +3499,7 @@ class ChannelEditorMixin:
class ChannelCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ChannelEditorMixin, CreateView):
model = SalesChannel
permission = 'can_change_organizer_settings'
permission = 'organizer.settings.general:write'
template_name = 'pretixcontrol/organizers/channel_add.html'
def get_object(self, queryset=None):
@@ -3527,7 +3571,7 @@ class ChannelCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMix
class ChannelUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ChannelEditorMixin, UpdateView):
model = SalesChannel
permission = 'can_change_organizer_settings'
permission = 'organizer.settings.general:write'
context_object_name = 'channel'
template_name = 'pretixcontrol/organizers/channel_edit.html'
@@ -3572,7 +3616,7 @@ class ChannelUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMix
class ChannelDeleteView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CompatDeleteView):
model = SalesChannel
template_name = 'pretixcontrol/organizers/channel_delete.html'
permission = 'can_change_organizer_settings'
permission = 'organizer.settings.general:write'
context_object_name = 'channel'
def get_object(self, queryset=None):
@@ -3628,7 +3672,7 @@ def channel_move(request, channel, up=True):
messages.success(request, _('The order of sales channels has been updated.'))
@organizer_permission_required("can_change_organizer_settings")
@organizer_permission_required("organizer.settings.general:write")
@require_http_methods(["POST"])
def channel_move_up(request, organizer, channel):
channel_move(request, channel, up=True)
@@ -3636,7 +3680,7 @@ def channel_move_up(request, organizer, channel):
organizer=request.organizer.slug)
@organizer_permission_required("can_change_organizer_settings")
@organizer_permission_required("organizer.settings.general:write")
@require_http_methods(["POST"])
def channel_move_down(request, organizer, channel):
channel_move(request, channel, up=False)
@@ -3645,7 +3689,7 @@ def channel_move_down(request, organizer, channel):
@transaction.atomic
@organizer_permission_required("can_change_organizer_settings")
@organizer_permission_required("organizer.settings.general:write")
@require_http_methods(["POST"])
def reorder_channels(request, organizer):
try:

View File

@@ -58,7 +58,7 @@ logger = logging.getLogger(__name__)
class BaseEditorView(EventPermissionRequiredMixin, TemplateView):
template_name = 'pretixcontrol/pdf/index.html'
permission = 'can_change_settings'
permission = 'event.settings.general:write'
accepted_formats = (
'application/pdf',
)

View File

@@ -85,7 +85,7 @@ class OrderSearch(PaginationMixin, ListView):
if not self.request.user.has_active_staff_session(self.request.session.session_key):
qs = qs.filter(
Q(event_id__in=self.request.user.get_events_with_permission('can_view_orders').values_list('id', flat=True))
Q(event_id__in=self.request.user.get_events_with_permission('event.orders:read').values_list('id', flat=True))
)
if self.filter_form.is_valid():
@@ -159,7 +159,7 @@ class PaymentSearch(PaginationMixin, ListView):
if not self.request.user.has_active_staff_session(self.request.session.session_key):
qs = qs.filter(
Q(order__event_id__in=self.request.user.get_events_with_permission('can_view_orders').values_list('id', flat=True))
Q(order__event_id__in=self.request.user.get_events_with_permission('event.orders:read').values_list('id', flat=True))
)
if self.filter_form.is_valid():

View File

@@ -76,7 +76,7 @@ class ShredderMixin:
class StartShredView(RecentAuthenticationRequiredMixin, EventPermissionRequiredMixin, ShredderMixin, TemplateView):
permission = 'can_change_orders'
permission = 'event.orders:write'
template_name = 'pretixcontrol/shredder/index.html'
def get_context_data(self, **kwargs):
@@ -87,7 +87,7 @@ class StartShredView(RecentAuthenticationRequiredMixin, EventPermissionRequiredM
class ShredDownloadView(RecentAuthenticationRequiredMixin, EventPermissionRequiredMixin, ShredderMixin, TemplateView):
permission = 'can_change_orders'
permission = 'event.orders:write'
template_name = 'pretixcontrol/shredder/download.html'
def get_context_data(self, **kwargs):
@@ -119,7 +119,7 @@ class ShredDownloadView(RecentAuthenticationRequiredMixin, EventPermissionRequir
class ShredExportView(RecentAuthenticationRequiredMixin, EventPermissionRequiredMixin, ShredderMixin, AsyncAction, View):
permission = 'can_change_orders'
permission = 'event.orders:write'
task = export
known_errortypes = ['ShredError']
@@ -148,7 +148,7 @@ class ShredExportView(RecentAuthenticationRequiredMixin, EventPermissionRequired
class ShredDoView(RecentAuthenticationRequiredMixin, EventPermissionRequiredMixin, ShredderMixin, AsyncAction, View):
permission = 'can_change_orders'
permission = 'event.orders:write'
task = shred
known_errortypes = ['ShredError']

View File

@@ -117,7 +117,7 @@ class SubEventList(EventPermissionRequiredMixin, PaginationMixin, SubEventQueryM
model = SubEvent
context_object_name = 'subevents'
template_name = 'pretixcontrol/subevents/index.html'
permission = 'can_change_settings'
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 = 'can_change_settings'
permission = 'event.subevents:write'
context_object_name = 'subevents'
def get_object(self, queryset=None) -> SubEvent:
@@ -241,7 +241,7 @@ class SubEventEditorMixin(MetaDataEditorMixin):
property=p,
disabled=(
p.protected and
not self.request.user.has_organizer_permission(self.request.organizer, 'can_change_organizer_settings', request=self.request)
not self.request.user.has_organizer_permission(self.request.organizer, 'organizer.settings.general:write', request=self.request)
),
default=self._default_meta.get(p.name, ''),
instance=val_instances.get(p.pk, self.meta_model(property=p, subevent=self.object)),
@@ -508,7 +508,7 @@ class SubEventEditorMixin(MetaDataEditorMixin):
class SubEventUpdate(EventPermissionRequiredMixin, SubEventEditorMixin, UpdateView):
model = SubEvent
template_name = 'pretixcontrol/subevents/detail.html'
permission = 'can_change_settings'
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 = 'can_change_settings'
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 = 'can_change_settings'
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 = 'can_change_settings'
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 = 'can_change_settings'
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,

View File

@@ -51,6 +51,7 @@ from pretix.base.models import (
ItemVariation, ItemVariationMetaValue, Order, OrderPosition, Organizer,
SubEventMetaValue, User, Voucher,
)
from pretix.base.models.organizer import TeamQuerySet
from pretix.control.forms.event import EventWizardCopyForm
from pretix.control.permissions import (
event_permission_required, organizer_permission_required,
@@ -172,7 +173,7 @@ def event_list(request):
return JsonResponse(doc)
@organizer_permission_required(("can_manage_gift_cards", "can_manage_reusable_media"))
@organizer_permission_required(("organizer.giftcards:read", "organizer.reusablemedia:write"))
def giftcard_select2(request, **kwargs):
query = request.GET.get('query', '')
try:
@@ -180,7 +181,7 @@ def giftcard_select2(request, **kwargs):
except ValueError:
page = 1
if request.user.has_organizer_permission(request.organizer, 'can_manage_gift_cards', request):
if request.user.has_organizer_permission(request.organizer, 'organizer.giftcards:write', request):
qs = request.organizer.issued_gift_cards.filter(
Q(secret__icontains=query)
).order_by('secret')
@@ -210,7 +211,7 @@ def giftcard_select2(request, **kwargs):
return JsonResponse(doc)
@organizer_permission_required(("can_manage_reusable_media", "can_manage_gift_cards"))
@organizer_permission_required(("organizer.reusablemedia:write", "organizer.giftcards:write"))
def ticket_select2(request, **kwargs):
query = request.GET.get('query', '')
try:
@@ -240,8 +241,13 @@ def ticket_select2(request, **kwargs):
qs_orders = qs_orders.filter(
exact_match | (
soft_match & (
Q(order__event__organizer_id__in=request.user.teams.filter(all_events=True, can_view_orders=True).values_list('organizer', flat=True))
| Q(order__event_id__in=request.user.teams.filter(can_view_orders=True).values_list('limit_events__id', flat=True))
Q(order__event__organizer_id__in=request.user.teams.filter(
TeamQuerySet.event_permission_q("event.orders:read"),
all_events=True,
).values_list('organizer', flat=True))
| Q(order__event_id__in=request.user.teams.filter(
TeamQuerySet.event_permission_q("event.orders:read")
).values_list('limit_events__id', flat=True))
)
)
)
@@ -270,7 +276,7 @@ def ticket_select2(request, **kwargs):
return JsonResponse(doc)
@organizer_permission_required("can_manage_customers")
@organizer_permission_required("organizer.customers:write")
def customer_select2(request, **kwargs):
query = request.GET.get('query', '')
try:
@@ -337,9 +343,9 @@ def nav_context_list(request):
if not request.user.has_active_staff_session(request.session.session_key):
qs_orders = qs_orders.filter(
Q(event__organizer_id__in=request.user.teams.filter(
all_events=True, can_view_orders=True).values_list('organizer', flat=True))
TeamQuerySet.event_permission_q("event.orders:read"), all_events=True).values_list('organizer', flat=True))
| Q(event_id__in=request.user.teams.filter(
can_view_orders=True).values_list('limit_events__id', flat=True))
TeamQuerySet.event_permission_q("event.orders:read")).values_list('limit_events__id', flat=True))
)
qs_vouchers = Voucher.objects.filter(
@@ -348,9 +354,9 @@ def nav_context_list(request):
if not request.user.has_active_staff_session(request.session.session_key):
qs_vouchers = qs_vouchers.filter(
Q(event__organizer_id__in=request.user.teams.filter(
all_events=True, can_view_vouchers=True).values_list('organizer', flat=True))
TeamQuerySet.event_permission_q("event.vouchers:read"), all_events=True).values_list('organizer', flat=True))
| Q(event_id__in=request.user.teams.filter(
can_view_vouchers=True).values_list('limit_events__id', flat=True))
TeamQuerySet.event_permission_q("event.vouchers:read")).values_list('limit_events__id', flat=True))
)
else:
qs_vouchers = Voucher.objects.none()
@@ -813,7 +819,7 @@ def organizer_select2(request):
qs = qs.filter(Q(name__icontains=term) | Q(slug__icontains=term))
if not request.user.has_active_staff_session(request.session.session_key):
if 'can_create' in request.GET:
qs = qs.filter(pk__in=request.user.teams.filter(can_create_events=True).values_list('organizer', flat=True))
qs = qs.filter(pk__in=request.user.teams.filter(TeamQuerySet.organizer_permission_q("organizer.events:create")).values_list('organizer', flat=True))
else:
qs = qs.filter(pk__in=request.user.teams.values_list('organizer', flat=True))
@@ -976,21 +982,21 @@ def item_meta_values(request, organizer, event):
var_matches = var_matches.filter(variation__item__event__organizer_id=organizer.pk)
all_access = (
request.user.has_active_staff_session(request.session.session_key)
or request.user.teams.filter(all_events=True, organizer=organizer, can_change_items=True).exists()
or request.user.teams.filter(TeamQuerySet.event_permission_q("event.items:write"), all_events=True, organizer=organizer).exists()
)
if not all_access:
defaults = defaults.filter(
event__id__in=request.user.teams.filter(can_change_items=True).values_list(
event__id__in=request.user.teams.filter(TeamQuerySet.event_permission_q("event.items:write")).values_list(
'limit_events__id', flat=True
)
)
matches = matches.filter(
item__event__id__in=request.user.teams.filter(can_change_items=True).values_list(
item__event__id__in=request.user.teams.filter(TeamQuerySet.event_permission_q("event.items:write")).values_list(
'limit_events__id', flat=True
)
)
var_matches = var_matches.filter(
variation__item__event__id__in=request.user.teams.filter(can_change_items=True).values_list(
variation__item__event__id__in=request.user.teams.filter(TeamQuerySet.event_permission_q("event.items:write")).values_list(
'limit_events__id', flat=True
)
)
@@ -1007,10 +1013,16 @@ def item_meta_values(request, organizer, event):
})
@organizer_permission_required(("can_view_orders", "can_change_organizer_settings"))
# 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'))
@@ -1045,10 +1057,16 @@ def devices_select2(request, **kwargs):
return JsonResponse(doc)
@organizer_permission_required(("can_view_orders", "can_change_event_settings", "can_change_organizer_settings"))
# 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'))

View File

@@ -63,6 +63,7 @@ from pretix.base.models import (
CartPosition, LogEntry, Voucher, WaitingListEntry,
)
from pretix.base.models.vouchers import generate_codes
from pretix.base.permissions import AnyPermissionOf
from pretix.base.services.mail import prefix_subject
from pretix.base.services.placeholders import get_sample_context
from pretix.base.services.vouchers import vouchers_send
@@ -83,7 +84,7 @@ class VoucherList(PaginationMixin, EventPermissionRequiredMixin, ListView):
model = Voucher
context_object_name = 'vouchers'
template_name = 'pretixcontrol/vouchers/index.html'
permission = 'can_view_vouchers'
permission = 'event.vouchers:read'
@scopes_disabled() # we have an event check here, and we can save some performance on subqueries
def get_queryset(self):
@@ -155,7 +156,7 @@ class VoucherList(PaginationMixin, EventPermissionRequiredMixin, ListView):
class VoucherTags(EventPermissionRequiredMixin, TemplateView):
template_name = 'pretixcontrol/vouchers/tags.html'
permission = 'can_view_vouchers'
permission = 'event.vouchers:read'
def get_queryset(self):
qs = self.request.event.vouchers.order_by('tag').filter(
@@ -196,7 +197,7 @@ class VoucherTags(EventPermissionRequiredMixin, TemplateView):
class VoucherDeleteCarts(EventPermissionRequiredMixin, CompatDeleteView):
model = Voucher
template_name = 'pretixcontrol/vouchers/delete_carts.html'
permission = 'can_change_vouchers'
permission = 'event.vouchers:write'
context_object_name = 'voucher'
def get_object(self, queryset=None) -> Voucher:
@@ -228,7 +229,7 @@ class VoucherDeleteCarts(EventPermissionRequiredMixin, CompatDeleteView):
class VoucherDelete(EventPermissionRequiredMixin, CompatDeleteView):
model = Voucher
template_name = 'pretixcontrol/vouchers/delete.html'
permission = 'can_change_vouchers'
permission = 'event.vouchers:write'
context_object_name = 'voucher'
def get_object(self, queryset=None) -> Voucher:
@@ -270,7 +271,7 @@ class VoucherDelete(EventPermissionRequiredMixin, CompatDeleteView):
class VoucherUpdate(EventPermissionRequiredMixin, UpdateView):
model = Voucher
template_name = 'pretixcontrol/vouchers/detail.html'
permission = ('can_change_vouchers', 'can_view_vouchers')
permission = AnyPermissionOf('event.vouchers:write', 'event.vouchers:read')
context_object_name = 'voucher'
def form_invalid(self, form):
@@ -286,7 +287,7 @@ class VoucherUpdate(EventPermissionRequiredMixin, UpdateView):
def get_form(self, form_class=None):
form = super().get_form(form_class)
if not self.request.user.has_event_permission(self.request.organizer, self.request.event, 'can_change_vouchers',
if not self.request.user.has_event_permission(self.request.organizer, self.request.event, 'event.vouchers:write',
request=self.request):
for f in form.fields.values():
f.disabled = True
@@ -313,7 +314,7 @@ class VoucherUpdate(EventPermissionRequiredMixin, UpdateView):
@transaction.atomic
def post(self, request, *args, **kwargs):
if not request.user.has_event_permission(request.organizer, request.event, 'can_change_vouchers',
if not request.user.has_event_permission(request.organizer, request.event, 'event.vouchers:write',
request=request):
raise PermissionDenied()
return super().post(request, *args, **kwargs)
@@ -344,7 +345,7 @@ class VoucherUpdate(EventPermissionRequiredMixin, UpdateView):
class VoucherCreate(EventPermissionRequiredMixin, CreateView):
model = Voucher
template_name = 'pretixcontrol/vouchers/detail.html'
permission = 'can_change_vouchers'
permission = 'event.vouchers:write'
context_object_name = 'voucher'
def form_invalid(self, form):
@@ -389,7 +390,7 @@ class VoucherCreate(EventPermissionRequiredMixin, CreateView):
class VoucherGo(EventPermissionRequiredMixin, View):
permission = 'can_view_vouchers'
permission = 'event.vouchers:read'
def get_voucher(self, code):
return Voucher.objects.get(code__iexact=code, event=self.request.event)
@@ -408,7 +409,7 @@ class VoucherGo(EventPermissionRequiredMixin, View):
class VoucherBulkCreate(EventPermissionRequiredMixin, AsyncFormView):
model = Voucher
template_name = 'pretixcontrol/vouchers/bulk.html'
permission = 'can_change_vouchers'
permission = 'event.vouchers:write'
context_object_name = 'voucher'
atomic_execute = True
@@ -540,7 +541,7 @@ class VoucherBulkCreate(EventPermissionRequiredMixin, AsyncFormView):
class VoucherBulkMailPreview(EventPermissionRequiredMixin, View):
permission = 'can_change_vouchers'
permission = 'event.vouchers:write'
# return the origin text if key is missing in dict
class SafeDict(dict):
@@ -579,7 +580,7 @@ class VoucherBulkMailPreview(EventPermissionRequiredMixin, View):
class VoucherRNG(EventPermissionRequiredMixin, View):
permission = 'can_change_vouchers'
permission = 'event.vouchers:write'
def get(self, request, *args, **kwargs):
try:
@@ -603,7 +604,7 @@ class VoucherRNG(EventPermissionRequiredMixin, View):
class VoucherBulkAction(EventPermissionRequiredMixin, View):
permission = 'can_change_vouchers'
permission = 'event.vouchers:write'
@cached_property
def objects(self):

View File

@@ -64,7 +64,7 @@ from . import UpdateView
class AutoAssign(EventPermissionRequiredMixin, AsyncAction, View):
task = assign_automatically
known_errortypes = ['WaitingListError']
permission = 'can_change_orders'
permission = 'event.orders:write'
def get_success_message(self, value):
return _('{num} vouchers have been created and sent out via email.').format(num=value)
@@ -154,7 +154,7 @@ class WaitingListQuerySetMixin:
class WaitingListActionView(EventPermissionRequiredMixin, WaitingListQuerySetMixin, View):
model = WaitingListEntry
permission = 'can_change_orders'
permission = 'event.orders:write'
def _redirect_back(self):
if "next" in self.request.GET and url_has_allowed_host_and_scheme(self.request.GET.get("next"), allowed_hosts=None):
@@ -244,7 +244,7 @@ class WaitingListView(EventPermissionRequiredMixin, WaitingListQuerySetMixin, Pa
model = WaitingListEntry
context_object_name = 'entries'
template_name = 'pretixcontrol/waitinglist/index.html'
permission = 'can_view_orders'
permission = 'event.orders:read'
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
@@ -372,7 +372,7 @@ class WaitingListView(EventPermissionRequiredMixin, WaitingListQuerySetMixin, Pa
class EntryDelete(EventPermissionRequiredMixin, CompatDeleteView):
model = WaitingListEntry
template_name = 'pretixcontrol/waitinglist/delete.html'
permission = 'can_change_orders'
permission = 'event.orders:write'
context_object_name = 'entry'
def get_object(self, queryset=None) -> WaitingListEntry:
@@ -405,7 +405,7 @@ class EntryDelete(EventPermissionRequiredMixin, CompatDeleteView):
class EntryEdit(EventPermissionRequiredMixin, UpdateView):
model = WaitingListEntry
template_name = 'pretixcontrol/waitinglist/edit.html'
permission = 'can_change_orders'
permission = 'event.orders:write'
form_class = WaitingListEntryEditForm
context_object_name = 'entry'

View File

@@ -0,0 +1,95 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
import warnings
# The first two mapping tables are used to migrate *configuration*, i.e. existing Team objects or Team objects
# created through old-style API calls. These need to be "complete on both sides", i.e. all new permissions are assigned
# to existing users in some scenario.
OLD_TO_NEW_EVENT_MIGRATION = {
"can_change_event_settings": [
"event.settings.general:write",
"event.settings.payment:write",
"event.settings.tax:write",
"event.settings.invoicing:write",
"event.subevents:write",
],
"can_change_items": ["event.items:write"],
"can_view_orders": ["event.orders:read"],
"can_change_orders": ["event.orders:write", "event:cancel"],
"can_checkin_orders": ["event.orders:checkin"],
"can_view_vouchers": ["event.vouchers:read"],
"can_change_vouchers": ["event.vouchers:write"],
}
OLD_TO_NEW_ORGANIZER_MIGRATION = {
"can_create_events": ["organizer.events:create"],
"can_change_organizer_settings": ["organizer.settings.general:write", "organizer.devices:read",
"organizer.devices:write", "organizer.seatingplans:write",
"organizer.outgoingmails:read"],
"can_change_teams": ["organizer.teams:write"],
"can_manage_gift_cards": ["organizer.giftcards:read", "organizer.giftcards:write"],
"can_manage_customers": ["organizer.customers:read", "organizer.customers:write"],
"can_manage_reusable_media": ["organizer.reusablemedia:read", "organizer.reusablemedia:write"],
}
# The second two mapping tables are used to migrate permission *checks*, i.e. they define which permissions a user needs
# to have to fulfill the "old-style condition". These are shorter than the migration mappings, since it does not
# make sense to check e.g. for event.settings.tax:write on every plugin that checks for can_change_event_settings
OLD_TO_NEW_EVENT_COMPAT = {
"can_change_event_settings": ["event.settings.general:write"],
"can_change_items": ["event.items:write"],
"can_view_orders": ["event.orders:read"],
"can_change_orders": ["event.orders:write"],
"can_checkin_orders": ["event.orders:checkin"],
"can_view_vouchers": ["event.vouchers:read"],
"can_change_vouchers": ["event.vouchers:write"],
}
OLD_TO_NEW_ORGANIZER_COMPAT = {
"can_create_events": ["organizer.events:create"],
"can_change_organizer_settings": ["organizer.settings.general:write"],
"can_change_teams": ["organizer.teams:write"],
"can_manage_gift_cards": ["organizer.giftcards:read", "organizer.giftcards:write"],
"can_manage_customers": ["organizer.customers:read", "organizer.customers:write"],
"can_manage_reusable_media": ["organizer.reusablemedia:read", "organizer.reusablemedia:write"],
}
class LegacyPermissionProperty:
name = None
def __set_name__(self, owner, name):
self.name = name
def __get__(self, instance, owner=None):
if instance is None:
return self
warnings.warn("Legacy permission attribute used", DeprecationWarning, stacklevel=2)
if self.name in OLD_TO_NEW_EVENT_COMPAT:
return instance.all_event_permissions or all(
kk in instance.limit_event_permissions for kk in OLD_TO_NEW_EVENT_COMPAT[self.name]
)
if self.name in OLD_TO_NEW_ORGANIZER_COMPAT:
return instance.all_organizer_permissions or all(
kk in instance.limit_organizer_permissions for kk in OLD_TO_NEW_ORGANIZER_COMPAT[self.name]
)
raise AttributeError("Unknown legacy attribute")

Some files were not shown because too many files have changed in this diff Show More