Compare commits

..

7 Commits

Author SHA1 Message Date
Richard Schreiber
4c05928215 keep data-inverse intact 2024-11-05 13:44:02 +01:00
Richard Schreiber
c069ff40c8 fix typo 2024-11-05 13:43:50 +01:00
Richard Schreiber
ea90ed008b Update src/pretix/static/pretixcontrol/js/ui/main.js
Co-authored-by: Mira <weller@rami.io>
2024-11-05 13:34:02 +01:00
Richard Schreiber
fe8e8fbbb9 Hide dependencies recursively if input will be disabled 2024-10-31 16:02:09 +01:00
Raphael Michel
81d7045b31 Public event filter: Make "all" option clearer (Z#23169843) (#4585)
* Public event filter: Make "all" option clearer

* Fix widget tests

* Update src/tests/presale/test_widget.py

Co-authored-by: Richard Schreiber <schreiber@rami.io>

---------

Co-authored-by: Richard Schreiber <schreiber@rami.io>
2024-10-29 17:31:51 +01:00
Richard Schreiber
f9502a3212 Fix testutils redis mock (#4586) 2024-10-29 16:37:06 +01:00
Richard Schreiber
a31f624417 Dev: add support for asyncio_default_fixture_loop_scope in pytest-asyncio (#4589) 2024-10-29 16:36:43 +01:00
12 changed files with 100 additions and 92 deletions

View File

@@ -100,7 +100,3 @@ API
.. automodule:: pretix.base.signals
:no-index:
:members: validate_event_settings, api_event_settings_fields
.. automodule:: pretix.api.signals
:no-index:
:members: register_device_security_profile

View File

@@ -117,7 +117,7 @@ dev = [
"isort==5.13.*",
"pep8-naming==0.14.*",
"potypo",
"pytest-asyncio",
"pytest-asyncio>=0.24",
"pytest-cache",
"pytest-cov",
"pytest-django==4.*",

View File

@@ -27,7 +27,7 @@ from rest_framework import exceptions
from rest_framework.authentication import TokenAuthentication
from pretix.api.auth.devicesecurity import (
FullAccessSecurityProfile, get_all_security_profiles,
DEVICE_SECURITY_PROFILES, FullAccessSecurityProfile,
)
from pretix.base.models import Device
@@ -58,8 +58,7 @@ class DeviceTokenAuthentication(TokenAuthentication):
def authenticate(self, request):
r = super().authenticate(request)
if r and isinstance(r[1], Device):
profiles = get_all_security_profiles()
profile = profiles.get(r[1].security_profile, FullAccessSecurityProfile())
profile = DEVICE_SECURITY_PROFILES.get(r[1].security_profile, FullAccessSecurityProfile)
if not profile.is_allowed(request):
raise exceptions.PermissionDenied('Request denied by device security profile.')
return r

View File

@@ -20,40 +20,13 @@
# <https://www.gnu.org/licenses/>.
#
import logging
from collections import OrderedDict
from django.dispatch import receiver
from django.utils.translation import gettext_lazy as _
from pretix.api.signals import register_device_security_profile
logger = logging.getLogger(__name__)
_ALL_PROFILES = None
class BaseSecurityProfile:
@property
def identifier(self) -> str:
"""
Unique identifier for this profile.
"""
raise NotImplementedError()
@property
def verbose_name(self) -> str:
"""
Human-readable name (can be a ``gettext_lazy`` object).
"""
raise NotImplementedError()
def is_allowed(self, request) -> bool:
"""
Return whether a given request should be allowed.
"""
raise NotImplementedError()
class FullAccessSecurityProfile(BaseSecurityProfile):
class FullAccessSecurityProfile:
identifier = 'full'
verbose_name = _('Full device access (reading and changing orders and gift cards, reading of products and settings)')
@@ -61,7 +34,7 @@ class FullAccessSecurityProfile(BaseSecurityProfile):
return True
class AllowListSecurityProfile(BaseSecurityProfile):
class AllowListSecurityProfile:
allowlist = ()
def is_allowed(self, request):
@@ -184,28 +157,88 @@ class PretixScanNoSyncSecurityProfile(AllowListSecurityProfile):
)
def get_all_security_profiles():
global _ALL_PROFILES
if _ALL_PROFILES:
return _ALL_PROFILES
types = OrderedDict()
for recv, ret in register_device_security_profile.send(None):
if isinstance(ret, (list, tuple)):
for r in ret:
types[r.identifier] = r
else:
types[ret.identifier] = ret
_ALL_PROFILES = types
return types
@receiver(register_device_security_profile, dispatch_uid="base_register_default_security_profiles")
def register_default_webhook_events(sender, **kwargs):
return (
FullAccessSecurityProfile(),
PretixScanSecurityProfile(),
PretixScanNoSyncSecurityProfile(),
PretixScanNoSyncNoSearchSecurityProfile(),
class PretixPosSecurityProfile(AllowListSecurityProfile):
identifier = 'pretixpos'
verbose_name = _('pretixPOS')
allowlist = (
('GET', 'api-v1:version'),
('GET', 'api-v1:device.eventselection'),
('GET', 'api-v1:idempotency.query'),
('GET', 'api-v1:device.info'),
('POST', 'api-v1:device.update'),
('POST', 'api-v1:device.revoke'),
('POST', 'api-v1:device.roll'),
('GET', 'api-v1:event-list'),
('GET', 'api-v1:event-detail'),
('GET', 'api-v1:subevent-list'),
('GET', 'api-v1:subevent-detail'),
('GET', 'api-v1:itemcategory-list'),
('GET', 'api-v1:item-list'),
('GET', 'api-v1:question-list'),
('GET', 'api-v1:quota-list'),
('GET', 'api-v1:taxrule-list'),
('GET', 'api-v1:ticketlayout-list'),
('GET', 'api-v1:ticketlayoutitem-list'),
('GET', 'api-v1:badgelayout-list'),
('GET', 'api-v1:badgeitem-list'),
('GET', 'api-v1:voucher-list'),
('GET', 'api-v1:voucher-detail'),
('GET', 'api-v1:order-list'),
('POST', 'api-v1:order-list'),
('GET', 'api-v1:order-detail'),
('DELETE', 'api-v1:orderposition-detail'),
('PATCH', 'api-v1:orderposition-detail'),
('GET', 'api-v1:orderposition-list'),
('GET', 'api-v1:orderposition-answer'),
('GET', 'api-v1:orderposition-pdf_image'),
('POST', 'api-v1:orderposition-printlog'),
('POST', 'api-v1:order-mark-canceled'),
('POST', 'api-v1:orderpayment-list'),
('POST', 'api-v1:orderrefund-list'),
('POST', 'api-v1:orderrefund-done'),
('POST', 'api-v1:cartposition-list'),
('POST', 'api-v1:cartposition-bulk-create'),
('GET', 'api-v1:checkinlist-list'),
('POST', 'api-v1:checkinlistpos-redeem'),
('POST', 'plugins:pretix_posbackend:order.posprintlog'),
('POST', 'plugins:pretix_posbackend:order.poslock'),
('DELETE', 'plugins:pretix_posbackend:order.poslock'),
('DELETE', 'api-v1:cartposition-detail'),
('GET', 'api-v1:giftcard-list'),
('POST', 'api-v1:giftcard-transact'),
('PATCH', 'api-v1:giftcard-detail'),
('GET', 'plugins:pretix_posbackend:posclosing-list'),
('POST', 'plugins:pretix_posbackend:posreceipt-list'),
('POST', 'plugins:pretix_posbackend:posclosing-list'),
('POST', 'plugins:pretix_posbackend:posdebugdump-list'),
('POST', 'plugins:pretix_posbackend:posdebuglogentry-list'),
('POST', 'plugins:pretix_posbackend:posdebuglogentry-bulk-create'),
('GET', 'plugins:pretix_posbackend:poscashier-list'),
('POST', 'plugins:pretix_posbackend:stripeterminal.token'),
('POST', 'plugins:pretix_posbackend:stripeterminal.paymentintent'),
('PUT', 'plugins:pretix_posbackend:file.upload'),
('GET', 'api-v1:revokedsecrets-list'),
('GET', 'api-v1:blockedsecrets-list'),
('GET', 'api-v1:event.settings'),
('GET', 'plugins:pretix_seating:event.event'),
('GET', 'plugins:pretix_seating:event.event.subevent'),
('GET', 'plugins:pretix_seating:event.plan'),
('GET', 'plugins:pretix_seating:selection.simple'),
('POST', 'api-v1:upload'),
('POST', 'api-v1:checkinrpc.redeem'),
('GET', 'api-v1:checkinrpc.search'),
('POST', 'api-v1:reusablemedium-lookup'),
('GET', 'api-v1:reusablemedium-list'),
('POST', 'api-v1:reusablemedium-list'),
)
DEVICE_SECURITY_PROFILES = {
k.identifier: k() for k in (
FullAccessSecurityProfile,
PretixScanSecurityProfile,
PretixScanNoSyncSecurityProfile,
PretixScanNoSyncNoSearchSecurityProfile,
PretixPosSecurityProfile,
)
}

View File

@@ -29,7 +29,6 @@ from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from pretix.api.auth.devicesecurity import get_all_security_profiles
from pretix.api.serializers import AsymmetricField
from pretix.api.serializers.i18n import I18nAwareModelSerializer
from pretix.api.serializers.order import CompatibleJSONField
@@ -298,7 +297,6 @@ class DeviceSerializer(serializers.ModelSerializer):
revoked = serializers.BooleanField(read_only=True)
initialized = serializers.DateTimeField(read_only=True)
initialization_token = serializers.DateTimeField(read_only=True)
security_profile = serializers.ChoiceField(choices=[], required=False, default="full")
class Meta:
model = Device
@@ -308,10 +306,6 @@ class DeviceSerializer(serializers.ModelSerializer):
'os_name', 'os_version', 'software_brand', 'software_version', 'security_profile'
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['security_profile'].choices = [(k, v.verbose_name) for k, v in get_all_security_profiles().items()]
class TeamInviteSerializer(serializers.ModelSerializer):
class Meta:

View File

@@ -32,17 +32,10 @@ from pretix.helpers.periodic import minimum_interval
register_webhook_events = Signal()
"""
This signal is sent out to get all known webhook events. Receivers should return an
instance of a subclass of ``pretix.api.webhooks.WebhookEvent`` or a list of such
instance of a subclass of pretix.api.webhooks.WebhookEvent or a list of such
instances.
"""
register_device_security_profile = Signal()
"""
This signal is sent out to get all known device security_profiles. Receivers should
return an instance of a subclass of ``pretix.api.auth.devicesecurity.BaseSecurityProfile``
or a list of such instances.
"""
@receiver(periodic_task)
@scopes_disabled()

View File

@@ -28,6 +28,7 @@ from django.utils.crypto import get_random_string
from django.utils.translation import gettext_lazy as _
from django_scopes import ScopedManager, scopes_disabled
from pretix.api.auth.devicesecurity import DEVICE_SECURITY_PROFILES
from pretix.base.models import LoggedModel
@@ -160,6 +161,7 @@ class Device(LoggedModel):
)
security_profile = models.CharField(
max_length=190,
choices=[(k, v.verbose_name) for k, v in DEVICE_SECURITY_PROFILES.items()],
default='full',
null=True,
blank=False

View File

@@ -54,7 +54,6 @@ from i18nfield.strings import LazyI18nString
from phonenumber_field.formfields import PhoneNumberField
from pytz import common_timezones
from pretix.api.auth.devicesecurity import get_all_security_profiles
from pretix.api.models import WebHook
from pretix.api.webhooks import get_all_webhook_events
from pretix.base.customersso.oidc import oidc_validate_and_complete_config
@@ -312,11 +311,6 @@ class DeviceForm(forms.ModelForm):
'-has_subevents', '-date_from'
)
self.fields['gate'].queryset = organizer.gates.all()
self.fields['security_profile'] = forms.ChoiceField(
label=self.fields['security_profile'].label,
help_text=self.fields['security_profile'].help_text,
choices=[(k, v.verbose_name) for k, v in get_all_security_profiles().items()],
)
def clean(self):
d = super().clean()
@@ -350,11 +344,6 @@ class DeviceBulkEditForm(forms.ModelForm):
'-has_subevents', '-date_from'
)
self.fields['gate'].queryset = organizer.gates.all()
self.fields['security_profile'] = forms.ChoiceField(
label=self.fields['security_profile'].label,
help_text=self.fields['security_profile'].help_text,
choices=[(k, v.verbose_name) for k, v in get_all_security_profiles().items()],
)
def clean(self):
d = super().clean()

View File

@@ -21,6 +21,7 @@
#
from django import forms
from django.conf import settings
from django.utils.translation import pgettext
from i18nfield.strings import LazyI18nString
from pretix.base.models import EventMetaValue, SubEventMetaValue
@@ -66,7 +67,7 @@ def meta_filtersets(organizer, event=None):
).values_list("value", flat=True).distinct())
choices = [(k, k) for k in sorted(existing_values)]
choices.insert(0, ("", ""))
choices.insert(0, ("", "-- %s --" % pgettext("filter_empty", "all")))
if len(choices) > 1:
fields[f"attr[{prop.name}]"] = {
"label": str(prop.public_label) or prop.name,

View File

@@ -378,6 +378,7 @@ var form_handlers = function (el) {
dependency = findDependency($(this).attr("data-display-dependency"), this),
update = function (ev) {
var enabled = dependency.toArray().some(function(d) {
if (d.disabled) return false;
if (d.type === 'checkbox' || d.type === 'radio') {
return d.checked;
} else if (d.type === 'select-one') {
@@ -398,7 +399,7 @@ var form_handlers = function (el) {
}
var $toggling = dependent;
if (dependent.attr("data-disable-dependent")) {
$toggling.attr('disabled', !enabled);
$toggling.attr('disabled', !enabled).trigger("change");
}
if (dependent.get(0).tagName.toLowerCase() !== "div") {
$toggling = dependent.closest('.form-group');

View File

@@ -40,7 +40,7 @@ def mocker_context():
def get_redis_connection(alias="default", write=True):
worker_id = os.environ.get("PYTEST_XDIST_WORKER")
if worker_id.startswith("gw"):
if worker_id and worker_id.startswith("gw"):
redis_port = 1000 + int(worker_id.replace("gw", ""))
else:
redis_port = 1000

View File

@@ -812,7 +812,7 @@ class WidgetCartTest(CartTestMixin, TestCase):
data = json.loads(response.content.decode())
assert data["meta_filter_fields"] == [
{
"choices": [["", ""], ["EN", "English"], ["DE", "German"]],
"choices": [["", "-- all --"], ["EN", "English"], ["DE", "German"]],
"key": "attr[Language]",
"label": "Language"
}
@@ -838,7 +838,7 @@ class WidgetCartTest(CartTestMixin, TestCase):
data = json.loads(response.content.decode())
assert data["meta_filter_fields"] == [
{
"choices": [["", ""], ["DE", "DE"], ["EN", "EN"]],
"choices": [["", "-- all --"], ["DE", "DE"], ["EN", "EN"]],
"key": "attr[Language]",
"label": "Language"
}
@@ -848,7 +848,7 @@ class WidgetCartTest(CartTestMixin, TestCase):
data = json.loads(response.content.decode())
assert data["meta_filter_fields"] == [
{
"choices": [["", ""], ["DE", "DE"]],
"choices": [["", "-- all --"], ["DE", "DE"]],
"key": "attr[Language]",
"label": "Language"
}