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

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