Compare commits

..

3 Commits

Author SHA1 Message Date
Raphael Michel
5f724fa780 REmove dead class 2024-11-11 15:39:42 +01:00
Raphael Michel
118f61292b Update src/pretix/api/signals.py
Co-authored-by: robbi5 <richt@rami.io>
2024-10-31 13:29:58 +01:00
Raphael Michel
458a22f6a3 Make API security profiles pluggable 2024-10-31 13:26:19 +01:00
18 changed files with 169 additions and 212 deletions

View File

@@ -100,3 +100,7 @@ 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>=0.24",
"pytest-asyncio",
"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 (
DEVICE_SECURITY_PROFILES, FullAccessSecurityProfile,
FullAccessSecurityProfile, get_all_security_profiles,
)
from pretix.base.models import Device
@@ -58,7 +58,8 @@ class DeviceTokenAuthentication(TokenAuthentication):
def authenticate(self, request):
r = super().authenticate(request)
if r and isinstance(r[1], Device):
profile = DEVICE_SECURITY_PROFILES.get(r[1].security_profile, FullAccessSecurityProfile)
profiles = get_all_security_profiles()
profile = 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,13 +20,40 @@
# <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 FullAccessSecurityProfile:
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):
identifier = 'full'
verbose_name = _('Full device access (reading and changing orders and gift cards, reading of products and settings)')
@@ -34,7 +61,7 @@ class FullAccessSecurityProfile:
return True
class AllowListSecurityProfile:
class AllowListSecurityProfile(BaseSecurityProfile):
allowlist = ()
def is_allowed(self, request):
@@ -157,88 +184,28 @@ class PretixScanNoSyncSecurityProfile(AllowListSecurityProfile):
)
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'),
)
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
DEVICE_SECURITY_PROFILES = {
k.identifier: k() for k in (
FullAccessSecurityProfile,
PretixScanSecurityProfile,
PretixScanNoSyncSecurityProfile,
PretixScanNoSyncNoSearchSecurityProfile,
PretixPosSecurityProfile,
@receiver(register_device_security_profile, dispatch_uid="base_register_default_security_profiles")
def register_default_webhook_events(sender, **kwargs):
return (
FullAccessSecurityProfile(),
PretixScanSecurityProfile(),
PretixScanNoSyncSecurityProfile(),
PretixScanNoSyncNoSearchSecurityProfile(),
)
}

View File

@@ -29,6 +29,7 @@ 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
@@ -297,6 +298,7 @@ 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
@@ -306,6 +308,10 @@ 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,10 +32,17 @@ 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

@@ -141,7 +141,7 @@ class CheckinList(LoggedModel):
return self.positions_query(ignore_status=False)
@scopes_disabled()
def _filter_positions_inside(self, qs, at_time=None):
def positions_inside_query(self, ignore_status=False, at_time=None):
if at_time is None:
c_q = []
else:
@@ -149,7 +149,7 @@ class CheckinList(LoggedModel):
if "postgresql" not in settings.DATABASES["default"]["ENGINE"]:
# Use a simple approach that works on all databases
qs = qs.annotate(
qs = self.positions_query(ignore_status=ignore_status).annotate(
last_entry=Subquery(
Checkin.objects.filter(
*c_q,
@@ -202,7 +202,7 @@ class CheckinList(LoggedModel):
.values("position_id", "type", "datetime", "cnt_exists_after")
.query.sql_with_params()
)
return qs.filter(
return self.positions_query(ignore_status=ignore_status).filter(
pk__in=RawSQL(
f"""
SELECT "position_id"
@@ -214,10 +214,6 @@ class CheckinList(LoggedModel):
)
)
@scopes_disabled()
def positions_inside_query(self, ignore_status=False, at_time=None):
return self._filter_positions_inside(self.positions_query(ignore_status=ignore_status), at_time=at_time)
@property
def positions_inside(self):
return self.positions_inside_query(None)

View File

@@ -28,7 +28,6 @@ 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
@@ -161,7 +160,6 @@ 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

@@ -1967,7 +1967,7 @@ class CheckinListAttendeeFilterForm(FilterForm):
if s == '1':
qs = qs.filter(last_entry__isnull=False)
elif s == '2':
qs = self.list._filter_positions_inside(qs)
qs = qs.filter(pk__in=self.list.positions_inside.values_list('pk'))
elif s == '3':
qs = qs.filter(last_entry__isnull=False).filter(
Q(last_exit__isnull=False) & Q(last_exit__gte=F('last_entry'))

View File

@@ -54,6 +54,7 @@ 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
@@ -311,6 +312,11 @@ 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()
@@ -344,6 +350,11 @@ 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

@@ -16,18 +16,8 @@
{% bootstrap_field form.internal_name layout="control" %}
</div>
{% bootstrap_field form.description layout="control" %}
{% bootstrap_field form.category_type layout="control" horizontal_field_class="big-radio-wrapper col-md-9" %}
<div class="row" data-display-dependency="#id_category_type_2">
<div class="col-md-offset-3 col-md-9">
<div class="alert alert-info">
{% blocktrans trimmed %}
Please note that cross-selling categories are intended as a marketing feature and are not
suitable for strictly ensuring that products are only available in certain combinations.
{% endblocktrans %}
</div>
</div>
</div>
{% bootstrap_field form.cross_selling_condition layout="control" horizontal_field_class="col-md-9" %}
{% bootstrap_field form.category_type layout="control" horizontal_field_class="big-radio-wrapper col-lg-9" %}
{% bootstrap_field form.cross_selling_condition layout="control" horizontal_field_class="col-lg-9" %}
{% bootstrap_field form.cross_selling_match_products layout="control" %}
</fieldset>
</div>

View File

@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-10-29 08:53+0000\n"
"PO-Revision-Date: 2024-10-29 21:00+0000\n"
"PO-Revision-Date: 2024-09-27 18:00+0000\n"
"Last-Translator: Anarion Dunedain <anarion80@gmail.com>\n"
"Language-Team: Polish <https://translate.pretix.eu/projects/pretix/pretix/pl/"
">\n"
@@ -18,7 +18,7 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 "
"|| n%100>=20) ? 1 : 2;\n"
"X-Generator: Weblate 5.8.1\n"
"X-Generator: Weblate 5.7.2\n"
#: pretix/_base_settings.py:79
msgid "English"
@@ -4606,40 +4606,43 @@ msgstr ""
#: pretix/base/models/items.py:114 pretix/base/models/items.py:159
#: pretix/control/forms/item.py:99
#, fuzzy
#| msgid "No category"
msgid "Normal category"
msgstr "Normalna kategoria"
msgstr "Brak kategorii"
#: pretix/base/models/items.py:115 pretix/control/forms/item.py:112
msgid "Normal + cross-selling category"
msgstr "Normalna + sprzedaż krzyżowa"
msgstr ""
#: pretix/base/models/items.py:116 pretix/control/forms/item.py:107
#, fuzzy
#| msgid "No category"
msgid "Cross-selling category"
msgstr "Kategoria sprzedaży krzyżowej"
msgstr "Brak kategorii"
#: pretix/base/models/items.py:124
msgid "Always show in cross-selling step"
msgstr "Zawsze pokazuj w kroku sprzedaży krzyżowej"
msgstr ""
#: pretix/base/models/items.py:125
msgid ""
"Only show products that qualify for a discount according to discount rules"
msgstr ""
"Pokaż tylko produkty kwalifikujące się do zniżki zgodnie z zasadami "
"rabatowymi"
#: pretix/base/models/items.py:126
msgid "Only show if the cart contains one of the following products"
msgstr ""
"Pokaż tylko, jeśli w koszyku znajduje się jeden z następujących produktów"
#: pretix/base/models/items.py:129
msgid "Cross-selling condition"
msgstr "Warunek sprzedaży krzyżowej"
msgstr ""
#: pretix/base/models/items.py:137
#, fuzzy
#| msgid "Count add-on products"
msgid "Cross-selling condition products"
msgstr "Produkty warunkowej sprzedaży krzyżowej"
msgstr "Policz produkty dodatkowe"
#: pretix/base/models/items.py:143
#: pretix/control/templates/pretixcontrol/items/categories.html:3
@@ -4651,11 +4654,13 @@ msgstr "Kategorie produktów"
#: pretix/base/models/items.py:149
#, python-brace-format
msgid "{category} ({category_type})"
msgstr "{category} ({category_type})"
msgstr ""
#: pretix/base/models/items.py:155
#, fuzzy
#| msgid "No category"
msgid "Add-on category"
msgstr "Kategoria dodatków"
msgstr "Brak kategorii"
#: pretix/base/models/items.py:222 pretix/base/models/items.py:278
msgid "Disable product for this date"
@@ -6018,8 +6023,10 @@ msgid "Ticket"
msgstr "Bilet"
#: pretix/base/models/orders.py:3405
#, fuzzy
#| msgid "Verification"
msgid "Certificate"
msgstr "Certyfikat"
msgstr "Weryfikacja"
#: pretix/base/models/orders.py:3406 pretix/control/views/event.py:367
#: pretix/control/views/event.py:372
@@ -6185,13 +6192,11 @@ msgstr ""
#, python-brace-format
msgid "Seat with zone {zone}, row {row}, and number {number} has no seat ID."
msgstr ""
"Miejsce w strefie {zone}, rzędzie {row} i numerze {number} nie ma "
"identyfikatora miejsca."
#: pretix/base/models/seating.py:71
#, python-brace-format
msgid "Multiple seats have the same ID: {id}"
msgstr "Wiele miejsc ma ten sam identyfikator: {id}"
msgstr ""
#: pretix/base/models/seating.py:199
#, python-brace-format
@@ -9039,8 +9044,10 @@ msgid "Ask for beneficiary"
msgstr "Zapytaj o beneficjenta"
#: pretix/base/settings.py:574
#, fuzzy
#| msgid "Custom recipient field"
msgid "Custom recipient field label"
msgstr "Etykieta pola niestandardowego odbiorcy"
msgstr "Niestandardowe pole odbiorcy"
#: pretix/base/settings.py:576
msgid ""
@@ -9057,8 +9064,10 @@ msgstr ""
"ona wyświetlana na fakturze poniżej nagłówka. Pole nie będzie wymagane."
#: pretix/base/settings.py:589
#, fuzzy
#| msgid "Custom recipient field"
msgid "Custom recipient field help text"
msgstr "Tekst pomocy dotyczący pola odbiorcy niestandardowego"
msgstr "Niestandardowe pole odbiorcy"
#: pretix/base/settings.py:591
msgid ""
@@ -9066,8 +9075,6 @@ msgid ""
"will be displayed underneath the field. It will not be displayed on the "
"invoice."
msgstr ""
"Jeśli używasz niestandardowego pola odbiorcy, możesz określić tekst pomocy, "
"który będzie wyświetlany pod polem. Nie będzie on wyświetlany na fakturze."
#: pretix/base/settings.py:601
msgid "Ask for VAT ID"
@@ -12505,9 +12512,12 @@ msgstr ""
"zakończenia przedsprzedaży"
#: pretix/base/timeline.py:106
#, fuzzy
#| msgctxt "timeline"
#| msgid "Customers can no longer modify their orders"
msgctxt "timeline"
msgid "Customers can no longer modify their order information"
msgstr "Klienci nie mogą już modyfikować swojego zamówienia"
msgstr "Klienci nie mogą już modyfikować swoich zamówień"
#: pretix/base/timeline.py:119
msgctxt "timeline"
@@ -12530,6 +12540,9 @@ msgid "Customers can no longer cancel paid orders"
msgstr "Klienci nie mogą już anulować opłaconych zamówień"
#: pretix/base/timeline.py:167
#, fuzzy
#| msgctxt "timeline"
#| msgid "Customers can no longer modify their orders"
msgctxt "timeline"
msgid "Customers can no longer make changes to their orders"
msgstr "Klienci nie mogą już modyfikować swoich zamówień"
@@ -14075,30 +14088,31 @@ msgstr ""
"wtyczki. Będzie on publicznie dostępny. Upewnij się, że jest on aktualny!"
#: pretix/control/forms/item.py:100
#, fuzzy
#| msgid "Products in this category are add-on products"
msgid ""
"Products in this category are regular products displayed on the front page."
msgstr ""
"Produkty w tej kategorii to zwykłe produkty wyświetlane na stronie głównej."
msgstr "Produkty w tej kategorii są dodatkami"
#: pretix/control/forms/item.py:103
#, fuzzy
#| msgid "Product category"
msgid "Add-on product category"
msgstr "Kategoria produktu dodatkowego"
msgstr "Kategoria produku"
#: pretix/control/forms/item.py:104
#, fuzzy
#| msgid "Products in this category are add-on products"
msgid ""
"Products in this category are add-on products and can only be bought as add-"
"ons."
msgstr ""
"Produkty w tej kategorii są produktami dodatkowymi i można je kupić "
"wyłącznie jako dodatki."
msgstr "Produkty w tej kategorii są dodatkami"
#: pretix/control/forms/item.py:108
msgid ""
"Products in this category are regular products, but are only shown in the "
"cross-selling step, according to the configuration below."
msgstr ""
"Produkty w tej kategorii to zwykłe produkty, ale są wyświetlane tylko na "
"etapie sprzedaży krzyżowej, zgodnie z konfiguracją poniżej."
#: pretix/control/forms/item.py:113
msgid ""
@@ -14106,9 +14120,6 @@ msgid ""
"but are additionally shown in the cross-selling step, according to the "
"configuration below."
msgstr ""
"Produkty w tej kategorii to zwykłe produkty wyświetlane na stronie głównej, "
"ale są dodatkowo pokazywane na etapie sprzedaży krzyżowej, zgodnie z "
"poniższą konfiguracją."
#: pretix/control/forms/item.py:141 pretix/control/forms/item.py:211
msgid "This field is required"
@@ -16526,9 +16537,12 @@ msgid "The check-in of position #{posid} on list \"{list}\" has been reverted."
msgstr "Zameldowanie pozycji #{posid} na liście \"{list}\" zostało cofnięte."
#: pretix/control/logdisplay.py:644
#, python-brace-format
#, fuzzy, python-brace-format
#| msgid ""
#| "Position #{posid} has been checked in at {datetime} for list \"{list}\"."
msgid "Position #{posid} has been printed at {datetime} with type \"{type}\"."
msgstr "Pozycja #{posid} została wydrukowana o {datetime} z typem „{type}”."
msgstr ""
"Pozycja #{posid} została zameldowana w {datetime} dla listy \"{list}\"."
#: pretix/control/logdisplay.py:667
#, python-brace-format
@@ -17214,9 +17228,6 @@ msgid ""
"check that you have completed all installation steps and your cronjob is "
"executed correctly."
msgstr ""
"Komponent cronjob pretix nie został uruchomiony w ciągu ostatnich godzin. "
"Sprawdź, czy ukończyłeś wszystkie kroki instalacji i czy twój cronjob został "
"wykonany poprawnie."
#: pretix/control/templates/pretixcontrol/base.html:435
msgid ""
@@ -20068,8 +20079,10 @@ msgstr ""
#: pretix/control/templates/pretixcontrol/item/base.html:24
#: pretix/control/templates/pretixcontrol/item/include_variations.html:79
#, fuzzy
#| msgid "Manage questions"
msgid "Manage quotas"
msgstr "Zarządzaj pulami"
msgstr "Zarządzaj pytaniami"
#: pretix/control/templates/pretixcontrol/item/base.html:27
#: pretix/control/templates/pretixcontrol/item/include_variations.html:82
@@ -20442,8 +20455,10 @@ msgid "Create a new category"
msgstr "Utwórz nową kategorię"
#: pretix/control/templates/pretixcontrol/items/categories.html:34
#, fuzzy
#| msgid "Category name"
msgid "Category type"
msgstr "Typ kategorii"
msgstr "Nazwa kategorii"
#: pretix/control/templates/pretixcontrol/items/categories.html:48
#: pretix/control/templates/pretixcontrol/items/discounts.html:138
@@ -25377,8 +25392,6 @@ msgid ""
"According to your event settings, sold out products are hidden from "
"customers. This way, customers will not be able to discover the waiting list."
msgstr ""
"Zgodnie z ustawieniami wydarzenia, wyprzedane produkty są ukryte przed "
"klientami. W ten sposób klienci nie będą mogli znaleźć listy oczekujących."
#: pretix/control/templates/pretixcontrol/waitinglist/index.html:36
msgid "Send vouchers"
@@ -31806,25 +31819,28 @@ msgid ""
"A product in your cart is only sold in combination with add-on products that "
"are no longer available. Please contact the event organizer."
msgstr ""
"Produkt w Twoim koszyku jest sprzedawany tylko w połączeniu z produktami "
"dodatkowymi, które nie są już dostępne. Skontaktuj się z organizatorem "
"wydarzenia."
#: pretix/presale/templates/pretixpresale/event/checkout_addons.html:20
msgid "We're now trying to book these add-ons for you!"
msgstr "Staramy się teraz zarezerwować te dodatki dla Ciebie!"
#: pretix/presale/templates/pretixpresale/event/checkout_addons.html:28
#, fuzzy
#| msgid "Additional settings"
msgid "Additional options for"
msgstr "Dodatkowe opcje dla"
msgstr "Dodatkowe ustawienia"
#: pretix/presale/templates/pretixpresale/event/checkout_addons.html:64
#, fuzzy
#| msgid "Top recommendation"
msgid "More recommendations"
msgstr "Więcej rekomendacji"
msgstr "Najlepsza rekomendacja"
#: pretix/presale/templates/pretixpresale/event/checkout_addons.html:71
#, fuzzy
#| msgid "Top recommendation"
msgid "Our recommendations"
msgstr "Nasze rekomendacje"
msgstr "Najlepsza rekomendacja"
#: pretix/presale/templates/pretixpresale/event/checkout_addons.html:89
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html:201
@@ -32830,8 +32846,10 @@ msgid "Payment pending"
msgstr "W toku płatności"
#: pretix/presale/templates/pretixpresale/event/fragment_product_list.html:19
#, fuzzy
#| msgid "Your orders for {event}"
msgid "Your order qualifies for a discount"
msgstr "Twoje zamówienia kwalifikuje się do zniżki"
msgstr "Twoje zamówienia na {event}"
#: pretix/presale/templates/pretixpresale/event/fragment_product_list.html:28
#: pretix/presale/templates/pretixpresale/event/voucher.html:78
@@ -34372,8 +34390,6 @@ msgid ""
"No ticket types are available for the waiting list, have a look at the "
"ticket shop instead."
msgstr ""
"Nie ma dostępnych typów biletów dla listy oczekujących, zamiast tego należy "
"udać się do sklepu z biletami."
#: pretix/presale/views/waiting.py:137 pretix/presale/views/waiting.py:161
msgid "Waiting lists are disabled for this event."

View File

@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-10-29 08:53+0000\n"
"PO-Revision-Date: 2024-10-30 19:00+0000\n"
"PO-Revision-Date: 2024-09-18 14:02+0000\n"
"Last-Translator: Tinna Sandström <tinna@coeo.events>\n"
"Language-Team: Swedish <https://translate.pretix.eu/projects/pretix/pretix/"
"sv/>\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.8.1\n"
"X-Generator: Weblate 5.7.2\n"
#: pretix/_base_settings.py:79
msgid "English"
@@ -22131,7 +22131,7 @@ msgid ""
"After starting this operation, depending on the size of your event, it might "
"take a few minutes or longer until all orders are processed."
msgstr ""
"Efter att du har startat denna process kan det, beroende på storleken på "
"Efter att du har startat denna operation kan det, beroende på storleken på "
"ditt evenemang, ta några minuter eller längre tid innan alla bokningar är "
"behandlade."
@@ -31866,7 +31866,7 @@ msgstr "Ändra kontaktinformation"
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html:167
msgid "Confirmations"
msgstr "Villkor"
msgstr "Bekräftelser"
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html:185
msgid ""

View File

@@ -462,28 +462,14 @@ class BadgeExporter(BaseExporter):
)),
('date_from',
forms.DateField(
label=_('Start event date'),
label=_('Start date'),
widget=forms.DateInput(attrs={'class': 'datepickerfield'}),
required=False,
help_text=_('Only include tickets for dates on or after this date.')
)),
('date_to',
forms.DateField(
label=_('End event date'),
widget=forms.DateInput(attrs={'class': 'datepickerfield'}),
required=False,
help_text=_('Only include tickets ordered on or before this date.')
)),
('order_date_from',
forms.DateField(
label=_('Start order date'),
widget=forms.DateInput(attrs={'class': 'datepickerfield'}),
required=False,
help_text=_('Only include tickets ordered on or after this date.')
)),
('order_date_to',
forms.DateField(
label=_('End order date'),
label=_('End date'),
widget=forms.DateInput(attrs={'class': 'datepickerfield'}),
required=False,
help_text=_('Only include tickets for dates on or before this date.')
@@ -495,7 +481,6 @@ class BadgeExporter(BaseExporter):
('name', _('Attendee name')),
('company', _('Attendee company')),
('code', _('Order code')),
('order_date', _('Order date')),
('date', _('Event date')),
] + ([
('name:{}'.format(k), _('Attendee name: {part}').format(part=label))
@@ -548,24 +533,6 @@ class BadgeExporter(BaseExporter):
), self.event.timezone)
qs = qs.filter(Q(subevent__date_from__lt=dt) | Q(subevent__isnull=True, order__event__date_from__lt=dt))
if form_data.get('order_date_from'):
if not isinstance(form_data.get('order_date_from'), date):
form_data['order_date_from'] = dateutil.parser.parse(form_data['order_date_from']).date()
df = make_aware(datetime.combine(
form_data['order_date_from'],
time(hour=0, minute=0, second=0)
), self.event.timezone)
qs = qs.filter(order__datetime__gte=df)
if form_data.get('order_date_to'):
if not isinstance(form_data.get('order_date_to'), date):
form_data['order_date_to'] = dateutil.parser.parse(form_data['order_date_to']).date()
dt = make_aware(datetime.combine(
form_data['order_date_to'] + timedelta(days=1),
time(hour=0, minute=0, second=0)
), self.event.timezone)
qs = qs.filter(order__datetime__lt=dt)
if form_data.get('order_by') == 'name':
qs = qs.annotate(
resolved_name=Case(
@@ -586,8 +553,6 @@ class BadgeExporter(BaseExporter):
).order_by('resolved_company', 'order__code')
elif form_data.get('order_by') == 'code':
qs = qs.order_by('order__code')
elif form_data.get('order_by') == 'order_date':
qs = qs.order_by('order__datetime')
elif form_data.get('order_by') == 'date':
qs = qs.annotate(ed=Coalesce('subevent__date_from', 'order__event__date_from')).order_by('ed', 'order__code')
elif form_data.get('order_by', '').startswith('name:'):

View File

@@ -21,7 +21,6 @@
#
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
@@ -67,7 +66,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, ("", "-- %s --" % pgettext("filter_empty", "all")))
choices.insert(0, ("", ""))
if len(choices) > 1:
fields[f"attr[{prop.name}]"] = {
"label": str(prop.public_label) or prop.name,

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 and worker_id.startswith("gw"):
if worker_id.startswith("gw"):
redis_port = 1000 + int(worker_id.replace("gw", ""))
else:
redis_port = 1000

View File

@@ -228,7 +228,6 @@ def checkin_list_env():
# checkin
Checkin.objects.create(position=op_a1_ticket, datetime=now() + timedelta(minutes=1), list=cl)
Checkin.objects.create(position=op_a3_ticket, list=cl)
Checkin.objects.create(position=op_a3_ticket, list=cl, type="exit")
return event, user, orga, [item_ticket, item_mascot], [order_pending, order_a1, order_a2, order_a3], \
[op_pending_ticket, op_a1_ticket, op_a1_mascot, op_a2_ticket, op_a3_ticket], cl
@@ -261,10 +260,8 @@ def test_checkins_list_ordering(client, checkin_list_env, order_key, expected):
@pytest.mark.django_db
@pytest.mark.parametrize("query, expected", [
('status=&item=&user=', ['A1Ticket', 'A1Mascot', 'A2Ticket', 'A3Ticket']),
('status=0&item=&user=', ['A1Mascot', 'A2Ticket']),
('status=1&item=&user=', ['A1Ticket', 'A3Ticket']),
('status=2&item=&user=', ['A1Ticket']),
('status=3&item=&user=', ['A3Ticket']),
('status=0&item=&user=', ['A1Mascot', 'A2Ticket']),
('status=&item=&user=a3dummy', ['A3Ticket']), # match order email
('status=&item=&user=a3dummy', ['A3Ticket']), # match order email,
('status=&item=&user=a4', ['A3Ticket']), # match attendee name

View File

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