Compare commits

..

2 Commits

Author SHA1 Message Date
Raphael Michel 4402987b8a Fix broken links 2026-03-27 16:50:55 +01:00
Raphael Michel 06278acd0a Overhaul contribution guide & add a AI policy 2026-03-27 16:47:12 +01:00
233 changed files with 76707 additions and 81926 deletions
+1 -1
View File
@@ -24,7 +24,7 @@ jobs:
name: Packaging
strategy:
matrix:
python-version: ["3.13"]
python-version: ["3.11"]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
+4 -4
View File
@@ -24,10 +24,10 @@ jobs:
name: Check gettext syntax
steps:
- uses: actions/checkout@v4
- name: Set up Python 3.13
- name: Set up Python 3.11
uses: actions/setup-python@v5
with:
python-version: 3.13
python-version: 3.11
- uses: actions/cache@v4
with:
path: ~/.cache/pip
@@ -49,10 +49,10 @@ jobs:
name: Spellcheck
steps:
- uses: actions/checkout@v4
- name: Set up Python 3.13
- name: Set up Python 3.11
uses: actions/setup-python@v5
with:
python-version: 3.13
python-version: 3.11
- uses: actions/cache@v4
with:
path: ~/.cache/pip
+6 -6
View File
@@ -24,10 +24,10 @@ jobs:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Set up Python 3.13
- name: Set up Python 3.11
uses: actions/setup-python@v5
with:
python-version: 3.13
python-version: 3.11
- uses: actions/cache@v4
with:
path: ~/.cache/pip
@@ -44,10 +44,10 @@ jobs:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Set up Python 3.13
- name: Set up Python 3.11
uses: actions/setup-python@v5
with:
python-version: 3.13
python-version: 3.11
- uses: actions/cache@v4
with:
path: ~/.cache/pip
@@ -64,10 +64,10 @@ jobs:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Set up Python 3.13
- name: Set up Python 3.11
uses: actions/setup-python@v5
with:
python-version: 3.13
python-version: 3.11
- name: Install Dependencies
run: pip3 install licenseheaders
- name: Run licenseheaders
+2 -4
View File
@@ -23,15 +23,13 @@ jobs:
name: Tests
strategy:
matrix:
python-version: ["3.11", "3.13", "3.14"]
python-version: ["3.10", "3.11", "3.13"]
database: [sqlite, postgres]
exclude:
- database: sqlite
python-version: "3.10"
- database: sqlite
python-version: "3.11"
- database: sqlite
python-version: "3.12"
services:
postgres:
image: postgres:15
@@ -83,4 +81,4 @@ jobs:
file: src/coverage.xml
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: false
if: matrix.database == 'postgres' && matrix.python-version == '3.13'
if: matrix.database == 'postgres' && matrix.python-version == '3.11'
+1 -2
View File
@@ -1,4 +1,4 @@
FROM python:3.13-trixie
FROM python:3.11-bookworm
RUN apt-get update && \
apt-get install -y --no-install-recommends \
@@ -31,7 +31,6 @@ RUN apt-get update && \
mkdir /etc/pretix && \
mkdir /data && \
useradd -ms /bin/bash -d /pretix -u 15371 pretixuser && \
chmod 0755 /pretix && \
echo 'pretixuser ALL=(ALL) NOPASSWD:SETENV: /usr/bin/supervisord' >> /etc/sudoers && \
mkdir /static && \
mkdir /etc/supervisord
+6 -13
View File
@@ -16,7 +16,6 @@ Field Type Description
id integer Internal ID of the program time
start datetime The start date time for this program time slot.
end datetime The end date time for this program time slot.
location multi-lingual string The program time slot's location (or ``null``)
===================================== ========================== =======================================================
.. versionchanged:: TODO
@@ -55,20 +54,17 @@ Endpoints
{
"id": 2,
"start": "2025-08-14T22:00:00Z",
"end": "2025-08-15T00:00:00Z",
"location": null
"end": "2025-08-15T00:00:00Z"
},
{
"id": 3,
"start": "2025-08-12T22:00:00Z",
"end": "2025-08-13T22:00:00Z",
"location": null
"end": "2025-08-13T22:00:00Z"
},
{
"id": 14,
"start": "2025-08-15T22:00:00Z",
"end": "2025-08-17T22:00:00Z",
"location": null
"end": "2025-08-17T22:00:00Z"
}
]
}
@@ -103,8 +99,7 @@ Endpoints
{
"id": 1,
"start": "2025-08-15T22:00:00Z",
"end": "2025-10-27T23:00:00Z",
"location": null
"end": "2025-10-27T23:00:00Z"
}
:param organizer: The ``slug`` field of the organizer to fetch
@@ -130,8 +125,7 @@ Endpoints
{
"start": "2025-08-15T10:00:00Z",
"end": "2025-08-15T22:00:00Z",
"location": null
"end": "2025-08-15T22:00:00Z"
}
**Example response**:
@@ -145,8 +139,7 @@ Endpoints
{
"id": 17,
"start": "2025-08-15T10:00:00Z",
"end": "2025-08-15T22:00:00Z",
"location": null
"end": "2025-08-15T22:00:00Z"
}
:param organizer: The ``slug`` field of the organizer of the event/item to create a program time for
+13 -14
View File
@@ -3,7 +3,7 @@ name = "pretix"
dynamic = ["version"]
description = "Reinventing presales, one ticket at a time"
readme = "README.rst"
requires-python = ">=3.11"
requires-python = ">=3.10"
license = {file = "LICENSE"}
keywords = ["tickets", "web", "shop", "ecommerce"]
authors = [
@@ -19,11 +19,10 @@ classifiers = [
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
"Environment :: Web Environment",
"License :: OSI Approved :: GNU Affero General Public License v3",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Framework :: Django :: 5.2",
"Framework :: Django :: 4.2",
]
dependencies = [
@@ -33,11 +32,11 @@ dependencies = [
"bleach==6.3.*",
"celery==5.6.*",
"chardet==5.2.*",
"cryptography>=47.0.0",
"cryptography>=44.0.0",
"css-inline==0.20.*",
"defusedcsv>=3.0.0",
"defusedcsv>=1.1.0",
"dnspython==2.*",
"Django[argon2]==5.2.*",
"Django[argon2]==4.2.*,>=4.2.26",
"django-bootstrap3==26.1",
"django-compressor==4.6.0",
"django-countries==8.2.*",
@@ -60,7 +59,7 @@ dependencies = [
"dnspython==2.8.*",
"drf_ujson2==1.7.*",
"geoip2==5.*",
"importlib_metadata==9.*", # Polyfill, we can probably drop this once we require Python 3.10+
"importlib_metadata==8.*", # Polyfill, we can probably drop this once we require Python 3.10+
"isoweek",
"jsonschema",
"kombu==5.6.*",
@@ -76,7 +75,7 @@ dependencies = [
"paypal-checkout-serversdk==1.0.*",
"PyJWT==2.12.*",
"phonenumberslite==9.0.*",
"Pillow==12.2.*",
"Pillow==12.1.*",
"pretix-plugin-build",
"protobuf==7.34.*",
"psycopg2-binary",
@@ -90,14 +89,14 @@ dependencies = [
"pytz-deprecation-shim==0.1.*",
"pyuca",
"qrcode==8.2",
"redis==7.4.*",
"redis==7.1.*",
"reportlab==4.4.*",
"requests==2.32.*",
"sentry-sdk==2.58.*",
"sentry-sdk==2.54.*",
"sepaxml==2.7.*",
"stripe==7.9.*",
"text-unidecode==1.*",
"tlds>=2026041800",
"tlds>=2020041600",
"tqdm==4.*",
"ua-parser==1.0.*",
"vobject==0.9.*",
@@ -117,7 +116,7 @@ dev = [
"isort==8.0.*",
"pep8-naming==0.15.*",
"potypo",
"pytest-asyncio>=1.3.0",
"pytest-asyncio>=0.24",
"pytest-cache",
"pytest-cov",
"pytest-django==4.*",
+1 -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
# <https://www.gnu.org/licenses/>.
#
__version__ = "2026.5.0.dev0"
__version__ = "2026.3.0.dev0"
+2 -2
View File
@@ -191,7 +191,7 @@ class InlineItemAddOnSerializer(serializers.ModelSerializer):
class InlineItemProgramTimeSerializer(serializers.ModelSerializer):
class Meta:
model = ItemProgramTime
fields = ('start', 'end', 'location')
fields = ('start', 'end')
class ItemBundleSerializer(serializers.ModelSerializer):
@@ -222,7 +222,7 @@ class ItemBundleSerializer(serializers.ModelSerializer):
class ItemProgramTimeSerializer(serializers.ModelSerializer):
class Meta:
model = ItemProgramTime
fields = ('id', 'start', 'end', 'location')
fields = ('id', 'start', 'end')
def validate(self, data):
data = super().validate(data)
+3 -25
View File
@@ -31,9 +31,7 @@ from pretix.api.serializers.order import OrderPositionSerializer
from pretix.api.serializers.organizer import (
CustomerSerializer, GiftCardSerializer,
)
from pretix.base.models import (
Device, Order, OrderPosition, ReusableMedium, TeamAPIToken,
)
from pretix.base.models import Order, OrderPosition, ReusableMedium
logger = logging.getLogger(__name__)
@@ -82,7 +80,8 @@ class ReusableMediaSerializer(I18nAwareModelSerializer):
)
if 'linked_orderposition' in self.context['request'].query_params.getlist('expand'):
# Permission Check performed in to_representation
# No additional permission check performed, documented limitation of the permission system
# Would get to complex/unusable otherwise since the permission depends on the event
self.fields['linked_orderposition'] = NestedOrderPositionSerializer(read_only=True)
else:
self.fields['linked_orderposition'] = serializers.PrimaryKeyRelatedField(
@@ -118,27 +117,6 @@ class ReusableMediaSerializer(I18nAwareModelSerializer):
)
return data
def to_representation(self, instance):
r = super().to_representation(instance)
request = self.context.get('request')
# late permission evaluations for checks that depend on the actual linked events
expand_nested = self.context['request'].query_params.getlist('expand')
perm_holder = request.auth if isinstance(request.auth, (Device, TeamAPIToken)) else request.user
if 'linked_orderposition' in expand_nested:
if instance.linked_orderposition is not None:
event = instance.linked_orderposition.order.event
if not perm_holder.has_event_permission(event.organizer, event, 'event.orders:read', request):
r['linked_orderposition'] = {'id': instance.linked_orderposition.id}
if 'linked_giftcard.owner_ticket' in expand_nested:
gc = instance.linked_giftcard
if gc is not None and gc.owner_ticket is not None:
event = gc.owner_ticket.order.event
if not perm_holder.has_event_permission(event.organizer, event, 'event.orders:read', request):
r['linked_giftcard']['owner_ticket'] = {'id': instance.linked_giftcard.owner_ticket.id}
return r
class Meta:
model = ReusableMedium
fields = (
+6 -13
View File
@@ -769,11 +769,7 @@ class PaymentDetailsField(serializers.Field):
pp = value.payment_provider
if not pp:
return {}
try:
return pp.api_payment_details(value)
except Exception:
logger.exception("Failed to retrieve payment_details")
return {}
return pp.api_payment_details(value)
class OrderPaymentSerializer(I18nAwareModelSerializer):
@@ -1416,7 +1412,6 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
qa = QuotaAvailability()
qa.queue(*[q for q, d in quota_diff_for_locking.items() if d > 0])
qa.compute()
v_avail = {}
# These are not technically correct as diff use due to the time offset applied above, so let's prevent accidental
# use further down
@@ -1446,13 +1441,11 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
voucher_usage[v] += 1
if voucher_usage[v] > 0:
if v not in v_avail:
v.refresh_from_db(fields=['redeemed'])
redeemed_in_carts = CartPosition.objects.filter(
Q(voucher=v) & Q(event=self.context['event']) & Q(expires__gte=now_dt)
).exclude(pk__in=[cp.pk for cp in delete_cps])
v_avail[v] = v.max_usages - v.redeemed - redeemed_in_carts.count()
if v_avail[v] < voucher_usage[v]:
redeemed_in_carts = CartPosition.objects.filter(
Q(voucher=pos_data['voucher']) & Q(event=self.context['event']) & Q(expires__gte=now_dt)
).exclude(pk__in=[cp.pk for cp in delete_cps])
v_avail = v.max_usages - v.redeemed - redeemed_in_carts.count()
if v_avail < voucher_usage[v]:
errs[i]['voucher'] = [
'The voucher has already been used the maximum number of times.'
]
-13
View File
@@ -286,19 +286,6 @@ class GiftCardSerializer(I18nAwareModelSerializer):
)
return data
def to_representation(self, instance):
r = super().to_representation(instance)
request = self.context.get('request')
# late permission evaluations for checks that depend on the actual linked events
if 'owner_ticket' in self.context['request'].query_params.getlist('expand'):
owner_ticket = instance.owner_ticket
if owner_ticket:
event = owner_ticket.order.event
perm_holder = request.auth if isinstance(request.auth, (Device, TeamAPIToken)) else request.user
if not perm_holder.has_event_permission(event.organizer, event, 'event.orders:read', request):
r['owner_ticket'] = {'id': instance.owner_ticket.id}
return r
class Meta:
model = GiftCard
fields = ('id', 'secret', 'issuance', 'value', 'currency', 'testmode', 'expires', 'conditions', 'owner_ticket',
+1 -1
View File
@@ -1122,7 +1122,7 @@ class CheckinViewSet(viewsets.ReadOnlyModelViewSet):
permission = 'event.orders:read'
def get_queryset(self):
qs = Checkin.all.filter(list__event=self.request.event).select_related(
qs = Checkin.all.filter().select_related(
"position",
"device",
)
+28 -43
View File
@@ -381,15 +381,12 @@ class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
resp = HttpResponse(ct.file.file.read(), content_type='text/uri-list')
return resp
else:
return FileResponse(
ct.file.file,
filename='{}-{}-{}{}'.format(
self.request.event.slug.upper(), order.code,
provider.identifier, ct.extension
),
as_attachment=True,
content_type=ct.type
resp = FileResponse(ct.file.file, content_type=ct.type)
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}{}"'.format(
self.request.event.slug.upper(), order.code,
provider.identifier, ct.extension
)
return resp
@action(detail=True, methods=['POST'])
def mark_paid(self, request, **kwargs):
@@ -1306,17 +1303,14 @@ class EventOrderPositionViewSet(OrderPositionViewSetMixin, viewsets.ModelViewSet
raise NotFound()
ftype, ignored = mimetypes.guess_type(answer.file.name)
return FileResponse(
answer.file,
filename='{}-{}-{}-{}'.format(
self.request.event.slug.upper(),
pos.order.code,
pos.positionid,
os.path.basename(answer.file.name).split('.', 1)[1]
),
as_attachment=True,
content_type=ftype or 'application/binary'
resp = FileResponse(answer.file, content_type=ftype or 'application/binary')
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}-{}"'.format(
self.request.event.slug.upper(),
pos.order.code,
pos.positionid,
os.path.basename(answer.file.name).split('.', 1)[1]
)
return resp
@action(detail=True, url_name="printlog", url_path="printlog", methods=["POST"])
def printlog(self, request, **kwargs):
@@ -1371,18 +1365,15 @@ class EventOrderPositionViewSet(OrderPositionViewSetMixin, viewsets.ModelViewSet
if hasattr(image_file, 'seek'):
image_file.seek(0)
return FileResponse(
image_file,
filename='{}-{}-{}-{}.{}'.format(
self.request.event.slug.upper(),
pos.order.code,
pos.positionid,
key,
extension,
),
as_attachment=True,
content_type=ftype or 'application/binary'
resp = FileResponse(image_file, content_type=ftype or 'application/binary')
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}-{}.{}"'.format(
self.request.event.slug.upper(),
pos.order.code,
pos.positionid,
key,
extension,
)
return resp
@action(detail=True, url_name='download', url_path='download/(?P<output>[^/]+)')
def download(self, request, output, **kwargs):
@@ -1408,15 +1399,12 @@ class EventOrderPositionViewSet(OrderPositionViewSetMixin, viewsets.ModelViewSet
resp = HttpResponse(ct.file.file.read(), content_type='text/uri-list')
return resp
else:
return FileResponse(
ct.file.file,
filename='{}-{}-{}-{}{}'.format(
self.request.event.slug.upper(), pos.order.code, pos.positionid,
provider.identifier, ct.extension
),
as_attachment=True,
content_type=ct.type
resp = FileResponse(ct.file.file, content_type=ct.type)
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}-{}{}"'.format(
self.request.event.slug.upper(), pos.order.code, pos.positionid,
provider.identifier, ct.extension
)
return resp
@action(detail=True, methods=['POST'])
def regenerate_secrets(self, request, **kwargs):
@@ -1998,12 +1986,9 @@ class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
if not invoice.file:
raise RetryException()
return FileResponse(
invoice.file.file,
filename='{}.pdf'.format(invoice.number),
as_attachment=True,
content_type='application/pdf'
)
resp = FileResponse(invoice.file.file, content_type='application/pdf')
resp['Content-Disposition'] = 'attachment; filename="{}.pdf"'.format(invoice.number)
return resp
@action(detail=True, methods=['POST'])
def transmit(self, request, **kwargs):
-80
View File
@@ -19,10 +19,7 @@
# 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 ipaddress
import logging
import smtplib
import socket
from itertools import groupby
from smtplib import SMTPResponseException
from typing import TypeVar
@@ -240,80 +237,3 @@ def base_renderers(sender, **kwargs):
def get_email_context(**kwargs):
return PlaceholderContext(**kwargs).render_all()
def create_connection(address, timeout=socket.getdefaulttimeout(),
source_address=None, *, all_errors=False):
# Taken from the python stdlib, extended with a check for local ips
host, port = address
exceptions = []
for res in socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM):
af, socktype, proto, canonname, sa = res
if not getattr(settings, "MAIL_CUSTOM_SMTP_ALLOW_PRIVATE_NETWORKS", False):
ip_addr = ipaddress.ip_address(sa[0])
if ip_addr.is_multicast:
raise socket.error(f"Request to multicast address {sa[0]} blocked")
if ip_addr.is_loopback or ip_addr.is_link_local:
raise socket.error(f"Request to local address {sa[0]} blocked")
if ip_addr.is_private:
raise socket.error(f"Request to private address {sa[0]} blocked")
sock = None
try:
sock = socket.socket(af, socktype, proto)
if timeout is not socket.getdefaulttimeout():
sock.settimeout(timeout)
if source_address:
sock.bind(source_address)
sock.connect(sa)
# Break explicitly a reference cycle
exceptions.clear()
return sock
except socket.error as exc:
if not all_errors:
exceptions.clear() # raise only the last error
exceptions.append(exc)
if sock is not None:
sock.close()
if len(exceptions):
try:
if not all_errors:
raise exceptions[0]
raise ExceptionGroup("create_connection failed", exceptions)
finally:
# Break explicitly a reference cycle
exceptions.clear()
else:
raise socket.error("getaddrinfo returns an empty list")
class CheckPrivateNetworkMixin:
# _get_socket taken 1:1 from smtplib, just with a call to our own create_connection
def _get_socket(self, host, port, timeout):
# This makes it simpler for SMTP_SSL to use the SMTP connect code
# and just alter the socket connection bit.
if timeout is not None and not timeout:
raise ValueError('Non-blocking socket (timeout=0) is not supported')
if self.debuglevel > 0:
self._print_debug('connect: to', (host, port), self.source_address)
return create_connection((host, port), timeout, self.source_address)
class SMTP(CheckPrivateNetworkMixin, smtplib.SMTP):
pass
# SMTP used here instead of mixin, because smtp.SMTP_SSL._get_socket calls super()._get_socket and then wraps this socket
# super()._get_socket needs to be our version from the mixin
class SMTP_SSL(smtplib.SMTP_SSL, SMTP): # noqa: N801
pass
class CheckPrivateNetworkSmtpBackend(EmailBackend):
@property
def connection_class(self):
return SMTP_SSL if self.use_ssl else SMTP
+3 -13
View File
@@ -47,7 +47,6 @@ from django.utils.formats import localize
from django.utils.translation import gettext, gettext_lazy as _
from pretix.base.models import Event
from pretix.base.models.auth import PermissionHolder
from pretix.helpers.safe_openpyxl import ( # NOQA: backwards compatibility for plugins using excel_safe
SafeWorkbook, remove_invalid_excel_chars as excel_safe,
)
@@ -60,20 +59,11 @@ class BaseExporter:
This is the base class for all data exporters
"""
def __init__(self, event, organizer, permission_holder: PermissionHolder=None, progress_callback=lambda v: None):
"""
:param event: Event context, can also be a queryset of events for multi-event exports
:param organizer: Organizer context
:param user: The user who triggered the export (or None).
:param token: The API token that triggered the export (or None).
:param device: The device that triggered the export (or None)
:param progress_callback: Callback function with progress
"""
def __init__(self, event, organizer, progress_callback=lambda v: None):
self.event = event
self.organizer = organizer
self.progress_callback = progress_callback
self.is_multievent = isinstance(event, QuerySet)
self.permission_holder = permission_holder
if isinstance(event, QuerySet):
self.events = event
self.event = None
@@ -190,7 +180,7 @@ class BaseExporter:
return True
@classmethod
def get_required_event_permission(cls) -> Optional[str]:
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.
@@ -205,7 +195,7 @@ class OrganizerLevelExportMixin:
raise TypeError("required_event_permission may not be called on OrganizerLevelExportMixin")
@classmethod
def get_required_organizer_permission(cls) -> Optional[str]:
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
allow everyone with any access to the organizer.
-14
View File
@@ -1103,25 +1103,13 @@ class PaymentListExporter(ListExporter):
def iterate_list(self, form_data):
provider_names = dict(get_all_payment_providers())
i_numbers = Invoice.objects.filter(
order=OuterRef('order_id'),
).values('order').annotate(
m=GroupConcat('full_invoice_no', delimiter=', ')
).values(
'm'
).order_by()
payments = OrderPayment.objects.filter(
order__event__in=self.events,
state__in=form_data.get('payment_states', [])
).annotate(
order_invoice_numbers=Subquery(i_numbers, output_field=CharField()),
).select_related('order').prefetch_related('order__event').order_by('created')
refunds = OrderRefund.objects.filter(
order__event__in=self.events,
state__in=form_data.get('refund_states', [])
).annotate(
order_invoice_numbers=Subquery(i_numbers, output_field=CharField()),
).select_related('order').prefetch_related('order__event').order_by('created')
if form_data.get('end_date_range'):
@@ -1147,7 +1135,6 @@ class PaymentListExporter(ListExporter):
headers = [
_('Event slug'), _('Order'), _('Payment ID'), _('Creation date'), _('Completion date'), _('Status'),
_('Status code'), _('Amount'), _('Payment method'), _('Comment'), _('Matching ID'), _('Payment details'),
_('Invoice numbers'),
]
yield headers
@@ -1185,7 +1172,6 @@ class PaymentListExporter(ListExporter):
obj.comment if isinstance(obj, OrderRefund) else "",
matching_id,
payment_details,
obj.order_invoice_numbers,
]
yield row
+2 -1
View File
@@ -196,7 +196,8 @@ class RegistrationForm(forms.Form):
def clean_password(self):
password1 = self.cleaned_data.get('password', '')
user = User(email=self.cleaned_data.get('email'))
validate_password(password1, user=user)
if validate_password(password1, user=user) is not None:
raise forms.ValidationError(_(password_validators_help_texts()), code='pw_invalid')
return password1
def clean_email(self):
+5 -11
View File
@@ -45,6 +45,7 @@ import pycountry
from django import forms
from django.conf import settings
from django.contrib import messages
from django.contrib.gis.geoip2 import GeoIP2
from django.core.exceptions import ValidationError
from django.core.files.uploadedfile import SimpleUploadedFile
from django.core.validators import (
@@ -90,7 +91,7 @@ from pretix.base.settings import (
COUNTRIES_WITH_STATE_IN_ADDRESS, COUNTRY_STATE_LABEL,
PERSON_NAME_SALUTATIONS, PERSON_NAME_SCHEMES, PERSON_NAME_TITLE_GROUPS,
)
from pretix.base.templatetags.rich_text import URL_RE, rich_text
from pretix.base.templatetags.rich_text import rich_text
from pretix.base.timemachine import time_machine_now
from pretix.control.forms import (
ExtFileField, ExtValidationMixin, SizeValidationMixin, SplitDateTimeField,
@@ -101,7 +102,6 @@ from pretix.helpers.countries import (
from pretix.helpers.escapejson import escapejson_attr
from pretix.helpers.http import get_client_ip
from pretix.helpers.i18n import get_format_without_seconds
from pretix.helpers.security import get_geoip
from pretix.presale.signals import question_form_fields
logger = logging.getLogger(__name__)
@@ -227,15 +227,9 @@ class NamePartsFormField(forms.MultiValueField):
# bots.
r'^[^$€/%§{}<>~]*$',
message=_('Please do not use special characters in names.')
),
RegexValidator(
URL_RE,
inverse_match=True,
message=_('Please do not use special characters in names.')
)
]
}
self.max_length = defaults['max_length']
self.scheme_name = kwargs.pop('scheme')
self.titles = kwargs.pop('titles')
self.scheme = PERSON_NAME_SCHEMES.get(self.scheme_name)
@@ -293,7 +287,7 @@ class NamePartsFormField(forms.MultiValueField):
if self.require_all_fields and not all(v for v in value):
raise forms.ValidationError(self.error_messages['incomplete'], code='required')
if sum(len(v) for v in value.values() if v) > (self.max_length or 250):
if sum(len(v) for v in value.values() if v) > 250:
raise forms.ValidationError(_('Please enter a shorter name.'), code='max_length')
if value.get("salutation") == "empty":
@@ -399,7 +393,7 @@ class WrappedPhoneNumberPrefixWidget(PhoneNumberPrefixWidget):
def guess_country_from_request(request, event):
if settings.HAS_GEOIP:
g = get_geoip()
g = GeoIP2()
try:
res = g.country(get_client_ip(request))
if res['country_code'] and len(res['country_code']) == 2:
@@ -939,7 +933,7 @@ class BaseQuestionsForm(forms.Form):
label=label, required=required,
help_text=help_text,
initial=_initial,
widget=TimePickerWidget(without_seconds=True),
widget=TimePickerWidget(time_format=get_format_without_seconds('TIME_INPUT_FORMATS')),
)
elif q.type == Question.TYPE_DATETIME:
if not help_text:
+5 -50
View File
@@ -43,10 +43,6 @@ from django.utils.timezone import get_current_timezone, now
from django.utils.translation import gettext_lazy as _
from pretix.helpers.format import PlainHtmlAlternativeString
from pretix.helpers.i18n import (
get_format_without_seconds, get_javascript_format,
get_javascript_format_without_seconds,
)
def replace_arabic_numbers(inp):
@@ -112,7 +108,7 @@ class DatePickerWidget(forms.DateInput):
class TimePickerWidget(forms.TimeInput):
def __init__(self, attrs=None, time_format=None, without_seconds=False):
def __init__(self, attrs=None, time_format=None):
attrs = attrs or {}
if 'placeholder' in attrs:
del attrs['placeholder']
@@ -121,27 +117,8 @@ class TimePickerWidget(forms.TimeInput):
time_attrs['class'] += ' timepickerfield'
time_attrs['autocomplete'] = 'off'
if time_format or without_seconds:
# Explicitly set data-format attributes for the JS layer instead of relying on the body-wide config
def time_format_attr():
if without_seconds:
return get_javascript_format_without_seconds(time_format or "TIME_INPUT_FORMATS")
return get_javascript_format(time_format or "TIME_INPUT_FORMATS")
time_attrs['data-format'] = lazy(time_format_attr, str)
def time_format_attr():
if without_seconds:
return get_javascript_format_without_seconds(time_format or "TIME_INPUT_FORMATS")
return get_javascript_format(time_format or "TIME_INPUT_FORMATS")
time_attrs['data-format'] = lazy(time_format_attr, str)
def placeholder():
if without_seconds:
tf = time_format or get_format_without_seconds('TIME_INPUT_FORMATS')
else:
tf = time_format or get_format('TIME_INPUT_FORMATS')[0]
tf = time_format or get_format('TIME_INPUT_FORMATS')[0]
return now().replace(
year=2000, month=1, day=1, hour=0, minute=0, second=0, microsecond=0
).strftime(tf)
@@ -205,7 +182,7 @@ class UploadedFileWidget(forms.ClearableFileInput):
class SplitDateTimePickerWidget(forms.SplitDateTimeWidget):
template_name = 'pretixbase/forms/widgets/splitdatetime.html'
def __init__(self, attrs=None, date_format=None, time_format=None, min_date=None, max_date=None, without_seconds=False):
def __init__(self, attrs=None, date_format=None, time_format=None, min_date=None, max_date=None):
attrs = attrs or {}
if 'placeholder' in attrs:
del attrs['placeholder']
@@ -228,36 +205,14 @@ class SplitDateTimePickerWidget(forms.SplitDateTimeWidget):
max_date if not isinstance(max_date, datetime) else max_date.astimezone(get_current_timezone()).date()
).isoformat()
if date_format or time_format or without_seconds:
# Explicitly set data-format attributes for the JS layer instead of relying on the body-wide config
def date_format_attr():
if without_seconds:
return get_javascript_format_without_seconds(date_format or "DATE_INPUT_FORMATS")
return get_javascript_format(date_format or "DATE_INPUT_FORMATS")
date_attrs['data-format'] = lazy(date_format_attr, str)
def time_format_attr():
if without_seconds:
return get_javascript_format_without_seconds(time_format or "TIME_INPUT_FORMATS")
return get_javascript_format(time_format or "TIME_INPUT_FORMATS")
time_attrs['data-format'] = lazy(time_format_attr, str)
def date_placeholder():
if without_seconds:
df = date_format or get_format_without_seconds('DATE_INPUT_FORMATS')
else:
df = date_format or get_format('DATE_INPUT_FORMATS')[0]
df = date_format or get_format('DATE_INPUT_FORMATS')[0]
return now().replace(
year=2000, month=12, day=31, hour=18, minute=0, second=0, microsecond=0
).strftime(df)
def time_placeholder():
if without_seconds:
tf = time_format or get_format_without_seconds('TIME_INPUT_FORMATS')
else:
tf = time_format or get_format('TIME_INPUT_FORMATS')[0]
tf = time_format or get_format('TIME_INPUT_FORMATS')[0]
return now().replace(
year=2000, month=1, day=1, hour=0, minute=0, second=0, microsecond=0
).strftime(tf)
@@ -36,9 +36,8 @@ from django.core.management.commands.makemigrations import Command as Parent
from ._migrations import monkeypatch_migrations
monkeypatch_migrations()
class Command(Parent):
def handle(self, *args, **kwargs):
monkeypatch_migrations()
return super().handle(*args, **kwargs)
pass
@@ -64,7 +64,7 @@ class Command(BaseCommand):
if not periodic_task.receivers or periodic_task.sender_receivers_cache.get(self) is NO_RECEIVERS:
return
for receiver in periodic_task._live_receivers(self)[0]:
for receiver in periodic_task._live_receivers(self):
name = f'{receiver.__module__}.{receiver.__name__}'
if options['list_tasks']:
print(name)
-13
View File
@@ -24,7 +24,6 @@ from urllib.parse import urlparse, urlsplit
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
from django.conf import settings
from django.core.exceptions import BadRequest
from django.http import Http404, HttpRequest, HttpResponse
from django.middleware.common import CommonMiddleware
from django.urls import get_script_prefix, resolve
@@ -348,18 +347,6 @@ class SecurityMiddleware(MiddlewareMixin):
return resp
class RejectInvalidInputMiddleware(MiddlewareMixin):
def process_request(self, request):
# Nullbytes in GET/POST parameters are mostly harmless, as they will later fail on database insertion, but it
# keeps spamming our error logs whenever someone tries to run a vulnerability scanner.
if "\x00" in request.META['QUERY_STRING'] or "%00" in request.META['QUERY_STRING']:
raise BadRequest("Invalid characters in input.")
if request.method in ('POST', 'PUT', 'PATCH') and request.content_type == "application/x-www-form-urlencoded":
if any("\x00" in value for key, value_list in request.POST.lists() for value in value_list):
raise BadRequest("Invalid characters in input.")
class CustomCommonMiddleware(CommonMiddleware):
def get_full_path_with_slash(self, request):
@@ -41,20 +41,16 @@ class Migration(migrations.Migration):
name='datetime',
field=models.DateTimeField(),
),
migrations.AddIndex(
'logentry',
models.Index(fields=('datetime', 'id'), name="pretixbase__datetim_b1fe5a_idx"),
migrations.AlterIndexTogether(
name='logentry',
index_together={('datetime', 'id')},
),
migrations.AddIndex(
'order',
models.Index(fields=["datetime", "id"], name="pretixbase__datetim_66aff0_idx"),
migrations.AlterIndexTogether(
name='order',
index_together={('datetime', 'id'), ('last_modified', 'id')},
),
migrations.AddIndex(
'order',
models.Index(fields=["last_modified", "id"], name="pretixbase__last_mo_4ebf8b_idx"),
),
migrations.AddIndex(
'transaction',
models.Index(fields=('datetime', 'id'), name="pretixbase__datetim_b20405_idx"),
migrations.AlterIndexTogether(
name='transaction',
index_together={('datetime', 'id')},
),
]
@@ -61,10 +61,7 @@ class Migration(migrations.Migration):
options={
'ordering': ('identifier', 'type', 'organizer'),
'unique_together': {('identifier', 'type', 'organizer')},
'indexes': [
models.Index(fields=('identifier', 'type', 'organizer'), name='reusable_medium_organizer_index'),
models.Index(fields=('updated', 'id'), name="pretixbase__updated_093277_idx")
],
'index_together': {('identifier', 'type', 'organizer'), ('updated', 'id')},
},
bases=(models.Model, pretix.base.models.base.LoggingMixin),
),
+25
View File
@@ -9,6 +9,31 @@ class Migration(migrations.Migration):
]
operations = [
migrations.RenameIndex(
model_name="logentry",
new_name="pretixbase__datetim_b1fe5a_idx",
old_fields=("datetime", "id"),
),
migrations.RenameIndex(
model_name="order",
new_name="pretixbase__datetim_66aff0_idx",
old_fields=("datetime", "id"),
),
migrations.RenameIndex(
model_name="order",
new_name="pretixbase__last_mo_4ebf8b_idx",
old_fields=("last_modified", "id"),
),
migrations.RenameIndex(
model_name="reusablemedium",
new_name="pretixbase__updated_093277_idx",
old_fields=("updated", "id"),
),
migrations.RenameIndex(
model_name="transaction",
new_name="pretixbase__datetim_b20405_idx",
old_fields=("datetime", "id"),
),
migrations.AlterField(
model_name="attendeeprofile",
name="id",
@@ -1,6 +1,6 @@
# Generated by Django 4.2.10 on 2024-04-02 15:16
from django.db import migrations, models
from django.db import migrations
class Migration(migrations.Migration):
@@ -10,8 +10,8 @@ class Migration(migrations.Migration):
]
operations = [
migrations.RemoveIndex(
"reusablemedium",
'reusable_medium_organizer_index',
migrations.AlterIndexTogether(
name="reusablemedium",
index_together=set(),
),
]
@@ -1,19 +0,0 @@
# Generated by Django 4.2.27 on 2026-01-21 12:06
import i18nfield.fields
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0298_pluggable_permissions"),
]
operations = [
migrations.AddField(
model_name="itemprogramtime",
name="location",
field=i18nfield.fields.I18nTextField(max_length=200, null=True),
)
]
-4
View File
@@ -70,10 +70,6 @@ def parse_csv(file, length=None, mode="strict", charset=None):
except ImportError:
charset = file.charset
data = data.decode(charset or "utf-8", mode)
# remove stray linebreaks from the end of the file
data = data.rstrip("\n")
# If the file was modified on a Mac, it only contains \r as line breaks
if '\r' in data and '\n' not in data:
data = data.replace('\r', '\n')
+3 -9
View File
@@ -29,9 +29,7 @@ import inspect
import logging
import os
import threading
from pathlib import Path
import django
from django.conf import settings
from django.db import transaction
@@ -76,14 +74,10 @@ def _transactions_mark_order_dirty(order_id, using=None):
if "PYTEST_CURRENT_TEST" in os.environ:
# We don't care about Order.objects.create() calls in test code so let's try to figure out if this is test code
# or not.
for frame in inspect.stack()[1:]:
if (
'pretix/base/models/orders' in frame.filename
or Path(frame.filename).is_relative_to(Path(django.__file__).parent)
):
# Ignore model- and django-internal code
for frame in inspect.stack():
if 'pretix/base/models/orders' in frame.filename:
continue
elif 'test_' in frame.filename or 'conftest.py' in frame.filename:
elif 'test_' in frame.filename or 'conftest.py in frame.filename':
return
elif 'pretix/' in frame.filename or 'pretix_' in frame.filename:
# This went through non-test code, let's consider it non-test
-21
View File
@@ -38,7 +38,6 @@ import operator
import secrets
from datetime import timedelta
from functools import reduce
from typing import Protocol
from django.conf import settings
from django.contrib.auth.models import (
@@ -68,14 +67,6 @@ class EmailAddressTakenError(IntegrityError):
pass
class PermissionHolder(Protocol):
def has_event_permission(self, organizer, event, perm_name=None, request=None, session_key=None) -> bool:
...
def has_organizer_permission(self, organizer, perm_name=None, request=None):
...
class UserManager(BaseUserManager):
"""
This is the user manager for our custom user model. See the User
@@ -705,18 +696,6 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
return self.teams.exists()
class UserWithStaffSession:
# Wrapper around a User object with a staff session, implementing the PermissionHolder Protocol
def __init__(self, user):
self.user = user
def has_event_permission(self, organizer, event, perm_name=None, request=None, session_key=None) -> bool:
return True
def has_organizer_permission(self, organizer, perm_name=None, request=None):
return True
class UserKnownLoginSource(models.Model):
user = models.ForeignKey('User', on_delete=models.CASCADE, related_name="known_login_sources")
agent_type = models.CharField(max_length=255, null=True, blank=True)
+1 -2
View File
@@ -229,7 +229,7 @@ class Device(LoggedModel):
"""
return self._organizer_permission_set() if self.organizer == organizer else set()
def has_event_permission(self, organizer, event, perm_name=None, request=None, session_key=None) -> bool:
def has_event_permission(self, organizer, event, perm_name=None, request=None) -> bool:
"""
Checks if this token is part of a team that grants access of type ``perm_name``
to the event ``event``.
@@ -238,7 +238,6 @@ class Device(LoggedModel):
:param event: The event to check
:param perm_name: The permission, e.g. ``event.orders:read``
:param request: This parameter is ignored and only defined for compatibility reasons.
:param session_key: This parameter is ignored and only defined for compatibility reasons.
:return: bool
"""
has_event_access = (self.all_events and organizer == self.organizer) or (
-6
View File
@@ -715,12 +715,6 @@ class Event(EventMixin, LoggedModel):
self.settings.name_scheme = 'given_family'
self.settings.payment_banktransfer_invoice_immediately = True
self.settings.low_availability_percentage = 10
self.settings.mail_send_order_free_attendee = True
self.settings.mail_send_order_placed_attendee = True
self.settings.mail_send_order_paid_attendee = True
self.settings.mail_send_order_approved_attendee = True
self.settings.mail_send_order_approved_free_attendee = True
self.settings.mail_text_download_reminder_attendee = True
@property
def social_image(self):
-7
View File
@@ -2306,17 +2306,10 @@ class ItemProgramTime(models.Model):
:type start: datetime
:param end: The date and time this program time ends
:type end: datetime
:param location: venue
:type location: str
"""
item = models.ForeignKey('Item', related_name='program_times', on_delete=models.CASCADE)
start = models.DateTimeField(verbose_name=_("Start"))
end = models.DateTimeField(verbose_name=_("End"))
location = I18nTextField(
null=True, blank=True,
max_length=200,
verbose_name=_("Location"),
)
def clean(self):
if hasattr(self, 'item') and self.item and self.item.event.has_subevents:
+1 -1
View File
@@ -88,7 +88,7 @@ class LogEntry(models.Model):
class Meta:
ordering = ('-datetime', '-id')
indexes = [models.Index(fields=["datetime", "id"], name="pretixbase__datetim_b1fe5a_idx")]
indexes = [models.Index(fields=["datetime", "id"])]
def display(self):
from pretix.base.logentrytype_registry import log_entry_types
+1 -1
View File
@@ -122,7 +122,7 @@ class ReusableMedium(LoggedModel):
class Meta:
unique_together = (("identifier", "type", "organizer"),)
indexes = [
models.Index(fields=("updated", "id"), name="pretixbase__updated_093277_idx"),
models.Index(fields=("updated", "id")),
]
ordering = "identifier", "type", "organizer"
+5 -5
View File
@@ -336,8 +336,8 @@ class Order(LockModel, LoggedModel):
verbose_name_plural = _("Orders")
ordering = ("-datetime", "-pk")
indexes = [
models.Index(fields=["datetime", "id"], name="pretixbase__datetim_66aff0_idx"),
models.Index(fields=["last_modified", "id"], name="pretixbase__last_mo_4ebf8b_idx"),
models.Index(fields=["datetime", "id"]),
models.Index(fields=["last_modified", "id"]),
]
constraints = [
models.UniqueConstraint(fields=["organizer", "code"], name="order_organizer_code_uniq"),
@@ -590,7 +590,7 @@ class Order(LockModel, LoggedModel):
not kwargs.get('force_save_with_deferred_fields', None) and
(not update_fields or ('require_approval' not in update_fields and 'status' not in update_fields))
):
_fail("It is unsafe to call save() on an Order with deferred fields since we can't check if you missed "
_fail("It is unsafe to call save() on an OrderFee with deferred fields since we can't check if you missed "
"creating a transaction. Call save(force_save_with_deferred_fields=True) if you really want to do "
"this.")
@@ -2841,7 +2841,7 @@ class OrderPosition(AbstractPosition):
if Transaction.key(self) != self.__initial_transaction_key or self.canceled != self.__initial_canceled or not self.pk:
_transactions_mark_order_dirty(self.order_id, using=kwargs.get('using', None))
elif not kwargs.get('force_save_with_deferred_fields', None):
_fail("It is unsafe to call save() on an OrderPosition with deferred fields since we can't check if you missed "
_fail("It is unsafe to call save() on an OrderFee with deferred fields since we can't check if you missed "
"creating a transaction. Call save(force_save_with_deferred_fields=True) if you really want to do "
"this.")
@@ -3080,7 +3080,7 @@ class Transaction(models.Model):
class Meta:
ordering = 'datetime', 'pk'
indexes = [
models.Index(fields=['datetime', 'id'], name="pretixbase__datetim_b20405_idx")
models.Index(fields=['datetime', 'id'])
]
def save(self, *args, **kwargs):
+1 -8
View File
@@ -319,9 +319,6 @@ class TeamQuerySet(models.QuerySet):
def event_permission_q(cls, perm_name):
from ..permissions import assert_valid_event_permission
if perm_name is None:
return Q()
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)
@@ -334,9 +331,6 @@ class TeamQuerySet(models.QuerySet):
def organizer_permission_q(cls, perm_name):
from ..permissions import assert_valid_organizer_permission
if perm_name is None:
return Q()
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)
@@ -556,7 +550,7 @@ class TeamAPIToken(models.Model):
"""
return self.team.organizer_permission_set() if self.team.organizer == organizer else set()
def has_event_permission(self, organizer, event, perm_name=None, request=None, session_key=None) -> bool:
def has_event_permission(self, organizer, event, perm_name=None, request=None) -> bool:
"""
Checks if this token is part of a team that grants access of type ``perm_name``
to the event ``event``.
@@ -565,7 +559,6 @@ class TeamAPIToken(models.Model):
:param event: The event to check
:param perm_name: The permission, e.g. ``event.orders:read``
:param request: This parameter is ignored and only defined for compatibility reasons.
:param session_key: This parameter is ignored and only defined for compatibility reasons.
:return: bool
"""
has_event_access = (self.team.all_events and organizer == self.team.organizer) or (
+15 -32
View File
@@ -54,7 +54,7 @@ from bidi import get_display
from django.conf import settings
from django.contrib.staticfiles import finders
from django.core.exceptions import ValidationError
from django.db.models import Exists, Max, Min, OuterRef
from django.db.models import Max, Min
from django.db.models.fields.files import FieldFile
from django.dispatch import receiver
from django.utils.deconstruct import deconstructible
@@ -76,7 +76,7 @@ from reportlab.pdfgen.canvas import Canvas
from reportlab.platypus import Paragraph
from pretix.base.i18n import language
from pretix.base.models import Checkin, Event, Order, OrderPosition, Question
from pretix.base.models import Event, Order, OrderPosition, Question
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.signals import layout_image_variables, layout_text_variables
from pretix.base.templatetags.money import money_filter
@@ -379,13 +379,6 @@ DEFAULT_VARIABLES = OrderedDict((
str(p) for p in generate_compressed_addon_list(op, order, ev)
])
}),
("checked_in_addons", {
"label": _("List of Checked-In Add-Ons"),
"editor_sample": _("Add-on 1\n2x Add-on 2"),
"evaluate": lambda op, order, ev: "\n".join([
str(p) for p in generate_compressed_addon_list(op, order, ev, only_checked_in=True)
])
}),
("organizer", {
"label": _("Organizer name"),
"editor_sample": _("Event organizer company"),
@@ -498,9 +491,9 @@ DEFAULT_VARIABLES = OrderedDict((
) if op.valid_until else ""
}),
("program_times", {
"label": _("Program times"),
"label": _("Program times: date and time"),
"editor_sample": _(
"2017-05-31 10:00 12:00, Room 1\n2017-05-31 14:00 16:00, Room 2\n2017-05-31 14:00 2017-06-01 14:00, Building A"),
"2017-05-31 10:00 12:00\n2017-05-31 14:00 16:00\n2017-05-31 14:00 2017-06-01 14:00"),
"evaluate": lambda op, order, ev: get_program_times(op, ev)
}),
("medium_identifier", {
@@ -748,31 +741,21 @@ def get_seat(op: OrderPosition):
def get_program_times(op: OrderPosition, ev: Event):
ptstr = []
for pt in op.item.program_times.all():
ptstr.append([
datetimerange(
pt.start.astimezone(ev.timezone),
pt.end.astimezone(ev.timezone),
as_html=False
),
(', ' + ', '.join(
l.strip() for l in str(pt.location).splitlines() if l.strip())
) if str(pt.location).strip() else ''
])
return '\n'.join(''.join(l) for l in ptstr)
return '\n'.join([
datetimerange(
pt.start.astimezone(ev.timezone),
pt.end.astimezone(ev.timezone),
as_html=False
) for pt in op.item.program_times.all()
])
def generate_compressed_addon_list(op, order, event, only_checked_in=False):
def generate_compressed_addon_list(op, order, event):
itemcount = defaultdict(int)
addon_qs = (
addons = [p for p in (
op.addons.all() if 'addons' in getattr(op, '_prefetched_objects_cache', {})
else op.addons.select_related('item', 'variation')
)
if only_checked_in:
addon_qs = addon_qs.filter(Exists(Checkin.objects.filter(position=OuterRef('pk'))), canceled=False)
addons = [p for p in addon_qs if not p.canceled]
) if not p.canceled]
for pos in addons:
itemcount[pos.item, pos.variation] += 1
@@ -929,7 +912,7 @@ class Renderer:
# We do not use str.format like in emails so we (a) can evaluate lazily and (b) can re-implement this
# 1:1 on other platforms that render PDFs through our API (libpretixprint)
return re.sub(r'\{([-a-zA-Z0-9:_]+)\}', replace, text)
return re.sub(r'\{([a-zA-Z0-9:_]+)\}', replace, text)
elif o['content'].startswith('itemmeta:'):
if op.variation_id:
-28
View File
@@ -38,7 +38,6 @@ SOURCE_NAMES = {
None: _('European Central Bank'), # backwards-compatibility
'eu:ecb:eurofxref-daily': _('European Central Bank'),
'cz:cnb:rate-fixing-daily': _('Czech National Bank'),
'pl:nbp:table-a': _('National Bank of Poland'),
}
@@ -50,7 +49,6 @@ def fetch_rates(sender, **kwargs):
source_tasks = {
'eu:ecb:eurofxref-daily': fetch_ecb_rates,
'cz:cnb:rate-fixing-daily': fetch_cnb_cz_rates,
'pl:nbp:table-a': fetch_nbp_pl_rates,
}
for source_name, task in source_tasks.items():
@@ -146,29 +144,3 @@ def fetch_cnb_cz_rates():
rate=rate,
)
)
@app.task()
def fetch_nbp_pl_rates():
"""
Fetches currency rates from the Polish National Bank.
"""
r = requests.get("https://api.nbp.pl/api/exchangerates/tables/A/", headers={
"Accept": "application/json",
})
r.raise_for_status()
data = r.json()[0]
source_date = datetime.strptime(data["effectiveDate"], "%Y-%m-%d").date()
for r in data["rates"]:
rate = Decimal(r["mid"]).quantize(Decimal('0.000001'))
ExchangeRate.objects.update_or_create(
source='pl:nbp:table-a',
source_currency=r["code"],
other_currency='PLN',
defaults=dict(
source_date=source_date,
rate=rate,
)
)
+3 -19
View File
@@ -40,7 +40,6 @@ from pretix.base.models import (
CachedFile, Device, Event, Organizer, ScheduledEventExport, TeamAPIToken,
User, cachedfile_name,
)
from pretix.base.models.auth import UserWithStaffSession
from pretix.base.models.exports import ScheduledOrganizerExport
from pretix.base.services.mail import mail
from pretix.base.services.tasks import (
@@ -212,12 +211,7 @@ def init_event_exporters(event, user=None, token=None, device=None, request=None
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,
permission_holder=token or device or (UserWithStaffSession(user) if staff_session else user),
**kwargs
)
exporter: BaseExporter = response(event=event, organizer=event.organizer, **kwargs)
if not exporter.available_for_user(user if user and user.is_authenticated else None):
continue
@@ -249,12 +243,7 @@ def init_organizer_exporters(
continue
if issubclass(response, OrganizerLevelExportMixin):
exporter: BaseExporter = response(
event=Event.objects.none(),
organizer=organizer,
permission_holder=token or device or (UserWithStaffSession(user) if staff_session else user),
**kwargs,
)
exporter: BaseExporter = response(event=Event.objects.none(), organizer=organizer, **kwargs)
try:
if not perm_holder.has_organizer_permission(organizer, response.get_required_organizer_permission(), request) and not staff_session:
@@ -306,12 +295,7 @@ def init_organizer_exporters(
if not _has_permission_on_any_team_cache[permission_name] and not staff_session:
continue
exporter: BaseExporter = response(
event=_event_list_cache[permission_name],
organizer=organizer,
permission_holder=token or device or (UserWithStaffSession(user) if staff_session else user),
**kwargs,
)
exporter: BaseExporter = response(event=_event_list_cache[permission_name], organizer=organizer, **kwargs)
if not exporter.available_for_user(user if user and user.is_authenticated else None):
continue
+1 -15
View File
@@ -58,7 +58,6 @@ from pretix.base.invoicing.transmission import (
from pretix.base.models import (
ExchangeRate, Invoice, InvoiceAddress, InvoiceLine, Order, OrderFee,
)
from pretix.base.models.orders import OrderPayment
from pretix.base.models.tax import EU_CURRENCIES
from pretix.base.services.tasks import (
TransactionAwareProfiledEventTask, TransactionAwareTask,
@@ -103,7 +102,7 @@ def build_invoice(invoice: Invoice) -> Invoice:
introductory = invoice.event.settings.get('invoice_introductory_text', as_type=LazyI18nString)
additional = invoice.event.settings.get('invoice_additional_text', as_type=LazyI18nString)
footer = invoice.event.settings.get('invoice_footer_text', as_type=LazyI18nString)
if lp and lp.payment_provider and lp.state not in (OrderPayment.PAYMENT_STATE_FAILED, OrderPayment.PAYMENT_STATE_CANCELED):
if lp and lp.payment_provider:
if 'payment' in inspect.signature(lp.payment_provider.render_invoice_text).parameters:
payment = str(lp.payment_provider.render_invoice_text(invoice.order, lp))
else:
@@ -205,19 +204,6 @@ def build_invoice(invoice: Invoice) -> Invoice:
invoice.foreign_currency_rate = rate.rate.quantize(Decimal('0.0001'), ROUND_HALF_UP)
invoice.foreign_currency_rate_date = rate.source_date
invoice.foreign_currency_source = 'cz:cnb:rate-fixing-daily'
elif invoice.event.settings.invoice_eu_currencies == 'PLN' and invoice.event.currency != 'PLN':
invoice.foreign_currency_display = 'PLN'
if settings.FETCH_ECB_RATES:
rate = ExchangeRate.objects.filter(
source='pl:nbp:table-a',
source_currency=invoice.event.currency,
other_currency=invoice.foreign_currency_display,
source_date__gt=now().date() - timedelta(days=7)
).first()
if rate:
invoice.foreign_currency_rate = rate.rate.quantize(Decimal('0.0001'), ROUND_HALF_UP)
invoice.foreign_currency_rate_date = rate.source_date
invoice.foreign_currency_source = 'pl:nbp:table-a'
except InvoiceAddress.DoesNotExist:
ia = None
+1 -1
View File
@@ -411,7 +411,7 @@ def mail_send_task(self, **kwargs) -> bool:
try:
outgoing_mail = OutgoingMail.objects.select_for_update(of=OF_SELF).get(pk=outgoing_mail)
except OutgoingMail.DoesNotExist:
logger.info(f"Ignoring job for non existing email {outgoing_mail}")
logger.info(f"Ignoring job for non existing email {outgoing_mail.guid}")
return False
if outgoing_mail.status == OutgoingMail.STATUS_INFLIGHT:
logger.info(f"Ignoring job for inflight email {outgoing_mail.guid}")
+50 -70
View File
@@ -67,9 +67,9 @@ from pretix.base.email import get_email_context
from pretix.base.i18n import get_language_without_region, language
from pretix.base.media import MEDIA_TYPES
from pretix.base.models import (
CartPosition, Device, Event, GiftCard, Item, ItemVariation, LogEntry,
Membership, Order, OrderPayment, OrderPosition, Quota, Seat,
SeatCategoryMapping, User, Voucher,
CartPosition, Device, Event, GiftCard, Item, ItemVariation, Membership,
Order, OrderPayment, OrderPosition, Quota, Seat, SeatCategoryMapping, User,
Voucher,
)
from pretix.base.models.event import SubEvent
from pretix.base.models.orders import (
@@ -727,6 +727,8 @@ def _check_positions(event: Event, now_dt: datetime, time_machine_now_dt: dateti
_check_date(event, time_machine_now_dt)
products_seen = Counter()
q_avail = Counter()
v_avail = Counter()
v_usages = Counter()
v_budget = {}
deleted_positions = set()
@@ -791,9 +793,6 @@ def _check_positions(event: Event, now_dt: datetime, time_machine_now_dt: dateti
shared_lock_objects=[event]
)
q_avail = Counter()
v_avail = Counter()
# Check maximum order size
limit = min(int(event.settings.max_items_per_order), settings.PRETIX_MAX_ORDER_SIZE)
if sum(1 for cp in sorted_positions if not cp.addon_to) > limit:
@@ -1619,7 +1618,7 @@ class OrderChangeManager:
MembershipOperation = namedtuple('MembershipOperation', ('position', 'membership'))
CancelOperation = namedtuple('CancelOperation', ('position', 'price_diff'))
AddOperation = namedtuple('AddOperation', ('item', 'variation', 'price', 'addon_to', 'subevent', 'seat', 'membership',
'valid_from', 'valid_until', 'is_bundled', 'result', 'count'))
'valid_from', 'valid_until', 'is_bundled', 'result'))
SplitOperation = namedtuple('SplitOperation', ('position',))
FeeValueOperation = namedtuple('FeeValueOperation', ('fee', 'value', 'price_diff'))
AddFeeOperation = namedtuple('AddFeeOperation', ('fee', 'price_diff'))
@@ -1633,24 +1632,16 @@ class OrderChangeManager:
ForceRecomputeOperation = namedtuple('ForceRecomputeOperation', tuple())
class AddPositionResult:
_positions: Optional[List[OrderPosition]]
_position: Optional[OrderPosition]
def __init__(self):
self._positions = None
self._position = None
@property
def position(self) -> OrderPosition:
if self._positions is None:
if self._position is None:
raise RuntimeError("Order position has not been created yet. Call commit() first on OrderChangeManager.")
if len(self._positions) != 1:
raise RuntimeError("More than one position created.")
return self._positions[0]
@property
def positions(self) -> List[OrderPosition]:
if self._positions is None:
raise RuntimeError("Order position has not been created yet. Call commit() first on OrderChangeManager.")
return self._positions
return self._position
def __init__(self, order: Order, user=None, auth=None, notify=True, reissue_invoice=True, allow_blocked_seats=False):
self.order = order
@@ -1857,12 +1848,8 @@ class OrderChangeManager:
def add_position(self, item: Item, variation: ItemVariation, price: Decimal, addon_to: OrderPosition = None,
subevent: SubEvent = None, seat: Seat = None, membership: Membership = None,
valid_from: datetime = None, valid_until: datetime = None, count: int = 1) -> 'OrderChangeManager.AddPositionResult':
if count < 1:
raise ValueError("Count must be positive")
valid_from: datetime = None, valid_until: datetime = None) -> 'OrderChangeManager.AddPositionResult':
if isinstance(seat, str):
if count > 1:
raise ValueError("Cannot combine count > 1 with seat")
if not seat:
seat = None
else:
@@ -1916,14 +1903,14 @@ class OrderChangeManager:
if self.order.event.settings.invoice_include_free or price.gross != Decimal('0.00'):
self._invoice_dirty = True
self._totaldiff_guesstimate += price.gross * count
self._quotadiff.update({q: count for q in new_quotas})
self._totaldiff_guesstimate += price.gross
self._quotadiff.update(new_quotas)
if seat:
self._seatdiff.update([seat])
result = self.AddPositionResult()
self._operations.append(self.AddOperation(item, variation, price, addon_to, subevent, seat, membership,
valid_from, valid_until, is_bundled, result, count))
valid_from, valid_until, is_bundled, result))
return result
def split(self, position: OrderPosition):
@@ -2543,35 +2530,29 @@ class OrderChangeManager:
secret_dirty.remove(position)
position.save(update_fields=['canceled', 'secret'])
elif isinstance(op, self.AddOperation):
new_pos = []
new_logs = []
for i in range(op.count):
pos = OrderPosition.objects.create(
item=op.item, variation=op.variation, addon_to=op.addon_to,
price=op.price.gross, order=self.order, tax_rate=op.price.rate, tax_code=op.price.code,
tax_value=op.price.tax, tax_rule=op.item.tax_rule,
positionid=nextposid, subevent=op.subevent, seat=op.seat,
used_membership=op.membership, valid_from=op.valid_from, valid_until=op.valid_until,
is_bundled=op.is_bundled,
)
nextposid += 1
new_pos.append(pos)
new_logs.append(self.order.log_action('pretix.event.order.changed.add', user=self.user, auth=self.auth, data={
'position': pos.pk,
'item': op.item.pk,
'variation': op.variation.pk if op.variation else None,
'addon_to': op.addon_to.pk if op.addon_to else None,
'price': op.price.gross,
'positionid': pos.positionid,
'membership': pos.used_membership_id,
'subevent': op.subevent.pk if op.subevent else None,
'seat': op.seat.pk if op.seat else None,
'valid_from': op.valid_from.isoformat() if op.valid_from else None,
'valid_until': op.valid_until.isoformat() if op.valid_until else None,
}, save=False))
op.result._positions = new_pos
LogEntry.bulk_create_and_postprocess(new_logs)
pos = OrderPosition.objects.create(
item=op.item, variation=op.variation, addon_to=op.addon_to,
price=op.price.gross, order=self.order, tax_rate=op.price.rate, tax_code=op.price.code,
tax_value=op.price.tax, tax_rule=op.item.tax_rule,
positionid=nextposid, subevent=op.subevent, seat=op.seat,
used_membership=op.membership, valid_from=op.valid_from, valid_until=op.valid_until,
is_bundled=op.is_bundled,
)
nextposid += 1
self.order.log_action('pretix.event.order.changed.add', user=self.user, auth=self.auth, data={
'position': pos.pk,
'item': op.item.pk,
'variation': op.variation.pk if op.variation else None,
'addon_to': op.addon_to.pk if op.addon_to else None,
'price': op.price.gross,
'positionid': pos.positionid,
'membership': pos.used_membership_id,
'subevent': op.subevent.pk if op.subevent else None,
'seat': op.seat.pk if op.seat else None,
'valid_from': op.valid_from.isoformat() if op.valid_from else None,
'valid_until': op.valid_until.isoformat() if op.valid_until else None,
})
op.result._position = pos
elif isinstance(op, self.SplitOperation):
position = position_cache.setdefault(op.position.pk, op.position)
split_positions.append(position)
@@ -2896,7 +2877,7 @@ class OrderChangeManager:
return total
def _check_order_size(self):
if (len(self.order.positions.all()) + sum([op.count for op in self._operations if isinstance(op, self.AddOperation)])) > settings.PRETIX_MAX_ORDER_SIZE:
if (len(self.order.positions.all()) + len([op for op in self._operations if isinstance(op, self.AddOperation)])) > settings.PRETIX_MAX_ORDER_SIZE:
raise OrderError(
self.error_messages['max_order_size'] % {
'max': settings.PRETIX_MAX_ORDER_SIZE,
@@ -2957,7 +2938,7 @@ class OrderChangeManager:
]) + len([
o for o in self._operations if isinstance(o, self.SplitOperation)
])
adds = sum([o.count for o in self._operations if isinstance(o, self.AddOperation)])
adds = len([o for o in self._operations if isinstance(o, self.AddOperation)])
if current > 0 and current - cancels + adds < 1:
raise OrderError(self.error_messages['complete_cancel'])
@@ -3004,18 +2985,17 @@ class OrderChangeManager:
elif isinstance(op, self.CancelOperation) and op.position in positions_to_fake_cart:
fake_cart.remove(positions_to_fake_cart[op.position])
elif isinstance(op, self.AddOperation):
for i in range(op.count):
cp = CartPosition(
event=self.event,
item=op.item,
variation=op.variation,
used_membership=op.membership,
subevent=op.subevent,
seat=op.seat,
)
cp.override_valid_from = op.valid_from
cp.override_valid_until = op.valid_until
fake_cart.append(cp)
cp = CartPosition(
event=self.event,
item=op.item,
variation=op.variation,
used_membership=op.membership,
subevent=op.subevent,
seat=op.seat,
)
cp.override_valid_from = op.valid_from
cp.override_valid_until = op.valid_until
fake_cart.append(cp)
try:
validate_memberships_in_order(self.order.customer, fake_cart, self.event, lock=True, ignored_order=self.order, testmode=self.order.testmode)
except ValidationError as e:
+1 -11
View File
@@ -100,7 +100,7 @@ def primary_font_kwargs():
choices = [('Open Sans', 'Open Sans')]
choices += sorted([
(a, FontSelect.FontOption(title=a, data=v)) for a, v in get_fonts(pdf_support_required=False).items()
(a, {"title": a, "data": v}) for a, v in get_fonts(pdf_support_required=False).items()
], key=lambda a: a[0])
return {
'choices': choices,
@@ -574,7 +574,6 @@ DEFAULTS = {
('True', _('Based on European Central Bank daily rates, whenever the invoice recipient is in an EU '
'country that uses a different currency.')),
('CZK', _('Based on Czech National Bank daily rates, whenever the invoice amount is not in CZK.')),
('PLN', _('Based on National Bank of Poland daily rates, whenever the invoice amount is not in PLN.')),
),
),
'serializer_kwargs': dict(
@@ -583,7 +582,6 @@ DEFAULTS = {
('True', _('Based on European Central Bank daily rates, whenever the invoice recipient is in an EU '
'country that uses a different currency.')),
('CZK', _('Based on Czech National Bank daily rates, whenever the invoice amount is not in CZK.')),
('PLN', _('Based on National Bank of Poland daily rates, whenever the invoice amount is not in PLN.')),
),
),
},
@@ -4150,14 +4148,6 @@ def validate_event_settings(event, settings_dict):
)
]}
)
if (
settings_dict.get('invoice_address_from_vat_id') and
settings_dict.get('invoice_address_from_country') and
settings_dict.get('invoice_address_from_country') not in VAT_ID_COUNTRIES
):
raise ValidationError({
'invoice_address_from_vat_id': _('VAT-ID is not supported for "{}".').format(settings_dict.get('invoice_address_from_country'))
})
payment_term_last = settings_dict.get('payment_term_last')
if payment_term_last and event.presale_end:
+19 -60
View File
@@ -32,7 +32,6 @@
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under the License.
import logging
import warnings
from typing import Any, Callable, Generic, List, Tuple, TypeVar
@@ -49,8 +48,6 @@ from .plugins import (
PLUGIN_LEVEL_ORGANIZER,
)
logger = logging.getLogger(__name__)
app_cache = {}
T = TypeVar('T')
@@ -63,25 +60,23 @@ def _populate_app_cache():
def get_defining_app(o):
# If sentry packed this in a wrapper, unpack that
module = getattr(o, "__module__", None)
if module and "sentry" in module:
if "sentry" in o.__module__:
o = o.__wrapped__
if hasattr(o, "__mocked_app"):
return o.__mocked_app
# Find the Django application this belongs to
searchpath = module or getattr(o.__class__, "__module__", None) or ""
searchpath = o.__module__
# Core modules are always active
if searchpath and any(searchpath.startswith(cm) for cm in settings.CORE_MODULES):
if any(searchpath.startswith(cm) for cm in settings.CORE_MODULES):
return 'CORE'
if not app_cache:
_populate_app_cache()
app = None
while searchpath:
while True:
app = app_cache.get(searchpath)
if "." not in searchpath or app:
break
@@ -162,7 +157,7 @@ class PluginSignal(Generic[T], django.dispatch.Signal):
if not app_cache:
_populate_app_cache()
for receiver in self._live_receivers(sender)[0]:
for receiver in self._sorted_receivers(sender):
if self._is_receiver_active(sender, receiver):
response = receiver(signal=self, sender=sender, **named)
responses.append((receiver, response))
@@ -184,7 +179,7 @@ class PluginSignal(Generic[T], django.dispatch.Signal):
if not app_cache:
_populate_app_cache()
for receiver in self._live_receivers(sender)[0]:
for receiver in self._sorted_receivers(sender):
if self._is_receiver_active(sender, receiver):
named[chain_kwarg_name] = response
response = receiver(signal=self, sender=sender, **named)
@@ -209,7 +204,7 @@ class PluginSignal(Generic[T], django.dispatch.Signal):
if not app_cache:
_populate_app_cache()
for receiver in self._live_receivers(sender)[0]:
for receiver in self._sorted_receivers(sender):
if self._is_receiver_active(sender, receiver):
try:
response = receiver(signal=self, sender=sender, **named)
@@ -219,35 +214,17 @@ class PluginSignal(Generic[T], django.dispatch.Signal):
responses.append((receiver, response))
return responses
def asend(self, sender: T, **named):
raise NotImplementedError() # NOQA
def asend_robust(self, sender: T, **named):
raise NotImplementedError() # NOQA
def _live_receivers(self, sender):
orig_list, orig_async_list = super()._live_receivers(sender)
if orig_async_list:
logger.error('Async receivers are not supported.')
raise NotImplementedError
def _getattr_fallback_to_class(obj, key):
return getattr(obj, key, getattr(obj.__class__, key))
def _is_core_module(receiver):
m = _getattr_fallback_to_class(receiver, "__module__")
return any(m.startswith(c) for c in settings.CORE_MODULES)
def _sorted_receivers(self, sender):
orig_list = self._live_receivers(sender)
sorted_list = sorted(
orig_list,
key=lambda receiver: (
0 if _is_core_module(receiver) else 1,
_getattr_fallback_to_class(receiver, "__module__"),
_getattr_fallback_to_class(receiver, "__name__"),
0 if any(receiver.__module__.startswith(m) for m in settings.CORE_MODULES) else 1,
receiver.__module__,
receiver.__name__,
)
)
return sorted_list, []
return sorted_list
class EventPluginSignal(PluginSignal[Event]):
@@ -323,41 +300,23 @@ class GlobalSignal(django.dispatch.Signal):
if not self.receivers or self.sender_receivers_cache.get(sender) is NO_RECEIVERS:
return response
for receiver in self._live_receivers(sender)[0]:
for receiver in self._live_receivers(sender):
named[chain_kwarg_name] = response
response = receiver(signal=self, sender=sender, **named)
return response
def asend(self, sender: T, **named):
raise NotImplementedError() # NOQA
def asend_robust(self, sender: T, **named):
raise NotImplementedError() # NOQA
def _live_receivers(self, sender):
# Ensure consistent sorting of receivers
orig_list, orig_async_list = super()._live_receivers(sender)
if orig_async_list:
logger.error('Async receivers are not supported.')
raise NotImplementedError
def _getattr_fallback_to_class(obj, key):
return getattr(obj, key, getattr(obj.__class__, key))
def _is_core_module(receiver):
m = _getattr_fallback_to_class(receiver, "__module__")
return any(m.startswith(c) for c in settings.CORE_MODULES)
orig_list = super()._live_receivers(sender)
sorted_list = sorted(
orig_list,
key=lambda receiver: (
0 if _is_core_module(receiver) else 1,
_getattr_fallback_to_class(receiver, "__module__"),
_getattr_fallback_to_class(receiver, "__name__"),
0 if any(receiver.__module__.startswith(m) for m in settings.CORE_MODULES) else 1,
receiver.__module__,
receiver.__name__,
)
)
return sorted_list, []
return sorted_list
class DeprecatedSignal(GlobalSignal):
-4
View File
@@ -20,7 +20,6 @@
# <https://www.gnu.org/licenses/>.
#
import logging
import re
from collections import defaultdict
from datetime import timedelta
from importlib import import_module
@@ -53,7 +52,6 @@ from pretix.celery_app import app
from pretix.helpers.http import redirect_to_url
logger = logging.getLogger('pretix.base.tasks')
RE_ASYNC_ID = re.compile(r"^[a-zA-Z0-9\-]+$")
class AsyncMixin:
@@ -135,8 +133,6 @@ class AsyncMixin:
def get_result(self, request):
if not request.GET.get('async_id'):
raise BadRequest("No async_id given")
if not RE_ASYNC_ID.match(request.GET.get('async_id')):
raise BadRequest("Invalid async_id given")
res = AsyncResult(request.GET.get('async_id'))
if 'ajax' in self.request.GET:
return JsonResponse(self._return_ajax_result(res, timeout=0.25))
-6
View File
@@ -34,7 +34,6 @@
import datetime
import os
from dataclasses import dataclass
from django import forms
from django.conf import settings
@@ -421,11 +420,6 @@ class SplitDateTimeField(forms.SplitDateTimeField):
class FontSelect(forms.RadioSelect):
option_template_name = 'pretixcontrol/font_option.html'
@dataclass
class FontOption:
title: str
data: str
class ItemMultipleChoiceField(SafeModelMultipleChoiceField):
def label_from_instance(self, obj):
+13 -20
View File
@@ -63,7 +63,7 @@ from pretix.base.forms import (
from pretix.base.models import Event, Organizer, TaxRule, Team
from pretix.base.models.event import EventFooterLink, EventMetaValue, SubEvent
from pretix.base.models.organizer import TeamQuerySet
from pretix.base.models.tax import TAX_CODE_LISTS, VAT_ID_COUNTRIES
from pretix.base.models.tax import TAX_CODE_LISTS
from pretix.base.reldate import RelativeDateField, RelativeDateTimeField
from pretix.base.services.placeholders import FormPlaceholderMixin
from pretix.base.settings import (
@@ -73,8 +73,8 @@ from pretix.base.settings import (
)
from pretix.base.validators import multimail_validate
from pretix.control.forms import (
FontSelect, MultipleLanguagesWidget, SalesChannelCheckboxSelectMultiple,
SlugWidget, SplitDateTimeField, SplitDateTimePickerWidget,
MultipleLanguagesWidget, SalesChannelCheckboxSelectMultiple, SlugWidget,
SplitDateTimeField, SplitDateTimePickerWidget,
)
from pretix.control.forms.widgets import Select2
from pretix.helpers.countries import CachedCountries
@@ -197,10 +197,10 @@ class EventWizardBasicsForm(I18nModelForm):
'presale_end': SplitDateTimeField,
}
widgets = {
'date_from': SplitDateTimePickerWidget(without_seconds=True),
'date_to': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_basics-date_from_0'}, without_seconds=True),
'presale_start': SplitDateTimePickerWidget(without_seconds=True),
'presale_end': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_basics-presale_start_0'}, without_seconds=True),
'date_from': SplitDateTimePickerWidget(),
'date_to': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_basics-date_from_0'}),
'presale_start': SplitDateTimePickerWidget(),
'presale_end': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_basics-presale_start_0'}),
'slug': SlugWidget,
}
@@ -521,23 +521,16 @@ class EventUpdateForm(I18nModelForm):
'limit_sales_channels': SafeModelMultipleChoiceField,
}
widgets = {
'date_from': SplitDateTimePickerWidget(without_seconds=True),
'date_to': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_date_from_0'}, without_seconds=True),
'date_admission': SplitDateTimePickerWidget(attrs={'data-date-default': '#id_date_from_0'}, without_seconds=True),
'presale_start': SplitDateTimePickerWidget(without_seconds=True),
'presale_end': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_presale_start_0'}, without_seconds=True),
'date_from': SplitDateTimePickerWidget(),
'date_to': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_date_from_0'}),
'date_admission': SplitDateTimePickerWidget(attrs={'data-date-default': '#id_date_from_0'}),
'presale_start': SplitDateTimePickerWidget(),
'presale_end': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_presale_start_0'}),
}
class EventSettingsValidationMixin:
def clean_invoice_address_from_vat_id(self):
value = self.cleaned_data.get('invoice_address_from_vat_id')
country = self.cleaned_data.get('invoice_address_from_country')
if value and country and country not in VAT_ID_COUNTRIES:
return None
return value
def clean(self):
data = super().clean()
settings_dict = self.obj.settings.freeze()
@@ -729,7 +722,7 @@ class EventSettingsForm(EventSettingsValidationMixin, FormPlaceholderMixin, Sett
del self.fields['event_list_filters']
del self.fields['event_calendar_future_only']
self.fields['primary_font'].choices = [('Open Sans', 'Open Sans')] + sorted([
(a, FontSelect.FontOption(title=a, data=v)) for a, v in get_fonts(self.event, pdf_support_required=False).items()
(a, {"title": a, "data": v}) for a, v in get_fonts(self.event, pdf_support_required=False).items()
], key=lambda a: a[0])
# create "virtual" fields for better UX when editing <name>_asked and <name>_required fields
+1 -1
View File
@@ -770,7 +770,7 @@ class EventOrderExpertFilterForm(EventOrderFilterForm):
)
elif q.type == Question.TYPE_TIME:
self.fields[fname] = forms.TimeField(
widget=TimePickerWidget(without_seconds=True),
widget=TimePickerWidget(time_format=get_format_without_seconds('TIME_INPUT_FORMATS')),
help_text=_('Exact matches only'),
**kwargs,
)
+5 -10
View File
@@ -245,8 +245,8 @@ class QuestionForm(I18nModelForm):
'valid_string_length_max',
]
widgets = {
'valid_datetime_min': SplitDateTimePickerWidget(without_seconds=True),
'valid_datetime_max': SplitDateTimePickerWidget(without_seconds=True),
'valid_datetime_min': SplitDateTimePickerWidget(),
'valid_datetime_max': SplitDateTimePickerWidget(),
'valid_date_min': DatePickerWidget(),
'valid_date_max': DatePickerWidget(),
'items': forms.CheckboxSelectMultiple(
@@ -574,7 +574,7 @@ class ItemCreateForm(I18nModelForm):
instance.bundles.create(bundled_item=b.bundled_item, bundled_variation=b.bundled_variation,
count=b.count, designated_price=b.designated_price)
for pt in self.cleaned_data['copy_from'].program_times.all():
instance.program_times.create(start=pt.start, end=pt.end, location=pt.location)
instance.program_times.create(start=pt.start, end=pt.end)
item_copy_data.send(sender=self.event, source=self.cleaned_data['copy_from'], target=instance)
@@ -1354,10 +1354,6 @@ class ItemProgramTimeForm(I18nModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['end'].widget.attrs['data-date-after'] = '#id_{prefix}-start_0'.format(prefix=self.prefix)
self.fields['location'].widget.attrs['rows'] = '3'
self.fields['location'].widget.attrs['placeholder'] = _(
'Sample Conference Center, Heidelberg, Germany'
)
class Meta:
model = ItemProgramTime
@@ -1365,13 +1361,12 @@ class ItemProgramTimeForm(I18nModelForm):
fields = [
'start',
'end',
'location'
]
field_classes = {
'start': forms.SplitDateTimeField,
'end': forms.SplitDateTimeField,
}
widgets = {
'start': SplitDateTimePickerWidget(without_seconds=True),
'end': SplitDateTimePickerWidget(without_seconds=True),
'start': SplitDateTimePickerWidget(),
'end': SplitDateTimePickerWidget(),
}
-8
View File
@@ -331,10 +331,6 @@ class OtherOperationsForm(forms.Form):
class OrderPositionAddForm(forms.Form):
count = forms.IntegerField(
label=_('Number of products to add'),
initial=1,
)
itemvar = forms.ChoiceField(
label=_('Product')
)
@@ -436,10 +432,6 @@ class OrderPositionAddForm(forms.Form):
d['used_membership'] = [m for m in self.memberships if str(m.pk) == d['used_membership']][0]
else:
d['used_membership'] = None
if d.get("count", 1) > 1 and d.get("seat"):
raise ValidationError({
"seat": _("You can not choose a seat when adding multiple products at once.")
})
return d
+7 -24
View File
@@ -28,7 +28,7 @@ from django.forms import formset_factory
from django.forms.utils import ErrorDict
from django.urls import reverse
from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django.utils.translation import gettext_lazy as _
from i18nfield.forms import I18nInlineFormSet
from pretix.base.forms import I18nModelForm
@@ -39,7 +39,6 @@ from pretix.base.reldate import RelativeDateTimeField, RelativeDateWrapper
from pretix.base.templatetags.money import money_filter
from pretix.control.forms import SplitDateTimeField, SplitDateTimePickerWidget
from pretix.control.forms.rrule import RRuleForm
from pretix.helpers.i18n import get_javascript_format_without_seconds
from pretix.helpers.money import change_decimal_field
@@ -81,11 +80,11 @@ class SubEventForm(I18nModelForm):
'presale_end': SplitDateTimeField,
}
widgets = {
'date_from': SplitDateTimePickerWidget(without_seconds=True),
'date_to': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_date_from_0'}, without_seconds=True),
'date_admission': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_date_from_0'}, without_seconds=True),
'presale_start': SplitDateTimePickerWidget(without_seconds=True),
'presale_end': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_presale_start_0'}, without_seconds=True),
'date_from': SplitDateTimePickerWidget(),
'date_to': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_date_from_0'}),
'date_admission': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_date_from_0'}),
'presale_start': SplitDateTimePickerWidget(),
'presale_end': SplitDateTimePickerWidget(attrs={'data-date-after': '#id_presale_start_0'}),
}
@@ -103,16 +102,6 @@ class SubEventBulkForm(SubEventForm):
required=False,
limit_choices=('date_from', 'date_to'),
)
skip_if_overlap = forms.BooleanField(
label=pgettext_lazy('subevent', 'Skip dates that overlap with any existing date'),
help_text=pgettext_lazy(
'subevent',
'This can be useful if all your dates happen in the same location and no repeated dates should '
'be created in conflict with existing special events. This respects even inactive dates and works best if '
'all dates have both a start and end time.'
),
required=False,
)
def __init__(self, *args, **kwargs):
self.event = kwargs['event']
@@ -163,7 +152,7 @@ class SubEventBulkEditForm(I18nModelForm):
self.fields[k + '_time'] = forms.TimeField(
label=self._meta.model._meta.get_field(k).verbose_name,
help_text=self._meta.model._meta.get_field(k).help_text,
widget=TimePickerWidget(without_seconds=True),
widget=TimePickerWidget(),
required=False,
)
@@ -507,12 +496,6 @@ class TimeForm(forms.Form):
required=False
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['time_from'].widget.attrs['data-format'] = get_javascript_format_without_seconds("TIME_INPUT_FORMATS")
self.fields['time_to'].widget.attrs['data-format'] = get_javascript_format_without_seconds("TIME_INPUT_FORMATS")
self.fields['time_admission'].widget.attrs['data-format'] = get_javascript_format_without_seconds("TIME_INPUT_FORMATS")
TimeFormSet = formset_factory(
TimeForm,
+1 -1
View File
@@ -363,7 +363,7 @@ def get_global_navigation(request):
'icon': 'dashboard',
},
]
if request.user.is_in_any_teams or request.user.is_staff:
if request.user.is_in_any_teams:
nav += [
{
'label': _('Events'),
@@ -34,7 +34,6 @@
{% bootstrap_form_errors form %}
{% bootstrap_field form.start layout="control" %}
{% bootstrap_field form.end layout="control" %}
{% bootstrap_field form.location layout="control" %}
</div>
</div>
{% endfor %}
@@ -60,7 +59,6 @@
<div class="panel-body form-horizontal">
{% bootstrap_field formset.empty_form.start layout="control" %}
{% bootstrap_field formset.empty_form.end layout="control" %}
{% bootstrap_field formset.empty_form.location layout="control" %}
</div>
</div>
{% endescapescript %}
@@ -329,7 +329,6 @@
{{ add_form.custom_error }}
</div>
{% endif %}
{% bootstrap_field add_form.count layout="control" %}
{% bootstrap_field add_form.itemvar layout="control" %}
{% bootstrap_field add_form.price addon_after=request.event.currency layout="control" %}
{% if add_form.addon_to %}
@@ -365,7 +364,6 @@
</div>
<div class="panel-body">
<div class="form-horizontal">
{% bootstrap_field add_position_formset.empty_form.count layout="control" %}
{% bootstrap_field add_position_formset.empty_form.itemvar layout="control" %}
{% bootstrap_field add_position_formset.empty_form.price addon_after=request.event.currency layout="control" %}
{% if add_position_formset.empty_form.addon_to %}
@@ -8,7 +8,7 @@
{% if page_obj.has_previous %}
{% if page_obj.previous_page_number > 1 %}
<li>
<a href="?{% url_replace request 'page' 1 %}" title="{% trans "Go to page 1" %}">
<a href="?{% url_replace request 'page' page_obj.num_pages %}" title="{% trans "Go to page 1" %}">
<span class="fa fa-angle-double-left"></span>
</a>
</li>
@@ -379,8 +379,6 @@
<i class="fa fa-calendar"></i> {% trans "Add many time slots" %}</button>
</p>
</div>
<hr />
{% bootstrap_field form.skip_if_overlap layout="control" horizontal_label_class='sr-only' horizontal_field_class='col-md-12' %}
</fieldset>
<fieldset>
<legend>{% trans "General information" %}</legend>
@@ -8,45 +8,7 @@
{% csrf_token %}
{% bootstrap_form_errors form %}
{% bootstrap_field form.name layout='horizontal' %}
<div class="form-group{% if form.devicetype.errors %} has-error{% endif %}">
<label class="col-md-3 control-label">{% trans "Device type" %}</label>
<div class="col-md-9">
<div>
<div class="big-radio radio">
<label>
<input type="radio" required value="totp" name="{{ form.devicetype.html_name }}" {% if form.devicetype.value == "totp" %}checked{% endif %}>
<strong>{% trans "Smartphone with Authenticator app" %}</strong><br>
<div class="help-block">
{% blocktrans trimmed %}
Use your smartphone with any Time-based One-Time-Password app like freeOTP, Google Authenticator or Proton Authenticator.
{% endblocktrans %}
</div>
</label>
</div>
<div class="big-radio radio">
<label>
<input type="radio" required value="webauthn" name="{{ form.devicetype.html_name }}" {% if form.devicetype.value == "webauthn" %}checked{% endif %}>
<strong>{% trans "WebAuthn-compatible hardware token" %}</strong><br>
<div class="help-block">
{% blocktrans trimmed %}
Use a hardware token like the Yubikey, or other biometric authentication like fingerprint or face recognition.
{% endblocktrans %}
</div>
</label>
</div>
</div>
{% if form.devicetype.errors %}
<div class="help-block">
{% for error in form.devicetype.errors %}
<p>{{ error|escape }}</p>
{% endfor %}
</div>
{% endif %}
</div>
</div>
{% bootstrap_field form.devicetype layout='horizontal' %}
<div class="form-group submit-group">
<button type="submit" class="btn btn-primary btn-save">
{% trans "Continue" %}
@@ -28,6 +28,11 @@
{% trans "iOS (iTunes)" %}
</a>
</li>
<li>
<a href="https://m.google.com/authenticator">
{% trans "Blackberry (Link via Google)" %}
</a>
</li>
</ul>
</li>
<li>
+1 -1
View File
@@ -641,7 +641,7 @@ def user_index(request):
ctx = {
'widgets': rearrange(widgets),
'can_create_event': request.user.teams.with_organizer_permission("organizer.events:create").exists() or request.user.is_staff,
'can_create_event': request.user.teams.with_organizer_permission("organizer.events:create").exists(),
'upcoming': widgets_for_event_qs(
request,
annotated_event_query(request, lazy=True).filter(
+13 -16
View File
@@ -234,21 +234,13 @@ class EventUpdate(DecoupleMixin, EventSettingsViewMixin, EventPermissionRequired
self.request.event.log_action('pretix.event.footerlinks.changed', user=self.request.user, data={
'data': self.footer_links_formset.cleaned_data
})
change_data = {
k: (form.cleaned_data.get(k).name
if isinstance(form.cleaned_data.get(k), File)
else form.cleaned_data.get(k))
for k in form.changed_data
}
meta_changed = {}
for f in self.meta_forms:
if f.has_changed():
meta_changed[f.property.name] = f.cleaned_data["value"]
if meta_changed:
change_data['meta_data'] = meta_changed
if change_data:
self.request.event.log_action('pretix.event.changed', user=self.request.user, data=change_data)
if form.has_changed():
self.request.event.log_action('pretix.event.changed', user=self.request.user, data={
k: (form.cleaned_data.get(k).name
if isinstance(form.cleaned_data.get(k), File)
else form.cleaned_data.get(k))
for k in form.changed_data
})
tickets.invalidate_cache.apply_async(kwargs={'event': self.request.event.pk})
messages.success(self.request, _('Your changes have been saved.'))
@@ -771,7 +763,12 @@ class InvoicePreview(EventPermissionRequiredMixin, View):
def get(self, request, *args, **kwargs):
fname, ftype, fcontent = build_preview_invoice_pdf(request.event)
resp = HttpResponse(fcontent, content_type=ftype)
resp['Content-Disposition'] = 'inline; filename="{}"'.format(fname)
if settings.DEBUG:
# attachment is more secure as we're dealing with user-generated stuff here, but inline is much more convenient during debugging
resp['Content-Disposition'] = 'inline; filename="{}"'.format(fname)
resp._csp_ignore = True
else:
resp['Content-Disposition'] = 'attachment; filename="{}"'.format(fname)
return resp
@@ -300,4 +300,5 @@ class SysReportView(AdministratorPermissionRequiredMixin, TemplateView):
resp = HttpResponse(data)
resp['Content-Type'] = mime
resp['Content-Disposition'] = 'inline; filename="{}"'.format(name)
resp._csp_ignore = True
return resp
+6 -2
View File
@@ -1447,8 +1447,12 @@ class ItemUpdateGeneral(ItemDetailMixin, EventPermissionRequiredMixin, MetaDataE
meta_changed = {}
for f in self.meta_forms:
if f.has_changed():
meta_changed[f.property.name] = f.cleaned_data["value"]
meta_changed.update({
k: (f.cleaned_data.get(k).name
if isinstance(f.cleaned_data.get(k), File)
else f.cleaned_data.get(k))
for k in f.changed_data
})
if meta_changed:
change_data['meta_data'] = meta_changed
+37 -25
View File
@@ -79,9 +79,9 @@ from pretix.base.email import get_email_context
from pretix.base.exporter import MultiSheetListExporter
from pretix.base.i18n import language
from pretix.base.models import (
CachedFile, CachedTicket, Checkin, Invoice, InvoiceAddress, Item,
ItemVariation, LogEntry, Order, QuestionAnswer, Quota,
ScheduledEventExport, generate_secret,
CachedCombinedTicket, CachedFile, CachedTicket, Checkin, Invoice,
InvoiceAddress, Item, ItemVariation, LogEntry, Order, QuestionAnswer,
Quota, ScheduledEventExport, generate_secret,
)
from pretix.base.models.orders import (
CancellationRequest, OrderFee, OrderPayment, OrderPosition, OrderRefund,
@@ -710,21 +710,34 @@ class OrderDownload(AsyncAction, OrderView):
resp = HttpResponseRedirect(value.file.file.read())
return resp
else:
return FileResponse(
value.file.file,
filename='{}-{}-{}-{}{}'.format(
self.request.event.slug.upper(), self.order.code, self.order_position.positionid,
self.output.identifier, value.extension
),
content_type=value.type
resp = FileResponse(value.file.file, content_type=value.type)
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}-{}{}"'.format(
self.request.event.slug.upper(), self.order.code, self.order_position.positionid,
self.output.identifier, value.extension
)
return resp
elif isinstance(value, CachedCombinedTicket):
if value.type == 'text/uri-list':
resp = HttpResponseRedirect(value.file.file.read())
return resp
else:
resp = FileResponse(value.file.file, content_type=value.type)
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}{}"'.format(
self.request.event.slug.upper(), self.order.code, self.output.identifier, value.extension
)
return resp
else:
return redirect(self.get_self_url())
def get_last_ct(self):
ct = CachedTicket.objects.filter(
order_position=self.order_position, provider=self.output.identifier, file__isnull=False
).last()
if 'position' in self.kwargs:
ct = CachedTicket.objects.filter(
order_position=self.order_position, provider=self.output.identifier, file__isnull=False
).last()
else:
ct = CachedCombinedTicket.objects.filter(
order=self.order, provider=self.output.identifier, file__isnull=False
).last()
if not ct or not ct.file:
return None
return ct
@@ -1818,15 +1831,15 @@ class InvoiceDownload(EventPermissionRequiredMixin, View):
return redirect(self.get_order_url())
try:
return FileResponse(
self.invoice.file.file,
filename='{}.pdf'.format(re.sub("[^a-zA-Z0-9-_.]+", "_", self.invoice.number)),
content_type='application/pdf'
)
resp = FileResponse(self.invoice.file.file, content_type='application/pdf')
except FileNotFoundError:
invoice_pdf_task.apply(args=(self.invoice.pk,))
return self.get(request, *args, **kwargs)
resp['Content-Disposition'] = 'inline; filename="{}.pdf"'.format(re.sub("[^a-zA-Z0-9-_.]+", "_", self.invoice.number))
resp._csp_ignore = True # Some browser's PDF readers do not work with CSP
return resp
class OrderExtend(OrderView):
permission = 'event.orders:write'
@@ -2046,13 +2059,12 @@ class OrderChange(OrderView):
else:
variation = None
try:
for i in range(f.cleaned_data.get("count", 1)):
ocm.add_position(item, variation,
f.cleaned_data['price'],
f.cleaned_data.get('addon_to'),
f.cleaned_data.get('subevent'),
f.cleaned_data.get('seat'),
f.cleaned_data.get('used_membership'))
ocm.add_position(item, variation,
f.cleaned_data['price'],
f.cleaned_data.get('addon_to'),
f.cleaned_data.get('subevent'),
f.cleaned_data.get('seat'),
f.cleaned_data.get('used_membership'))
except OrderError as e:
f.custom_error = str(e)
return False
+1 -1
View File
@@ -1322,7 +1322,7 @@ class DeviceUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixi
def form_valid(self, form):
if form.has_changed():
self.object.log_action('pretix.device.changed', user=self.request.user, data={
k: form.cleaned_data[k] if k != 'limit_events' else [e.id for e in form.cleaned_data[k]]
k: getattr(self.object, k) if k != 'limit_events' else [e.id for e in getattr(self.object, k).all()]
for k in form.changed_data
})
+8 -2
View File
@@ -263,7 +263,12 @@ class BaseEditorView(EventPermissionRequiredMixin, TemplateView):
resp = HttpResponse(data, content_type=mimet)
ftype = fname.split(".")[-1]
resp['Content-Disposition'] = 'inline; filename="ticket-preview.{}"'.format(ftype)
if settings.DEBUG:
# attachment is more secure as we're dealing with user-generated stuff here, but inline is much more convenient during debugging
resp['Content-Disposition'] = 'inline; filename="ticket-preview.{}"'.format(ftype)
resp._csp_ignore = True
else:
resp['Content-Disposition'] = 'attachment; filename="ticket-preview.{}"'.format(ftype)
return resp
elif "data" in request.POST:
if cf:
@@ -304,5 +309,6 @@ class FontsCSSView(TemplateView):
class PdfView(TemplateView):
def get(self, request, *args, **kwargs):
cf = get_object_or_404(CachedFile, id=kwargs.get("filename"), filename="background_preview.pdf")
resp = FileResponse(cf.file, filename=cf.filename, content_type='application/pdf')
resp = FileResponse(cf.file, content_type='application/pdf')
resp['Content-Disposition'] = 'attachment; filename="{}"'.format(cf.filename)
return resp
+13 -62
View File
@@ -79,7 +79,6 @@ from pretix.control.views import PaginationMixin
from pretix.control.views.event import MetaDataEditorMixin
from pretix.helpers import GroupConcat
from pretix.helpers.compat import CompatDeleteView
from pretix.helpers.i18n import get_format_without_seconds
from pretix.helpers.models import modelcopy
@@ -541,31 +540,20 @@ class SubEventUpdate(EventPermissionRequiredMixin, SubEventEditorMixin, UpdateVi
# TODO: LogEntry?
messages.success(self.request, _('Your changes have been saved.'))
change_data = {
k: (form.cleaned_data.get(k).name
if isinstance(form.cleaned_data.get(k), File)
else form.cleaned_data.get(k))
for k in form.changed_data
}
meta_changed = {}
for f in self.meta_forms:
if f.has_changed():
meta_changed[f.property.name] = f.cleaned_data["value"]
if meta_changed:
change_data['meta_data'] = meta_changed
for f in self.plugin_forms:
change_data.update({
k: (f.cleaned_data.get(k).name
if isinstance(f.cleaned_data.get(k), File)
else f.cleaned_data.get(k))
for k in f.changed_data
})
if change_data:
if form.has_changed() or any(f.has_changed() for f in self.plugin_forms):
data = {
k: form.cleaned_data.get(k) for k in form.changed_data
}
for f in self.plugin_forms:
data.update({
k: (f.cleaned_data.get(k).name
if isinstance(f.cleaned_data.get(k), File)
else f.cleaned_data.get(k))
for k in f.changed_data
})
self.object.log_action(
'pretix.subevent.changed', user=self.request.user, data=change_data
'pretix.subevent.changed', user=self.request.user, data=data
)
for f in self.plugin_forms:
f.subevent = self.object
f.save()
@@ -640,14 +628,6 @@ class SubEventCreate(SubEventEditorMixin, EventPermissionRequiredMixin, CreateVi
else f.cleaned_data.get(k))
for k in f.cleaned_data
})
meta_changed = {}
for f in self.meta_forms:
if f.has_changed():
meta_changed[f.property.name] = f.cleaned_data["value"]
if meta_changed:
data['meta_data'] = meta_changed
form.instance.log_action('pretix.subevent.added', data=dict(data), user=self.request.user)
self.save_formset(form.instance)
@@ -804,7 +784,7 @@ class SubEventBulkCreate(SubEventEditorMixin, EventPermissionRequiredMixin, Asyn
ctx['rrule_formset'] = self.rrule_formset
ctx['time_formset'] = self.time_formset
tf = get_format_without_seconds('TIME_INPUT_FORMATS')
tf = get_format('TIME_INPUT_FORMATS')[0]
ctx['time_admission_sample'] = time(8, 30, 0).strftime(tf)
ctx['time_begin_sample'] = time(9, 0, 0).strftime(tf)
ctx['time_end_sample'] = time(18, 0, 0).strftime(tf)
@@ -937,35 +917,6 @@ class SubEventBulkCreate(SubEventEditorMixin, EventPermissionRequiredMixin, Asyn
if len(subevents) > 100_000:
raise ValidationError(_('Please do not create more than 100.000 dates at once.'))
if form.cleaned_data.get("skip_if_overlap") and subevents:
def overlaps(a_from, a_to, b_from, b_to):
if a_from == b_from:
return True
if a_from > b_from:
# a starts after b
# check if it starts before b ends
return b_to and a_from < b_to
# a starts before b
# check if it ends before b starts
return a_to and a_to > b_from
date_min = min(se.date_from for se in subevents)
date_max = max(se.date_to or se.date_from for se in subevents)
dates_existing = list(self.request.event.subevents.annotate(
date_fromto=Coalesce('date_to', 'date_from'),
).filter(
date_from__lte=date_max,
date_fromto__gte=date_min,
).values('date_from', 'date_to'))
subevents = [
se for se in subevents if not any(
overlaps(se.date_from, se.date_to, other['date_from'], other['date_to'])
for other in dates_existing
)
]
if not subevents:
raise ValidationError(_('All dates would be skipped because they conflict with existing dates.'))
for i, se in enumerate(subevents):
se.save(clear_cache=False)
if i % 100 == 0:
+3 -3
View File
@@ -316,7 +316,7 @@ def nav_context_list(request):
page = 1
qs_events = request.user.get_events_with_any_permission(request).filter(
Q(name__icontains=i18ncomp(query)) | Q(slug__icontains=query) | Q(domain__domainname__iexact=query)
Q(name__icontains=i18ncomp(query)) | Q(slug__icontains=query)
).annotate(
min_from=Min('subevents__date_from'),
max_from=Max('subevents__date_from'),
@@ -331,7 +331,7 @@ def nav_context_list(request):
else:
qs_orga = Organizer.objects.filter(pk__in=request.user.teams.values_list('organizer', flat=True))
if query:
qs_orga = qs_orga.filter(Q(name__icontains=query) | Q(slug__icontains=query) | Q(domains__domainname__iexact=query))
qs_orga = qs_orga.filter(Q(name__icontains=query) | Q(slug__icontains=query))
qs_orga = qs_orga.annotate(
n_events=Count("events")
).order_by("-n_events")
@@ -619,7 +619,7 @@ def checkinlist_select2(request, **kwargs):
qs = request.event.checkin_lists.select_related('subevent').filter(
qf
).order_by('subevent__date_from', 'name', 'pk')
).order_by('name')
total = qs.count()
pagesize = 20
-132
View File
@@ -19,26 +19,12 @@
# 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 ipaddress
import socket
import sys
import types
from datetime import datetime
from http import cookies
from django.conf import settings
from PIL import Image
from requests.adapters import HTTPAdapter
from urllib3.connection import HTTPConnection, HTTPSConnection
from urllib3.connectionpool import HTTPConnectionPool, HTTPSConnectionPool
from urllib3.exceptions import (
ConnectTimeoutError, HTTPError, LocationParseError, NameResolutionError,
NewConnectionError,
)
from urllib3.util.connection import (
_TYPE_SOCKET_OPTIONS, _set_socket_options, allowed_gai_family,
)
from urllib3.util.timeout import _DEFAULT_TIMEOUT
def monkeypatch_vobject_performance():
@@ -103,123 +89,6 @@ def monkeypatch_requests_timeout():
HTTPAdapter.send = httpadapter_send
def monkeypatch_urllib3_ssrf_protection():
"""
pretix allows HTTP requests to untrusted URLs, e.g. through webhooks or external API URLs. This is dangerous since
it can allow access to private networks that should not be reachable by users ("server-side request forgery", SSRF).
Validating URLs at submission is not sufficient, since with DNS rebinding an attacker can make a domain name pass
validation and then resolve to a private IP address on actual execution. Unfortunately, there seems no clean solution
to this in Python land, so we monkeypatch urllib3's connection management to check the IP address to be external
*after* the DNS resolution.
This does not work when a global http(s) proxy is used, but in that scenario the proxy can perform the validation.
"""
if getattr(settings, "ALLOW_HTTP_TO_PRIVATE_NETWORKS", False):
# Settings are not supposed to change during runtime, so we can optimize performance and complexity by skipping
# this if not needed.
return
def create_connection(
address: tuple[str, int],
timeout=_DEFAULT_TIMEOUT,
source_address: tuple[str, int] | None = None,
socket_options: _TYPE_SOCKET_OPTIONS | None = None,
) -> socket.socket:
# This is copied from urllib3.util.connection v2.3.0
host, port = address
if host.startswith("["):
host = host.strip("[]")
err = None
# Using the value from allowed_gai_family() in the context of getaddrinfo lets
# us select whether to work with IPv4 DNS records, IPv6 records, or both.
# The original create_connection function always returns all records.
family = allowed_gai_family()
try:
host.encode("idna")
except UnicodeError:
raise LocationParseError(f"'{host}', label empty or too long") from None
for res in socket.getaddrinfo(host, port, family, socket.SOCK_STREAM):
af, socktype, proto, canonname, sa = res
if not getattr(settings, "ALLOW_HTTP_TO_PRIVATE_NETWORKS", False):
ip_addr = ipaddress.ip_address(sa[0])
if ip_addr.is_multicast:
raise HTTPError(f"Request to multicast address {sa[0]} blocked")
if ip_addr.is_loopback or ip_addr.is_link_local:
raise HTTPError(f"Request to local address {sa[0]} blocked")
if ip_addr.is_private:
raise HTTPError(f"Request to private address {sa[0]} blocked")
sock = None
try:
sock = socket.socket(af, socktype, proto)
# If provided, set socket level options before connecting.
_set_socket_options(sock, socket_options)
if timeout is not _DEFAULT_TIMEOUT:
sock.settimeout(timeout)
if source_address:
sock.bind(source_address)
sock.connect(sa)
# Break explicitly a reference cycle
err = None
return sock
except OSError as _:
err = _
if sock is not None:
sock.close()
if err is not None:
try:
raise err
finally:
# Break explicitly a reference cycle
err = None
else:
raise OSError("getaddrinfo returns an empty list")
class ProtectionMixin:
def _new_conn(self) -> socket.socket:
# This is 1:1 the version from urllib3.connection.HTTPConnection._new_conn v2.3.0
# just with a call to our own create_connection
try:
sock = create_connection(
(self._dns_host, self.port),
self.timeout,
source_address=self.source_address,
socket_options=self.socket_options,
)
except socket.gaierror as e:
raise NameResolutionError(self.host, self, e) from e
except socket.timeout as e:
raise ConnectTimeoutError(
self,
f"Connection to {self.host} timed out. (connect timeout={self.timeout})",
) from e
except OSError as e:
raise NewConnectionError(
self, f"Failed to establish a new connection: {e}"
) from e
sys.audit("http.client.connect", self, self.host, self.port)
return sock
class ProtectedHTTPConnection(ProtectionMixin, HTTPConnection):
pass
class ProtectedHTTPSConnection(ProtectionMixin, HTTPSConnection):
pass
HTTPConnectionPool.ConnectionCls = ProtectedHTTPConnection
HTTPSConnectionPool.ConnectionCls = ProtectedHTTPSConnection
def monkeypatch_cookie_morsel():
# See https://code.djangoproject.com/ticket/34613
cookies.Morsel._flags.add("partitioned")
@@ -230,5 +99,4 @@ def monkeypatch_all_at_ready():
monkeypatch_vobject_performance()
monkeypatch_pillow_safer()
monkeypatch_requests_timeout()
monkeypatch_urllib3_ssrf_protection()
monkeypatch_cookie_morsel()
+4 -10
View File
@@ -25,7 +25,7 @@ import time
from django.conf import settings
from django.contrib.auth import login as auth_login
from django.contrib.gis import geoip2
from django.contrib.gis.geoip2 import GeoIP2
from django.core.cache import cache
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
@@ -63,20 +63,14 @@ def get_user_agent_hash(request):
_geoip = None
def get_geoip() -> geoip2.GeoIP2:
# See https://code.djangoproject.com/ticket/36988#ticket
def _get_country(request):
global _geoip
geoip2.SUPPORTED_DATABASE_TYPES.add("Geoacumen-Country")
if not _geoip:
_geoip = geoip2.GeoIP2()
return _geoip
_geoip = GeoIP2()
def _get_country(request):
try:
res = get_geoip().country(get_client_ip(request))
res = _geoip.country(get_client_ip(request))
except AddressNotFoundError:
return None
return res['country_code']
+5 -5
View File
@@ -173,7 +173,6 @@ def create_thumbnail(source, size, formats=None):
# filesystem path, this only works because _open() uses safe_join, which accepts absolute paths if they match the
# expected base dir. For NanoCDN Files, this works because source.name is set to the storage path.
source_rb = default_storage.open(source_name, mode='rb')
source_ext = os.path.splitext(source_name)[1].lower()
image = Image.open(BytesIO(source_rb.read()), formats=formats or settings.PILLOW_FORMATS_QUESTIONS_IMAGE)
try:
@@ -184,14 +183,11 @@ def create_thumbnail(source, size, formats=None):
frames = []
durations = []
for f in ImageSequence.Iterator(image):
if f.mode in ("P", "PA") and source_ext == '.png':
f = f.convert('RGBA')
if f.mode not in ("1", "L", "RGB", "RGBA"):
f = f.convert('RGB')
durations.append(f.info.get("duration", 1000))
frames.append(resize_image(f, size))
image_out = frames[0]
save_kwargs = {}
source_ext = os.path.splitext(source_name)[1].lower()
if source_ext == '.jpg' or source_ext == '.jpeg':
# Yields better file sizes for photos
@@ -215,6 +211,10 @@ def create_thumbnail(source, size, formats=None):
checksum = hashlib.md5(image.tobytes()).hexdigest()
name = checksum + '.' + size.replace('^', 'c') + '.' + target_ext
buffer = BytesIO()
if image_out.mode == "P" and source_ext == '.png':
image_out = image_out.convert('RGBA')
if image_out.mode not in ("1", "L", "RGB", "RGBA"):
image_out = image_out.convert('RGB')
image_out.save(fp=buffer, format=target_ext.upper(), quality=quality, **save_kwargs)
imgfile = ContentFile(buffer.getvalue())
File diff suppressed because it is too large Load Diff
+14 -14
View File
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-04-28 09:04+0000\n"
"POT-Creation-Date: 2026-03-17 14:06+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -632,56 +632,56 @@ msgstr ""
msgid "Unknown error."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:310
#: pretix/static/pretixcontrol/js/ui/main.js:309
msgid "Your color has great contrast and will provide excellent accessibility."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:314
#: pretix/static/pretixcontrol/js/ui/main.js:313
msgid ""
"Your color has decent contrast and is sufficient for minimum accessibility "
"requirements."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:318
#: pretix/static/pretixcontrol/js/ui/main.js:317
msgid ""
"Your color has insufficient contrast to white. Accessibility of your site "
"will be impacted."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:446
#: pretix/static/pretixcontrol/js/ui/main.js:466
#: pretix/static/pretixcontrol/js/ui/main.js:445
#: pretix/static/pretixcontrol/js/ui/main.js:465
msgid "Search query"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:464
#: pretix/static/pretixcontrol/js/ui/main.js:463
msgid "All"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:465
#: pretix/static/pretixcontrol/js/ui/main.js:464
msgid "None"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:469
#: pretix/static/pretixcontrol/js/ui/main.js:468
msgid "Selected only"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:842
#: pretix/static/pretixcontrol/js/ui/main.js:841
msgid "Enter page number between 1 and %(max)s."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:845
#: pretix/static/pretixcontrol/js/ui/main.js:844
msgid "Invalid page number."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1003
#: pretix/static/pretixcontrol/js/ui/main.js:1002
msgid "Use a different name internally"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1043
#: pretix/static/pretixcontrol/js/ui/main.js:1042
msgid "Click to close"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1124
#: pretix/static/pretixcontrol/js/ui/main.js:1123
msgid "You have unsaved changes!"
msgstr ""
File diff suppressed because it is too large Load Diff
+14 -14
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-04-28 09:04+0000\n"
"POT-Creation-Date: 2026-03-17 14:06+0000\n"
"PO-Revision-Date: 2021-09-15 11:22+0000\n"
"Last-Translator: Mohamed Tawfiq <mtawfiq@wafyapp.com>\n"
"Language-Team: Arabic <https://translate.pretix.eu/projects/pretix/pretix-js/"
@@ -666,13 +666,13 @@ msgstr "توليد الرسائل …"
msgid "Unknown error."
msgstr "خطأ غير معروف."
#: pretix/static/pretixcontrol/js/ui/main.js:310
#: pretix/static/pretixcontrol/js/ui/main.js:309
#, fuzzy
#| msgid "Your color has great contrast and is very easy to read!"
msgid "Your color has great contrast and will provide excellent accessibility."
msgstr "اللون يتمتع بتباين كبير وتسهل قراءته!"
#: pretix/static/pretixcontrol/js/ui/main.js:314
#: pretix/static/pretixcontrol/js/ui/main.js:313
#, fuzzy
#| msgid "Your color has decent contrast and is probably good-enough to read!"
msgid ""
@@ -680,46 +680,46 @@ msgid ""
"requirements."
msgstr "اللون يحظى بتباين معقول ويمكن أن يكون مناسب للقراءة!"
#: pretix/static/pretixcontrol/js/ui/main.js:318
#: pretix/static/pretixcontrol/js/ui/main.js:317
msgid ""
"Your color has insufficient contrast to white. Accessibility of your site "
"will be impacted."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:446
#: pretix/static/pretixcontrol/js/ui/main.js:466
#: pretix/static/pretixcontrol/js/ui/main.js:445
#: pretix/static/pretixcontrol/js/ui/main.js:465
msgid "Search query"
msgstr "البحث في الاستفسارات"
#: pretix/static/pretixcontrol/js/ui/main.js:464
#: pretix/static/pretixcontrol/js/ui/main.js:463
msgid "All"
msgstr "الكل"
#: pretix/static/pretixcontrol/js/ui/main.js:465
#: pretix/static/pretixcontrol/js/ui/main.js:464
msgid "None"
msgstr "لا شيء"
#: pretix/static/pretixcontrol/js/ui/main.js:469
#: pretix/static/pretixcontrol/js/ui/main.js:468
msgid "Selected only"
msgstr "المختارة فقط"
#: pretix/static/pretixcontrol/js/ui/main.js:842
#: pretix/static/pretixcontrol/js/ui/main.js:841
msgid "Enter page number between 1 and %(max)s."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:845
#: pretix/static/pretixcontrol/js/ui/main.js:844
msgid "Invalid page number."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1003
#: pretix/static/pretixcontrol/js/ui/main.js:1002
msgid "Use a different name internally"
msgstr "قم باستخدم اسم مختلف داخليا"
#: pretix/static/pretixcontrol/js/ui/main.js:1043
#: pretix/static/pretixcontrol/js/ui/main.js:1042
msgid "Click to close"
msgstr "اضغط لاغلاق الصفحة"
#: pretix/static/pretixcontrol/js/ui/main.js:1124
#: pretix/static/pretixcontrol/js/ui/main.js:1123
msgid "You have unsaved changes!"
msgstr "لم تقم بحفظ التعديلات!"
File diff suppressed because it is too large Load Diff
+14 -14
View File
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-04-28 09:04+0000\n"
"POT-Creation-Date: 2026-03-17 14:06+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -632,56 +632,56 @@ msgstr ""
msgid "Unknown error."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:310
#: pretix/static/pretixcontrol/js/ui/main.js:309
msgid "Your color has great contrast and will provide excellent accessibility."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:314
#: pretix/static/pretixcontrol/js/ui/main.js:313
msgid ""
"Your color has decent contrast and is sufficient for minimum accessibility "
"requirements."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:318
#: pretix/static/pretixcontrol/js/ui/main.js:317
msgid ""
"Your color has insufficient contrast to white. Accessibility of your site "
"will be impacted."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:446
#: pretix/static/pretixcontrol/js/ui/main.js:466
#: pretix/static/pretixcontrol/js/ui/main.js:445
#: pretix/static/pretixcontrol/js/ui/main.js:465
msgid "Search query"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:464
#: pretix/static/pretixcontrol/js/ui/main.js:463
msgid "All"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:465
#: pretix/static/pretixcontrol/js/ui/main.js:464
msgid "None"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:469
#: pretix/static/pretixcontrol/js/ui/main.js:468
msgid "Selected only"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:842
#: pretix/static/pretixcontrol/js/ui/main.js:841
msgid "Enter page number between 1 and %(max)s."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:845
#: pretix/static/pretixcontrol/js/ui/main.js:844
msgid "Invalid page number."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1003
#: pretix/static/pretixcontrol/js/ui/main.js:1002
msgid "Use a different name internally"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1043
#: pretix/static/pretixcontrol/js/ui/main.js:1042
msgid "Click to close"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1124
#: pretix/static/pretixcontrol/js/ui/main.js:1123
msgid "You have unsaved changes!"
msgstr ""
File diff suppressed because it is too large Load Diff
+14 -14
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-04-28 09:04+0000\n"
"POT-Creation-Date: 2026-03-17 14:06+0000\n"
"PO-Revision-Date: 2025-10-31 17:00+0000\n"
"Last-Translator: Núria Masclans <nuriamasclansserrat@gmail.com>\n"
"Language-Team: Catalan <https://translate.pretix.eu/projects/pretix/pretix-"
@@ -644,11 +644,11 @@ msgstr "Generant missatges…"
msgid "Unknown error."
msgstr "Error desconegut."
#: pretix/static/pretixcontrol/js/ui/main.js:310
#: pretix/static/pretixcontrol/js/ui/main.js:309
msgid "Your color has great contrast and will provide excellent accessibility."
msgstr "El teu color té molt contrast i garanteix bona accessibilitat."
#: pretix/static/pretixcontrol/js/ui/main.js:314
#: pretix/static/pretixcontrol/js/ui/main.js:313
msgid ""
"Your color has decent contrast and is sufficient for minimum accessibility "
"requirements."
@@ -656,7 +656,7 @@ msgstr ""
"El teu color té un contrast acceptable i compleix els requisits mínims "
"daccessibilitat."
#: pretix/static/pretixcontrol/js/ui/main.js:318
#: pretix/static/pretixcontrol/js/ui/main.js:317
msgid ""
"Your color has insufficient contrast to white. Accessibility of your site "
"will be impacted."
@@ -664,40 +664,40 @@ msgstr ""
"El color no té prou contrast amb el blanc i pot afectar a l'accessibilitat "
"del lloc web."
#: pretix/static/pretixcontrol/js/ui/main.js:446
#: pretix/static/pretixcontrol/js/ui/main.js:466
#: pretix/static/pretixcontrol/js/ui/main.js:445
#: pretix/static/pretixcontrol/js/ui/main.js:465
msgid "Search query"
msgstr "Consulta de cerca"
#: pretix/static/pretixcontrol/js/ui/main.js:464
#: pretix/static/pretixcontrol/js/ui/main.js:463
msgid "All"
msgstr "Tots"
#: pretix/static/pretixcontrol/js/ui/main.js:465
#: pretix/static/pretixcontrol/js/ui/main.js:464
msgid "None"
msgstr "Cap"
#: pretix/static/pretixcontrol/js/ui/main.js:469
#: pretix/static/pretixcontrol/js/ui/main.js:468
msgid "Selected only"
msgstr "Només seleccionats"
#: pretix/static/pretixcontrol/js/ui/main.js:842
#: pretix/static/pretixcontrol/js/ui/main.js:841
msgid "Enter page number between 1 and %(max)s."
msgstr "Introdueix un número de pàgina entre 1 i %(max)s."
#: pretix/static/pretixcontrol/js/ui/main.js:845
#: pretix/static/pretixcontrol/js/ui/main.js:844
msgid "Invalid page number."
msgstr "Número de pàgina no vàlid."
#: pretix/static/pretixcontrol/js/ui/main.js:1003
#: pretix/static/pretixcontrol/js/ui/main.js:1002
msgid "Use a different name internally"
msgstr "Utilitza un nom diferent internament"
#: pretix/static/pretixcontrol/js/ui/main.js:1043
#: pretix/static/pretixcontrol/js/ui/main.js:1042
msgid "Click to close"
msgstr "Prem per tancar"
#: pretix/static/pretixcontrol/js/ui/main.js:1124
#: pretix/static/pretixcontrol/js/ui/main.js:1123
msgid "You have unsaved changes!"
msgstr "Tens canvis sense desar!"
File diff suppressed because it is too large Load Diff
+14 -14
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-04-28 09:04+0000\n"
"POT-Creation-Date: 2026-03-17 14:06+0000\n"
"PO-Revision-Date: 2026-01-08 04:00+0000\n"
"Last-Translator: Jiří Pastrňák <jiri@pastrnak.email>\n"
"Language-Team: Czech <https://translate.pretix.eu/projects/pretix/pretix-js/"
@@ -657,57 +657,57 @@ msgstr "Vytváření zpráv…"
msgid "Unknown error."
msgstr "Neznámá chyba."
#: pretix/static/pretixcontrol/js/ui/main.js:310
#: pretix/static/pretixcontrol/js/ui/main.js:309
msgid "Your color has great contrast and will provide excellent accessibility."
msgstr "Tato barva má velmi dobrý kontrast a je velmi dobře čitelná."
#: pretix/static/pretixcontrol/js/ui/main.js:314
#: pretix/static/pretixcontrol/js/ui/main.js:313
msgid ""
"Your color has decent contrast and is sufficient for minimum accessibility "
"requirements."
msgstr ""
"Tato barva má slušný kontrast a pravděpodobně je dostatečně dobře čitelná."
#: pretix/static/pretixcontrol/js/ui/main.js:318
#: pretix/static/pretixcontrol/js/ui/main.js:317
msgid ""
"Your color has insufficient contrast to white. Accessibility of your site "
"will be impacted."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:446
#: pretix/static/pretixcontrol/js/ui/main.js:466
#: pretix/static/pretixcontrol/js/ui/main.js:445
#: pretix/static/pretixcontrol/js/ui/main.js:465
msgid "Search query"
msgstr "Hledaný výraz"
#: pretix/static/pretixcontrol/js/ui/main.js:464
#: pretix/static/pretixcontrol/js/ui/main.js:463
msgid "All"
msgstr "Všechny"
#: pretix/static/pretixcontrol/js/ui/main.js:465
#: pretix/static/pretixcontrol/js/ui/main.js:464
msgid "None"
msgstr "Žádný"
#: pretix/static/pretixcontrol/js/ui/main.js:469
#: pretix/static/pretixcontrol/js/ui/main.js:468
msgid "Selected only"
msgstr "Pouze vybrané"
#: pretix/static/pretixcontrol/js/ui/main.js:842
#: pretix/static/pretixcontrol/js/ui/main.js:841
msgid "Enter page number between 1 and %(max)s."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:845
#: pretix/static/pretixcontrol/js/ui/main.js:844
msgid "Invalid page number."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1003
#: pretix/static/pretixcontrol/js/ui/main.js:1002
msgid "Use a different name internally"
msgstr "Interně používat jiný název"
#: pretix/static/pretixcontrol/js/ui/main.js:1043
#: pretix/static/pretixcontrol/js/ui/main.js:1042
msgid "Click to close"
msgstr "Kliknutím zavřete"
#: pretix/static/pretixcontrol/js/ui/main.js:1124
#: pretix/static/pretixcontrol/js/ui/main.js:1123
msgid "You have unsaved changes!"
msgstr "Máte neuložené změny!"
File diff suppressed because it is too large Load Diff
+14 -14
View File
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-04-28 09:04+0000\n"
"POT-Creation-Date: 2026-03-17 14:06+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -633,56 +633,56 @@ msgstr ""
msgid "Unknown error."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:310
#: pretix/static/pretixcontrol/js/ui/main.js:309
msgid "Your color has great contrast and will provide excellent accessibility."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:314
#: pretix/static/pretixcontrol/js/ui/main.js:313
msgid ""
"Your color has decent contrast and is sufficient for minimum accessibility "
"requirements."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:318
#: pretix/static/pretixcontrol/js/ui/main.js:317
msgid ""
"Your color has insufficient contrast to white. Accessibility of your site "
"will be impacted."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:446
#: pretix/static/pretixcontrol/js/ui/main.js:466
#: pretix/static/pretixcontrol/js/ui/main.js:445
#: pretix/static/pretixcontrol/js/ui/main.js:465
msgid "Search query"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:464
#: pretix/static/pretixcontrol/js/ui/main.js:463
msgid "All"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:465
#: pretix/static/pretixcontrol/js/ui/main.js:464
msgid "None"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:469
#: pretix/static/pretixcontrol/js/ui/main.js:468
msgid "Selected only"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:842
#: pretix/static/pretixcontrol/js/ui/main.js:841
msgid "Enter page number between 1 and %(max)s."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:845
#: pretix/static/pretixcontrol/js/ui/main.js:844
msgid "Invalid page number."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1003
#: pretix/static/pretixcontrol/js/ui/main.js:1002
msgid "Use a different name internally"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1043
#: pretix/static/pretixcontrol/js/ui/main.js:1042
msgid "Click to close"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1124
#: pretix/static/pretixcontrol/js/ui/main.js:1123
msgid "You have unsaved changes!"
msgstr ""
File diff suppressed because it is too large Load Diff
+17 -17
View File
@@ -6,8 +6,8 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-04-28 09:04+0000\n"
"PO-Revision-Date: 2026-04-22 18:00+0000\n"
"POT-Creation-Date: 2026-03-17 14:06+0000\n"
"PO-Revision-Date: 2024-07-10 15:00+0000\n"
"Last-Translator: Nikolai <nikolai@lengefeldt.de>\n"
"Language-Team: Danish <https://translate.pretix.eu/projects/pretix/pretix-js/"
"da/>\n"
@@ -16,7 +16,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.17\n"
"X-Generator: Weblate 5.6.1\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
@@ -682,56 +682,56 @@ msgstr "Opretter beskeder …"
msgid "Unknown error."
msgstr "Ukendt fejl."
#: pretix/static/pretixcontrol/js/ui/main.js:310
#: pretix/static/pretixcontrol/js/ui/main.js:309
msgid "Your color has great contrast and will provide excellent accessibility."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:314
#: pretix/static/pretixcontrol/js/ui/main.js:313
msgid ""
"Your color has decent contrast and is sufficient for minimum accessibility "
"requirements."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:318
#: pretix/static/pretixcontrol/js/ui/main.js:317
msgid ""
"Your color has insufficient contrast to white. Accessibility of your site "
"will be impacted."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:446
#: pretix/static/pretixcontrol/js/ui/main.js:466
#: pretix/static/pretixcontrol/js/ui/main.js:445
#: pretix/static/pretixcontrol/js/ui/main.js:465
msgid "Search query"
msgstr "Søgeforespørgsel"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:464
#: pretix/static/pretixcontrol/js/ui/main.js:463
msgid "All"
msgstr "Alle"
#: pretix/static/pretixcontrol/js/ui/main.js:465
#: pretix/static/pretixcontrol/js/ui/main.js:464
msgid "None"
msgstr "Ingen"
#: pretix/static/pretixcontrol/js/ui/main.js:469
#: pretix/static/pretixcontrol/js/ui/main.js:468
msgid "Selected only"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:842
#: pretix/static/pretixcontrol/js/ui/main.js:841
msgid "Enter page number between 1 and %(max)s."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:845
#: pretix/static/pretixcontrol/js/ui/main.js:844
msgid "Invalid page number."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1003
#: pretix/static/pretixcontrol/js/ui/main.js:1002
msgid "Use a different name internally"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1043
#: pretix/static/pretixcontrol/js/ui/main.js:1042
msgid "Click to close"
msgstr "Klik for at lukke"
#: pretix/static/pretixcontrol/js/ui/main.js:1124
#: pretix/static/pretixcontrol/js/ui/main.js:1123
msgid "You have unsaved changes!"
msgstr "Du har ændringer, der ikke er gemt!"
File diff suppressed because it is too large Load Diff
+14 -14
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-04-28 09:04+0000\n"
"POT-Creation-Date: 2026-03-17 14:06+0000\n"
"PO-Revision-Date: 2026-03-17 14:27+0000\n"
"Last-Translator: Raphael Michel <michel@rami.io>\n"
"Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix-js/"
@@ -650,12 +650,12 @@ msgstr "Generiere Nachrichten…"
msgid "Unknown error."
msgstr "Unbekannter Fehler."
#: pretix/static/pretixcontrol/js/ui/main.js:310
#: pretix/static/pretixcontrol/js/ui/main.js:309
msgid "Your color has great contrast and will provide excellent accessibility."
msgstr ""
"Diese Farbe hat einen sehr guten Kontrast und trägt zur Barrierefreiheit bei!"
#: pretix/static/pretixcontrol/js/ui/main.js:314
#: pretix/static/pretixcontrol/js/ui/main.js:313
msgid ""
"Your color has decent contrast and is sufficient for minimum accessibility "
"requirements."
@@ -663,7 +663,7 @@ msgstr ""
"Diese Farbe hat einen ausreichenden Kontrast und genügt den "
"Mindestanforderungen der Barrierefreiheit."
#: pretix/static/pretixcontrol/js/ui/main.js:318
#: pretix/static/pretixcontrol/js/ui/main.js:317
msgid ""
"Your color has insufficient contrast to white. Accessibility of your site "
"will be impacted."
@@ -671,40 +671,40 @@ msgstr ""
"Diese Farbe hat keinen ausreichenden Kontrast zu weiß. Die Barrierefreiheit "
"der Seite ist eingeschränkt."
#: pretix/static/pretixcontrol/js/ui/main.js:446
#: pretix/static/pretixcontrol/js/ui/main.js:466
#: pretix/static/pretixcontrol/js/ui/main.js:445
#: pretix/static/pretixcontrol/js/ui/main.js:465
msgid "Search query"
msgstr "Suchbegriff"
#: pretix/static/pretixcontrol/js/ui/main.js:464
#: pretix/static/pretixcontrol/js/ui/main.js:463
msgid "All"
msgstr "Alle"
#: pretix/static/pretixcontrol/js/ui/main.js:465
#: pretix/static/pretixcontrol/js/ui/main.js:464
msgid "None"
msgstr "Keine"
#: pretix/static/pretixcontrol/js/ui/main.js:469
#: pretix/static/pretixcontrol/js/ui/main.js:468
msgid "Selected only"
msgstr "Nur ausgewählte"
#: pretix/static/pretixcontrol/js/ui/main.js:842
#: pretix/static/pretixcontrol/js/ui/main.js:841
msgid "Enter page number between 1 and %(max)s."
msgstr "Geben Sie eine Seitenzahl zwischen 1 und %(max)s ein."
#: pretix/static/pretixcontrol/js/ui/main.js:845
#: pretix/static/pretixcontrol/js/ui/main.js:844
msgid "Invalid page number."
msgstr "Ungültige Seitenzahl."
#: pretix/static/pretixcontrol/js/ui/main.js:1003
#: pretix/static/pretixcontrol/js/ui/main.js:1002
msgid "Use a different name internally"
msgstr "Intern einen anderen Namen verwenden"
#: pretix/static/pretixcontrol/js/ui/main.js:1043
#: pretix/static/pretixcontrol/js/ui/main.js:1042
msgid "Click to close"
msgstr "Klicken zum Schließen"
#: pretix/static/pretixcontrol/js/ui/main.js:1124
#: pretix/static/pretixcontrol/js/ui/main.js:1123
msgid "You have unsaved changes!"
msgstr "Sie haben ungespeicherte Änderungen!"
-5
View File
@@ -32,7 +32,6 @@ ausgecheckt
ausgeklappt
auswahl
Authentication
Authenticator
Authenticator-App
Autorisierungscode
Autorisierungs-Endpunktes
@@ -131,7 +130,6 @@ Eingangsscan
Einlassbuchung
Einlassdatum
Einlasskontrolle
Einmalpasswörter
einzuchecken
email
E-Mail-Renderer
@@ -165,7 +163,6 @@ Explorer
FA
Favicon
F-Droid
freeOTP
Footer
Footer-Link
Footer-Text
@@ -302,7 +299,6 @@ Personalisierung
P.IVA
PKCE-Erweiterung
Platzhalterzeichen
PLN
Play
Plugin
Plugins
@@ -561,7 +557,6 @@ Zahlungs-ID
Zahlungspflichtig
Zehnerkarten
Zeitbasiert
zeitbasierte
Zeitslotbuchung
Zimpler
ZIP-Datei
File diff suppressed because it is too large Load Diff
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-04-28 09:04+0000\n"
"POT-Creation-Date: 2026-03-17 14:06+0000\n"
"PO-Revision-Date: 2026-03-17 14:30+0000\n"
"Last-Translator: Raphael Michel <michel@rami.io>\n"
"Language-Team: German (informal) <https://translate.pretix.eu/projects/"
@@ -650,12 +650,12 @@ msgstr "Generiere Nachrichten…"
msgid "Unknown error."
msgstr "Unbekannter Fehler."
#: pretix/static/pretixcontrol/js/ui/main.js:310
#: pretix/static/pretixcontrol/js/ui/main.js:309
msgid "Your color has great contrast and will provide excellent accessibility."
msgstr ""
"Diese Farbe hat einen sehr guten Kontrast und trägt zur Barrierefreiheit bei!"
#: pretix/static/pretixcontrol/js/ui/main.js:314
#: pretix/static/pretixcontrol/js/ui/main.js:313
msgid ""
"Your color has decent contrast and is sufficient for minimum accessibility "
"requirements."
@@ -663,7 +663,7 @@ msgstr ""
"Diese Farbe hat einen ausreichenden Kontrast und genügt den "
"Mindestanforderungen der Barrierefreiheit."
#: pretix/static/pretixcontrol/js/ui/main.js:318
#: pretix/static/pretixcontrol/js/ui/main.js:317
msgid ""
"Your color has insufficient contrast to white. Accessibility of your site "
"will be impacted."
@@ -671,40 +671,40 @@ msgstr ""
"Diese Farbe hat keinen ausreichenden Kontrast zu weiß. Die Barrierefreiheit "
"der Seite ist eingeschränkt."
#: pretix/static/pretixcontrol/js/ui/main.js:446
#: pretix/static/pretixcontrol/js/ui/main.js:466
#: pretix/static/pretixcontrol/js/ui/main.js:445
#: pretix/static/pretixcontrol/js/ui/main.js:465
msgid "Search query"
msgstr "Suchbegriff"
#: pretix/static/pretixcontrol/js/ui/main.js:464
#: pretix/static/pretixcontrol/js/ui/main.js:463
msgid "All"
msgstr "Alle"
#: pretix/static/pretixcontrol/js/ui/main.js:465
#: pretix/static/pretixcontrol/js/ui/main.js:464
msgid "None"
msgstr "Keine"
#: pretix/static/pretixcontrol/js/ui/main.js:469
#: pretix/static/pretixcontrol/js/ui/main.js:468
msgid "Selected only"
msgstr "Nur ausgewählte"
#: pretix/static/pretixcontrol/js/ui/main.js:842
#: pretix/static/pretixcontrol/js/ui/main.js:841
msgid "Enter page number between 1 and %(max)s."
msgstr "Gib eine Seitenzahl zwischen 1 und %(max)s ein."
#: pretix/static/pretixcontrol/js/ui/main.js:845
#: pretix/static/pretixcontrol/js/ui/main.js:844
msgid "Invalid page number."
msgstr "Ungültige Seitenzahl."
#: pretix/static/pretixcontrol/js/ui/main.js:1003
#: pretix/static/pretixcontrol/js/ui/main.js:1002
msgid "Use a different name internally"
msgstr "Intern einen anderen Namen verwenden"
#: pretix/static/pretixcontrol/js/ui/main.js:1043
#: pretix/static/pretixcontrol/js/ui/main.js:1042
msgid "Click to close"
msgstr "Klicken zum Schließen"
#: pretix/static/pretixcontrol/js/ui/main.js:1124
#: pretix/static/pretixcontrol/js/ui/main.js:1123
msgid "You have unsaved changes!"
msgstr "Du hast ungespeicherte Änderungen!"
@@ -32,7 +32,6 @@ ausgecheckt
ausgeklappt
auswahl
Authentication
Authenticator
Authenticator-App
Autorisierungscode
Autorisierungs-Endpunktes
@@ -131,7 +130,6 @@ Eingangsscan
Einlassbuchung
Einlassdatum
Einlasskontrolle
Einmalpasswörter
einzuchecken
email
E-Mail-Renderer
@@ -165,7 +163,6 @@ Explorer
FA
Favicon
F-Droid
freeOTP
Footer
Footer-Link
Footer-Text
@@ -302,7 +299,6 @@ Personalisierung
P.IVA
PKCE-Erweiterung
Platzhalterzeichen
PLN
Play
Plugin
Plugins
@@ -561,7 +557,6 @@ Zahlungs-ID
Zahlungspflichtig
Zehnerkarten
Zeitbasiert
zeitbasierte
Zeitslotbuchung
Zimpler
ZIP-Datei
+1332 -1395
View File
File diff suppressed because it is too large Load Diff
+14 -14
View File
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-04-28 09:04+0000\n"
"POT-Creation-Date: 2026-03-17 14:06+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -632,56 +632,56 @@ msgstr ""
msgid "Unknown error."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:310
#: pretix/static/pretixcontrol/js/ui/main.js:309
msgid "Your color has great contrast and will provide excellent accessibility."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:314
#: pretix/static/pretixcontrol/js/ui/main.js:313
msgid ""
"Your color has decent contrast and is sufficient for minimum accessibility "
"requirements."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:318
#: pretix/static/pretixcontrol/js/ui/main.js:317
msgid ""
"Your color has insufficient contrast to white. Accessibility of your site "
"will be impacted."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:446
#: pretix/static/pretixcontrol/js/ui/main.js:466
#: pretix/static/pretixcontrol/js/ui/main.js:445
#: pretix/static/pretixcontrol/js/ui/main.js:465
msgid "Search query"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:464
#: pretix/static/pretixcontrol/js/ui/main.js:463
msgid "All"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:465
#: pretix/static/pretixcontrol/js/ui/main.js:464
msgid "None"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:469
#: pretix/static/pretixcontrol/js/ui/main.js:468
msgid "Selected only"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:842
#: pretix/static/pretixcontrol/js/ui/main.js:841
msgid "Enter page number between 1 and %(max)s."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:845
#: pretix/static/pretixcontrol/js/ui/main.js:844
msgid "Invalid page number."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1003
#: pretix/static/pretixcontrol/js/ui/main.js:1002
msgid "Use a different name internally"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1043
#: pretix/static/pretixcontrol/js/ui/main.js:1042
msgid "Click to close"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1124
#: pretix/static/pretixcontrol/js/ui/main.js:1123
msgid "You have unsaved changes!"
msgstr ""
File diff suppressed because it is too large Load Diff
+14 -14
View File
@@ -7,7 +7,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-04-28 09:04+0000\n"
"POT-Creation-Date: 2026-03-17 14:06+0000\n"
"PO-Revision-Date: 2024-12-22 00:00+0000\n"
"Last-Translator: Dimitris Tsimpidis <tsimpidisd@gmail.com>\n"
"Language-Team: Greek <https://translate.pretix.eu/projects/pretix/pretix-js/"
@@ -689,14 +689,14 @@ msgstr "Δημιουργία μηνυμάτων …"
msgid "Unknown error."
msgstr "Άγνωστο σφάλμα."
#: pretix/static/pretixcontrol/js/ui/main.js:310
#: pretix/static/pretixcontrol/js/ui/main.js:309
#, fuzzy
#| msgid "Your color has great contrast and is very easy to read!"
msgid "Your color has great contrast and will provide excellent accessibility."
msgstr ""
"Το χρώμα σας έχει μεγάλη αντίθεση και είναι πολύ εύκολο να το διαβάσετε!"
#: pretix/static/pretixcontrol/js/ui/main.js:314
#: pretix/static/pretixcontrol/js/ui/main.js:313
#, fuzzy
#| msgid "Your color has decent contrast and is probably good-enough to read!"
msgid ""
@@ -706,46 +706,46 @@ msgstr ""
"Το χρώμα σας έχει αξιοπρεπή αντίθεση και είναι ίσως αρκετά καλό για να "
"διαβάσετε!"
#: pretix/static/pretixcontrol/js/ui/main.js:318
#: pretix/static/pretixcontrol/js/ui/main.js:317
msgid ""
"Your color has insufficient contrast to white. Accessibility of your site "
"will be impacted."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:446
#: pretix/static/pretixcontrol/js/ui/main.js:466
#: pretix/static/pretixcontrol/js/ui/main.js:445
#: pretix/static/pretixcontrol/js/ui/main.js:465
msgid "Search query"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:464
#: pretix/static/pretixcontrol/js/ui/main.js:463
msgid "All"
msgstr "Όλα"
#: pretix/static/pretixcontrol/js/ui/main.js:465
#: pretix/static/pretixcontrol/js/ui/main.js:464
msgid "None"
msgstr "Κανένας"
#: pretix/static/pretixcontrol/js/ui/main.js:469
#: pretix/static/pretixcontrol/js/ui/main.js:468
msgid "Selected only"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:842
#: pretix/static/pretixcontrol/js/ui/main.js:841
msgid "Enter page number between 1 and %(max)s."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:845
#: pretix/static/pretixcontrol/js/ui/main.js:844
msgid "Invalid page number."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1003
#: pretix/static/pretixcontrol/js/ui/main.js:1002
msgid "Use a different name internally"
msgstr "Χρησιμοποιήστε διαφορετικό όνομα εσωτερικά"
#: pretix/static/pretixcontrol/js/ui/main.js:1043
#: pretix/static/pretixcontrol/js/ui/main.js:1042
msgid "Click to close"
msgstr "Κάντε κλικ για να κλείσετε"
#: pretix/static/pretixcontrol/js/ui/main.js:1124
#: pretix/static/pretixcontrol/js/ui/main.js:1123
msgid "You have unsaved changes!"
msgstr ""
File diff suppressed because it is too large Load Diff
+14 -14
View File
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-04-28 09:04+0000\n"
"POT-Creation-Date: 2026-03-17 14:06+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -632,56 +632,56 @@ msgstr ""
msgid "Unknown error."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:310
#: pretix/static/pretixcontrol/js/ui/main.js:309
msgid "Your color has great contrast and will provide excellent accessibility."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:314
#: pretix/static/pretixcontrol/js/ui/main.js:313
msgid ""
"Your color has decent contrast and is sufficient for minimum accessibility "
"requirements."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:318
#: pretix/static/pretixcontrol/js/ui/main.js:317
msgid ""
"Your color has insufficient contrast to white. Accessibility of your site "
"will be impacted."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:446
#: pretix/static/pretixcontrol/js/ui/main.js:466
#: pretix/static/pretixcontrol/js/ui/main.js:445
#: pretix/static/pretixcontrol/js/ui/main.js:465
msgid "Search query"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:464
#: pretix/static/pretixcontrol/js/ui/main.js:463
msgid "All"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:465
#: pretix/static/pretixcontrol/js/ui/main.js:464
msgid "None"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:469
#: pretix/static/pretixcontrol/js/ui/main.js:468
msgid "Selected only"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:842
#: pretix/static/pretixcontrol/js/ui/main.js:841
msgid "Enter page number between 1 and %(max)s."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:845
#: pretix/static/pretixcontrol/js/ui/main.js:844
msgid "Invalid page number."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1003
#: pretix/static/pretixcontrol/js/ui/main.js:1002
msgid "Use a different name internally"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1043
#: pretix/static/pretixcontrol/js/ui/main.js:1042
msgid "Click to close"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/main.js:1124
#: pretix/static/pretixcontrol/js/ui/main.js:1123
msgid "You have unsaved changes!"
msgstr ""
File diff suppressed because it is too large Load Diff
+18 -18
View File
@@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-04-28 09:04+0000\n"
"PO-Revision-Date: 2026-03-30 03:00+0000\n"
"POT-Creation-Date: 2026-03-17 14:06+0000\n"
"PO-Revision-Date: 2026-01-27 14:51+0000\n"
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
"Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/pretix-"
"js/es/>\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.16.2\n"
"X-Generator: Weblate 5.15.2\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
@@ -60,7 +60,7 @@ msgstr "PayPal Paga Después"
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:41
msgid "iDEAL | Wero"
msgstr "iDEAL | Wero"
msgstr ""
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:42
msgid "SEPA Direct Debit"
@@ -329,7 +329,7 @@ msgstr "Pedido no aprobado"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:68
msgid "Checked-in Tickets"
msgstr "Billetes registrados"
msgstr "Registro de código QR"
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:69
msgid "Valid Tickets"
@@ -648,12 +648,12 @@ msgstr "Generando mensajes…"
msgid "Unknown error."
msgstr "Error desconocido."
#: pretix/static/pretixcontrol/js/ui/main.js:310
#: pretix/static/pretixcontrol/js/ui/main.js:309
msgid "Your color has great contrast and will provide excellent accessibility."
msgstr ""
"El color tiene un gran contraste y proporcionará una excelente accesibilidad."
#: pretix/static/pretixcontrol/js/ui/main.js:314
#: pretix/static/pretixcontrol/js/ui/main.js:313
msgid ""
"Your color has decent contrast and is sufficient for minimum accessibility "
"requirements."
@@ -661,7 +661,7 @@ msgstr ""
"El color tiene un contraste decente y es suficiente para los requisitos "
"mínimos de accesibilidad."
#: pretix/static/pretixcontrol/js/ui/main.js:318
#: pretix/static/pretixcontrol/js/ui/main.js:317
msgid ""
"Your color has insufficient contrast to white. Accessibility of your site "
"will be impacted."
@@ -669,40 +669,40 @@ msgstr ""
"El color no tiene suficiente contraste con el blanco. La accesibilidad de su "
"sitio se verá afectada."
#: pretix/static/pretixcontrol/js/ui/main.js:446
#: pretix/static/pretixcontrol/js/ui/main.js:466
#: pretix/static/pretixcontrol/js/ui/main.js:445
#: pretix/static/pretixcontrol/js/ui/main.js:465
msgid "Search query"
msgstr "Consulta de búsqueda"
#: pretix/static/pretixcontrol/js/ui/main.js:464
#: pretix/static/pretixcontrol/js/ui/main.js:463
msgid "All"
msgstr "Todos"
#: pretix/static/pretixcontrol/js/ui/main.js:465
#: pretix/static/pretixcontrol/js/ui/main.js:464
msgid "None"
msgstr "Ninguno"
#: pretix/static/pretixcontrol/js/ui/main.js:469
#: pretix/static/pretixcontrol/js/ui/main.js:468
msgid "Selected only"
msgstr "Solamente seleccionados"
#: pretix/static/pretixcontrol/js/ui/main.js:842
#: pretix/static/pretixcontrol/js/ui/main.js:841
msgid "Enter page number between 1 and %(max)s."
msgstr "Introduce un número de página entre 1 y %(max)s."
#: pretix/static/pretixcontrol/js/ui/main.js:845
#: pretix/static/pretixcontrol/js/ui/main.js:844
msgid "Invalid page number."
msgstr "Número de página inválido."
#: pretix/static/pretixcontrol/js/ui/main.js:1003
#: pretix/static/pretixcontrol/js/ui/main.js:1002
msgid "Use a different name internally"
msgstr "Usar un nombre diferente internamente"
#: pretix/static/pretixcontrol/js/ui/main.js:1043
#: pretix/static/pretixcontrol/js/ui/main.js:1042
msgid "Click to close"
msgstr "Click para cerrar"
#: pretix/static/pretixcontrol/js/ui/main.js:1124
#: pretix/static/pretixcontrol/js/ui/main.js:1123
msgid "You have unsaved changes!"
msgstr "¡Tienes cambios sin guardar!"

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