Compare commits

..

1 Commits

Author SHA1 Message Date
Phin Wolkwitz
8164f469d3 Prefetch program times, add test for query count 2026-01-19 18:11:12 +01:00
334 changed files with 95541 additions and 103469 deletions

View File

@@ -197,11 +197,10 @@ Permissions & security profiles
Device authentication is currently hardcoded to grant the following permissions: Device authentication is currently hardcoded to grant the following permissions:
* Read event meta data and products etc. * View event meta data and products etc.
* Read and write orders * View orders
* Read and write gift cards * Change orders
* Read and write reusable media * Manage gift cards
* Read vouchers
Devices cannot change events or products and cannot access vouchers. Devices cannot change events or products and cannot access vouchers.

View File

@@ -30,7 +30,7 @@ software_brand string Device software
software_version string Device software version (read-only) software_version string Device software version (read-only)
created datetime Creation time created datetime Creation time
initialized datetime Time of initialization (or ``null``) initialized datetime Time of initialization (or ``null``)
initialization_token string Token for initialization (field invisible without write permission) initialization_token string Token for initialization
revoked boolean Whether this device no longer has access revoked boolean Whether this device no longer has access
security_profile string The name of a supported security profile restricting API access security_profile string The name of a supported security profile restricting API access
===================================== ========================== ======================================================= ===================================== ========================== =======================================================

View File

@@ -65,6 +65,8 @@ Endpoints
Returns a list of all events within a given organizer the authenticated user/token has access to. Returns a list of all events within a given organizer the authenticated user/token has access to.
Permission required: "Can change event settings"
**Example request**: **Example request**:
.. sourcecode:: http .. sourcecode:: http
@@ -159,6 +161,8 @@ Endpoints
Returns information on one event, identified by its slug. Returns information on one event, identified by its slug.
Permission required: "Can change event settings"
**Example request**: **Example request**:
.. sourcecode:: http .. sourcecode:: http
@@ -230,6 +234,8 @@ Endpoints
Please note that events cannot be created as 'live' using this endpoint. Quotas and payment must be added to the Please note that events cannot be created as 'live' using this endpoint. Quotas and payment must be added to the
event before sales can go live. event before sales can go live.
Permission required: "Can create events"
**Example request**: **Example request**:
.. sourcecode:: http .. sourcecode:: http
@@ -332,6 +338,8 @@ Endpoints
Please note that you can only copy from events under the same organizer this way. Use the ``clone_from`` parameter Please note that you can only copy from events under the same organizer this way. Use the ``clone_from`` parameter
when creating a new event for this instead. when creating a new event for this instead.
Permission required: "Can create events"
**Example request**: **Example request**:
.. sourcecode:: http .. sourcecode:: http
@@ -425,6 +433,8 @@ Endpoints
Updates an event Updates an event
Permission required: "Can change event settings"
**Example request**: **Example request**:
.. sourcecode:: http .. sourcecode:: http
@@ -500,6 +510,8 @@ Endpoints
Delete an event. Note that events with orders cannot be deleted to ensure data integrity. Delete an event. Note that events with orders cannot be deleted to ensure data integrity.
Permission required: "Can change event settings"
**Example request**: **Example request**:
.. sourcecode:: http .. sourcecode:: http
@@ -549,6 +561,8 @@ organizer level.
Get current values of event settings. Get current values of event settings.
Permission required: "Can change event settings" (Exception: with device auth, *some* settings can always be *read*.)
**Example request**: **Example request**:
.. sourcecode:: http .. sourcecode:: http
@@ -601,8 +615,6 @@ organizer level.
Updates event settings. Note that ``PUT`` is not allowed here, only ``PATCH``. Updates event settings. Note that ``PUT`` is not allowed here, only ``PATCH``.
Permission "Can change event settings" is always required. Some keys require additional permissions.
.. warning:: .. warning::
Settings can be stored at different levels in pretix. If a value is not set on event level, a default setting Settings can be stored at different levels in pretix. If a value is not set on event level, a default setting

View File

@@ -110,6 +110,8 @@ Endpoints
Updates an organizer. Currently only the ``plugins`` field may be updated. Updates an organizer. Currently only the ``plugins`` field may be updated.
Permission required: "Can change organizer settings"
**Example request**: **Example request**:
.. sourcecode:: http .. sourcecode:: http
@@ -170,6 +172,8 @@ information about the properties.
Get current values of organizer settings. Get current values of organizer settings.
Permission required: "Can change organizer settings"
**Example request**: **Example request**:
.. sourcecode:: http .. sourcecode:: http

View File

@@ -154,7 +154,7 @@ Endpoints
.. http:post:: /api/v1/organizers/(organizer)/reusablemedia/lookup/ .. http:post:: /api/v1/organizers/(organizer)/reusablemedia/lookup/
Look up a new reusable medium by its identifier. In some cases, this might lead to the automatic creation of a new Look up a new reusable medium by its identifier. In some cases, this might lead to the automatic creation of a new
medium behind the scenes, therefore this endpoint requires write permissions. medium behind the scenes.
This endpoint, and this endpoint only, might return media from a different organizer if there is a cross-acceptance This endpoint, and this endpoint only, might return media from a different organizer if there is a cross-acceptance
agreement. In this case, only linked gift cards will be returned, no order position or customer records, agreement. In this case, only linked gift cards will be returned, no order position or customer records,

View File

@@ -154,6 +154,8 @@ Endpoints
Creates a new subevent. Creates a new subevent.
Permission required: "Can create events"
**Example request**: **Example request**:
.. sourcecode:: http .. sourcecode:: http
@@ -298,6 +300,8 @@ Endpoints
provide all fields of the resource, other fields will be reset to default. With ``PATCH``, you only need to provide provide all fields of the resource, other fields will be reset to default. With ``PATCH``, you only need to provide
the fields that you want to change. the fields that you want to change.
Permission required: "Can change event settings"
**Example request**: **Example request**:
.. sourcecode:: http .. sourcecode:: http
@@ -369,6 +373,8 @@ Endpoints
Delete a sub-event. Note that events with orders cannot be deleted to ensure data integrity. Delete a sub-event. Note that events with orders cannot be deleted to ensure data integrity.
Permission required: "Can change event settings"
**Example request**: **Example request**:
.. sourcecode:: http .. sourcecode:: http

View File

@@ -24,57 +24,21 @@ all_events boolean Whether this te
limit_events list List of event slugs this team has access to limit_events list List of event slugs this team has access to
require_2fa boolean Whether members of this team are required to use require_2fa boolean Whether members of this team are required to use
two-factor authentication two-factor authentication
all_event_permissions bool Whether members of this team are granted all event-level can_create_events boolean
permissions, including future additions can_change_teams boolean
limit_event_permissions list of strings The event-level permissions team members are granted can_change_organizer_settings boolean
all_organizer_permissions bool Whether members of this team are granted all organizer-level can_manage_customers boolean
permissions, including future additions can_manage_reusable_media boolean
all_organizer_permissions list of strings The organizer-level permissions team members are granted can_manage_gift_cards boolean
can_create_events boolean **DEPRECATED**. Legacy interface, use ``limit_organizer_permissions``. can_change_event_settings boolean
can_change_teams boolean **DEPRECATED**. Legacy interface, use ``limit_organizer_permissions``. can_change_items boolean
can_change_organizer_settings boolean **DEPRECATED**. Legacy interface, use ``limit_organizer_permissions``. can_view_orders boolean
can_manage_customers boolean **DEPRECATED**. Legacy interface, use ``limit_organizer_permissions``. can_change_orders boolean
can_manage_reusable_media boolean **DEPRECATED**. Legacy interface, use ``limit_organizer_permissions``. can_view_vouchers boolean
can_manage_gift_cards boolean **DEPRECATED**. Legacy interface, use ``limit_organizer_permissions``. can_change_vouchers boolean
can_change_event_settings boolean **DEPRECATED**. Legacy interface, use ``limit_event_permissions``. can_checkin_orders boolean
can_change_items boolean **DEPRECATED**. Legacy interface, use ``limit_event_permissions``.
can_view_orders boolean **DEPRECATED**. Legacy interface, use ``limit_event_permissions``.
can_change_orders boolean **DEPRECATED**. Legacy interface, use ``limit_event_permissions``.
can_view_vouchers boolean **DEPRECATED**. Legacy interface, use ``limit_event_permissions``.
can_change_vouchers boolean **DEPRECATED**. Legacy interface, use ``limit_event_permissions``.
can_checkin_orders boolean **DEPRECATED**. Legacy interface, use ``limit_event_permissions``.
===================================== ========================== ======================================================= ===================================== ========================== =======================================================
Possible values for ``limit_organizer_permissions`` defined in the core pretix system (plugins might add more)::
organizer.events:create
organizer.settings.general:write
organizer.teams:write
organizer.seatingplans:write
organizer.giftcards:read
organizer.giftcards:write
organizer.customers:read
organizer.customers:write
organizer.reusablemedia:read
organizer.reusablemedia:write
organizer.devices:read
organizer.devices:write
Possible values for ``limit_event_permissions`` defined in the core pretix system (plugins might add more)::
event.settings.general:write
event.settings.payment:write
event.settings.tax:write
event.settings.invoicing:write
event.subevents:write
event.items:write
event.orders:read
event.orders:write
event.orders:checkin
event.vouchers:read
event.vouchers:write
event:cancel
Team member resource Team member resource
-------------------- --------------------
@@ -157,10 +121,6 @@ Team endpoints
"all_events": true, "all_events": true,
"limit_events": [], "limit_events": [],
"require_2fa": true, "require_2fa": true,
"all_event_permissions": true,
"limit_event_permissions": [],
"all_organizer_permissions": true,
"limit_organizer_permissions": [],
"can_create_events": true, "can_create_events": true,
... ...
} }
@@ -199,10 +159,6 @@ Team endpoints
"all_events": true, "all_events": true,
"limit_events": [], "limit_events": [],
"require_2fa": true, "require_2fa": true,
"all_event_permissions": true,
"limit_event_permissions": [],
"all_organizer_permissions": true,
"limit_organizer_permissions": [],
"can_create_events": true, "can_create_events": true,
... ...
} }
@@ -231,10 +187,7 @@ Team endpoints
"all_events": true, "all_events": true,
"limit_events": [], "limit_events": [],
"require_2fa": true, "require_2fa": true,
"all_event_permissions": true, "can_create_events": true,
"limit_event_permissions": [],
"all_organizer_permissions": true,
"limit_organizer_permissions": [],
... ...
} }
@@ -252,10 +205,6 @@ Team endpoints
"all_events": true, "all_events": true,
"limit_events": [], "limit_events": [],
"require_2fa": true, "require_2fa": true,
"all_event_permissions": true,
"limit_event_permissions": [],
"all_organizer_permissions": true,
"limit_organizer_permissions": [],
"can_create_events": true, "can_create_events": true,
... ...
} }
@@ -283,8 +232,7 @@ Team endpoints
Content-Length: 94 Content-Length: 94
{ {
"all_organizer_permissions": false, "can_create_events": true
"limit_organizer_permissions": ["organizer.events:create"]
} }
**Example response**: **Example response**:
@@ -301,10 +249,6 @@ Team endpoints
"all_events": true, "all_events": true,
"limit_events": [], "limit_events": [],
"require_2fa": true, "require_2fa": true,
"all_event_permissions": true,
"limit_event_permissions": [],
"all_organizer_permissions": false,
"limit_organizer_permissions": ["organizer.events:create"],
"can_create_events": true, "can_create_events": true,
... ...
} }

View File

@@ -55,12 +55,12 @@ your views:
) )
class AdminView(EventPermissionRequiredMixin, View): class AdminView(EventPermissionRequiredMixin, View):
permission = 'event.orders:read' permission = 'can_view_orders'
... ...
@event_permission_required('event.orders:read') @event_permission_required('can_view_orders')
def admin_view(request, organizer, event): def admin_view(request, organizer, event):
... ...
@@ -78,7 +78,7 @@ event-related views, there is also a signal that allows you to add the view to t
@receiver(nav_event, dispatch_uid='friends_tickets_nav') @receiver(nav_event, dispatch_uid='friends_tickets_nav')
def navbar_info(sender, request, **kwargs): def navbar_info(sender, request, **kwargs):
url = resolve(request.path_info) url = resolve(request.path_info)
if not request.user.has_event_permission(request.organizer, request.event, 'event.vouchers:read'): if not request.user.has_event_permission(request.organizer, request.event, 'can_change_vouchers'):
return [] return []
return [{ return [{
'label': _('My plugin view'), 'label': _('My plugin view'),
@@ -118,7 +118,7 @@ for good integration. If you just want to display a form, you could do it like t
class MySettingsView(EventSettingsViewMixin, EventSettingsFormView): class MySettingsView(EventSettingsViewMixin, EventSettingsFormView):
model = Event model = Event
permission = 'event.settings.general:write' permission = 'can_change_settings'
form_class = MySettingsForm form_class = MySettingsForm
template_name = 'my_plugin/settings.html' template_name = 'my_plugin/settings.html'
@@ -204,13 +204,13 @@ In case of ``orga_router`` and ``event_router``, permission checking is done for
in the control panel. However, you need to make sure on your own only to return the correct subset of data! ``request in the control panel. However, you need to make sure on your own only to return the correct subset of data! ``request
.event`` and ``request.organizer`` are available as usual. .event`` and ``request.organizer`` are available as usual.
To require a special permission like ``event.orders:read``, you do not need to inherit from a special ViewSet base To require a special permission like ``can_view_orders``, you do not need to inherit from a special ViewSet base
class, you can just set the ``permission`` attribute on your viewset: class, you can just set the ``permission`` attribute on your viewset:
.. code-block:: python .. code-block:: python
class MyViewSet(ModelViewSet): class MyViewSet(ModelViewSet):
permission = 'event.orders:read' permission = 'can_view_orders'
... ...
If you want to check the permission only for some methods of your viewset, you have to do it yourself. Note here that If you want to check the permission only for some methods of your viewset, you have to do it yourself. Note here that
@@ -220,7 +220,7 @@ following:
.. code-block:: python .. code-block:: python
perm_holder = (request.auth if isinstance(request.auth, TeamAPIToken) else request.user) perm_holder = (request.auth if isinstance(request.auth, TeamAPIToken) else request.user)
if perm_holder.has_event_permission(request.event.organizer, request.event, 'event.orders:read'): if perm_holder.has_event_permission(request.event.organizer, request.event, 'can_view_orders'):
... ...

View File

@@ -80,24 +80,8 @@ The exporter class
.. autoattribute:: category .. autoattribute:: category
.. autoattribute:: feature
.. autoattribute:: export_form_fields .. autoattribute:: export_form_fields
.. autoattribute:: repeatable_read
.. automethod:: render .. automethod:: render
This is an abstract method, you **must** override this! This is an abstract method, you **must** override this!
.. automethod:: available_for_user
.. automethod:: get_required_event_permission
On organizer level, by default exporters are expected to handle a *set of events* and the system will automatically
add a form field that allows the selection of events, limited to events the user has correct permissions for. If this
does not fit your exporter, because it is not related to events, you should **also** inherit from the following class:
.. class:: pretix.base.exporter.OrganizerLevelExportMixin
.. automethod:: get_required_organizer_permission

View File

@@ -14,8 +14,7 @@ Core
:members: periodic_task, event_live_issues, event_copy_data, email_filter, register_notification_types, notification, :members: periodic_task, event_live_issues, event_copy_data, email_filter, register_notification_types, notification,
item_copy_data, register_sales_channel_types, register_global_settings, quota_availability, global_email_filter, item_copy_data, register_sales_channel_types, register_global_settings, quota_availability, global_email_filter,
register_ticket_secret_generators, gift_card_transaction_display, register_ticket_secret_generators, gift_card_transaction_display,
register_text_placeholders, register_mail_placeholders, device_info_updated, register_text_placeholders, register_mail_placeholders, device_info_updated
register_event_permission_groups, register_organizer_permission_groups
Order events Order events
"""""""""""" """"""""""""

View File

@@ -196,7 +196,7 @@ A simple implementation could look like this:
.. code-block:: python .. code-block:: python
class MyNotificationType(NotificationType): class MyNotificationType(NotificationType):
required_permission = "event.orders:read" required_permission = "can_view_orders"
action_type = "pretix.event.order.paid" action_type = "pretix.event.order.paid"
verbose_name = _("Order has been paid") verbose_name = _("Order has been paid")

View File

@@ -2,7 +2,7 @@ Permissions
=========== ===========
pretix uses a fine-grained permission system to control who is allowed to control what parts of the system. pretix uses a fine-grained permission system to control who is allowed to control what parts of the system.
The central concept here is the concept of *Teams*. You can read more on `configuring teams and permissions`_ The central concept here is the concept of *Teams*. You can read more on `configuring teams and permissions <user-teams>`_
and the :class:`pretix.base.models.Team` model in the respective parts of the documentation. The basic digest is: and the :class:`pretix.base.models.Team` model in the respective parts of the documentation. The basic digest is:
An organizer account can have any number of teams, and any number of users can be part of a team. A team can be An organizer account can have any number of teams, and any number of users can be part of a team. A team can be
assigned a set of permissions and connected to some or all of the events of the organizer. assigned a set of permissions and connected to some or all of the events of the organizer.
@@ -25,8 +25,8 @@ permission level to access a view:
class MyOrgaView(OrganizerPermissionRequiredMixin, View): class MyOrgaView(OrganizerPermissionRequiredMixin, View):
permission = 'organizer.settings.general:write' permission = 'can_change_organizer_settings'
# Only users with the permission ``organizer.settings.general:write`` on # Only users with the permission ``can_change_organizer_settings`` on
# this organizer can access this # this organizer can access this
@@ -35,9 +35,9 @@ permission level to access a view:
# Only users with *any* permission on this organizer can access this # Only users with *any* permission on this organizer can access this
@organizer_permission_required('organizer.settings.general:write') @organizer_permission_required('can_change_organizer_settings')
def my_orga_view(request, organizer, **kwargs): def my_orga_view(request, organizer, **kwargs):
# Only users with the permission ``organizer.settings.general:write`` on # Only users with the permission ``can_change_organizer_settings`` on
# this organizer can access this # this organizer can access this
@@ -56,8 +56,8 @@ Of course, the same is available on event level:
class MyEventView(EventPermissionRequiredMixin, View): class MyEventView(EventPermissionRequiredMixin, View):
permission = 'event.settings.general:write' permission = 'can_change_event_settings'
# Only users with the permission ``event.settings.general:write`` on # Only users with the permission ``can_change_event_settings`` on
# this event can access this # this event can access this
@@ -66,9 +66,9 @@ Of course, the same is available on event level:
# Only users with *any* permission on this event can access this # Only users with *any* permission on this event can access this
@event_permission_required('event.settings.general:write') @event_permission_required('can_change_event_settings')
def my_event_view(request, organizer, **kwargs): def my_event_view(request, organizer, **kwargs):
# Only users with the permission ``event.settings.general:write`` on # Only users with the permission ``can_change_event_settings`` on
# this event can access this # this event can access this
@@ -121,7 +121,7 @@ When creating your own ``viewset`` using Django REST framework, you just need to
and pretix will check it automatically for you:: and pretix will check it automatically for you::
class MyModelViewSet(viewsets.ReadOnlyModelViewSet): class MyModelViewSet(viewsets.ReadOnlyModelViewSet):
permission = 'event.orders:read' permission = 'can_view_orders'
Checking permission in code Checking permission in code
--------------------------- ---------------------------
@@ -136,12 +136,12 @@ Return all users that are in any team that is connected to this event::
Return all users that are in a team with a specific permission for this event:: Return all users that are in a team with a specific permission for this event::
>>> event.get_users_with_permission('event.orders:read') >>> event.get_users_with_permission('can_change_event_settings')
<QuerySet: …> <QuerySet: …>
Determine if a user has a certain permission for a specific event:: Determine if a user has a certain permission for a specific event::
>>> user.has_event_permission(organizer, event, 'event.orders:read', request=request) >>> user.has_event_permission(organizer, event, 'can_change_event_settings', request=request)
True True
Determine if a user has any permission for a specific event:: Determine if a user has any permission for a specific event::
@@ -153,27 +153,27 @@ In the two previous commands, the ``request`` argument is optional, but required
The same method exists for organizer-level permissions:: The same method exists for organizer-level permissions::
>>> user.has_organizer_permission(organizer, 'event.orders:read', request=request) >>> user.has_organizer_permission(organizer, 'can_change_event_settings', request=request)
True True
Sometimes, it might be more useful to get the set of permissions at once:: Sometimes, it might be more useful to get the set of permissions at once::
>>> user.get_event_permission_set(organizer, event) >>> user.get_event_permission_set(organizer, event)
{'event.settings.general:write', 'event.orders:read', 'event.orders:write'} {'can_change_event_settings', 'can_view_orders', 'can_change_orders'}
>>> user.get_organizer_permission_set(organizer, event) >>> user.get_organizer_permission_set(organizer, event)
{'organizer.settings.general:write', 'organizer.events:create'} {'can_change_organizer_settings', 'can_create_events'}
Within a view on the ``/control`` subpath, the results of these two methods are already available in the Within a view on the ``/control`` subpath, the results of these two methods are already available in the
``request.eventpermset`` and ``request.orgapermset`` properties. This makes it convenient to query them in templates:: ``request.eventpermset`` and ``request.orgapermset`` properties. This makes it convenient to query them in templates::
{% if "event.orders:write" in request.eventpermset %} {% if "can_change_orders" in request.eventpermset %}
{% endif %} {% endif %}
You can also do the reverse to get any events a user has access to:: You can also do the reverse to get any events a user has access to::
>>> user.get_events_with_permission('event.settings.general:write', request=request) >>> user.get_events_with_permission('can_change_event_settings', request=request)
<QuerySet: …> <QuerySet: …>
>>> user.get_events_with_any_permission(request=request) >>> user.get_events_with_any_permission(request=request)
@@ -195,53 +195,3 @@ staff mode is active. You can check if a user is in staff mode using their sessi
Staff mode has a hard time limit and during staff mode, a middleware will log all requests made by that user. Later, Staff mode has a hard time limit and during staff mode, a middleware will log all requests made by that user. Later,
the user is able to also save a message to comment on what they did in their administrative session. This feature is the user is able to also save a message to comment on what they did in their administrative session. This feature is
intended to help compliance with data protection rules as imposed e.g. by GDPR. intended to help compliance with data protection rules as imposed e.g. by GDPR.
Adding permissions
------------------
Plugins can add permissions through the ``register_event_permission_groups`` and ``register_organizer_permission_groups``.
We recommend to use this only for very significant permissions, as the system will become less usable with too many
permission levels, especially since the team page will show all permission options, even those of disabled plugins.
To register your permissions, you need to register a **permission group** (often representing an area of functionality
or a key model). Below that group, there are **actions**, which represent the actual permissions. Permissions will be
generated as ``<group_name>:<action>``. Then, you need to define **options** which are the valid combinations of the
actions that should be possible to select for a team. This two-step mechanism exists to provide a better user experience
and avoid impossible combinations like "write but not read".
Example::
@receiver(register_event_permission_groups)
def register_plugin_event_permissions(sender, **kwargs):
return [
PermissionGroup(
name="pretix_myplugin.resource",
label=_("Resources"),
actions=["read", "write"],
options=[
PermissionOption(actions=tuple(), label=_("No access")),
PermissionOption(actions=("read",), label=_("View")),
PermissionOption(actions=("read", "write"), label=_("View and change")),
],
help_text=_("Some help text")
),
]
@receiver(register_organizer_permission_groups)
def register_plugin_organizer_permissions(sender, **kwargs):
return [
PermissionGroup(
name="pretix_myplugin.resource",
label=_("Resources"),
actions=["read", "write"],
options=[
PermissionOption(actions=tuple(), label=_("No access")),
PermissionOption(actions=("read",), label=_("View")),
PermissionOption(actions=("read", "write"), label=_("View and change")),
],
help_text=_("Some help text")
),
]
.. _configuring teams and permissions: https://docs.pretix.eu/guides/teams/

View File

@@ -41,7 +41,7 @@ dependencies = [
"django-compressor==4.6.0", "django-compressor==4.6.0",
"django-countries==8.2.*", "django-countries==8.2.*",
"django-filter==25.1", "django-filter==25.1",
"django-formset-js-improved==0.5.0.5", "django-formset-js-improved==0.5.0.4",
"django-formtools==2.5.1", "django-formtools==2.5.1",
"django-hierarkey==2.0.*,>=2.0.1", "django-hierarkey==2.0.*,>=2.0.1",
"django-hijack==3.7.*", "django-hijack==3.7.*",
@@ -65,7 +65,7 @@ dependencies = [
"kombu==5.6.*", "kombu==5.6.*",
"libsass==0.23.*", "libsass==0.23.*",
"lxml", "lxml",
"markdown==3.10.1", # 3.3.5 requires importlib-metadata>=4.4, but django-bootstrap3 requires importlib-metadata<3. "markdown==3.10", # 3.3.5 requires importlib-metadata>=4.4, but django-bootstrap3 requires importlib-metadata<3.
# We can upgrade markdown again once django-bootstrap3 upgrades or once we drop Python 3.6 and 3.7 # We can upgrade markdown again once django-bootstrap3 upgrades or once we drop Python 3.6 and 3.7
"mt-940==4.30.*", "mt-940==4.30.*",
"oauthlib==3.3.*", "oauthlib==3.3.*",
@@ -80,7 +80,7 @@ dependencies = [
"protobuf==6.33.*", "protobuf==6.33.*",
"psycopg2-binary", "psycopg2-binary",
"pycountry", "pycountry",
"pycparser==3.0", "pycparser==2.23",
"pycryptodome==3.23.*", "pycryptodome==3.23.*",
"pypdf==6.5.*", "pypdf==6.5.*",
"python-bidi==0.6.*", # Support for Arabic in reportlab "python-bidi==0.6.*", # Support for Arabic in reportlab
@@ -92,7 +92,7 @@ dependencies = [
"redis==7.1.*", "redis==7.1.*",
"reportlab==4.4.*", "reportlab==4.4.*",
"requests==2.32.*", "requests==2.32.*",
"sentry-sdk==2.50.*", "sentry-sdk==2.49.*",
"sepaxml==2.7.*", "sepaxml==2.7.*",
"stripe==7.9.*", "stripe==7.9.*",
"text-unidecode==1.*", "text-unidecode==1.*",

View File

@@ -19,4 +19,4 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see # You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>. # <https://www.gnu.org/licenses/>.
# #
__version__ = "2026.2.0.dev0" __version__ = "2025.11.0.dev0"

View File

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

View File

@@ -300,7 +300,7 @@ class EventSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
def ignored_meta_properties(self): def ignored_meta_properties(self):
perm_holder = (self.context['request'].auth if isinstance(self.context['request'].auth, (Device, TeamAPIToken)) perm_holder = (self.context['request'].auth if isinstance(self.context['request'].auth, (Device, TeamAPIToken))
else self.context['request'].user) else self.context['request'].user)
if perm_holder.has_organizer_permission(self.context['request'].organizer, 'organizer.settings.general:write', request=self.context['request']): if perm_holder.has_organizer_permission(self.context['request'].organizer, 'can_change_organizer_settings', request=self.context['request']):
return [] return []
return [k for k, p in self.meta_properties.items() if p.protected] return [k for k, p in self.meta_properties.items() if p.protected]
@@ -561,7 +561,7 @@ class SubEventSerializer(I18nAwareModelSerializer):
def ignored_meta_properties(self): def ignored_meta_properties(self):
perm_holder = (self.context['request'].auth if isinstance(self.context['request'].auth, (Device, TeamAPIToken)) perm_holder = (self.context['request'].auth if isinstance(self.context['request'].auth, (Device, TeamAPIToken))
else self.context['request'].user) else self.context['request'].user)
if perm_holder.has_organizer_permission(self.context['request'].organizer, 'organizer.settings.general:write', request=self.context['request']): if perm_holder.has_organizer_permission(self.context['request'].organizer, 'can_change_organizer_settings', request=self.context['request']):
return [] return []
return [k for k, p in self.meta_properties.items() if p.protected] return [k for k, p in self.meta_properties.items() if p.protected]
@@ -707,10 +707,7 @@ class TaxRuleSerializer(CountryFieldMixin, I18nAwareModelSerializer):
class EventSettingsSerializer(SettingsSerializer): class EventSettingsSerializer(SettingsSerializer):
default_write_permission = 'event.settings.general:write'
default_fields = [ default_fields = [
# These are readable for all users with access to the events, therefore secrets stored in the settings store
# should not be included!
'imprint_url', 'imprint_url',
'checkout_email_helptext', 'checkout_email_helptext',
'presale_has_ended_text', 'presale_has_ended_text',
@@ -809,7 +806,6 @@ class EventSettingsSerializer(SettingsSerializer):
'invoice_reissue_after_modify', 'invoice_reissue_after_modify',
'invoice_include_free', 'invoice_include_free',
'invoice_generate', 'invoice_generate',
'invoice_generate_only_business',
'invoice_period', 'invoice_period',
'invoice_numbers_consecutive', 'invoice_numbers_consecutive',
'invoice_numbers_prefix', 'invoice_numbers_prefix',
@@ -1083,16 +1079,16 @@ class SeatSerializer(I18nAwareModelSerializer):
def prefetch_expanded_data(self, items, request, expand_fields): def prefetch_expanded_data(self, items, request, expand_fields):
if 'orderposition' in expand_fields: if 'orderposition' in expand_fields:
if 'event.orders:read' not in request.eventpermset: if 'can_view_orders' not in request.eventpermset:
raise PermissionDenied('event.orders:read permission required for expand=orderposition') raise PermissionDenied('can_view_orders permission required for expand=orderposition')
prefetch_by_id(items, OrderPosition.objects.prefetch_related('order'), 'orderposition_id', 'orderposition') prefetch_by_id(items, OrderPosition.objects.prefetch_related('order'), 'orderposition_id', 'orderposition')
if 'cartposition' in expand_fields: if 'cartposition' in expand_fields:
if 'event.orders:read' not in request.eventpermset: if 'can_view_orders' not in request.eventpermset:
raise PermissionDenied('event.orders:read permission required for expand=cartposition') raise PermissionDenied('can_view_orders permission required for expand=cartposition')
prefetch_by_id(items, CartPosition.objects, 'cartposition_id', 'cartposition') prefetch_by_id(items, CartPosition.objects, 'cartposition_id', 'cartposition')
if 'voucher' in expand_fields: if 'voucher' in expand_fields:
if 'event.vouchers:read' not in request.eventpermset: if 'can_view_vouchers' not in request.eventpermset:
raise PermissionDenied('event.vouchers:read permission required for expand=voucher') raise PermissionDenied('can_view_vouchers permission required for expand=voucher')
prefetch_by_id(items, Voucher.objects, 'voucher_id', 'voucher') prefetch_by_id(items, Voucher.objects, 'voucher_id', 'voucher')
def __init__(self, instance, *args, **kwargs): def __init__(self, instance, *args, **kwargs):

View File

@@ -55,10 +55,11 @@ class ExporterSerializer(serializers.Serializer):
class JobRunSerializer(serializers.Serializer): class JobRunSerializer(serializers.Serializer):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
ex = kwargs.pop('exporter') ex = kwargs.pop('exporter')
events = kwargs.pop('events', None)
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
if ex.is_multievent and not isinstance(ex, OrganizerLevelExportMixin): if events is not None and not isinstance(ex, OrganizerLevelExportMixin):
self.fields["events"] = serializers.SlugRelatedField( self.fields["events"] = serializers.SlugRelatedField(
queryset=ex.events, queryset=events,
required=False, required=False,
allow_empty=False, allow_empty=False,
slug_field='slug', slug_field='slug',

View File

@@ -24,7 +24,7 @@ from decimal import Decimal
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from rest_framework import serializers from rest_framework import serializers
from rest_framework.exceptions import PermissionDenied, ValidationError from rest_framework.exceptions import ValidationError
from pretix.api.serializers.i18n import I18nAwareModelSerializer from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.api.serializers.order import OrderPositionSerializer from pretix.api.serializers.order import OrderPositionSerializer
@@ -66,9 +66,6 @@ class ReusableMediaSerializer(I18nAwareModelSerializer):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
if 'linked_giftcard' in self.context['request'].query_params.getlist('expand'): if 'linked_giftcard' in self.context['request'].query_params.getlist('expand'):
if not self.context["can_read_giftcards"]:
raise PermissionDenied("No permission to access gift card details.")
self.fields['linked_giftcard'] = NestedGiftCardSerializer(read_only=True, context=self.context) self.fields['linked_giftcard'] = NestedGiftCardSerializer(read_only=True, context=self.context)
if 'linked_giftcard.owner_ticket' in self.context['request'].query_params.getlist('expand'): if 'linked_giftcard.owner_ticket' in self.context['request'].query_params.getlist('expand'):
self.fields['linked_giftcard'].fields['owner_ticket'] = NestedOrderPositionSerializer(read_only=True, context=self.context) self.fields['linked_giftcard'].fields['owner_ticket'] = NestedOrderPositionSerializer(read_only=True, context=self.context)
@@ -80,8 +77,6 @@ class ReusableMediaSerializer(I18nAwareModelSerializer):
) )
if 'linked_orderposition' in self.context['request'].query_params.getlist('expand'): if 'linked_orderposition' in self.context['request'].query_params.getlist('expand'):
# No additional permission check performed, documented limitation of the permission system
# Would get to complex/unusable otherwise since the permission depends on the event
self.fields['linked_orderposition'] = NestedOrderPositionSerializer(read_only=True) self.fields['linked_orderposition'] = NestedOrderPositionSerializer(read_only=True)
else: else:
self.fields['linked_orderposition'] = serializers.PrimaryKeyRelatedField( self.fields['linked_orderposition'] = serializers.PrimaryKeyRelatedField(
@@ -91,9 +86,6 @@ class ReusableMediaSerializer(I18nAwareModelSerializer):
) )
if 'customer' in self.context['request'].query_params.getlist('expand'): if 'customer' in self.context['request'].query_params.getlist('expand'):
if not self.context["can_read_customers"]:
raise PermissionDenied("No permission to access customer details.")
self.fields['customer'] = CustomerSerializer(read_only=True) self.fields['customer'] = CustomerSerializer(read_only=True)
else: else:
self.fields['customer'] = serializers.SlugRelatedField( self.fields['customer'] = serializers.SlugRelatedField(

View File

@@ -191,7 +191,7 @@ class InvoiceAddressSerializer(I18nAwareModelSerializer):
{"transmission_info": {r: "This field is required for the selected type of invoice transmission."}} {"transmission_info": {r: "This field is required for the selected type of invoice transmission."}}
) )
break # do not call else branch of for loop break # do not call else branch of for loop
elif t.is_exclusive(self.context["request"].event, data.get("country"), data.get("is_business")): elif t.exclusive:
if t.is_available(self.context["request"].event, data.get("country"), data.get("is_business")): if t.is_available(self.context["request"].event, data.get("country"), data.get("is_business")):
raise ValidationError({ raise ValidationError({
"transmission_type": "The transmission type '%s' must be used for this country or address type." % ( "transmission_type": "The transmission type '%s' must be used for this country or address type." % (
@@ -613,7 +613,7 @@ class OrderPositionSerializer(I18nAwareModelSerializer):
# /events/…/checkinlists/…/positions/ # /events/…/checkinlists/…/positions/
# We're unable to check this on this level if we're on /checkinrpc/, in which case we rely on the view # We're unable to check this on this level if we're on /checkinrpc/, in which case we rely on the view
# layer to not set pdf_data=true in the first place. # layer to not set pdf_data=true in the first place.
request and hasattr(request, 'eventpermset') and 'event.orders:read' not in request.eventpermset request and hasattr(request, 'eventpermset') and 'can_view_orders' not in request.eventpermset
) )
if ('pdf_data' in self.context and not self.context['pdf_data']) or pdf_data_forbidden: if ('pdf_data' in self.context and not self.context['pdf_data']) or pdf_data_forbidden:
self.fields.pop('pdf_data', None) self.fields.pop('pdf_data', None)
@@ -704,16 +704,6 @@ class CheckinListOrderPositionSerializer(OrderPositionSerializer):
if 'answers.question' in self.context['expand']: if 'answers.question' in self.context['expand']:
self.fields['answers'].child.fields['question'] = QuestionSerializer(read_only=True) self.fields['answers'].child.fields['question'] = QuestionSerializer(read_only=True)
if 'addons' in self.context['expand']:
# Experimental feature, undocumented on purpose for now in case we need to remove it again
# for performance reasons
subl = CheckinListOrderPositionSerializer(read_only=True, many=True, context={
**self.context,
'expand': [v for v in self.context['expand'] if v != 'addons'],
'pdf_data': False,
})
self.fields['addons'] = subl
class OrderPaymentTypeField(serializers.Field): class OrderPaymentTypeField(serializers.Field):
# TODO: Remove after pretix 2.2 # TODO: Remove after pretix 2.2

View File

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

View File

@@ -37,8 +37,6 @@ logger = logging.getLogger(__name__)
class SettingsSerializer(serializers.Serializer): class SettingsSerializer(serializers.Serializer):
default_fields = [] default_fields = []
readonly_fields = [] readonly_fields = []
default_write_permission = 'organizer.settings.general:write'
write_permission_required = {}
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.changed_data = [] self.changed_data = []
@@ -60,17 +58,9 @@ class SettingsSerializer(serializers.Serializer):
f._label = str(form_kwargs.get('label', fname)) f._label = str(form_kwargs.get('label', fname))
f._help_text = str(form_kwargs.get('help_text')) f._help_text = str(form_kwargs.get('help_text'))
f.parent = self f.parent = self
self.write_permission_required[fname] = DEFAULTS[fname].get('write_permission', self.default_write_permission)
self.fields[fname] = f self.fields[fname] = f
def validate(self, attrs): def validate(self, attrs):
for k in attrs.keys():
p = self.write_permission_required.get(k, self.default_write_permission)
if p not in self.context["permissions"]:
raise ValidationError({k: f"Setting this field requires permission {p}"})
return {k: v for k, v in attrs.items() if k not in self.readonly_fields} return {k: v for k, v in attrs.items() if k not in self.readonly_fields}
def update(self, instance: HierarkeyProxy, validated_data): def update(self, instance: HierarkeyProxy, validated_data):

View File

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

View File

@@ -118,11 +118,11 @@ class CheckinListViewSet(viewsets.ModelViewSet):
def _get_permission_name(self, request): def _get_permission_name(self, request):
if request.path.endswith('/failed_checkins/'): if request.path.endswith('/failed_checkins/'):
return 'event.orders:checkin', 'event.orders:write' return 'can_checkin_orders', 'can_change_orders'
elif request.method in SAFE_METHODS: elif request.method in SAFE_METHODS:
return 'event.orders:read', 'event.orders:checkin', return 'can_view_orders', 'can_checkin_orders',
else: else:
return 'event.settings.general:write' return 'can_change_event_settings'
def get_queryset(self): def get_queryset(self):
qs = self.request.event.checkin_lists.prefetch_related( qs = self.request.event.checkin_lists.prefetch_related(
@@ -381,21 +381,15 @@ def _checkin_list_position_queryset(checkinlists, ignore_status=False, ignore_pr
qs = qs.filter(reduce(operator.or_, lists_qs)) qs = qs.filter(reduce(operator.or_, lists_qs))
prefetch_related = [
Prefetch(
lookup='checkins',
queryset=Checkin.objects.filter(list_id__in=[cl.pk for cl in checkinlists]).select_related('device')
),
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')),
'answers', 'answers__options', 'answers__question',
]
select_related = [
'item', 'variation', 'order', 'addon_to', 'order__invoice_address', 'order', 'seat'
]
if pdf_data: if pdf_data:
qs = qs.prefetch_related( qs = qs.prefetch_related(
# Don't add to list, we don't want to propagate to addons Prefetch(
lookup='checkins',
queryset=Checkin.objects.filter(list_id__in=[cl.pk for cl in checkinlists]).select_related('device')
),
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')),
'answers', 'answers__options', 'answers__question',
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation')),
Prefetch('order', Order.objects.select_related('invoice_address').prefetch_related( Prefetch('order', Order.objects.select_related('invoice_address').prefetch_related(
Prefetch( Prefetch(
'event', 'event',
@@ -410,39 +404,32 @@ def _checkin_list_position_queryset(checkinlists, ignore_status=False, ignore_pr
) )
) )
)) ))
).select_related(
'item', 'variation', 'item__category', 'addon_to', 'order', 'order__invoice_address', 'seat'
) )
else:
qs = qs.prefetch_related(
Prefetch(
lookup='checkins',
queryset=Checkin.objects.filter(list_id__in=[cl.pk for cl in checkinlists]).select_related('device')
),
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')),
'answers', 'answers__options', 'answers__question',
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation'))
).select_related('item', 'variation', 'order', 'addon_to', 'order__invoice_address', 'order', 'seat')
if expand and 'subevent' in expand: if expand and 'subevent' in expand:
prefetch_related += [ qs = qs.prefetch_related(
'subevent', 'subevent__event', 'subevent__subeventitem_set', 'subevent__subeventitemvariation_set', 'subevent', 'subevent__event', 'subevent__subeventitem_set', 'subevent__subeventitemvariation_set',
'subevent__seat_category_mappings', 'subevent__meta_values' 'subevent__seat_category_mappings', 'subevent__meta_values'
] )
if expand and 'item' in expand: if expand and 'item' in expand:
prefetch_related += [ qs = qs.prefetch_related('item', 'item__addons', 'item__bundles', 'item__meta_values',
'item', 'item__addons', 'item__bundles', 'item__meta_values', 'item__variations').select_related('item__tax_rule')
'item__variations',
]
select_related.append('item__tax_rule')
if expand and 'variation' in expand: if expand and 'variation' in expand:
prefetch_related += [ qs = qs.prefetch_related('variation', 'variation__meta_values')
'variation', 'variation__meta_values',
]
if expand and 'addons' in expand:
prefetch_related += [
Prefetch('addons', OrderPosition.objects.prefetch_related(*prefetch_related).select_related(*select_related)),
]
else:
prefetch_related += [
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation'))
]
if pdf_data:
select_related.remove("order") # Don't need it twice on this queryset
qs = qs.prefetch_related(*prefetch_related).select_related(*select_related)
return qs return qs
@@ -470,7 +457,7 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
'event': op.order.event, 'event': op.order.event,
'pdf_data': pdf_data and ( 'pdf_data': pdf_data and (
user if user and user.is_authenticated else auth user if user and user.is_authenticated else auth
).has_event_permission(request.organizer, event, 'event.orders:read', request), ).has_event_permission(request.organizer, event, 'can_view_orders', request),
} }
common_checkin_args = dict( common_checkin_args = dict(
@@ -835,8 +822,8 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
} }
filterset_class = CheckinOrderPositionFilter filterset_class = CheckinOrderPositionFilter
permission = ('event.orders:read', 'event.orders:checkin') permission = ('can_view_orders', 'can_checkin_orders')
write_permission = ('event.orders:write', 'event.orders:checkin') write_permission = ('can_change_orders', 'can_checkin_orders')
def get_serializer_context(self): def get_serializer_context(self):
ctx = super().get_serializer_context() ctx = super().get_serializer_context()
@@ -867,7 +854,7 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
expand=self.request.query_params.getlist('expand'), expand=self.request.query_params.getlist('expand'),
) )
if 'pk' not in self.request.resolver_match.kwargs and 'event.orders:read' not in self.request.eventpermset \ if 'pk' not in self.request.resolver_match.kwargs and 'can_view_orders' not in self.request.eventpermset \
and len(self.request.query_params.get('search', '')) < 3: and len(self.request.query_params.get('search', '')) < 3:
qs = qs.none() qs = qs.none()
@@ -916,9 +903,9 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
class CheckinRPCRedeemView(views.APIView): class CheckinRPCRedeemView(views.APIView):
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
if isinstance(self.request.auth, (TeamAPIToken, Device)): if isinstance(self.request.auth, (TeamAPIToken, Device)):
events = self.request.auth.get_events_with_permission(('event.orders:write', 'event.orders:checkin')) events = self.request.auth.get_events_with_permission(('can_change_orders', 'can_checkin_orders'))
elif self.request.user.is_authenticated: elif self.request.user.is_authenticated:
events = self.request.user.get_events_with_permission(('event.orders:write', 'event.orders:checkin'), self.request).filter( events = self.request.user.get_events_with_permission(('can_change_orders', 'can_checkin_orders'), self.request).filter(
organizer=self.request.organizer organizer=self.request.organizer
) )
else: else:
@@ -979,16 +966,15 @@ class CheckinRPCSearchView(ListAPIView):
def get_serializer_context(self): def get_serializer_context(self):
ctx = super().get_serializer_context() ctx = super().get_serializer_context()
ctx['expand'] = self.request.query_params.getlist('expand') ctx['expand'] = self.request.query_params.getlist('expand')
ctx['organizer'] = self.request.organizer
ctx['pdf_data'] = False ctx['pdf_data'] = False
return ctx return ctx
@cached_property @cached_property
def lists(self): def lists(self):
if isinstance(self.request.auth, (TeamAPIToken, Device)): if isinstance(self.request.auth, (TeamAPIToken, Device)):
events = self.request.auth.get_events_with_permission(('event.orders:read', 'event.orders:checkin')) events = self.request.auth.get_events_with_permission(('can_view_orders', 'can_checkin_orders'))
elif self.request.user.is_authenticated: elif self.request.user.is_authenticated:
events = self.request.user.get_events_with_permission(('event.orders:read', 'event.orders:checkin'), self.request).filter( events = self.request.user.get_events_with_permission(('can_view_orders', 'can_checkin_orders'), self.request).filter(
organizer=self.request.organizer organizer=self.request.organizer
) )
else: else:
@@ -1005,9 +991,9 @@ class CheckinRPCSearchView(ListAPIView):
@cached_property @cached_property
def has_full_access_permission(self): def has_full_access_permission(self):
if isinstance(self.request.auth, (TeamAPIToken, Device)): if isinstance(self.request.auth, (TeamAPIToken, Device)):
events = self.request.auth.get_events_with_permission('event.orders:read') events = self.request.auth.get_events_with_permission('can_view_orders')
elif self.request.user.is_authenticated: elif self.request.user.is_authenticated:
events = self.request.user.get_events_with_permission('event.orders:read', self.request).filter( events = self.request.user.get_events_with_permission('can_view_orders', self.request).filter(
organizer=self.request.organizer organizer=self.request.organizer
) )
else: else:
@@ -1034,9 +1020,9 @@ class CheckinRPCSearchView(ListAPIView):
class CheckinRPCAnnulView(views.APIView): class CheckinRPCAnnulView(views.APIView):
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
if isinstance(self.request.auth, (TeamAPIToken, Device)): if isinstance(self.request.auth, (TeamAPIToken, Device)):
events = self.request.auth.get_events_with_permission(('event.orders:write', 'event.orders:checkin')) events = self.request.auth.get_events_with_permission(('can_change_orders', 'can_checkin_orders'))
elif self.request.user.is_authenticated: elif self.request.user.is_authenticated:
events = self.request.user.get_events_with_permission(('event.orders:write', 'event.orders:checkin'), self.request).filter( events = self.request.user.get_events_with_permission(('can_change_orders', 'can_checkin_orders'), self.request).filter(
organizer=self.request.organizer organizer=self.request.organizer
) )
else: else:
@@ -1114,7 +1100,7 @@ class CheckinViewSet(viewsets.ReadOnlyModelViewSet):
filterset_class = CheckinFilter filterset_class = CheckinFilter
ordering = ('created', 'id') ordering = ('created', 'id')
ordering_fields = ('created', 'datetime', 'id',) ordering_fields = ('created', 'datetime', 'id',)
permission = 'event.orders:read' permission = 'can_view_orders'
def get_queryset(self): def get_queryset(self):
qs = Checkin.all.filter().select_related( qs = Checkin.all.filter().select_related(

View File

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

View File

@@ -341,7 +341,7 @@ class CloneEventViewSet(viewsets.ModelViewSet):
lookup_field = 'slug' lookup_field = 'slug'
lookup_url_kwarg = 'event' lookup_url_kwarg = 'event'
http_method_names = ['post'] http_method_names = ['post']
write_permission = 'event.settings.general:write' write_permission = 'can_create_events'
def get_serializer_context(self): def get_serializer_context(self):
ctx = super().get_serializer_context() ctx = super().get_serializer_context()
@@ -350,12 +350,6 @@ class CloneEventViewSet(viewsets.ModelViewSet):
return ctx return ctx
def perform_create(self, serializer): def perform_create(self, serializer):
# Weird edge case: Requires settings permission on the event (to read) but also on the organizer (two write)
perm_holder = (self.request.auth if isinstance(self.request.auth, (Device, TeamAPIToken))
else self.request.user)
if not perm_holder.has_organizer_permission(self.request.organizer, "organizer.events:create", request=self.request):
raise PermissionDenied("No permission to create events")
serializer.save(organizer=self.request.organizer) serializer.save(organizer=self.request.organizer)
serializer.instance.log_action( serializer.instance.log_action(
@@ -432,7 +426,7 @@ with scopes_disabled():
class SubEventViewSet(ConditionalListView, viewsets.ModelViewSet): class SubEventViewSet(ConditionalListView, viewsets.ModelViewSet):
serializer_class = SubEventSerializer serializer_class = SubEventSerializer
queryset = SubEvent.objects.none() queryset = SubEvent.objects.none()
write_permission = 'event.subevents:write' write_permission = 'can_change_event_settings'
filter_backends = (DjangoFilterBackend, TotalOrderingFilter) filter_backends = (DjangoFilterBackend, TotalOrderingFilter)
ordering = ('date_from',) ordering = ('date_from',)
ordering_fields = ('id', 'date_from', 'last_modified') ordering_fields = ('id', 'date_from', 'last_modified')
@@ -552,7 +546,7 @@ class SubEventViewSet(ConditionalListView, viewsets.ModelViewSet):
class TaxRuleViewSet(ConditionalListView, viewsets.ModelViewSet): class TaxRuleViewSet(ConditionalListView, viewsets.ModelViewSet):
serializer_class = TaxRuleSerializer serializer_class = TaxRuleSerializer
queryset = TaxRule.objects.none() queryset = TaxRule.objects.none()
write_permission = 'event.settings.tax:write' write_permission = 'can_change_event_settings'
def get_queryset(self): def get_queryset(self):
return self.request.event.tax_rules.all() return self.request.event.tax_rules.all()
@@ -595,7 +589,7 @@ class TaxRuleViewSet(ConditionalListView, viewsets.ModelViewSet):
class ItemMetaPropertiesViewSet(viewsets.ModelViewSet): class ItemMetaPropertiesViewSet(viewsets.ModelViewSet):
serializer_class = ItemMetaPropertiesSerializer serializer_class = ItemMetaPropertiesSerializer
queryset = ItemMetaProperty.objects.none() queryset = ItemMetaProperty.objects.none()
write_permission = 'event.settings.general:write' write_permission = 'can_change_event_settings'
def get_queryset(self): def get_queryset(self):
qs = self.request.event.item_meta_properties.all() qs = self.request.event.item_meta_properties.all()
@@ -642,18 +636,19 @@ class ItemMetaPropertiesViewSet(viewsets.ModelViewSet):
class EventSettingsView(views.APIView): class EventSettingsView(views.APIView):
permission = None permission = None
write_permission = 'event.settings.general:write' write_permission = 'can_change_event_settings'
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
if isinstance(request.auth, Device): if isinstance(request.auth, Device):
s = DeviceEventSettingsSerializer(instance=request.event.settings, event=request.event, context={ s = DeviceEventSettingsSerializer(instance=request.event.settings, event=request.event, context={
'request': request, 'permissions': request.eventpermset 'request': request
})
elif 'can_change_event_settings' in request.eventpermset:
s = EventSettingsSerializer(instance=request.event.settings, event=request.event, context={
'request': request
}) })
else: else:
s = EventSettingsSerializer(instance=request.event.settings, event=request.event, context={ raise PermissionDenied()
'request': request, 'permissions': request.eventpermset,
})
if 'explain' in request.GET: if 'explain' in request.GET:
return Response({ return Response({
fname: { fname: {
@@ -667,7 +662,7 @@ class EventSettingsView(views.APIView):
def patch(self, request, *wargs, **kwargs): def patch(self, request, *wargs, **kwargs):
s = EventSettingsSerializer(instance=request.event.settings, data=request.data, partial=True, s = EventSettingsSerializer(instance=request.event.settings, data=request.data, partial=True,
event=request.event, context={'request': request, 'permissions': request.eventpermset}) event=request.event, context={'request': request})
s.is_valid(raise_exception=True) s.is_valid(raise_exception=True)
with transaction.atomic(): with transaction.atomic():
s.save() s.save()
@@ -679,7 +674,7 @@ class EventSettingsView(views.APIView):
) )
s = EventSettingsSerializer( s = EventSettingsSerializer(
instance=request.event.settings, event=request.event, context={ instance=request.event.settings, event=request.event, context={
'request': request, 'permissions': request.eventpermset 'request': request
}) })
return Response(s.data) return Response(s.data)
@@ -706,7 +701,7 @@ class SeatFilter(FilterSet):
class SeatViewSet(ConditionalListView, viewsets.ModelViewSet): class SeatViewSet(ConditionalListView, viewsets.ModelViewSet):
serializer_class = SeatSerializer serializer_class = SeatSerializer
queryset = Seat.objects.none() queryset = Seat.objects.none()
write_permission = 'event.settings.general:write' write_permission = 'can_change_event_settings'
filter_backends = (DjangoFilterBackend, ) filter_backends = (DjangoFilterBackend, )
filterset_class = SeatFilter filterset_class = SeatFilter

View File

@@ -38,12 +38,14 @@ from pretix.api.serializers.exporters import (
ExporterSerializer, JobRunSerializer, ScheduledEventExportSerializer, ExporterSerializer, JobRunSerializer, ScheduledEventExportSerializer,
ScheduledOrganizerExportSerializer, ScheduledOrganizerExportSerializer,
) )
from pretix.base.exporter import OrganizerLevelExportMixin
from pretix.base.models import ( from pretix.base.models import (
CachedFile, Device, ScheduledEventExport, ScheduledOrganizerExport, CachedFile, Device, Event, ScheduledEventExport, ScheduledOrganizerExport,
TeamAPIToken, TeamAPIToken,
) )
from pretix.base.services.export import ( from pretix.base.services.export import export, multiexport
export, init_event_exporters, init_organizer_exporters, multiexport, from pretix.base.signals import (
register_data_exporters, register_multievent_data_exporters,
) )
from pretix.helpers.http import ChunkBasedFileResponse from pretix.helpers.http import ChunkBasedFileResponse
@@ -109,7 +111,7 @@ class ExportersMixin:
@action(detail=True, methods=['POST']) @action(detail=True, methods=['POST'])
def run(self, *args, **kwargs): def run(self, *args, **kwargs):
instance = self.get_object() instance = self.get_object()
serializer = JobRunSerializer(exporter=instance, data=self.request.data) serializer = JobRunSerializer(exporter=instance, data=self.request.data, **self.get_serializer_kwargs())
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
cf = CachedFile(web_download=True) cf = CachedFile(web_download=True)
@@ -134,34 +136,27 @@ class ExportersMixin:
class EventExportersViewSet(ExportersMixin, viewsets.ViewSet): class EventExportersViewSet(ExportersMixin, viewsets.ViewSet):
permission = None permission = 'can_view_orders'
def get_serializer_kwargs(self):
return {}
@cached_property @cached_property
def exporters(self): def exporters(self):
raw_exporters = list(init_event_exporters(
event=self.request.event,
user=self.request.user if self.request.user and self.request.user.is_authenticated else None,
token=self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None,
device=self.request.auth if isinstance(self.request.auth, Device) else None,
request=self.request,
))
exporters = [] exporters = []
responses = register_data_exporters.send(self.request.event)
raw_exporters = [response(self.request.event, self.request.organizer) for r, response in responses if response]
raw_exporters = [
ex for ex in raw_exporters
if ex.available_for_user(self.request.user if self.request.user and self.request.user.is_authenticated else None)
]
for ex in sorted(raw_exporters, key=lambda ex: str(ex.verbose_name)): for ex in sorted(raw_exporters, key=lambda ex: str(ex.verbose_name)):
ex._serializer = JobRunSerializer(exporter=ex) ex._serializer = JobRunSerializer(exporter=ex)
exporters.append(ex) exporters.append(ex)
return exporters return exporters
def do_export(self, cf, instance, data): def do_export(self, cf, instance, data):
return export.apply_async(args=( return export.apply_async(args=(self.request.event.id, str(cf.id), instance.identifier, data))
self.request.event.id,
), kwargs={
'user': self.request.user.pk if self.request.user and self.request.user.is_authenticated else None,
'token': self.request.auth.pk if isinstance(self.request.auth, TeamAPIToken) else None,
'device': self.request.auth.pk if isinstance(self.request.auth, Device) else None,
'fileid': str(cf.id),
'provider': instance.identifier,
'form_data': data,
})
class OrganizerExportersViewSet(ExportersMixin, viewsets.ViewSet): class OrganizerExportersViewSet(ExportersMixin, viewsets.ViewSet):
@@ -169,23 +164,47 @@ class OrganizerExportersViewSet(ExportersMixin, viewsets.ViewSet):
@cached_property @cached_property
def exporters(self): def exporters(self):
raw_exporters = list(init_organizer_exporters(
organizer=self.request.organizer,
user=self.request.user if self.request.user and self.request.user.is_authenticated else None,
token=self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None,
device=self.request.auth if isinstance(self.request.auth, Device) else None,
request=self.request,
))
exporters = [] exporters = []
if isinstance(self.request.auth, (Device, TeamAPIToken)):
perm_holder = self.request.auth
else:
perm_holder = self.request.user
events = perm_holder.get_events_with_permission('can_view_orders', request=self.request).filter(
organizer=self.request.organizer
)
responses = register_multievent_data_exporters.send(self.request.organizer)
raw_exporters = [
response(Event.objects.none() if issubclass(response, OrganizerLevelExportMixin) else events, self.request.organizer)
for r, response in responses
if response
]
raw_exporters = [
ex for ex in raw_exporters
if (
not isinstance(ex, OrganizerLevelExportMixin) or
perm_holder.has_organizer_permission(self.request.organizer, ex.organizer_required_permission, self.request)
) and ex.available_for_user(self.request.user if self.request.user and self.request.user.is_authenticated else None)
]
for ex in sorted(raw_exporters, key=lambda ex: str(ex.verbose_name)): for ex in sorted(raw_exporters, key=lambda ex: str(ex.verbose_name)):
ex._serializer = JobRunSerializer(exporter=ex) ex._serializer = JobRunSerializer(exporter=ex, events=events)
exporters.append(ex) exporters.append(ex)
return exporters return exporters
def get_serializer_kwargs(self):
if isinstance(self.request.auth, (Device, TeamAPIToken)):
perm_holder = self.request.auth
else:
perm_holder = self.request.user
return {
'events': perm_holder.get_events_with_permission('can_view_orders', request=self.request).filter(
organizer=self.request.organizer
)
}
def do_export(self, cf, instance, data): def do_export(self, cf, instance, data):
return multiexport.apply_async(kwargs={ return multiexport.apply_async(kwargs={
'organizer': self.request.organizer.id, 'organizer': self.request.organizer.id,
'user': self.request.user.id if self.request.user and self.request.user.is_authenticated else None, 'user': self.request.user.id if self.request.user.is_authenticated else None,
'token': self.request.auth.pk if isinstance(self.request.auth, TeamAPIToken) else None, 'token': self.request.auth.pk if isinstance(self.request.auth, TeamAPIToken) else None,
'device': self.request.auth.pk if isinstance(self.request.auth, Device) else None, 'device': self.request.auth.pk if isinstance(self.request.auth, Device) else None,
'fileid': str(cf.id), 'fileid': str(cf.id),
@@ -203,11 +222,11 @@ class ScheduledExportersViewSet(viewsets.ModelViewSet):
class ScheduledEventExportViewSet(ScheduledExportersViewSet): class ScheduledEventExportViewSet(ScheduledExportersViewSet):
serializer_class = ScheduledEventExportSerializer serializer_class = ScheduledEventExportSerializer
queryset = ScheduledEventExport.objects.none() queryset = ScheduledEventExport.objects.none()
permission = 'event.orders:read' permission = 'can_view_orders'
def get_queryset(self): def get_queryset(self):
perm_holder = self.request.auth if isinstance(self.request.auth, (TeamAPIToken, Device)) else self.request.user perm_holder = self.request.auth if isinstance(self.request.auth, (TeamAPIToken, Device)) else self.request.user
if not perm_holder.has_event_permission(self.request.organizer, self.request.event, 'event.settings.general:write', if not perm_holder.has_event_permission(self.request.organizer, self.request.event, 'can_change_event_settings',
request=self.request): request=self.request):
if self.request.user.is_authenticated: if self.request.user.is_authenticated:
qs = self.request.event.scheduled_exports.filter(owner=self.request.user) qs = self.request.event.scheduled_exports.filter(owner=self.request.user)
@@ -239,13 +258,8 @@ class ScheduledEventExportViewSet(ScheduledExportersViewSet):
@cached_property @cached_property
def exporters(self): def exporters(self):
exporters = list(init_event_exporters( responses = register_data_exporters.send(self.request.event)
event=self.request.event, exporters = [response(self.request.event, self.request.organizer) for r, response in responses if response]
user=self.request.user if self.request.user and self.request.user.is_authenticated else None,
token=self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None,
device=self.request.auth if isinstance(self.request.auth, Device) else None,
request=self.request,
))
return {e.identifier: e for e in exporters} return {e.identifier: e for e in exporters}
def perform_update(self, serializer): def perform_update(self, serializer):
@@ -277,7 +291,7 @@ class ScheduledOrganizerExportViewSet(ScheduledExportersViewSet):
def get_queryset(self): def get_queryset(self):
perm_holder = self.request.auth if isinstance(self.request.auth, (TeamAPIToken, Device)) else self.request.user perm_holder = self.request.auth if isinstance(self.request.auth, (TeamAPIToken, Device)) else self.request.user
if not perm_holder.has_organizer_permission(self.request.organizer, 'organizer.settings.general:write', if not perm_holder.has_organizer_permission(self.request.organizer, 'can_change_organizer_settings',
request=self.request): request=self.request):
if self.request.user.is_authenticated: if self.request.user.is_authenticated:
qs = self.request.organizer.scheduled_exports.filter(owner=self.request.user) qs = self.request.organizer.scheduled_exports.filter(owner=self.request.user)
@@ -307,15 +321,23 @@ class ScheduledOrganizerExportViewSet(ScheduledExportersViewSet):
ctx['exporters'] = self.exporters ctx['exporters'] = self.exporters
return ctx return ctx
@cached_property
def events(self):
if isinstance(self.request.auth, (TeamAPIToken, Device)):
return self.request.auth.get_events_with_permission('can_view_orders')
elif self.request.user.is_authenticated:
return self.request.user.get_events_with_permission('can_view_orders', self.request).filter(
organizer=self.request.organizer
)
@cached_property @cached_property
def exporters(self): def exporters(self):
exporters = list(init_organizer_exporters( responses = register_multievent_data_exporters.send(self.request.organizer)
organizer=self.request.organizer, exporters = [
user=self.request.user if self.request.user and self.request.user.is_authenticated else None, response(Event.objects.none() if issubclass(response, OrganizerLevelExportMixin) else self.events,
token=self.request.auth if isinstance(self.request.auth, TeamAPIToken) else None, self.request.organizer)
device=self.request.auth if isinstance(self.request.auth, Device) else None, for r, response in responses if response
request=self.request, ]
))
return {e.identifier: e for e in exporters} return {e.identifier: e for e in exporters}
def perform_update(self, serializer): def perform_update(self, serializer):

View File

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

View File

@@ -62,8 +62,8 @@ with scopes_disabled():
class ReusableMediaViewSet(viewsets.ModelViewSet): class ReusableMediaViewSet(viewsets.ModelViewSet):
serializer_class = ReusableMediaSerializer serializer_class = ReusableMediaSerializer
queryset = ReusableMedium.objects.none() queryset = ReusableMedium.objects.none()
permission = 'organizer.reusablemedia:read' permission = 'can_manage_reusable_media'
write_permission = 'organizer.reusablemedia:write' write_permission = 'can_manage_reusable_media'
filter_backends = (DjangoFilterBackend, OrderingFilter) filter_backends = (DjangoFilterBackend, OrderingFilter)
ordering = ('-updated', '-id') ordering = ('-updated', '-id')
ordering_fields = ('created', 'updated', 'identifier', 'type', 'id') ordering_fields = ('created', 'updated', 'identifier', 'type', 'id')
@@ -95,8 +95,6 @@ class ReusableMediaViewSet(viewsets.ModelViewSet):
def get_serializer_context(self): def get_serializer_context(self):
ctx = super().get_serializer_context() ctx = super().get_serializer_context()
ctx['organizer'] = self.request.organizer ctx['organizer'] = self.request.organizer
ctx['can_read_giftcards'] = 'organizer.giftcards:read' in self.request.orgapermset
ctx['can_read_customers'] = 'organizer.customers:read' in self.request.orgapermset
return ctx return ctx
@transaction.atomic() @transaction.atomic()

View File

@@ -317,7 +317,7 @@ class OrderViewSetMixin:
class OrganizerOrderViewSet(OrderViewSetMixin, viewsets.ReadOnlyModelViewSet): class OrganizerOrderViewSet(OrderViewSetMixin, viewsets.ReadOnlyModelViewSet):
def get_base_queryset(self): def get_base_queryset(self):
perm = "event.orders:read" if self.request.method in SAFE_METHODS else "event.orders:write" perm = "can_view_orders" if self.request.method in SAFE_METHODS else "can_change_orders"
if isinstance(self.request.auth, (TeamAPIToken, Device)): if isinstance(self.request.auth, (TeamAPIToken, Device)):
return Order.objects.filter( return Order.objects.filter(
event__organizer=self.request.organizer, event__organizer=self.request.organizer,
@@ -338,8 +338,8 @@ class OrganizerOrderViewSet(OrderViewSetMixin, viewsets.ReadOnlyModelViewSet):
class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet): class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
permission = 'event.orders:read' permission = 'can_view_orders'
write_permission = 'event.orders:write' write_permission = 'can_change_orders'
def get_serializer_context(self): def get_serializer_context(self):
ctx = super().get_serializer_context() ctx = super().get_serializer_context()
@@ -1078,8 +1078,8 @@ class OrderPositionViewSet(viewsets.ModelViewSet):
ordering = ('order__datetime', 'positionid') ordering = ('order__datetime', 'positionid')
ordering_fields = ('order__code', 'order__datetime', 'positionid', 'attendee_name', 'order__status',) ordering_fields = ('order__code', 'order__datetime', 'positionid', 'attendee_name', 'order__status',)
filterset_class = OrderPositionFilter filterset_class = OrderPositionFilter
permission = 'event.orders:read' permission = 'can_view_orders'
write_permission = 'event.orders:write' write_permission = 'can_change_orders'
ordering_custom = { ordering_custom = {
'attendee_name': { 'attendee_name': {
'_order': F('display_name').asc(nulls_first=True), '_order': F('display_name').asc(nulls_first=True),
@@ -1580,8 +1580,8 @@ class OrderPositionViewSet(viewsets.ModelViewSet):
class PaymentViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet): class PaymentViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
serializer_class = OrderPaymentSerializer serializer_class = OrderPaymentSerializer
queryset = OrderPayment.objects.none() queryset = OrderPayment.objects.none()
permission = 'event.orders:read' permission = 'can_view_orders'
write_permission = 'event.orders:write' write_permission = 'can_change_orders'
lookup_field = 'local_id' lookup_field = 'local_id'
def get_serializer_context(self): def get_serializer_context(self):
@@ -1757,8 +1757,8 @@ class PaymentViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
class RefundViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet): class RefundViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
serializer_class = OrderRefundSerializer serializer_class = OrderRefundSerializer
queryset = OrderRefund.objects.none() queryset = OrderRefund.objects.none()
permission = 'event.orders:read' permission = 'can_view_orders'
write_permission = 'event.orders:write' write_permission = 'can_change_orders'
lookup_field = 'local_id' lookup_field = 'local_id'
def get_queryset(self): def get_queryset(self):
@@ -1915,18 +1915,13 @@ class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
ordering = ('nr',) ordering = ('nr',)
ordering_fields = ('nr', 'date') ordering_fields = ('nr', 'date')
filterset_class = InvoiceFilter filterset_class = InvoiceFilter
permission = 'can_view_orders'
lookup_url_kwarg = 'number' lookup_url_kwarg = 'number'
lookup_field = 'nr' lookup_field = 'nr'
write_permission = 'can_change_orders'
def _get_permission_name(self, request):
if 'event' in request.resolver_match.kwargs:
if request.method not in SAFE_METHODS:
return "event.orders:write"
return "event.orders:read"
return None # org-level is handled by event__in check
def get_queryset(self): def get_queryset(self):
perm = "event.orders:read" if self.request.method in SAFE_METHODS else "event.orders:write" perm = "can_view_orders" if self.request.method in SAFE_METHODS else "can_change_orders"
if getattr(self.request, 'event', None): if getattr(self.request, 'event', None):
qs = self.request.event.invoices qs = self.request.event.invoices
elif isinstance(self.request.auth, (TeamAPIToken, Device)): elif isinstance(self.request.auth, (TeamAPIToken, Device)):
@@ -2036,7 +2031,7 @@ class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
else: else:
order = Order.objects.select_for_update(of=OF_SELF).get(pk=inv.order_id) order = Order.objects.select_for_update(of=OF_SELF).get(pk=inv.order_id)
c = generate_cancellation(inv) c = generate_cancellation(inv)
if invoice_qualified(order): if inv.order.status != Order.STATUS_CANCELED:
inv = generate_invoice(order) inv = generate_invoice(order)
else: else:
inv = c inv = c
@@ -2067,8 +2062,8 @@ class RevokedSecretViewSet(viewsets.ReadOnlyModelViewSet):
ordering = ('-created',) ordering = ('-created',)
ordering_fields = ('created', 'secret') ordering_fields = ('created', 'secret')
filterset_class = RevokedSecretFilter filterset_class = RevokedSecretFilter
permission = 'event.orders:read' permission = 'can_view_orders'
write_permission = 'event.orders:write' write_permission = 'can_change_orders'
def get_queryset(self): def get_queryset(self):
return RevokedTicketSecret.objects.filter(event=self.request.event) return RevokedTicketSecret.objects.filter(event=self.request.event)
@@ -2089,8 +2084,8 @@ class BlockedSecretViewSet(viewsets.ReadOnlyModelViewSet):
filter_backends = (DjangoFilterBackend, TotalOrderingFilter) filter_backends = (DjangoFilterBackend, TotalOrderingFilter)
ordering = ('-updated', '-pk') ordering = ('-updated', '-pk')
filterset_class = BlockedSecretFilter filterset_class = BlockedSecretFilter
permission = 'event.orders:read' permission = 'can_view_orders'
write_permission = 'event.orders:write' write_permission = 'can_change_orders'
def get_queryset(self): def get_queryset(self):
return BlockedTicketSecret.objects.filter(event=self.request.event) return BlockedTicketSecret.objects.filter(event=self.request.event)
@@ -2125,7 +2120,7 @@ class TransactionViewSet(viewsets.ReadOnlyModelViewSet):
ordering = ('datetime', 'pk') ordering = ('datetime', 'pk')
ordering_fields = ('datetime', 'created', 'id',) ordering_fields = ('datetime', 'created', 'id',)
filterset_class = TransactionFilter filterset_class = TransactionFilter
permission = 'event.orders:read' permission = 'can_view_orders'
def get_queryset(self): def get_queryset(self):
return Transaction.objects.filter(order__event=self.request.event).select_related("order") return Transaction.objects.filter(order__event=self.request.event).select_related("order")
@@ -2142,11 +2137,11 @@ class OrganizerTransactionViewSet(TransactionViewSet):
if isinstance(self.request.auth, (TeamAPIToken, Device)): if isinstance(self.request.auth, (TeamAPIToken, Device)):
qs = qs.filter( qs = qs.filter(
order__event__in=self.request.auth.get_events_with_permission("event.orders:read"), order__event__in=self.request.auth.get_events_with_permission("can_view_orders"),
) )
elif self.request.user.is_authenticated: elif self.request.user.is_authenticated:
qs = qs.filter( qs = qs.filter(
order__event__in=self.request.user.get_events_with_permission("event.orders:read", request=self.request) order__event__in=self.request.user.get_events_with_permission("can_view_orders", request=self.request)
) )
else: else:
raise PermissionDenied("Unknown authentication scheme") raise PermissionDenied("Unknown authentication scheme")

View File

@@ -70,7 +70,7 @@ class OrganizerViewSet(mixins.UpdateModelMixin, viewsets.ReadOnlyModelViewSet):
filter_backends = (TotalOrderingFilter,) filter_backends = (TotalOrderingFilter,)
ordering = ('slug',) ordering = ('slug',)
ordering_fields = ('name', 'slug') ordering_fields = ('name', 'slug')
write_permission = "organizer.settings.general:write" write_permission = "can_change_organizer_settings"
def get_queryset(self): def get_queryset(self):
if self.request.user.is_authenticated: if self.request.user.is_authenticated:
@@ -154,8 +154,8 @@ class OrganizerViewSet(mixins.UpdateModelMixin, viewsets.ReadOnlyModelViewSet):
class SeatingPlanViewSet(viewsets.ModelViewSet): class SeatingPlanViewSet(viewsets.ModelViewSet):
serializer_class = SeatingPlanSerializer serializer_class = SeatingPlanSerializer
queryset = SeatingPlan.objects.none() queryset = SeatingPlan.objects.none()
permission = None permission = 'can_change_organizer_settings'
write_permission = 'organizer.seatingplans:write' write_permission = 'can_change_organizer_settings'
def get_queryset(self): def get_queryset(self):
return self.request.organizer.seating_plans.order_by('name') return self.request.organizer.seating_plans.order_by('name')
@@ -221,8 +221,8 @@ with scopes_disabled():
class GiftCardViewSet(viewsets.ModelViewSet): class GiftCardViewSet(viewsets.ModelViewSet):
serializer_class = GiftCardSerializer serializer_class = GiftCardSerializer
queryset = GiftCard.objects.none() queryset = GiftCard.objects.none()
permission = 'organizer.giftcards:read' permission = 'can_manage_gift_cards'
write_permission = 'organizer.giftcards:write' write_permission = 'can_manage_gift_cards'
filter_backends = (DjangoFilterBackend,) filter_backends = (DjangoFilterBackend,)
filterset_class = GiftCardFilter filterset_class = GiftCardFilter
@@ -323,8 +323,8 @@ class GiftCardViewSet(viewsets.ModelViewSet):
class GiftCardTransactionViewSet(viewsets.ReadOnlyModelViewSet): class GiftCardTransactionViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = GiftCardTransactionSerializer serializer_class = GiftCardTransactionSerializer
queryset = GiftCardTransaction.objects.none() queryset = GiftCardTransaction.objects.none()
permission = 'organizer.giftcards:read' permission = 'can_manage_gift_cards'
write_permission = 'organizer.giftcards:write' write_permission = 'can_manage_gift_cards'
@cached_property @cached_property
def giftcard(self): def giftcard(self):
@@ -341,8 +341,8 @@ class GiftCardTransactionViewSet(viewsets.ReadOnlyModelViewSet):
class TeamViewSet(viewsets.ModelViewSet): class TeamViewSet(viewsets.ModelViewSet):
serializer_class = TeamSerializer serializer_class = TeamSerializer
queryset = Team.objects.none() queryset = Team.objects.none()
permission = 'organizer.teams:write' permission = 'can_change_teams'
write_permission = 'organizer.teams:write' write_permission = 'can_change_teams'
def get_queryset(self): def get_queryset(self):
return self.request.organizer.teams.order_by('pk') return self.request.organizer.teams.order_by('pk')
@@ -381,8 +381,8 @@ class TeamViewSet(viewsets.ModelViewSet):
class TeamMemberViewSet(DestroyModelMixin, viewsets.ReadOnlyModelViewSet): class TeamMemberViewSet(DestroyModelMixin, viewsets.ReadOnlyModelViewSet):
serializer_class = TeamMemberSerializer serializer_class = TeamMemberSerializer
queryset = User.objects.none() queryset = User.objects.none()
permission = 'organizer.teams:write' permission = 'can_change_teams'
write_permission = 'organizer.teams:write' write_permission = 'can_change_teams'
@cached_property @cached_property
def team(self): def team(self):
@@ -410,8 +410,8 @@ class TeamMemberViewSet(DestroyModelMixin, viewsets.ReadOnlyModelViewSet):
class TeamInviteViewSet(CreateModelMixin, DestroyModelMixin, viewsets.ReadOnlyModelViewSet): class TeamInviteViewSet(CreateModelMixin, DestroyModelMixin, viewsets.ReadOnlyModelViewSet):
serializer_class = TeamInviteSerializer serializer_class = TeamInviteSerializer
queryset = TeamInvite.objects.none() queryset = TeamInvite.objects.none()
permission = 'organizer.teams:write' permission = 'can_change_teams'
write_permission = 'organizer.teams:write' write_permission = 'can_change_teams'
@cached_property @cached_property
def team(self): def team(self):
@@ -447,8 +447,8 @@ class TeamInviteViewSet(CreateModelMixin, DestroyModelMixin, viewsets.ReadOnlyMo
class TeamAPITokenViewSet(CreateModelMixin, DestroyModelMixin, viewsets.ReadOnlyModelViewSet): class TeamAPITokenViewSet(CreateModelMixin, DestroyModelMixin, viewsets.ReadOnlyModelViewSet):
serializer_class = TeamAPITokenSerializer serializer_class = TeamAPITokenSerializer
queryset = TeamAPIToken.objects.none() queryset = TeamAPIToken.objects.none()
permission = 'organizer.teams:write' permission = 'can_change_teams'
write_permission = 'organizer.teams:write' write_permission = 'can_change_teams'
@cached_property @cached_property
def team(self): def team(self):
@@ -511,8 +511,8 @@ class DeviceViewSet(mixins.CreateModelMixin,
GenericViewSet): GenericViewSet):
serializer_class = DeviceSerializer serializer_class = DeviceSerializer
queryset = Device.objects.none() queryset = Device.objects.none()
permission = 'organizer.devices:read' permission = 'can_change_organizer_settings'
write_permission = 'organizer.devices:write' write_permission = 'can_change_organizer_settings'
lookup_field = 'device_id' lookup_field = 'device_id'
def get_queryset(self): def get_queryset(self):
@@ -521,9 +521,6 @@ class DeviceViewSet(mixins.CreateModelMixin,
def get_serializer_context(self): def get_serializer_context(self):
ctx = super().get_serializer_context() ctx = super().get_serializer_context()
ctx['organizer'] = self.request.organizer ctx['organizer'] = self.request.organizer
ctx['can_see_tokens'] = (
self.request.user if self.request.user and self.request.user.is_authenticated else self.request.auth
).has_organizer_permission(self.request.organizer, 'organizer.devices:write', request=self.request)
return ctx return ctx
@transaction.atomic() @transaction.atomic()
@@ -550,11 +547,11 @@ class DeviceViewSet(mixins.CreateModelMixin,
class OrganizerSettingsView(views.APIView): class OrganizerSettingsView(views.APIView):
permission = None permission = None
write_permission = 'organizer.settings.general:write' write_permission = 'can_change_organizer_settings'
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
s = OrganizerSettingsSerializer(instance=request.organizer.settings, organizer=request.organizer, context={ s = OrganizerSettingsSerializer(instance=request.organizer.settings, organizer=request.organizer, context={
'request': request, 'permissions': request.orgapermset 'request': request
}) })
if 'explain' in request.GET: if 'explain' in request.GET:
return Response({ return Response({
@@ -571,7 +568,7 @@ class OrganizerSettingsView(views.APIView):
s = OrganizerSettingsSerializer( s = OrganizerSettingsSerializer(
instance=request.organizer.settings, data=request.data, partial=True, instance=request.organizer.settings, data=request.data, partial=True,
organizer=request.organizer, context={ organizer=request.organizer, context={
'request': request, 'permissions': request.orgapermset 'request': request
} }
) )
s.is_valid(raise_exception=True) s.is_valid(raise_exception=True)
@@ -583,7 +580,7 @@ class OrganizerSettingsView(views.APIView):
} }
) )
s = OrganizerSettingsSerializer(instance=request.organizer.settings, organizer=request.organizer, context={ s = OrganizerSettingsSerializer(instance=request.organizer.settings, organizer=request.organizer, context={
'request': request, 'permissions': request.orgapermset 'request': request
}) })
return Response(s.data) return Response(s.data)
@@ -600,8 +597,7 @@ with scopes_disabled():
class CustomerViewSet(viewsets.ModelViewSet): class CustomerViewSet(viewsets.ModelViewSet):
serializer_class = CustomerSerializer serializer_class = CustomerSerializer
queryset = Customer.objects.none() queryset = Customer.objects.none()
permission = 'organizer.customers:read' permission = 'can_manage_customers'
write_permission = 'organizer.customers:write'
lookup_field = 'identifier' lookup_field = 'identifier'
filter_backends = (DjangoFilterBackend,) filter_backends = (DjangoFilterBackend,)
filterset_class = CustomerFilter filterset_class = CustomerFilter
@@ -661,7 +657,7 @@ class CustomerViewSet(viewsets.ModelViewSet):
class MembershipTypeViewSet(viewsets.ModelViewSet): class MembershipTypeViewSet(viewsets.ModelViewSet):
serializer_class = MembershipTypeSerializer serializer_class = MembershipTypeSerializer
queryset = MembershipType.objects.none() queryset = MembershipType.objects.none()
permission = 'organizer.settings.general:write' permission = 'can_change_organizer_settings'
def get_queryset(self): def get_queryset(self):
qs = self.request.organizer.membership_types.all() qs = self.request.organizer.membership_types.all()
@@ -718,8 +714,7 @@ with scopes_disabled():
class MembershipViewSet(viewsets.ModelViewSet): class MembershipViewSet(viewsets.ModelViewSet):
serializer_class = MembershipSerializer serializer_class = MembershipSerializer
queryset = Membership.objects.none() queryset = Membership.objects.none()
permission = 'organizer.customers:read' permission = 'can_manage_customers'
write_permission = 'organizer.customers:write'
filter_backends = (DjangoFilterBackend,) filter_backends = (DjangoFilterBackend,)
filterset_class = MembershipFilter filterset_class = MembershipFilter
@@ -769,8 +764,8 @@ with scopes_disabled():
class SalesChannelViewSet(viewsets.ModelViewSet): class SalesChannelViewSet(viewsets.ModelViewSet):
serializer_class = SalesChannelSerializer serializer_class = SalesChannelSerializer
queryset = SalesChannel.objects.none() queryset = SalesChannel.objects.none()
permission = 'organizer.settings.general:write' permission = 'can_change_organizer_settings'
write_permission = 'organizer.settings.general:write' write_permission = 'can_change_organizer_settings'
filter_backends = (DjangoFilterBackend,) filter_backends = (DjangoFilterBackend,)
filterset_class = SalesChannelFilter filterset_class = SalesChannelFilter
lookup_field = 'identifier' lookup_field = 'identifier'

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -39,8 +39,8 @@ from zoneinfo import ZoneInfo
from django import forms from django import forms
from django.conf import settings from django.conf import settings
from django.db.models import ( from django.db.models import (
Case, CharField, Count, DateTimeField, Exists, F, IntegerField, Max, Min, Case, CharField, Count, DateTimeField, F, IntegerField, Max, Min, OuterRef,
OuterRef, Q, Subquery, Sum, When, Q, Subquery, Sum, When,
) )
from django.db.models.functions import Coalesce from django.db.models.functions import Coalesce
from django.dispatch import receiver from django.dispatch import receiver
@@ -144,18 +144,6 @@ class OrderListExporter(MultiSheetListExporter):
d = OrderedDict(d) d = OrderedDict(d)
if not self.is_multievent and not self.event.has_subevents: if not self.is_multievent and not self.event.has_subevents:
del d['event_date_range'] del d['event_date_range']
if not self.is_multievent:
d["items"] = forms.ModelMultipleChoiceField(
label=_("Products"),
queryset=self.event.items.all(),
widget=forms.CheckboxSelectMultiple(
attrs={"class": "scrolling-multiple-choice"}
),
help_text=_("If none are selected, all products are included. Orders are included if they contain "
"at least one position of this product. The order totals etc. still include all products "
"contained in the order."),
required=False,
)
return d return d
def _get_all_payment_methods(self, qs): def _get_all_payment_methods(self, qs):
@@ -261,14 +249,6 @@ class OrderListExporter(MultiSheetListExporter):
pcnt=Subquery(s, output_field=IntegerField()) pcnt=Subquery(s, output_field=IntegerField())
).select_related('invoice_address', 'customer') ).select_related('invoice_address', 'customer')
if form_data.get('items'):
qs = qs.filter(
Exists(OrderPosition.all.filter(
order=OuterRef('pk'),
item__in=form_data["items"]
))
)
qs = self._date_filter(qs, form_data, rel='') qs = self._date_filter(qs, form_data, rel='')
if form_data['paid_only']: if form_data['paid_only']:
@@ -384,7 +364,7 @@ class OrderListExporter(MultiSheetListExporter):
order.invoice_address.city, order.invoice_address.city,
order.invoice_address.country if order.invoice_address.country else order.invoice_address.country if order.invoice_address.country else
order.invoice_address.country_old, order.invoice_address.country_old,
order.invoice_address.state_for_address, order.invoice_address.state,
order.invoice_address.custom_field, order.invoice_address.custom_field,
order.invoice_address.vat_id, order.invoice_address.vat_id,
] ]
@@ -460,14 +440,6 @@ class OrderListExporter(MultiSheetListExporter):
if form_data['paid_only']: if form_data['paid_only']:
qs = qs.filter(order__status=Order.STATUS_PAID, canceled=False) qs = qs.filter(order__status=Order.STATUS_PAID, canceled=False)
if form_data.get('items'):
qs = qs.filter(
Exists(OrderPosition.all.filter(
order=OuterRef('order'),
item__in=form_data["items"]
))
)
qs = self._date_filter(qs, form_data, rel='order__') qs = self._date_filter(qs, form_data, rel='order__')
return qs return qs
@@ -543,7 +515,7 @@ class OrderListExporter(MultiSheetListExporter):
order.invoice_address.city, order.invoice_address.city,
order.invoice_address.country if order.invoice_address.country else order.invoice_address.country if order.invoice_address.country else
order.invoice_address.country_old, order.invoice_address.country_old,
order.invoice_address.state_for_address, order.invoice_address.state,
order.invoice_address.vat_id, order.invoice_address.vat_id,
] ]
except InvoiceAddress.DoesNotExist: except InvoiceAddress.DoesNotExist:
@@ -563,11 +535,6 @@ class OrderListExporter(MultiSheetListExporter):
if form_data['paid_only']: if form_data['paid_only']:
qs = qs.filter(order__status=Order.STATUS_PAID, canceled=False) qs = qs.filter(order__status=Order.STATUS_PAID, canceled=False)
if form_data.get('items'):
qs = qs.filter(
item__in=form_data["items"]
)
qs = self._date_filter(qs, form_data, rel='order__') qs = self._date_filter(qs, form_data, rel='order__')
return qs return qs
@@ -650,7 +617,6 @@ class OrderListExporter(MultiSheetListExporter):
_('Country'), _('Country'),
pgettext('address', 'State'), pgettext('address', 'State'),
_('Voucher'), _('Voucher'),
_('Voucher budget usage'),
_('Pseudonymization ID'), _('Pseudonymization ID'),
_('Ticket secret'), _('Ticket secret'),
_('Seat ID'), _('Seat ID'),
@@ -766,9 +732,8 @@ class OrderListExporter(MultiSheetListExporter):
op.zipcode or '', op.zipcode or '',
op.city or '', op.city or '',
op.country if op.country else '', op.country if op.country else '',
op.state_for_address or '', op.state or '',
op.voucher.code if op.voucher else '', op.voucher.code if op.voucher else '',
op.voucher_budget_use if op.voucher_budget_use else '',
op.pseudonymization_id, op.pseudonymization_id,
op.secret, op.secret,
] ]
@@ -832,7 +797,7 @@ class OrderListExporter(MultiSheetListExporter):
order.invoice_address.city, order.invoice_address.city,
order.invoice_address.country if order.invoice_address.country else order.invoice_address.country if order.invoice_address.country else
order.invoice_address.country_old, order.invoice_address.country_old,
order.invoice_address.state_for_address, order.invoice_address.state,
order.invoice_address.vat_id, order.invoice_address.vat_id,
] ]
except InvoiceAddress.DoesNotExist: except InvoiceAddress.DoesNotExist:
@@ -1235,14 +1200,11 @@ class QuotaListExporter(ListExporter):
class GiftcardTransactionListExporter(OrganizerLevelExportMixin, ListExporter): class GiftcardTransactionListExporter(OrganizerLevelExportMixin, ListExporter):
identifier = 'giftcardtransactionlist' identifier = 'giftcardtransactionlist'
verbose_name = gettext_lazy('Gift card transactions') verbose_name = gettext_lazy('Gift card transactions')
organizer_required_permission = 'can_manage_gift_cards'
category = pgettext_lazy('export_category', 'Gift cards') category = pgettext_lazy('export_category', 'Gift cards')
description = gettext_lazy('Download a spreadsheet of all gift card transactions.') description = gettext_lazy('Download a spreadsheet of all gift card transactions.')
repeatable_read = False repeatable_read = False
@classmethod
def get_required_organizer_permission(cls) -> str:
return 'organizer.giftcards:read'
@property @property
def additional_form_fields(self): def additional_form_fields(self):
d = [ d = [
@@ -1345,13 +1307,10 @@ class GiftcardRedemptionListExporter(ListExporter):
class GiftcardListExporter(OrganizerLevelExportMixin, ListExporter): class GiftcardListExporter(OrganizerLevelExportMixin, ListExporter):
identifier = 'giftcardlist' identifier = 'giftcardlist'
verbose_name = gettext_lazy('Gift cards') verbose_name = gettext_lazy('Gift cards')
organizer_required_permission = 'can_manage_gift_cards'
category = pgettext_lazy('export_category', 'Gift cards') category = pgettext_lazy('export_category', 'Gift cards')
description = gettext_lazy('Download a spreadsheet of all gift cards including their current value.') description = gettext_lazy('Download a spreadsheet of all gift cards including their current value.')
@classmethod
def get_required_organizer_permission(cls) -> str:
return 'organizer.giftcards:read'
@property @property
def additional_form_fields(self): def additional_form_fields(self):
return OrderedDict( return OrderedDict(

View File

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

View File

@@ -1417,7 +1417,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
self.instance.transmission_type = transmission_type.identifier self.instance.transmission_type = transmission_type.identifier
self.instance.transmission_info = transmission_type.form_data_to_transmission_info(data) self.instance.transmission_info = transmission_type.form_data_to_transmission_info(data)
elif transmission_type.is_exclusive(self.event, data.get("country"), data.get("is_business")): elif transmission_type.exclusive:
if transmission_type.is_available(self.event, data.get("country"), data.get("is_business")): if transmission_type.is_available(self.event, data.get("country"), data.get("is_business")):
raise ValidationError({ raise ValidationError({
"transmission_type": "The transmission type '%s' must be used for this country or address type." % ( "transmission_type": "The transmission type '%s' must be used for this country or address type." % (

View File

@@ -141,7 +141,7 @@ def get_babel_locale():
for locale in try_locales: for locale in try_locales:
if localedata.exists(locale): if localedata.exists(locale):
return localedata.normalize_locale(locale) return locale
return "en" return "en"

View File

@@ -36,11 +36,9 @@ class ItalianSdITransmissionType(TransmissionType):
identifier = "it_sdi" identifier = "it_sdi"
verbose_name = pgettext_lazy("italian_invoice", "Italian Exchange System (SdI)") verbose_name = pgettext_lazy("italian_invoice", "Italian Exchange System (SdI)")
public_name = pgettext_lazy("italian_invoice", "Exchange System (SdI)") public_name = pgettext_lazy("italian_invoice", "Exchange System (SdI)")
exclusive = True
enforce_transmission = True enforce_transmission = True
def is_exclusive(self, event, country: Country, is_business: bool) -> bool:
return str(country) == "IT"
def is_available(self, event, country: Country, is_business: bool): def is_available(self, event, country: Country, is_business: bool):
return str(country) == "IT" and super().is_available(event, country, is_business) return str(country) == "IT" and super().is_available(event, country, is_business)

View File

@@ -179,12 +179,6 @@ class PeppolTransmissionType(TransmissionType):
def is_available(self, event, country: Country, is_business: bool): def is_available(self, event, country: Country, is_business: bool):
return is_business and super().is_available(event, country, is_business) return is_business and super().is_available(event, country, is_business)
def is_exclusive(self, event, country: Country, is_business: bool) -> bool:
if is_business and str(country) == "BE" and event and event.settings.invoice_address_from_country == "BE":
# Peppol is required to be used for intra-Belgian B2B invoices
return True
return False
@property @property
def invoice_address_form_fields(self) -> dict: def invoice_address_form_fields(self) -> dict:
return { return {

View File

@@ -58,6 +58,15 @@ class TransmissionType:
""" """
return 100 return 100
@property
def exclusive(self) -> bool:
"""
If a transmission type is exclusive, no other type can be chosen if this type is
available. Use e.g. if a certain transmission type is legally required in a certain
jurisdiction.
"""
return False
@property @property
def enforce_transmission(self) -> bool: def enforce_transmission(self) -> bool:
""" """
@@ -73,15 +82,6 @@ class TransmissionType:
for provider, _ in providers for provider, _ in providers
) )
def is_exclusive(self, event, country: Country, is_business: bool) -> bool:
"""
If a transmission type is exclusive, no other type can be chosen if this type is
available. Use e.g. if a certain transmission type is legally required in a certain
jurisdiction. Event can be None in organizer-level contexts. Exclusiveness has no effect if
the type is not available.
"""
return False
def invoice_address_form_fields_required(self, country: Country, is_business: bool): def invoice_address_form_fields_required(self, country: Country, is_business: bool):
return set() return set()

View File

@@ -1,129 +0,0 @@
from django.db import migrations, models
from pretix.helpers.permission_migration import (
OLD_TO_NEW_EVENT_MIGRATION, OLD_TO_NEW_ORGANIZER_MIGRATION,
)
def migrate_teams_forward(apps, schema_editor):
Team = apps.get_model("pretixbase", "Team")
for team in Team.objects.iterator():
if all(getattr(team, k) for k in OLD_TO_NEW_EVENT_MIGRATION.keys() if k != "can_checkin_orders"):
team.all_event_permissions = True
team.limit_event_permissions = {}
else:
team.all_event_permissions = False
for k, v in OLD_TO_NEW_EVENT_MIGRATION.items():
if getattr(team, k):
team.limit_event_permissions.update({kk: True for kk in v})
if all(getattr(team, k) for k in OLD_TO_NEW_ORGANIZER_MIGRATION.keys()):
team.all_organizer_permissions = True
team.limit_organizer_permissions = {}
else:
team.all_organizer_permissions = False
for k, v in OLD_TO_NEW_ORGANIZER_MIGRATION.items():
if getattr(team, k):
team.limit_organizer_permissions.update({kk: True for kk in v})
team.save(update_fields=[
"all_event_permissions", "limit_event_permissions", "all_organizer_permissions", "limit_organizer_permissions"
])
def migrate_teams_backward(apps, schema_editor):
Team = apps.get_model("pretixbase", "Team")
for team in Team.objects.iterator():
for k, v in OLD_TO_NEW_EVENT_MIGRATION.items():
setattr(team, k, team.all_event_permissions or all(team.limit_event_permissions.get(kk) for kk in v))
for k, v in OLD_TO_NEW_ORGANIZER_MIGRATION.items():
setattr(team, k, team.all_organizer_permissions or all(team.limit_organizer_permissions.get(kk) for kk in v))
team.save()
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0296_invoice_invoice_from_state"),
]
operations = [
migrations.AddField(
model_name="team",
name="all_event_permissions",
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name="team",
name="all_organizer_permissions",
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name="team",
name="limit_event_permissions",
field=models.JSONField(default=dict),
),
migrations.AddField(
model_name="team",
name="limit_organizer_permissions",
field=models.JSONField(default=dict),
),
migrations.RunPython(
migrate_teams_forward,
migrate_teams_backward,
),
migrations.RemoveField(
model_name="team",
name="can_change_event_settings",
),
migrations.RemoveField(
model_name="team",
name="can_change_items",
),
migrations.RemoveField(
model_name="team",
name="can_change_orders",
),
migrations.RemoveField(
model_name="team",
name="can_change_organizer_settings",
),
migrations.RemoveField(
model_name="team",
name="can_change_teams",
),
migrations.RemoveField(
model_name="team",
name="can_change_vouchers",
),
migrations.RemoveField(
model_name="team",
name="can_checkin_orders",
),
migrations.RemoveField(
model_name="team",
name="can_create_events",
),
migrations.RemoveField(
model_name="team",
name="can_manage_customers",
),
migrations.RemoveField(
model_name="team",
name="can_manage_gift_cards",
),
migrations.RemoveField(
model_name="team",
name="can_manage_reusable_media",
),
migrations.RemoveField(
model_name="team",
name="can_view_orders",
),
migrations.RemoveField(
model_name="team",
name="can_view_vouchers",
),
]

View File

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

View File

@@ -349,7 +349,7 @@ class AttendeeProfile(models.Model):
def state_name(self): def state_name(self):
sd = pycountry.subdivisions.get(code='{}-{}'.format(self.country, self.state)) sd = pycountry.subdivisions.get(code='{}-{}'.format(self.country, self.state))
if sd: if sd:
return _(sd.name) return sd.name
return self.state return self.state
@property @property

View File

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

View File

@@ -1386,13 +1386,14 @@ class Event(EventMixin, LoggedModel):
from .auth import User from .auth import User
if permission: if permission:
qs = Team.objects.with_event_permission(permission) kwargs = {permission: True}
else: else:
qs = Team.objects.all() kwargs = {}
team_with_perm = qs.filter( team_with_perm = Team.objects.filter(
members__pk=OuterRef('pk'), members__pk=OuterRef('pk'),
organizer=self.organizer, organizer=self.organizer,
**kwargs
).filter( ).filter(
Q(all_events=True) | Q(limit_events__pk=self.pk) Q(all_events=True) | Q(limit_events__pk=self.pk)
) )

View File

@@ -594,11 +594,10 @@ class Item(LoggedModel):
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
verbose_name=_("Only show after sellout of"), verbose_name=_("Only show after sellout of"),
help_text=_("If you select a product here, this product will only be shown when that product is " help_text=_("If you select a product here, this product will only be shown when that product is "
"no longer available. This will happen either because the other product has sold out or because " "sold out. If combined with the option to hide sold-out products, this allows you to "
"the time is outside of the sales window for the other product. If combined with the option " "swap out products for more expensive ones once the cheaper option is sold out. There might "
"to hide sold-out products, this allows you to swap out products for more expensive ones once " "be a short period in which both products are visible while all tickets of the referenced "
"the cheaper option is sold out. There might be a short period in which both products are visible " "product are reserved, but not yet sold.")
"while all tickets of the referenced product are reserved, but not yet sold.")
) )
hidden_if_item_available_mode = models.CharField( hidden_if_item_available_mode = models.CharField(
choices=UNAVAIL_MODES, choices=UNAVAIL_MODES,

View File

@@ -1675,7 +1675,7 @@ class AbstractPosition(RoundingCorrectionMixin, models.Model):
def state_name(self): def state_name(self):
sd = pycountry.subdivisions.get(code='{}-{}'.format(self.country, self.state)) sd = pycountry.subdivisions.get(code='{}-{}'.format(self.country, self.state))
if sd: if sd:
return _(sd.name) return sd.name
return self.state return self.state
@property @property
@@ -3480,7 +3480,7 @@ class InvoiceAddress(models.Model):
def state_name(self): def state_name(self):
sd = pycountry.subdivisions.get(code='{}-{}'.format(self.country, self.state)) sd = pycountry.subdivisions.get(code='{}-{}'.format(self.country, self.state))
if sd: if sd:
return _(sd.name) return sd.name
return self.state return self.state
@property @property

View File

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

View File

@@ -159,7 +159,6 @@ class WaitingListEntry(LoggedModel):
if availability[1] is None or availability[1] < 1: if availability[1] is None or availability[1] < 1:
raise WaitingListException(_('This product is currently not available.')) raise WaitingListException(_('This product is currently not available.'))
event = self.event
ev = self.subevent or self.event ev = self.subevent or self.event
if ev.seat_category_mappings.filter(product=self.item).exists(): if ev.seat_category_mappings.filter(product=self.item).exists():
# Generally, we advertise the waiting list to be based on quotas only. This makes it dangerous # Generally, we advertise the waiting list to be based on quotas only. This makes it dangerous
@@ -192,7 +191,6 @@ class WaitingListEntry(LoggedModel):
with transaction.atomic(): with transaction.atomic():
locked_wle = WaitingListEntry.objects.select_for_update(of=OF_SELF).get(pk=self.pk) locked_wle = WaitingListEntry.objects.select_for_update(of=OF_SELF).get(pk=self.pk)
locked_wle.event = event
if locked_wle.voucher: if locked_wle.voucher:
raise WaitingListException(_('A voucher has already been sent to this person.')) raise WaitingListException(_('A voucher has already been sent to this person.'))
e = locked_wle.email e = locked_wle.email
@@ -229,7 +227,6 @@ class WaitingListEntry(LoggedModel):
locked_wle.save() locked_wle.save()
self.refresh_from_db() self.refresh_from_db()
self.event = event
with language(self.locale, self.event.settings.region): with language(self.locale, self.event.settings.region):
self.send_mail( self.send_mail(

View File

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

View File

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

View File

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

View File

@@ -521,20 +521,9 @@ def invoice_pdf_task(invoice: int):
def invoice_qualified(order: Order): def invoice_qualified(order: Order):
if order.total == Decimal('0.00'): if order.total == Decimal('0.00') or order.require_approval or \
order.sales_channel.identifier not in order.event.settings.get('invoice_generate_sales_channels'):
return False return False
if order.require_approval:
return False
if order.sales_channel.identifier not in order.event.settings.invoice_generate_sales_channels:
return False
if order.status in (Order.STATUS_CANCELED, Order.STATUS_EXPIRED):
return False
if order.event.settings.invoice_generate_only_business:
try:
ia = order.invoice_address
return ia.is_business
except InvoiceAddress.DoesNotExist:
return False
return True return True

View File

@@ -112,8 +112,7 @@ def dictsum(*dicts) -> dict:
def order_overview( def order_overview(
event: Event, subevent: SubEvent=None, date_filter='', date_from=None, date_until=None, fees=False, event: Event, subevent: SubEvent=None, date_filter='', date_from=None, date_until=None, fees=False,
admission_only=False, base_qs=None, base_fees_qs=None, subevent_date_from=None, subevent_date_until=None, admission_only=False, base_qs=None, base_fees_qs=None, subevent_date_from=None, subevent_date_until=None
skip_empty_lines=False,
) -> Tuple[List[Tuple[ItemCategory, List[Item]]], Dict[str, Tuple[Decimal, Decimal]]]: ) -> Tuple[List[Tuple[ItemCategory, List[Item]]], Dict[str, Tuple[Decimal, Decimal]]]:
items = event.items.all().select_related( items = event.items.all().select_related(
'category', # for re-grouping 'category', # for re-grouping
@@ -206,21 +205,13 @@ def order_overview(
for l in states.keys(): for l in states.keys():
var.num[l] = num[l].get((item.id, variid), (0, 0, 0)) var.num[l] = num[l].get((item.id, variid), (0, 0, 0))
var.num['total'] = num['total'].get((item.id, variid), (0, 0, 0)) var.num['total'] = num['total'].get((item.id, variid), (0, 0, 0))
var._skip = all(v[0] == 0 for v in var.num.values())
for l in states.keys(): for l in states.keys():
item.num[l] = tuplesum(var.num[l] for var in item.all_variations) item.num[l] = tuplesum(var.num[l] for var in item.all_variations)
item.num['total'] = tuplesum(var.num['total'] for var in item.all_variations) item.num['total'] = tuplesum(var.num['total'] for var in item.all_variations)
if skip_empty_lines:
item.all_variations = [v for v in item.all_variations if not v._skip]
item._skip = not item.all_variations
else: else:
for l in states.keys(): for l in states.keys():
item.num[l] = num[l].get((item.id, None), (0, 0, 0)) item.num[l] = num[l].get((item.id, None), (0, 0, 0))
item.num['total'] = num['total'].get((item.id, None), (0, 0, 0)) item.num['total'] = num['total'].get((item.id, None), (0, 0, 0))
item._skip = all(v[0] == 0 for v in item.num.values())
if skip_empty_lines:
items = [i for i in items if not i._skip]
nonecat = ItemCategory(name=_('Uncategorized')) nonecat = ItemCategory(name=_('Uncategorized'))
# Regroup those by category # Regroup those by category

View File

@@ -76,7 +76,7 @@ from pretix.base.validators import multimail_validate
from pretix.control.forms import ( from pretix.control.forms import (
ExtFileField, FontSelect, MultipleLanguagesWidget, SingleLanguageWidget, ExtFileField, FontSelect, MultipleLanguagesWidget, SingleLanguageWidget,
) )
from pretix.helpers.countries import CachedCountries, pycountry_add from pretix.helpers.countries import CachedCountries
ROUNDING_MODES = ( ROUNDING_MODES = (
('line', _('Compute taxes for every line individually')), ('line', _('Compute taxes for every line individually')),
@@ -344,7 +344,6 @@ DEFAULTS = {
'type': bool, 'type': bool,
'form_class': forms.BooleanField, 'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField, 'serializer_class': serializers.BooleanField,
'write_permission': 'event.settings.tax:write',
'form_kwargs': dict( 'form_kwargs': dict(
label=_("Show net prices instead of gross prices in the product list"), label=_("Show net prices instead of gross prices in the product list"),
help_text=_("Independent of your choice, the cart will show gross prices as this is the price that needs to be " help_text=_("Independent of your choice, the cart will show gross prices as this is the price that needs to be "
@@ -492,7 +491,6 @@ DEFAULTS = {
'type': str, 'type': str,
'form_class': forms.ChoiceField, 'form_class': forms.ChoiceField,
'serializer_class': serializers.ChoiceField, 'serializer_class': serializers.ChoiceField,
'write_permission': 'event.settings.tax:write',
'form_kwargs': dict( 'form_kwargs': dict(
label=_("Rounding of taxes"), label=_("Rounding of taxes"),
widget=forms.RadioSelect, widget=forms.RadioSelect,
@@ -512,17 +510,15 @@ DEFAULTS = {
'type': bool, 'type': bool,
'form_class': forms.BooleanField, 'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField, 'serializer_class': serializers.BooleanField,
'write_permission': 'event.settings.invoicing:write',
'form_kwargs': dict( 'form_kwargs': dict(
label=_("Ask for invoice address"), label=_("Ask for invoice address"),
), )
}, },
'invoice_address_not_asked_free': { 'invoice_address_not_asked_free': {
'default': 'False', 'default': 'False',
'type': bool, 'type': bool,
'form_class': forms.BooleanField, 'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField, 'serializer_class': serializers.BooleanField,
'write_permission': 'event.settings.invoicing:write',
'form_kwargs': dict( 'form_kwargs': dict(
label=_('Do not ask for invoice address if an order is free'), label=_('Do not ask for invoice address if an order is free'),
) )
@@ -532,7 +528,6 @@ DEFAULTS = {
'type': bool, 'type': bool,
'form_class': forms.BooleanField, 'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField, 'serializer_class': serializers.BooleanField,
'write_permission': 'event.settings.invoicing:write',
'form_kwargs': dict( 'form_kwargs': dict(
label=_("Require customer name"), label=_("Require customer name"),
) )
@@ -542,7 +537,6 @@ DEFAULTS = {
'type': bool, 'type': bool,
'form_class': forms.BooleanField, 'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField, 'serializer_class': serializers.BooleanField,
'write_permission': 'event.settings.invoicing:write',
'form_kwargs': dict( 'form_kwargs': dict(
label=_("Show attendee names on invoices"), label=_("Show attendee names on invoices"),
) )
@@ -552,7 +546,6 @@ DEFAULTS = {
'type': bool, 'type': bool,
'form_class': forms.BooleanField, 'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField, 'serializer_class': serializers.BooleanField,
'write_permission': 'event.settings.invoicing:write',
'form_kwargs': dict( 'form_kwargs': dict(
label=_("Show event location on invoices"), label=_("Show event location on invoices"),
help_text=_("The event location will be shown below the list of products if it is the same for all " help_text=_("The event location will be shown below the list of products if it is the same for all "
@@ -564,7 +557,6 @@ DEFAULTS = {
'type': str, 'type': str,
'form_class': forms.ChoiceField, 'form_class': forms.ChoiceField,
'serializer_class': serializers.ChoiceField, 'serializer_class': serializers.ChoiceField,
'write_permission': 'event.settings.invoicing:write',
'form_kwargs': dict( 'form_kwargs': dict(
label=_("Show exchange rates"), label=_("Show exchange rates"),
widget=forms.RadioSelect, widget=forms.RadioSelect,
@@ -588,7 +580,6 @@ DEFAULTS = {
'default': 'False', 'default': 'False',
'form_class': forms.BooleanField, 'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField, 'serializer_class': serializers.BooleanField,
'write_permission': 'event.settings.invoicing:write',
'type': bool, 'type': bool,
'form_kwargs': dict( 'form_kwargs': dict(
label=_("Require invoice address"), label=_("Require invoice address"),
@@ -599,7 +590,6 @@ DEFAULTS = {
'default': 'False', 'default': 'False',
'form_class': forms.BooleanField, 'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField, 'serializer_class': serializers.BooleanField,
'write_permission': 'event.settings.invoicing:write',
'type': bool, 'type': bool,
'form_kwargs': dict( 'form_kwargs': dict(
label=_("Require a business address"), label=_("Require a business address"),
@@ -612,7 +602,6 @@ DEFAULTS = {
'type': bool, 'type': bool,
'form_class': forms.BooleanField, 'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField, 'serializer_class': serializers.BooleanField,
'write_permission': 'event.settings.invoicing:write',
'form_kwargs': dict( 'form_kwargs': dict(
label=_("Ask for beneficiary"), label=_("Ask for beneficiary"),
widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_invoice_address_asked'}), widget=forms.CheckboxInput(attrs={'data-checkbox-dependency': '#id_invoice_address_asked'}),
@@ -623,7 +612,6 @@ DEFAULTS = {
'type': LazyI18nString, 'type': LazyI18nString,
'form_class': I18nFormField, 'form_class': I18nFormField,
'serializer_class': I18nField, 'serializer_class': I18nField,
'write_permission': 'event.settings.invoicing:write',
'form_kwargs': dict( 'form_kwargs': dict(
label=_("Custom recipient field label"), label=_("Custom recipient field label"),
widget=I18nTextInput, widget=I18nTextInput,
@@ -639,7 +627,6 @@ DEFAULTS = {
'type': LazyI18nString, 'type': LazyI18nString,
'form_class': I18nFormField, 'form_class': I18nFormField,
'serializer_class': I18nField, 'serializer_class': I18nField,
'write_permission': 'event.settings.invoicing:write',
'form_kwargs': dict( 'form_kwargs': dict(
label=_("Custom recipient field help text"), label=_("Custom recipient field help text"),
widget=I18nTextInput, widget=I18nTextInput,
@@ -652,7 +639,6 @@ DEFAULTS = {
'type': bool, 'type': bool,
'form_class': forms.BooleanField, 'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField, 'serializer_class': serializers.BooleanField,
'write_permission': 'event.settings.invoicing:write',
'form_kwargs': dict( 'form_kwargs': dict(
label=_("Ask for VAT ID"), label=_("Ask for VAT ID"),
help_text=format_lazy( help_text=format_lazy(
@@ -668,7 +654,6 @@ DEFAULTS = {
'type': list, 'type': list,
'form_class': forms.MultipleChoiceField, 'form_class': forms.MultipleChoiceField,
'serializer_class': serializers.MultipleChoiceField, 'serializer_class': serializers.MultipleChoiceField,
'write_permission': 'event.settings.invoicing:write',
'serializer_kwargs': dict( 'serializer_kwargs': dict(
choices=lazy( choices=lazy(
lambda *args: sorted([(cc, gettext(Country(cc).name)) for cc in VAT_ID_COUNTRIES], key=lambda c: c[1]), lambda *args: sorted([(cc, gettext(Country(cc).name)) for cc in VAT_ID_COUNTRIES], key=lambda c: c[1]),
@@ -696,7 +681,6 @@ DEFAULTS = {
'type': LazyI18nString, 'type': LazyI18nString,
'form_class': I18nFormField, 'form_class': I18nFormField,
'serializer_class': I18nField, 'serializer_class': I18nField,
'write_permission': 'event.settings.invoicing:write',
'form_kwargs': dict( 'form_kwargs': dict(
label=_("Invoice address explanation"), label=_("Invoice address explanation"),
widget=I18nMarkdownTextarea, widget=I18nMarkdownTextarea,
@@ -709,7 +693,6 @@ DEFAULTS = {
'type': bool, 'type': bool,
'form_class': forms.BooleanField, 'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField, 'serializer_class': serializers.BooleanField,
'write_permission': 'event.settings.invoicing:write',
'form_kwargs': dict( 'form_kwargs': dict(
label=_("Show paid amount on partially paid invoices"), label=_("Show paid amount on partially paid invoices"),
help_text=_("If an invoice has already been paid partially, this option will add the paid and pending " help_text=_("If an invoice has already been paid partially, this option will add the paid and pending "
@@ -721,7 +704,6 @@ DEFAULTS = {
'type': bool, 'type': bool,
'form_class': forms.BooleanField, 'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField, 'serializer_class': serializers.BooleanField,
'write_permission': 'event.settings.invoicing:write',
'form_kwargs': dict( 'form_kwargs': dict(
label=_("Show free products on invoices"), label=_("Show free products on invoices"),
help_text=_("Note that invoices will never be generated for orders that contain only free " help_text=_("Note that invoices will never be generated for orders that contain only free "
@@ -733,7 +715,6 @@ DEFAULTS = {
'type': bool, 'type': bool,
'form_class': forms.BooleanField, 'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField, 'serializer_class': serializers.BooleanField,
'write_permission': 'event.settings.invoicing:write',
'form_kwargs': dict( 'form_kwargs': dict(
label=_("Show expiration date of order"), label=_("Show expiration date of order"),
help_text=_("The expiration date will not be shown if the invoice is generated after the order is paid."), help_text=_("The expiration date will not be shown if the invoice is generated after the order is paid."),
@@ -745,7 +726,6 @@ DEFAULTS = {
'form_class': forms.IntegerField, 'form_class': forms.IntegerField,
'serializer_class': serializers.IntegerField, 'serializer_class': serializers.IntegerField,
'serializer_kwargs': dict(), 'serializer_kwargs': dict(),
'write_permission': 'event.settings.invoicing:write',
'form_kwargs': dict( 'form_kwargs': dict(
label=_("Minimum length of invoice number after prefix"), label=_("Minimum length of invoice number after prefix"),
help_text=_("The part of your invoice number after your prefix will be filled up with leading zeros up to this length, e.g. INV-001 or INV-00001."), help_text=_("The part of your invoice number after your prefix will be filled up with leading zeros up to this length, e.g. INV-001 or INV-00001."),
@@ -759,7 +739,6 @@ DEFAULTS = {
'type': bool, 'type': bool,
'form_class': forms.BooleanField, 'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField, 'serializer_class': serializers.BooleanField,
'write_permission': 'event.settings.invoicing:write',
'form_kwargs': dict( 'form_kwargs': dict(
label=_("Generate invoices with consecutive numbers"), label=_("Generate invoices with consecutive numbers"),
help_text=_("If deactivated, the order code will be used in the invoice number."), help_text=_("If deactivated, the order code will be used in the invoice number."),
@@ -770,7 +749,6 @@ DEFAULTS = {
'type': str, 'type': str,
'form_class': forms.CharField, 'form_class': forms.CharField,
'serializer_class': serializers.CharField, 'serializer_class': serializers.CharField,
'write_permission': 'event.settings.invoicing:write',
'form_kwargs': dict( 'form_kwargs': dict(
label=_("Invoice number prefix"), label=_("Invoice number prefix"),
help_text=_("This will be prepended to invoice numbers. If you leave this field empty, your event slug will " help_text=_("This will be prepended to invoice numbers. If you leave this field empty, your event slug will "
@@ -798,7 +776,6 @@ DEFAULTS = {
'type': str, 'type': str,
'form_class': forms.CharField, 'form_class': forms.CharField,
'serializer_class': serializers.CharField, 'serializer_class': serializers.CharField,
'write_permission': 'event.settings.invoicing:write',
'form_kwargs': dict( 'form_kwargs': dict(
label=_("Invoice number prefix for cancellations"), label=_("Invoice number prefix for cancellations"),
help_text=_("This will be prepended to invoice numbers of cancellations. If you leave this field empty, " help_text=_("This will be prepended to invoice numbers of cancellations. If you leave this field empty, "
@@ -822,7 +799,6 @@ DEFAULTS = {
'type': bool, 'type': bool,
'form_class': forms.BooleanField, 'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField, 'serializer_class': serializers.BooleanField,
'write_permission': 'event.settings.invoicing:write',
'form_kwargs': dict( 'form_kwargs': dict(
label=_("Highlight order code to make it stand out visibly"), label=_("Highlight order code to make it stand out visibly"),
help_text=_("Only respected by some invoice renderers."), help_text=_("Only respected by some invoice renderers."),
@@ -834,7 +810,6 @@ DEFAULTS = {
'form_class': forms.ChoiceField, 'form_class': forms.ChoiceField,
'serializer_class': serializers.ChoiceField, 'serializer_class': serializers.ChoiceField,
'serializer_kwargs': lambda: dict(**invoice_font_kwargs()), 'serializer_kwargs': lambda: dict(**invoice_font_kwargs()),
'write_permission': 'event.settings.invoicing:write',
'form_kwargs': lambda: dict( 'form_kwargs': lambda: dict(
label=_('Font'), label=_('Font'),
help_text=_("Only respected by some invoice renderers."), help_text=_("Only respected by some invoice renderers."),
@@ -845,7 +820,6 @@ DEFAULTS = {
'invoice_renderer': { 'invoice_renderer': {
'default': 'classic', # default for new events is 'modern1' 'default': 'classic', # default for new events is 'modern1'
'type': str, 'type': str,
'write_permission': 'event.settings.invoicing:write',
}, },
'ticket_secret_generator': { 'ticket_secret_generator': {
'default': 'random', 'default': 'random',
@@ -922,7 +896,6 @@ DEFAULTS = {
'type': LazyI18nString, 'type': LazyI18nString,
'form_class': I18nFormField, 'form_class': I18nFormField,
'serializer_class': I18nField, 'serializer_class': I18nField,
'write_permission': 'event.settings.payment:write',
'form_kwargs': dict( 'form_kwargs': dict(
widget=I18nMarkdownTextarea, widget=I18nMarkdownTextarea,
widget_kwargs={'attrs': { widget_kwargs={'attrs': {
@@ -944,7 +917,6 @@ DEFAULTS = {
('minutes', _("in minutes")) ('minutes', _("in minutes"))
), ),
), ),
'write_permission': 'event.settings.payment:write',
'form_kwargs': dict( 'form_kwargs': dict(
label=_("Set payment term"), label=_("Set payment term"),
widget=forms.RadioSelect, widget=forms.RadioSelect,
@@ -962,7 +934,6 @@ DEFAULTS = {
'type': int, 'type': int,
'form_class': forms.IntegerField, 'form_class': forms.IntegerField,
'serializer_class': serializers.IntegerField, 'serializer_class': serializers.IntegerField,
'write_permission': 'event.settings.payment:write',
'form_kwargs': dict( 'form_kwargs': dict(
label=_('Payment term in days'), label=_('Payment term in days'),
widget=forms.NumberInput( widget=forms.NumberInput(
@@ -988,7 +959,6 @@ DEFAULTS = {
'type': bool, 'type': bool,
'form_class': forms.BooleanField, 'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField, 'serializer_class': serializers.BooleanField,
'write_permission': 'event.settings.payment:write',
'form_kwargs': dict( 'form_kwargs': dict(
label=_('Only end payment terms on weekdays'), label=_('Only end payment terms on weekdays'),
help_text=_("If this is activated and the payment term of any order ends on a Saturday or Sunday, it will be " help_text=_("If this is activated and the payment term of any order ends on a Saturday or Sunday, it will be "
@@ -1006,7 +976,6 @@ DEFAULTS = {
'type': int, 'type': int,
'form_class': forms.IntegerField, 'form_class': forms.IntegerField,
'serializer_class': serializers.IntegerField, 'serializer_class': serializers.IntegerField,
'write_permission': 'event.settings.payment:write',
'form_kwargs': dict( 'form_kwargs': dict(
label=_('Payment term in minutes'), label=_('Payment term in minutes'),
help_text=_("The number of minutes after placing an order the user has to pay to preserve their reservation. " help_text=_("The number of minutes after placing an order the user has to pay to preserve their reservation. "
@@ -1031,7 +1000,6 @@ DEFAULTS = {
'type': RelativeDateWrapper, 'type': RelativeDateWrapper,
'form_class': RelativeDateField, 'form_class': RelativeDateField,
'serializer_class': SerializerRelativeDateField, 'serializer_class': SerializerRelativeDateField,
'write_permission': 'event.settings.payment:write',
'form_kwargs': dict( 'form_kwargs': dict(
label=_('Last date of payments'), label=_('Last date of payments'),
help_text=_("The last date any payments are accepted. This has precedence over the terms " help_text=_("The last date any payments are accepted. This has precedence over the terms "
@@ -1044,7 +1012,6 @@ DEFAULTS = {
'type': bool, 'type': bool,
'form_class': forms.BooleanField, 'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField, 'serializer_class': serializers.BooleanField,
'write_permission': 'event.settings.payment:write',
'form_kwargs': dict( 'form_kwargs': dict(
label=_('Automatically expire unpaid orders'), label=_('Automatically expire unpaid orders'),
help_text=_("If checked, all unpaid orders will automatically go from 'pending' to 'expired' " help_text=_("If checked, all unpaid orders will automatically go from 'pending' to 'expired' "
@@ -1057,7 +1024,6 @@ DEFAULTS = {
'type': int, 'type': int,
'form_class': forms.IntegerField, 'form_class': forms.IntegerField,
'serializer_class': serializers.IntegerField, 'serializer_class': serializers.IntegerField,
'write_permission': 'event.settings.payment:write',
'form_kwargs': dict( 'form_kwargs': dict(
label=_('Expiration delay'), label=_('Expiration delay'),
help_text=_("The order will only actually expire this many days after the expiration date communicated " help_text=_("The order will only actually expire this many days after the expiration date communicated "
@@ -1080,7 +1046,6 @@ DEFAULTS = {
'type': bool, 'type': bool,
'form_class': forms.BooleanField, 'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField, 'serializer_class': serializers.BooleanField,
'write_permission': 'event.settings.payment:write',
'form_kwargs': dict( 'form_kwargs': dict(
label=_('Hide "payment pending" state on customer-facing pages'), label=_('Hide "payment pending" state on customer-facing pages'),
help_text=_("The payment instructions panel will still be shown to the primary customer, but no indication " help_text=_("The payment instructions panel will still be shown to the primary customer, but no indication "
@@ -1092,11 +1057,9 @@ DEFAULTS = {
'default': 'True', 'default': 'True',
'type': bool, 'type': bool,
'serializer_class': serializers.BooleanField, 'serializer_class': serializers.BooleanField,
'write_permission': 'event.settings.payment:write',
}, },
'payment_giftcard_public_name': { 'payment_giftcard_public_name': {
'default': LazyI18nString.from_gettext(gettext_noop('Gift card')), 'default': LazyI18nString.from_gettext(gettext_noop('Gift card')),
'write_permission': 'event.settings.payment:write',
'type': LazyI18nString 'type': LazyI18nString
}, },
'payment_giftcard_public_description': { 'payment_giftcard_public_description': {
@@ -1105,12 +1068,10 @@ DEFAULTS = {
'enough credit to pay for the full order, you will be shown this page again and you can either ' 'enough credit to pay for the full order, you will be shown this page again and you can either '
'redeem another gift card or select a different payment method for the difference.' 'redeem another gift card or select a different payment method for the difference.'
)), )),
'write_permission': 'event.settings.payment:write',
'type': LazyI18nString 'type': LazyI18nString
}, },
'payment_resellers__restrict_to_sales_channels': { 'payment_resellers__restrict_to_sales_channels': {
'default': ['resellers'], 'default': ['resellers'],
'write_permission': 'event.settings.payment:write',
'type': list 'type': list
}, },
'payment_term_accept_late': { 'payment_term_accept_late': {
@@ -1118,7 +1079,6 @@ DEFAULTS = {
'type': bool, 'type': bool,
'form_class': forms.BooleanField, 'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField, 'serializer_class': serializers.BooleanField,
'write_permission': 'event.settings.payment:write',
'form_kwargs': dict( 'form_kwargs': dict(
label=_('Accept late payments'), label=_('Accept late payments'),
help_text=_("Accept payments for orders even when they are in 'expired' state as long as enough " help_text=_("Accept payments for orders even when they are in 'expired' state as long as enough "
@@ -1148,7 +1108,6 @@ DEFAULTS = {
('none', _('Charge no taxes')), ('none', _('Charge no taxes')),
), ),
), ),
'write_permission': 'event.settings.payment:write',
'form_kwargs': dict( 'form_kwargs': dict(
label=_("Tax handling on payment fees"), label=_("Tax handling on payment fees"),
widget=forms.RadioSelect, widget=forms.RadioSelect,
@@ -1195,7 +1154,6 @@ DEFAULTS = {
('paid', _('Automatically on payment or when required by payment method')), ('paid', _('Automatically on payment or when required by payment method')),
), ),
), ),
'write_permission': 'event.settings.invoicing:write',
'form_kwargs': dict( 'form_kwargs': dict(
label=_("Generate invoices"), label=_("Generate invoices"),
widget=forms.RadioSelect, widget=forms.RadioSelect,
@@ -1224,7 +1182,6 @@ DEFAULTS = {
('invoice_date', _('Invoice date')), ('invoice_date', _('Invoice date')),
), ),
), ),
'write_permission': 'event.settings.invoicing:write',
'form_kwargs': dict( 'form_kwargs': dict(
label=_("Date of service"), label=_("Date of service"),
widget=forms.RadioSelect, widget=forms.RadioSelect,
@@ -1245,7 +1202,6 @@ DEFAULTS = {
'type': bool, 'type': bool,
'form_class': forms.BooleanField, 'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField, 'serializer_class': serializers.BooleanField,
'write_permission': 'event.settings.invoicing:write',
'form_kwargs': dict( 'form_kwargs': dict(
label=_("Automatically cancel and reissue invoice on address changes"), label=_("Automatically cancel and reissue invoice on address changes"),
help_text=_("If customers change their invoice address on an existing order, the invoice will " help_text=_("If customers change their invoice address on an existing order, the invoice will "
@@ -1258,7 +1214,6 @@ DEFAULTS = {
'type': bool, 'type': bool,
'form_class': forms.BooleanField, 'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField, 'serializer_class': serializers.BooleanField,
'write_permission': 'event.settings.invoicing:write',
'form_kwargs': dict( 'form_kwargs': dict(
label=_("Allow to update existing invoices"), label=_("Allow to update existing invoices"),
help_text=_("By default, invoices can never again be changed once they are issued. In most countries, we " help_text=_("By default, invoices can never again be changed once they are issued. In most countries, we "
@@ -1268,24 +1223,13 @@ DEFAULTS = {
}, },
'invoice_generate_sales_channels': { 'invoice_generate_sales_channels': {
'default': json.dumps(['web']), 'default': json.dumps(['web']),
'write_permission': 'event.settings.invoicing:write',
'type': list 'type': list
}, },
'invoice_generate_only_business': {
'default': 'False',
'type': bool,
'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField,
'form_kwargs': dict(
label=_("Only issue invoices to business customers"),
)
},
'invoice_address_from': { 'invoice_address_from': {
'default': '', 'default': '',
'type': str, 'type': str,
'form_class': forms.CharField, 'form_class': forms.CharField,
'serializer_class': serializers.CharField, 'serializer_class': serializers.CharField,
'write_permission': 'event.settings.invoicing:write',
'form_kwargs': dict( 'form_kwargs': dict(
label=_("Address line"), label=_("Address line"),
widget=forms.Textarea(attrs={ widget=forms.Textarea(attrs={
@@ -1301,7 +1245,6 @@ DEFAULTS = {
'type': str, 'type': str,
'form_class': forms.CharField, 'form_class': forms.CharField,
'serializer_class': serializers.CharField, 'serializer_class': serializers.CharField,
'write_permission': 'event.settings.invoicing:write',
'form_kwargs': dict( 'form_kwargs': dict(
max_length=190, max_length=190,
label=_("Company name"), label=_("Company name"),
@@ -1312,7 +1255,6 @@ DEFAULTS = {
'type': str, 'type': str,
'form_class': forms.CharField, 'form_class': forms.CharField,
'serializer_class': serializers.CharField, 'serializer_class': serializers.CharField,
'write_permission': 'event.settings.invoicing:write',
'form_kwargs': dict( 'form_kwargs': dict(
widget=forms.TextInput(attrs={ widget=forms.TextInput(attrs={
'placeholder': '12345' 'placeholder': '12345'
@@ -1326,7 +1268,6 @@ DEFAULTS = {
'type': str, 'type': str,
'form_class': forms.CharField, 'form_class': forms.CharField,
'serializer_class': serializers.CharField, 'serializer_class': serializers.CharField,
'write_permission': 'event.settings.invoicing:write',
'form_kwargs': dict( 'form_kwargs': dict(
widget=forms.TextInput(attrs={ widget=forms.TextInput(attrs={
'placeholder': _('Random City') 'placeholder': _('Random City')
@@ -1343,7 +1284,6 @@ DEFAULTS = {
'serializer_kwargs': { 'serializer_kwargs': {
'choices': [('', '')], 'choices': [('', '')],
}, },
'write_permission': 'event.settings.invoicing:write',
'form_kwargs': { 'form_kwargs': {
"label": pgettext_lazy('address', 'State'), "label": pgettext_lazy('address', 'State'),
'choices': [('', '')], 'choices': [('', '')],
@@ -1355,7 +1295,6 @@ DEFAULTS = {
'form_class': forms.ChoiceField, 'form_class': forms.ChoiceField,
'serializer_class': serializers.ChoiceField, 'serializer_class': serializers.ChoiceField,
'serializer_kwargs': lambda: dict(**country_choice_kwargs()), 'serializer_kwargs': lambda: dict(**country_choice_kwargs()),
'write_permission': 'event.settings.invoicing:write',
'form_kwargs': lambda: dict( 'form_kwargs': lambda: dict(
label=_('Country'), label=_('Country'),
widget=forms.Select(attrs={ widget=forms.Select(attrs={
@@ -1369,7 +1308,6 @@ DEFAULTS = {
'type': str, 'type': str,
'form_class': forms.CharField, 'form_class': forms.CharField,
'serializer_class': serializers.CharField, 'serializer_class': serializers.CharField,
'write_permission': 'event.settings.invoicing:write',
'form_kwargs': dict( 'form_kwargs': dict(
label=_("Domestic tax ID"), label=_("Domestic tax ID"),
help_text=_("e.g. tax number in Germany, ABN in Australia, …"), help_text=_("e.g. tax number in Germany, ABN in Australia, …"),
@@ -1381,7 +1319,6 @@ DEFAULTS = {
'type': str, 'type': str,
'form_class': forms.CharField, 'form_class': forms.CharField,
'serializer_class': serializers.CharField, 'serializer_class': serializers.CharField,
'write_permission': 'event.settings.invoicing:write',
'form_kwargs': dict( 'form_kwargs': dict(
label=_("EU VAT ID"), label=_("EU VAT ID"),
max_length=190, max_length=190,
@@ -1392,7 +1329,6 @@ DEFAULTS = {
'type': LazyI18nString, 'type': LazyI18nString,
'form_class': I18nFormField, 'form_class': I18nFormField,
'serializer_class': I18nField, 'serializer_class': I18nField,
'write_permission': 'event.settings.invoicing:write',
'form_kwargs': dict( 'form_kwargs': dict(
widget=I18nTextarea, widget=I18nTextarea,
widget_kwargs={'attrs': { widget_kwargs={'attrs': {
@@ -1410,7 +1346,6 @@ DEFAULTS = {
'type': LazyI18nString, 'type': LazyI18nString,
'form_class': I18nFormField, 'form_class': I18nFormField,
'serializer_class': I18nField, 'serializer_class': I18nField,
'write_permission': 'event.settings.invoicing:write',
'form_kwargs': dict( 'form_kwargs': dict(
widget=I18nTextarea, widget=I18nTextarea,
widget_kwargs={'attrs': { widget_kwargs={'attrs': {
@@ -1428,7 +1363,6 @@ DEFAULTS = {
'type': LazyI18nString, 'type': LazyI18nString,
'form_class': I18nFormField, 'form_class': I18nFormField,
'serializer_class': I18nField, 'serializer_class': I18nField,
'write_permission': 'event.settings.invoicing:write',
'form_kwargs': dict( 'form_kwargs': dict(
widget=I18nTextarea, widget=I18nTextarea,
widget_kwargs={'attrs': { widget_kwargs={'attrs': {
@@ -1443,7 +1377,6 @@ DEFAULTS = {
}, },
'invoice_language': { 'invoice_language': {
'default': '__user__', 'default': '__user__',
'write_permission': 'event.settings.invoicing:write',
'type': str 'type': str
}, },
'invoice_email_attachment': { 'invoice_email_attachment': {
@@ -1451,7 +1384,6 @@ DEFAULTS = {
'type': bool, 'type': bool,
'form_class': forms.BooleanField, 'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField, 'serializer_class': serializers.BooleanField,
'write_permission': 'event.settings.invoicing:write',
'form_kwargs': dict( 'form_kwargs': dict(
label=_("Attach invoices to emails"), label=_("Attach invoices to emails"),
help_text=_("If invoices are automatically generated for all orders, they will be attached to the order " help_text=_("If invoices are automatically generated for all orders, they will be attached to the order "
@@ -1465,7 +1397,6 @@ DEFAULTS = {
'type': str, 'type': str,
'form_class': forms.CharField, 'form_class': forms.CharField,
'serializer_class': serializers.CharField, 'serializer_class': serializers.CharField,
'write_permission': 'event.settings.invoicing:write',
'form_kwargs': dict( 'form_kwargs': dict(
label=_("Email address to receive a copy of each invoice"), label=_("Email address to receive a copy of each invoice"),
help_text=_("Each newly created invoice will be sent to this email address shortly after creation. You can " help_text=_("Each newly created invoice will be sent to this email address shortly after creation. You can "
@@ -3297,8 +3228,7 @@ Your {organizer} team""")) # noqa: W291
'image/png', 'image/jpeg', 'image/gif' 'image/png', 'image/jpeg', 'image/gif'
], ],
max_size=settings.FILE_UPLOAD_MAX_SIZE_IMAGE, max_size=settings.FILE_UPLOAD_MAX_SIZE_IMAGE,
), )
'write_permission': 'event.settings.invoicing:write',
}, },
'frontpage_text': { 'frontpage_text': {
'default': '', 'default': '',
@@ -3997,7 +3927,7 @@ COUNTRIES_WITH_STATE_IN_ADDRESS = {
'MX': (['State', 'Federal district', 'Federal entity'], 'short'), 'MX': (['State', 'Federal district', 'Federal entity'], 'short'),
'US': (['State', 'Outlying area', 'District'], 'short'), 'US': (['State', 'Outlying area', 'District'], 'short'),
'IT': (['Province', 'Free municipal consortium', 'Metropolitan city', 'Autonomous province', 'IT': (['Province', 'Free municipal consortium', 'Metropolitan city', 'Autonomous province',
'Decentralized regional entity'], 'short'), 'Free municipal consortium', 'Decentralized regional entity'], 'short'),
} }
COUNTRY_STATE_LABEL = { COUNTRY_STATE_LABEL = {
# Countries in which the "State" field should not be called "State" # Countries in which the "State" field should not be called "State"
@@ -4005,8 +3935,6 @@ COUNTRY_STATE_LABEL = {
'JP': pgettext_lazy('address', 'Prefecture'), 'JP': pgettext_lazy('address', 'Prefecture'),
'IT': pgettext_lazy('address', 'Province'), 'IT': pgettext_lazy('address', 'Province'),
} }
# Workaround for https://github.com/pretix/pretix/issues/5796
pycountry_add(pycountry.subdivisions, code="IT-AO", country_code="IT", name="Valle d'Aosta", parent="23", parent_code="IT-23", type="Province")
settings_hierarkey = Hierarkey(attribute_name='settings') settings_hierarkey = Hierarkey(attribute_name='settings')

View File

@@ -561,18 +561,6 @@ however for this signal, the ``sender`` **may also be None** to allow creating t
notification settings! notification settings!
""" """
register_event_permission_groups = GlobalSignal()
"""
This signal is sent out to get all known permissions. Receivers should return an
instance of pretix.base.permissions.PermissionGroup or a list of such instances.
"""
register_organizer_permission_groups = GlobalSignal()
"""
This signal is sent out to get all known permissions. Receivers should return an
instance of pretix.base.permissions.PermissionGroup or a list of such instances.
"""
notification = EventPluginSignal() notification = EventPluginSignal()
""" """
Arguments: ``logentry_id``, ``notification_type`` Arguments: ``logentry_id``, ``notification_type``
@@ -1110,9 +1098,6 @@ api_event_settings_fields = EventPluginSignal()
This signal is sent out to collect serializable settings fields for the API. You are expected to This signal is sent out to collect serializable settings fields for the API. You are expected to
return a dictionary mapping names of attributes in the settings store to DRF serializer field instances. return a dictionary mapping names of attributes in the settings store to DRF serializer field instances.
These are readable for all users with access to the events, therefore secrets stored in the settings store
should not be included!
As with all event-plugin signals, the ``sender`` keyword argument will contain the event. As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
""" """

View File

@@ -32,11 +32,7 @@ from pretix.base.models import ItemVariation
from pretix.base.reldate import RelativeDateWrapper from pretix.base.reldate import RelativeDateWrapper
from pretix.base.signals import timeline_events from pretix.base.signals import timeline_events
TimelineEvent = namedtuple( TimelineEvent = namedtuple('TimelineEvent', ('event', 'subevent', 'datetime', 'description', 'edit_url'))
'TimelineEvent',
('event', 'subevent', 'datetime', 'description', 'edit_url', 'edit_permission'),
defaults=(None, None, None, None, None, 'event.settings.general:write')
)
def timeline_for_event(event, subevent=None): def timeline_for_event(event, subevent=None):
@@ -50,7 +46,6 @@ def timeline_for_event(event, subevent=None):
'subevent': subevent.pk 'subevent': subevent.pk
} }
) )
ev_edit_permission = 'event.subevents:write'
else: else:
ev_edit_url = reverse( ev_edit_url = reverse(
'control:event.settings', kwargs={ 'control:event.settings', kwargs={
@@ -58,14 +53,12 @@ def timeline_for_event(event, subevent=None):
'organizer': event.organizer.slug 'organizer': event.organizer.slug
} }
) )
ev_edit_permission = 'event.settings.general:write'
tl.append(TimelineEvent( tl.append(TimelineEvent(
event=event, subevent=subevent, event=event, subevent=subevent,
datetime=ev.date_from, datetime=ev.date_from,
description=pgettext_lazy('timeline', 'Your event starts'), description=pgettext_lazy('timeline', 'Your event starts'),
edit_url=ev_edit_url + '#id_date_from_0', edit_url=ev_edit_url + '#id_date_from_0'
edit_permission=ev_edit_permission,
)) ))
if ev.date_to: if ev.date_to:
@@ -73,8 +66,7 @@ def timeline_for_event(event, subevent=None):
event=event, subevent=subevent, event=event, subevent=subevent,
datetime=ev.date_to, datetime=ev.date_to,
description=pgettext_lazy('timeline', 'Your event ends'), description=pgettext_lazy('timeline', 'Your event ends'),
edit_url=ev_edit_url + '#id_date_to_0', edit_url=ev_edit_url + '#id_date_to_0'
edit_permission=ev_edit_permission,
)) ))
if ev.date_admission: if ev.date_admission:
@@ -82,8 +74,7 @@ def timeline_for_event(event, subevent=None):
event=event, subevent=subevent, event=event, subevent=subevent,
datetime=ev.date_admission, datetime=ev.date_admission,
description=pgettext_lazy('timeline', 'Admissions for your event start'), description=pgettext_lazy('timeline', 'Admissions for your event start'),
edit_url=ev_edit_url + '#id_date_admission_0', edit_url=ev_edit_url + '#id_date_admission_0'
edit_permission=ev_edit_permission,
)) ))
if ev.presale_start: if ev.presale_start:
@@ -91,8 +82,7 @@ def timeline_for_event(event, subevent=None):
event=event, subevent=subevent, event=event, subevent=subevent,
datetime=ev.presale_start, datetime=ev.presale_start,
description=pgettext_lazy('timeline', 'Start of ticket sales'), description=pgettext_lazy('timeline', 'Start of ticket sales'),
edit_url=ev_edit_url + '#id_presale_start_0', edit_url=ev_edit_url + '#id_presale_start_0'
edit_permission=ev_edit_permission,
)) ))
tl.append(TimelineEvent( tl.append(TimelineEvent(
@@ -107,8 +97,7 @@ def timeline_for_event(event, subevent=None):
) if not ev.presale_end else ( ) if not ev.presale_end else (
pgettext_lazy('timeline', 'End of ticket sales') pgettext_lazy('timeline', 'End of ticket sales')
), ),
edit_url=ev_edit_url + '#id_presale_end_0', edit_url=ev_edit_url + '#id_presale_end_0'
edit_permission=ev_edit_permission,
)) ))
rd = event.settings.get('last_order_modification_date', as_type=RelativeDateWrapper) rd = event.settings.get('last_order_modification_date', as_type=RelativeDateWrapper)
@@ -117,8 +106,7 @@ def timeline_for_event(event, subevent=None):
event=event, subevent=subevent, event=event, subevent=subevent,
datetime=rd.datetime(ev), datetime=rd.datetime(ev),
description=pgettext_lazy('timeline', 'Customers can no longer modify their order information'), description=pgettext_lazy('timeline', 'Customers can no longer modify their order information'),
edit_url=ev_edit_url + '#id_settings-last_order_modification_date_0_0', edit_url=ev_edit_url + '#id_settings-last_order_modification_date_0_0'
edit_permission='event.settings.general:write',
)) ))
rd = event.settings.get('payment_term_last', as_type=RelativeDateWrapper) rd = event.settings.get('payment_term_last', as_type=RelativeDateWrapper)
@@ -134,8 +122,7 @@ def timeline_for_event(event, subevent=None):
edit_url=reverse('control:event.settings.payment', kwargs={ edit_url=reverse('control:event.settings.payment', kwargs={
'event': event.slug, 'event': event.slug,
'organizer': event.organizer.slug 'organizer': event.organizer.slug
}), })
edit_permission='event.settings.payment:write',
)) ))
rd = event.settings.get('ticket_download_date', as_type=RelativeDateWrapper) rd = event.settings.get('ticket_download_date', as_type=RelativeDateWrapper)
@@ -147,8 +134,7 @@ def timeline_for_event(event, subevent=None):
edit_url=reverse('control:event.settings.tickets', kwargs={ edit_url=reverse('control:event.settings.tickets', kwargs={
'event': event.slug, 'event': event.slug,
'organizer': event.organizer.slug 'organizer': event.organizer.slug
}), })
edit_permission='event.settings.general:write',
)) ))
rd = event.settings.get('cancel_allow_user_until', as_type=RelativeDateWrapper) rd = event.settings.get('cancel_allow_user_until', as_type=RelativeDateWrapper)
@@ -160,8 +146,7 @@ def timeline_for_event(event, subevent=None):
edit_url=reverse('control:event.settings.cancel', kwargs={ edit_url=reverse('control:event.settings.cancel', kwargs={
'event': event.slug, 'event': event.slug,
'organizer': event.organizer.slug 'organizer': event.organizer.slug
}), })
edit_permission='event.settings.general:write',
)) ))
rd = event.settings.get('cancel_allow_user_paid_until', as_type=RelativeDateWrapper) rd = event.settings.get('cancel_allow_user_paid_until', as_type=RelativeDateWrapper)
@@ -173,8 +158,7 @@ def timeline_for_event(event, subevent=None):
edit_url=reverse('control:event.settings.cancel', kwargs={ edit_url=reverse('control:event.settings.cancel', kwargs={
'event': event.slug, 'event': event.slug,
'organizer': event.organizer.slug 'organizer': event.organizer.slug
}), })
edit_permission='event.settings.general:write',
)) ))
rd = event.settings.get('change_allow_user_until', as_type=RelativeDateWrapper) rd = event.settings.get('change_allow_user_until', as_type=RelativeDateWrapper)
@@ -186,8 +170,7 @@ def timeline_for_event(event, subevent=None):
edit_url=reverse('control:event.settings.cancel', kwargs={ edit_url=reverse('control:event.settings.cancel', kwargs={
'event': event.slug, 'event': event.slug,
'organizer': event.organizer.slug 'organizer': event.organizer.slug
}), })
edit_permission='event.settings.general:write',
)) ))
rd = event.settings.get('waiting_list_auto_disable', as_type=RelativeDateWrapper) rd = event.settings.get('waiting_list_auto_disable', as_type=RelativeDateWrapper)
@@ -199,8 +182,7 @@ def timeline_for_event(event, subevent=None):
edit_url=reverse('control:event.settings', kwargs={ edit_url=reverse('control:event.settings', kwargs={
'event': event.slug, 'event': event.slug,
'organizer': event.organizer.slug 'organizer': event.organizer.slug
}) + '#waiting-list-open', }) + '#waiting-list-open'
edit_permission='event.settings.general:write',
)) ))
if not event.has_subevents: if not event.has_subevents:
@@ -214,8 +196,7 @@ def timeline_for_event(event, subevent=None):
edit_url=reverse('control:event.settings.mail', kwargs={ edit_url=reverse('control:event.settings.mail', kwargs={
'event': event.slug, 'event': event.slug,
'organizer': event.organizer.slug 'organizer': event.organizer.slug
}), })
edit_permission='event.settings.general:write',
)) ))
if subevent: if subevent:
@@ -229,8 +210,7 @@ def timeline_for_event(event, subevent=None):
'event': event.slug, 'event': event.slug,
'organizer': event.organizer.slug, 'organizer': event.organizer.slug,
'subevent': subevent.pk, 'subevent': subevent.pk,
}), })
edit_permission='event.subevents:write',
)) ))
if sei.available_until: if sei.available_until:
tl.append(TimelineEvent( tl.append(TimelineEvent(
@@ -241,8 +221,7 @@ def timeline_for_event(event, subevent=None):
'event': event.slug, 'event': event.slug,
'organizer': event.organizer.slug, 'organizer': event.organizer.slug,
'subevent': subevent.pk, 'subevent': subevent.pk,
}), })
edit_permission='event.subevents:write',
)) ))
for sei in subevent.var_overrides.values(): for sei in subevent.var_overrides.values():
if sei.available_from: if sei.available_from:
@@ -255,8 +234,7 @@ def timeline_for_event(event, subevent=None):
'event': event.slug, 'event': event.slug,
'organizer': event.organizer.slug, 'organizer': event.organizer.slug,
'subevent': subevent.pk, 'subevent': subevent.pk,
}), })
edit_permission='event.subevents:write',
)) ))
if sei.available_until: if sei.available_until:
tl.append(TimelineEvent( tl.append(TimelineEvent(
@@ -268,8 +246,7 @@ def timeline_for_event(event, subevent=None):
'event': event.slug, 'event': event.slug,
'organizer': event.organizer.slug, 'organizer': event.organizer.slug,
'subevent': subevent.pk, 'subevent': subevent.pk,
}), })
edit_permission='event.subevents:write',
)) ))
for d in event.discounts.filter(Q(available_from__isnull=False) | Q(available_until__isnull=False)): for d in event.discounts.filter(Q(available_from__isnull=False) | Q(available_until__isnull=False)):
@@ -282,8 +259,7 @@ def timeline_for_event(event, subevent=None):
'event': event.slug, 'event': event.slug,
'organizer': event.organizer.slug, 'organizer': event.organizer.slug,
'discount': d.pk, 'discount': d.pk,
}), })
edit_permission='event.items:write',
)) ))
if d.available_until: if d.available_until:
tl.append(TimelineEvent( tl.append(TimelineEvent(
@@ -294,8 +270,7 @@ def timeline_for_event(event, subevent=None):
'event': event.slug, 'event': event.slug,
'organizer': event.organizer.slug, 'organizer': event.organizer.slug,
'discount': d.pk, 'discount': d.pk,
}), })
edit_permission='event.items:write',
)) ))
for p in event.items.filter(Q(available_from__isnull=False) | Q(available_until__isnull=False)): for p in event.items.filter(Q(available_from__isnull=False) | Q(available_until__isnull=False)):
@@ -308,8 +283,7 @@ def timeline_for_event(event, subevent=None):
'event': event.slug, 'event': event.slug,
'organizer': event.organizer.slug, 'organizer': event.organizer.slug,
'item': p.pk, 'item': p.pk,
}) + '#id_available_from_0', }) + '#id_available_from_0'
edit_permission='event.items:write',
)) ))
if p.available_until: if p.available_until:
tl.append(TimelineEvent( tl.append(TimelineEvent(
@@ -320,8 +294,7 @@ def timeline_for_event(event, subevent=None):
'event': event.slug, 'event': event.slug,
'organizer': event.organizer.slug, 'organizer': event.organizer.slug,
'item': p.pk, 'item': p.pk,
}) + '#id_available_until_0', }) + '#id_available_until_0'
edit_permission='event.items:write',
)) ))
for v in ItemVariation.objects.filter( for v in ItemVariation.objects.filter(
@@ -340,8 +313,7 @@ def timeline_for_event(event, subevent=None):
'event': event.slug, 'event': event.slug,
'organizer': event.organizer.slug, 'organizer': event.organizer.slug,
'item': v.item.pk, 'item': v.item.pk,
}) + '#tab-0-3-open', }) + '#tab-0-3-open'
edit_permission='event.items:write',
)) ))
if v.available_until: if v.available_until:
tl.append(TimelineEvent( tl.append(TimelineEvent(
@@ -355,8 +327,7 @@ def timeline_for_event(event, subevent=None):
'event': event.slug, 'event': event.slug,
'organizer': event.organizer.slug, 'organizer': event.organizer.slug,
'item': v.item.pk, 'item': v.item.pk,
}) + '#tab-0-3-open', }) + '#tab-0-3-open'
edit_permission='event.items:write',
)) ))
pprovs = event.get_payment_providers() pprovs = event.get_payment_providers()
@@ -386,8 +357,7 @@ def timeline_for_event(event, subevent=None):
'event': event.slug, 'event': event.slug,
'organizer': event.organizer.slug, 'organizer': event.organizer.slug,
'provider': pprov.identifier, 'provider': pprov.identifier,
}), })
edit_permission='event.settings.payment:write',
)) ))
availability_date = pprov.settings.get('_availability_date', as_type=RelativeDateWrapper) availability_date = pprov.settings.get('_availability_date', as_type=RelativeDateWrapper)
if availability_date: if availability_date:
@@ -405,8 +375,7 @@ def timeline_for_event(event, subevent=None):
'event': event.slug, 'event': event.slug,
'organizer': event.organizer.slug, 'organizer': event.organizer.slug,
'provider': pprov.identifier, 'provider': pprov.identifier,
}), })
edit_permission='event.settings.payment:write',
)) ))
for recv, resp in timeline_events.send(sender=event, subevent=subevent): for recv, resp in timeline_events.send(sender=event, subevent=subevent):

View File

@@ -82,7 +82,7 @@ def _info(cc):
statelist = [s for s in pycountry.subdivisions.get(country_code=cc) if s.type in types] statelist = [s for s in pycountry.subdivisions.get(country_code=cc) if s.type in types]
return { return {
'data': [ 'data': [
{'name': gettext(s.name), 'code': s.code[3:]} {'name': s.name, 'code': s.code[3:]}
for s in sorted(statelist, key=lambda s: s.name) for s in sorted(statelist, key=lambda s: s.name)
], ],
**info, **info,
@@ -109,7 +109,7 @@ def address_form(request):
for t in get_transmission_types(): for t in get_transmission_types():
if t.is_available(event=event, country=country, is_business=is_business): if t.is_available(event=event, country=country, is_business=is_business):
result = {"name": str(t.public_name), "code": t.identifier} result = {"name": str(t.public_name), "code": t.identifier}
if t.is_exclusive(event=event, country=country, is_business=is_business): if t.exclusive:
info["transmission_types"] = [result] info["transmission_types"] = [result]
break break
else: else:

View File

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

View File

@@ -62,7 +62,6 @@ from pretix.base.forms import (
) )
from pretix.base.models import Event, Organizer, TaxRule, Team from pretix.base.models import Event, Organizer, TaxRule, Team
from pretix.base.models.event import EventFooterLink, EventMetaValue, SubEvent from pretix.base.models.event import EventFooterLink, EventMetaValue, SubEvent
from pretix.base.models.organizer import TeamQuerySet
from pretix.base.models.tax import TAX_CODE_LISTS from pretix.base.models.tax import TAX_CODE_LISTS
from pretix.base.reldate import RelativeDateField, RelativeDateTimeField from pretix.base.reldate import RelativeDateField, RelativeDateTimeField
from pretix.base.services.placeholders import FormPlaceholderMixin from pretix.base.services.placeholders import FormPlaceholderMixin
@@ -105,7 +104,7 @@ class EventWizardFoundationForm(forms.Form):
qs = Organizer.objects.all() qs = Organizer.objects.all()
if not self.user.has_active_staff_session(self.session.session_key): if not self.user.has_active_staff_session(self.session.session_key):
qs = qs.filter( qs = qs.filter(
id__in=self.user.teams.filter(TeamQuerySet.organizer_permission_q("organizer.events:create")).values_list('organizer', flat=True) id__in=self.user.teams.filter(can_create_events=True).values_list('organizer', flat=True)
) )
self.fields['organizer'] = forms.ModelChoiceField( self.fields['organizer'] = forms.ModelChoiceField(
label=_("Organizer"), label=_("Organizer"),
@@ -263,12 +262,8 @@ class EventWizardBasicsForm(I18nModelForm):
@staticmethod @staticmethod
def has_control_rights(user, organizer, session): def has_control_rights(user, organizer, session):
return user.teams.filter( return user.teams.filter(
TeamQuerySet.event_permission_q("event.items:write"), organizer=organizer, all_events=True, can_change_event_settings=True, can_change_items=True,
TeamQuerySet.event_permission_q("event.orders:write"), can_change_orders=True, can_change_vouchers=True
TeamQuerySet.event_permission_q("event.vouchers:write"),
TeamQuerySet.event_permission_q("event.settings.general:write"),
organizer=organizer,
all_events=True,
).exists() or user.has_active_staff_session(session.session_key) ).exists() or user.has_active_staff_session(session.session_key)
@@ -299,14 +294,9 @@ class EventWizardCopyForm(forms.Form):
return Event.objects.all() return Event.objects.all()
return Event.objects.filter( return Event.objects.filter(
Q(organizer_id__in=user.teams.filter( Q(organizer_id__in=user.teams.filter(
# TODO: review these! all_events=True, can_change_event_settings=True, can_change_items=True
# Restrict cross-organizer copying further than same-organizer copying?
TeamQuerySet.event_permission_q("event.settings.general:write"),
TeamQuerySet.event_permission_q("event.items:write"),
all_events=True,
).values_list('organizer', flat=True)) | Q(id__in=user.teams.filter( ).values_list('organizer', flat=True)) | Q(id__in=user.teams.filter(
TeamQuerySet.event_permission_q("event.settings.general:write"), can_change_event_settings=True, can_change_items=True
TeamQuerySet.event_permission_q("event.items:write"),
).values_list('limit_events__id', flat=True)) ).values_list('limit_events__id', flat=True))
) )
@@ -949,7 +939,6 @@ class InvoiceSettingsForm(EventSettingsValidationMixin, SettingsForm):
'invoice_show_payments', 'invoice_show_payments',
'invoice_reissue_after_modify', 'invoice_reissue_after_modify',
'invoice_generate', 'invoice_generate',
'invoice_generate_only_business',
'invoice_period', 'invoice_period',
'invoice_attendee_name', 'invoice_attendee_name',
'invoice_event_location', 'invoice_event_location',

View File

@@ -1110,7 +1110,7 @@ class OrderPaymentSearchFilterForm(forms.Form):
self.fields['organizer'].queryset = Organizer.objects.filter( self.fields['organizer'].queryset = Organizer.objects.filter(
pk__in=self.request.user.teams.values_list('organizer', flat=True) pk__in=self.request.user.teams.values_list('organizer', flat=True)
) )
self.fields['event'].queryset = self.request.user.get_events_with_permission('event.orders:read') self.fields['event'].queryset = self.request.user.get_events_with_permission('can_view_orders')
self.fields['provider'].choices += get_all_payment_providers() self.fields['provider'].choices += get_all_payment_providers()
@@ -1315,10 +1315,10 @@ class QuestionAnswerFilterForm(forms.Form):
if date_range is not None: if date_range is not None:
d_start, d_end = resolve_timeframe_to_datetime_start_inclusive_end_exclusive(now(), date_range, self.event.timezone) d_start, d_end = resolve_timeframe_to_datetime_start_inclusive_end_exclusive(now(), date_range, self.event.timezone)
if d_start: opqs = opqs.filter(
opqs = opqs.filter(subevent__date_from__gte=d_start) subevent__date_from__gte=d_start,
if d_end: subevent__date_from__lt=d_end
opqs = opqs.filter(subevent__date_from__lt=d_end) )
s = fdata.get("status", Order.STATUS_PENDING + Order.STATUS_PAID) s = fdata.get("status", Order.STATUS_PENDING + Order.STATUS_PAID)
if s != "": if s != "":

View File

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

View File

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

View File

@@ -43,29 +43,24 @@ def get_event_navigation(request: HttpRequest):
'icon': 'dashboard', 'icon': 'dashboard',
} }
] ]
event_settings = [] if 'can_change_event_settings' in request.eventpermset:
if "event.settings.general:write" in request.eventpermset: event_settings = [
event_settings.append({ {
'label': _('General'), 'label': _('General'),
'url': reverse('control:event.settings', kwargs={ 'url': reverse('control:event.settings', kwargs={
'event': request.event.slug, 'event': request.event.slug,
'organizer': request.event.organizer.slug, 'organizer': request.event.organizer.slug,
}), }),
'active': url.url_name == 'event.settings', 'active': url.url_name == 'event.settings',
}) },
{
if "event.settings.payment:write" in request.eventpermset or "event.settings.general:write" in request.eventpermset: 'label': _('Payment'),
event_settings.append({ 'url': reverse('control:event.settings.payment', kwargs={
'label': _('Payment'), 'event': request.event.slug,
'url': reverse('control:event.settings.payment', kwargs={ 'organizer': request.event.organizer.slug,
'event': request.event.slug, }),
'organizer': request.event.organizer.slug, 'active': url.url_name in ('event.settings.payment', 'event.settings.payment.provider'),
}), },
'active': url.url_name in ('event.settings.payment', 'event.settings.payment.provider'),
})
if "event.settings.general:write" in request.eventpermset:
event_settings += [
{ {
'label': _('Plugins'), 'label': _('Plugins'),
'url': reverse('control:event.settings.plugins', kwargs={ 'url': reverse('control:event.settings.plugins', kwargs={
@@ -89,31 +84,23 @@ def get_event_navigation(request: HttpRequest):
'organizer': request.event.organizer.slug, 'organizer': request.event.organizer.slug,
}), }),
'active': url.url_name == 'event.settings.mail', 'active': url.url_name == 'event.settings.mail',
} },
] {
'label': _('Taxes'),
if "event.settings.tax:write" in request.eventpermset or "event.settings.general:write" in request.eventpermset: 'url': reverse('control:event.settings.tax', kwargs={
event_settings.append({ 'event': request.event.slug,
'label': _('Taxes'), 'organizer': request.event.organizer.slug,
'url': reverse('control:event.settings.tax', kwargs={ }),
'event': request.event.slug, 'active': url.url_name.startswith('event.settings.tax'),
'organizer': request.event.organizer.slug, },
}), {
'active': url.url_name.startswith('event.settings.tax'), 'label': _('Invoicing'),
}) 'url': reverse('control:event.settings.invoice', kwargs={
'event': request.event.slug,
if "event.settings.invoicing:write" in request.eventpermset or "event.settings.general:write" in request.eventpermset: 'organizer': request.event.organizer.slug,
event_settings.append({ }),
'label': _('Invoicing'), 'active': url.url_name == 'event.settings.invoice',
'url': reverse('control:event.settings.invoice', kwargs={ },
'event': request.event.slug,
'organizer': request.event.organizer.slug,
}),
'active': url.url_name == 'event.settings.invoice',
})
if "event.settings.general:write" in request.eventpermset:
event_settings += [
{ {
'label': pgettext_lazy('action', 'Cancellation'), 'label': pgettext_lazy('action', 'Cancellation'),
'url': reverse('control:event.settings.cancel', kwargs={ 'url': reverse('control:event.settings.cancel', kwargs={
@@ -131,87 +118,88 @@ def get_event_navigation(request: HttpRequest):
'active': url.url_name == 'event.settings.widget', 'active': url.url_name == 'event.settings.widget',
}, },
] ]
# It would be better to allow plugins to handle the permission themselves, but for backwards compatibility
# we need to have it in the "if" statement
event_settings += sorted( event_settings += sorted(
sum((list(a[1]) for a in nav_event_settings.send(request.event, request=request)), []), sum((list(a[1]) for a in nav_event_settings.send(request.event, request=request)), []),
key=lambda r: r['label'] key=lambda r: r['label']
) )
if event_settings:
nav.append({ nav.append({
'label': _('Settings'), 'label': _('Settings'),
'url': event_settings[0]["url"], 'url': reverse('control:event.settings', kwargs={
'event': request.event.slug,
'organizer': request.event.organizer.slug,
}),
'active': False, 'active': False,
'icon': 'wrench', 'icon': 'wrench',
'children': event_settings 'children': event_settings
}) })
nav.append({ if 'can_change_items' in request.eventpermset:
'label': _('Products'),
'url': reverse('control:event.items', kwargs={
'event': request.event.slug,
'organizer': request.event.organizer.slug,
}),
'active': False,
'icon': 'ticket',
'children': [
{
'label': _('Products'),
'url': reverse('control:event.items', kwargs={
'event': request.event.slug,
'organizer': request.event.organizer.slug,
}),
'active': url.url_name in (
'event.item', 'event.items.add', 'event.items') or "event.item." in url.url_name,
},
{
'label': _('Quotas'),
'url': reverse('control:event.items.quotas', kwargs={
'event': request.event.slug,
'organizer': request.event.organizer.slug,
}),
'active': 'event.items.quota' in url.url_name,
},
{
'label': _('Categories'),
'url': reverse('control:event.items.categories', kwargs={
'event': request.event.slug,
'organizer': request.event.organizer.slug,
}),
'active': 'event.items.categories' in url.url_name,
},
{
'label': _('Questions'),
'url': reverse('control:event.items.questions', kwargs={
'event': request.event.slug,
'organizer': request.event.organizer.slug,
}),
'active': 'event.items.questions' in url.url_name,
},
{
'label': _('Discounts'),
'url': reverse('control:event.items.discounts', kwargs={
'event': request.event.slug,
'organizer': request.event.organizer.slug,
}),
'active': 'event.items.discounts' in url.url_name,
},
]
})
if request.event.has_subevents:
nav.append({ nav.append({
'label': pgettext_lazy('subevent', 'Dates'), 'label': _('Products'),
'url': reverse('control:event.subevents', kwargs={ 'url': reverse('control:event.items', kwargs={
'event': request.event.slug, 'event': request.event.slug,
'organizer': request.event.organizer.slug, 'organizer': request.event.organizer.slug,
}), }),
'active': ('event.subevent' in url.url_name), 'active': False,
'icon': 'calendar', 'icon': 'ticket',
'children': [
{
'label': _('Products'),
'url': reverse('control:event.items', kwargs={
'event': request.event.slug,
'organizer': request.event.organizer.slug,
}),
'active': url.url_name in (
'event.item', 'event.items.add', 'event.items') or "event.item." in url.url_name,
},
{
'label': _('Quotas'),
'url': reverse('control:event.items.quotas', kwargs={
'event': request.event.slug,
'organizer': request.event.organizer.slug,
}),
'active': 'event.items.quota' in url.url_name,
},
{
'label': _('Categories'),
'url': reverse('control:event.items.categories', kwargs={
'event': request.event.slug,
'organizer': request.event.organizer.slug,
}),
'active': 'event.items.categories' in url.url_name,
},
{
'label': _('Questions'),
'url': reverse('control:event.items.questions', kwargs={
'event': request.event.slug,
'organizer': request.event.organizer.slug,
}),
'active': 'event.items.questions' in url.url_name,
},
{
'label': _('Discounts'),
'url': reverse('control:event.items.discounts', kwargs={
'event': request.event.slug,
'organizer': request.event.organizer.slug,
}),
'active': 'event.items.discounts' in url.url_name,
},
]
}) })
if 'event.orders:read' in request.eventpermset: if 'can_change_event_settings' in request.eventpermset:
if request.event.has_subevents:
nav.append({
'label': pgettext_lazy('subevent', 'Dates'),
'url': reverse('control:event.subevents', kwargs={
'event': request.event.slug,
'organizer': request.event.organizer.slug,
}),
'active': ('event.subevent' in url.url_name),
'icon': 'calendar',
})
if 'can_view_orders' in request.eventpermset:
children = [ children = [
{ {
'label': _('All orders'), 'label': _('All orders'),
@@ -254,7 +242,7 @@ def get_event_navigation(request: HttpRequest):
'active': 'event.orders.waitinglist' in url.url_name, 'active': 'event.orders.waitinglist' in url.url_name,
}, },
] ]
if 'event.orders:write' in request.eventpermset: if 'can_change_orders' in request.eventpermset:
children.append({ children.append({
'label': _('Import'), 'label': _('Import'),
'url': reverse('control:event.orders.import', kwargs={ 'url': reverse('control:event.orders.import', kwargs={
@@ -273,18 +261,8 @@ def get_event_navigation(request: HttpRequest):
'icon': 'shopping-cart', 'icon': 'shopping-cart',
'children': children 'children': children
}) })
else:
nav.append({
'label': _('Export'),
'url': reverse('control:event.orders.export', kwargs={
'event': request.event.slug,
'organizer': request.event.organizer.slug,
}),
'active': 'event.orders.export' in url.url_name,
'icon': 'download',
})
if 'event.vouchers:read' in request.eventpermset: if 'can_view_vouchers' in request.eventpermset:
nav.append({ nav.append({
'label': _('Vouchers'), 'label': _('Vouchers'),
'url': reverse('control:event.vouchers', kwargs={ 'url': reverse('control:event.vouchers', kwargs={
@@ -313,7 +291,7 @@ def get_event_navigation(request: HttpRequest):
] ]
}) })
if 'event.orders:read' in request.eventpermset or 'event.settings.general:write' in request.eventpermset: if 'can_view_orders' in request.eventpermset:
nav.append({ nav.append({
'label': pgettext_lazy('navigation', 'Check-in'), 'label': pgettext_lazy('navigation', 'Check-in'),
'url': reverse('control:event.orders.checkinlists', kwargs={ 'url': reverse('control:event.orders.checkinlists', kwargs={
@@ -502,7 +480,7 @@ def get_organizer_navigation(request):
'icon': 'calendar', 'icon': 'calendar',
}, },
] ]
if 'organizer.settings.general:write' in request.orgapermset: if 'can_change_organizer_settings' in request.orgapermset:
nav.append({ nav.append({
'label': _('Settings'), 'label': _('Settings'),
'url': reverse('control:organizer.edit', kwargs={ 'url': reverse('control:organizer.edit', kwargs={
@@ -556,7 +534,7 @@ def get_organizer_navigation(request):
] ]
}) })
if 'organizer.teams:write' in request.orgapermset: if 'can_change_teams' in request.orgapermset:
nav.append({ nav.append({
'label': _('Teams'), 'label': _('Teams'),
'url': reverse('control:organizer.teams', kwargs={ 'url': reverse('control:organizer.teams', kwargs={
@@ -566,7 +544,7 @@ def get_organizer_navigation(request):
'icon': 'group', 'icon': 'group',
}) })
if 'organizer.giftcards:read' in request.orgapermset or 'organizer.giftcards:write' in request.orgapermset: if 'can_manage_gift_cards' in request.orgapermset:
children = [] children = []
children.append({ children.append({
'label': _('Gift cards'), 'label': _('Gift cards'),
@@ -576,7 +554,7 @@ def get_organizer_navigation(request):
'active': 'organizer.giftcard' in url.url_name and 'acceptance' not in url.url_name, 'active': 'organizer.giftcard' in url.url_name and 'acceptance' not in url.url_name,
'children': children, 'children': children,
}) })
if 'organizer.settings.general:write' in request.orgapermset: if 'can_change_organizer_settings' in request.orgapermset:
children.append( children.append(
{ {
'label': _('Acceptance'), 'label': _('Acceptance'),
@@ -597,7 +575,7 @@ def get_organizer_navigation(request):
if request.organizer.settings.customer_accounts: if request.organizer.settings.customer_accounts:
children = [] children = []
if 'organizer.customers:read' in request.orgapermset or 'organizer.customers:write' in request.orgapermset: if 'can_manage_customers' in request.orgapermset:
children.append( children.append(
{ {
'label': _('Customers'), 'label': _('Customers'),
@@ -607,7 +585,7 @@ def get_organizer_navigation(request):
'active': 'organizer.customer' in url.url_name, 'active': 'organizer.customer' in url.url_name,
} }
) )
if 'organizer.settings.general:write' in request.orgapermset: if 'can_change_organizer_settings' in request.orgapermset:
children.append( children.append(
{ {
'label': _('Membership types'), 'label': _('Membership types'),
@@ -646,17 +624,16 @@ def get_organizer_navigation(request):
}) })
if request.organizer.settings.reusable_media_active: if request.organizer.settings.reusable_media_active:
if 'organizer.reusablemedia:read' in request.orgapermset or 'organizer.reusablemedia:write' in request.orgapermset: nav.append({
nav.append({ 'label': _('Reusable media'),
'label': _('Reusable media'), 'url': reverse('control:organizer.reusable_media', kwargs={
'url': reverse('control:organizer.reusable_media', kwargs={ 'organizer': request.organizer.slug
'organizer': request.organizer.slug }),
}), 'icon': 'key',
'icon': 'key', 'active': 'organizer.reusable_medi' in url.url_name,
'active': 'organizer.reusable_medi' in url.url_name, })
})
if 'organizer.devices:read' in request.orgapermset or 'organizer.devices:write' in request.orgapermset: if 'can_change_organizer_settings' in request.orgapermset:
nav.append({ nav.append({
'label': _('Devices'), 'label': _('Devices'),
'url': reverse('control:organizer.devices', kwargs={ 'url': reverse('control:organizer.devices', kwargs={
@@ -690,7 +667,7 @@ def get_organizer_navigation(request):
'icon': 'download', 'icon': 'download',
}) })
if 'organizer.settings.general:write' in request.orgapermset: if 'can_change_organizer_settings' in request.orgapermset:
merge_in(nav, [{ merge_in(nav, [{
'parent': reverse('control:organizer.export', kwargs={ 'parent': reverse('control:organizer.export', kwargs={
'organizer': request.organizer.slug, 'organizer': request.organizer.slug,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -12,7 +12,6 @@
<legend>{% trans "Invoice generation" %}</legend> <legend>{% trans "Invoice generation" %}</legend>
{% bootstrap_field form.invoice_generate layout="control" %} {% bootstrap_field form.invoice_generate layout="control" %}
{% bootstrap_field form.invoice_generate_sales_channels layout="control" %} {% bootstrap_field form.invoice_generate_sales_channels layout="control" %}
{% bootstrap_field form.invoice_generate_only_business layout="control" %}
{% bootstrap_field form.invoice_email_attachment layout="control" %} {% bootstrap_field form.invoice_email_attachment layout="control" %}
{% bootstrap_field form.invoice_email_organizer layout="control" %} {% bootstrap_field form.invoice_email_organizer layout="control" %}
{% bootstrap_field form.invoice_language layout="control" %} {% bootstrap_field form.invoice_language layout="control" %}
@@ -112,6 +111,11 @@
<span class="text-success"> <span class="text-success">
<span class="fa fa-check fa-fw"></span> <span class="fa fa-check fa-fw"></span>
{% trans "Available" %} {% trans "Available" %}
{% if t.exclusive %}
<span data-toggle="tooltip" title="{% trans "When this type is available for an invoice address, no other type can be selected." %}">
{% trans "(exclusive)" %}
</span>
{% endif %}
</span> </span>
{% else %} {% else %}
<span class="text-muted"> <span class="text-muted">
@@ -165,15 +169,13 @@
</p> </p>
</fieldset> </fieldset>
</div> </div>
{% if "event.settings.invoicing:write" in request.eventpermset %} <div class="form-group submit-group">
<div class="form-group submit-group"> <button type="submit" class="btn btn-default btn-lg" name="preview" value="preview" formtarget="_blank">
<button type="submit" class="btn btn-default btn-lg" name="preview" value="preview" formtarget="_blank"> {% trans "Save and show preview" %}
{% trans "Save and show preview" %} </button>
</button> <button type="submit" class="btn btn-primary btn-save">
<button type="submit" class="btn btn-primary btn-save"> {% trans "Save" %}
{% trans "Save" %} </button>
</button> </div>
</div>
{% endif %}
</form> </form>
{% endblock %} {% endblock %}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -22,7 +22,7 @@
{{ s.owner.fullname|default:s.owner.email }} {{ s.owner.fullname|default:s.owner.email }}
</span> </span>
</div> </div>
<div class="col-lg-4 col-md-5 col-xs-12"> <div class="col-lg-5 col-md-6 col-xs-12">
{% if s.schedule_next_run %} {% if s.schedule_next_run %}
<span class="fa fa-clock-o fa-fw"></span> <span class="fa fa-clock-o fa-fw"></span>
{% trans "Next run:" %} {% trans "Next run:" %}
@@ -53,7 +53,7 @@
{{ s.mail_subject }} {{ s.mail_subject }}
</span> </span>
</div> </div>
<div class="col-lg-3 col-md-3 col-xs-12 text-right"> <div class="col-lg-2 col-md-2 col-xs-12 text-right">
<form action="{% url "control:event.orders.export.do" event=request.event.slug organizer=request.organizer.slug %}" <form action="{% url "control:event.orders.export.do" event=request.event.slug organizer=request.organizer.slug %}"
method="post" class="form-horizontal" data-asynctask data-asynctask-download method="post" class="form-horizontal" data-asynctask data-asynctask-download
data-asynctask-long> data-asynctask-long>
@@ -73,9 +73,6 @@
<a href="?identifier={{ s.export_identifier }}&scheduled={{ s.pk }}" class="btn btn-default" title="{% trans "Edit" %}" data-toggle="tooltip"> <a href="?identifier={{ s.export_identifier }}&scheduled={{ s.pk }}" class="btn btn-default" title="{% trans "Edit" %}" data-toggle="tooltip">
<span class="fa fa-edit"></span> <span class="fa fa-edit"></span>
</a> </a>
<a href="?identifier={{ s.export_identifier }}&scheduled_copy_from={{ s.pk }}" class="btn btn-default" title="{% trans "Copy" %}" data-toggle="tooltip">
<span class="fa fa-copy"></span>
</a>
{% endif %} {% endif %}
<a href="{% url "control:event.orders.export.scheduled.delete" event=request.event.slug organizer=request.event.organizer.slug pk=s.pk %}" class="btn btn-danger" title="{% trans "Delete" %}" data-toggle="tooltip"> <a href="{% url "control:event.orders.export.scheduled.delete" event=request.event.slug organizer=request.event.organizer.slug pk=s.pk %}" class="btn btn-danger" title="{% trans "Delete" %}" data-toggle="tooltip">
<span class="fa fa-trash"></span> <span class="fa fa-trash"></span>
@@ -115,9 +112,5 @@
</a> </a>
{% endfor %} {% endfor %}
</div> </div>
{% empty %}
<p class="empty-collection">
{% trans "There are no exporters available for you." %}
</p>
{% endfor %} {% endfor %}
{% endblock %} {% endblock %}

View File

@@ -42,11 +42,7 @@
<div class="form-group submit-group"> <div class="form-group submit-group">
<button formaction="{{ request.get_full_path }}" name="schedule" value="save" type="submit" <button formaction="{{ request.get_full_path }}" name="schedule" value="save" type="submit"
class="btn btn-primary btn-save" data-no-asynctask> class="btn btn-primary btn-save" data-no-asynctask>
{% if scheduled_copy_from %} {% trans "Save" %}
{% trans "Save copy" %}
{% else %}
{% trans "Save" %}
{% endif %}
</button> </button>
</div> </div>
{% else %} {% else %}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -22,7 +22,7 @@
{{ s.owner.fullname|default:s.owner.email }} {{ s.owner.fullname|default:s.owner.email }}
</span> </span>
</div> </div>
<div class="col-lg-4 col-md-5 col-xs-12"> <div class="col-lg-5 col-md-6 col-xs-12">
{% if s.schedule_next_run %} {% if s.schedule_next_run %}
<span class="fa fa-clock-o fa-fw"></span> <span class="fa fa-clock-o fa-fw"></span>
{% trans "Next run:" %} {% trans "Next run:" %}
@@ -53,7 +53,7 @@
{{ s.mail_subject }} {{ s.mail_subject }}
</span> </span>
</div> </div>
<div class="col-lg-3 col-md-3 col-xs-12 text-right"> <div class="col-lg-2 col-md-2 col-xs-12 text-right">
<form action="{% url "control:organizer.export.do" organizer=request.organizer.slug %}" <form action="{% url "control:organizer.export.do" organizer=request.organizer.slug %}"
method="post" class="form-horizontal" data-asynctask data-asynctask-download method="post" class="form-horizontal" data-asynctask data-asynctask-download
data-asynctask-long> data-asynctask-long>
@@ -73,9 +73,6 @@
<a href="?identifier={{ s.export_identifier }}&scheduled={{ s.pk }}" class="btn btn-default" title="{% trans "Edit" %}" data-toggle="tooltip"> <a href="?identifier={{ s.export_identifier }}&scheduled={{ s.pk }}" class="btn btn-default" title="{% trans "Edit" %}" data-toggle="tooltip">
<span class="fa fa-edit"></span> <span class="fa fa-edit"></span>
</a> </a>
<a href="?identifier={{ s.export_identifier }}&scheduled_copy_from={{ s.pk }}" class="btn btn-default" title="{% trans "Copy" %}" data-toggle="tooltip">
<span class="fa fa-copy"></span>
</a>
{% endif %} {% endif %}
<a href="{% url "control:organizer.export.scheduled.delete" organizer=request.organizer.slug pk=s.pk %}" class="btn btn-danger" title="{% trans "Delete" %}" data-toggle="tooltip"> <a href="{% url "control:organizer.export.scheduled.delete" organizer=request.organizer.slug pk=s.pk %}" class="btn btn-danger" title="{% trans "Delete" %}" data-toggle="tooltip">
<span class="fa fa-trash"></span> <span class="fa fa-trash"></span>
@@ -115,9 +112,5 @@
</a> </a>
{% endfor %} {% endfor %}
</div> </div>
{% empty %}
<p class="empty-collection">
{% trans "There are no exporters available for you." %}
</p>
{% endfor %} {% endfor %}
{% endblock %} {% endblock %}

View File

@@ -43,11 +43,7 @@
<div class="form-group submit-group"> <div class="form-group submit-group">
<button formaction="{{ request.get_full_path }}" name="schedule" value="save" type="submit" <button formaction="{{ request.get_full_path }}" name="schedule" value="save" type="submit"
class="btn btn-primary btn-save" data-no-asynctask> class="btn btn-primary btn-save" data-no-asynctask>
{% if scheduled_copy_from %} {% trans "Save" %}
{% trans "Save copy" %}
{% else %}
{% trans "Save" %}
{% endif %}
</button> </button>
</div> </div>
{% else %} {% else %}

View File

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

View File

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

View File

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

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