Compare commits

..
Author SHA1 Message Date
Raphael Michel a3a07f5b76 Do not use redis cache at import time
During our [2026-06-27 incident](https://pretix.eu/about/en/blog/20260630-pretix-hosted-outage/),
we noticed that pretix is using redis at import time. This means that
gunicorn and celery process were unable to start on servers who could
currently not reach redis. This is kinda mitigated through auto-restart
on systemd or docker level, but that's not really how it is supposed to
work. Celery even has smart retry/reconnect logic that becomes pointless
this way.
2026-06-30 13:10:45 +02:00
120 changed files with 788 additions and 1745 deletions
+2 -3
View File
@@ -53,7 +53,6 @@ dependencies = [
"django-oauth-toolkit==2.3.*",
"django-otp==1.7.*",
"django-phonenumber-field==8.4.*",
"django-querytagger==0.0.3",
"django-redis==6.0.*",
"django-scopes==2.0.*",
"django-statici18n==2.7.*",
@@ -77,7 +76,7 @@ dependencies = [
"paypal-checkout-serversdk==1.0.*",
"PyJWT==2.13.*",
"phonenumberslite==9.0.*",
"Pillow==12.3.*",
"Pillow==12.2.*",
"pretix-plugin-build",
"protobuf==7.35.*",
"psycopg2-binary",
@@ -94,7 +93,7 @@ dependencies = [
"redis==7.4.*",
"reportlab==4.5.*",
"requests==2.32.*",
"sentry-sdk==2.64.*",
"sentry-sdk==2.63.*",
"sepaxml==2.7.*",
"stripe==7.9.*",
"text-unidecode==1.*",
+2 -2
View File
@@ -6,8 +6,8 @@ localecompile:
./manage.py compilemessages
localegen:
./manage.py makemessages --keep-pot --add-location file --ignore "pretix/static/npm_dir/*" $(LNGS)
./manage.py makemessages --keep-pot --add-location file -e js,ts,vue -d djangojs --ignore "pretix/static/npm_dir/*" --ignore "pretix/helpers/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static.dist/*" --ignore "data/*" --ignore "pretix/static/rrule/*" --ignore "build/*" $(LNGS)
./manage.py makemessages --keep-pot --ignore "pretix/static/npm_dir/*" $(LNGS)
./manage.py makemessages --keep-pot -e js,ts,vue -d djangojs --ignore "pretix/static/npm_dir/*" --ignore "pretix/helpers/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static/jsi18n/*" --ignore "pretix/static.dist/*" --ignore "data/*" --ignore "pretix/static/rrule/*" --ignore "build/*" $(LNGS)
staticfiles: npminstall npmbuild jsi18n
./manage.py collectstatic --noinput
+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.7.0.dev0"
__version__ = "2026.6.0.dev0"
-1
View File
@@ -118,7 +118,6 @@ ALL_LANGUAGES = [
('sv', _('Swedish')),
('es', _('Spanish')),
('es-419', _('Spanish (Latin America)')),
('th', _('Thai')),
('tr', _('Turkish')),
('uk', _('Ukrainian')),
]
+16 -10
View File
@@ -53,6 +53,7 @@ from django.db.models import QuerySet
from django.forms import Select, widgets
from django.forms.widgets import FILE_INPUT_CONTRADICTION
from django.utils.formats import date_format
from django.utils.functional import lazy
from django.utils.html import escape
from django.utils.safestring import mark_safe
from django.utils.text import format_lazy
@@ -324,16 +325,21 @@ class WrappedPhonePrefixSelect(Select):
initial = None
def __init__(self, initial=None):
choices = [("", "---------")]
def _get_choices():
choices = [("", "---------")]
if initial:
for prefix, values in COUNTRY_CODE_TO_REGION_CODE.items():
if all(v == REGION_CODE_FOR_NON_GEO_ENTITY for v in values):
continue
if initial in values:
self.initial = "+%d" % prefix
break
choices += get_phone_prefixes_sorted_and_localized()
return choices
choices = lazy(_get_choices, list)()
if initial:
for prefix, values in COUNTRY_CODE_TO_REGION_CODE.items():
if all(v == REGION_CODE_FOR_NON_GEO_ENTITY for v in values):
continue
if initial in values:
self.initial = "+%d" % prefix
break
choices += get_phone_prefixes_sorted_and_localized()
super().__init__(choices=choices, attrs={
'aria-label': pgettext_lazy('phonenumber', 'International area code'),
'autocomplete': 'tel-country-code',
@@ -1398,7 +1404,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
elif self.validate_vat_id and vat_id_applicable:
try:
normalized_id = validate_vat_id(data.get('vat_id'), str(data.get('country')))
self.instance.vat_id_validated = bool(normalized_id)
self.instance.vat_id_validated = True
self.instance.vat_id = data['vat_id'] = normalized_id
except VATIDFinalError as e:
if self.all_optional:
@@ -0,0 +1,29 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
from django.core.management.base import BaseCommand
class Command(BaseCommand):
help = "Do nothing. Useful for startup performance testing."
def handle(self, *args, **options):
pass
+46 -76
View File
@@ -19,8 +19,6 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
import logging
import re
from collections import OrderedDict
from urllib.parse import urlparse, urlsplit
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
@@ -45,8 +43,6 @@ from pretix.multidomain.urlreverse import (
)
from pretix.presale.style import get_fonts
logger = logging.getLogger(__name__)
_supported = None
@@ -227,26 +223,7 @@ def _parse_csp(header):
return h
VALID_CSP_DIRECTIVES = [
"child-src", "connect-src", "default-src", "fenced-frame-src", "font-src", "form-action", "frame-src", "img-src",
"manifest-src", "media-src", "object-src", "prefetch-src", "report-uri", "script-src", "script-src-elem",
"script-src-attr", "style-src", "style-src-elem", "style-src-attr", "worker-src",
]
CSP_ILLEGAL_CHARS = re.compile(r'[\s,;]')
def _sanitize_csp(h):
for k, v in h.items():
if k not in VALID_CSP_DIRECTIVES:
raise ValueError("Invalid CSP directive " + k)
if any(CSP_ILLEGAL_CHARS.search(el) for el in v):
logger.warning("Stripping invalid component from CSP: %r", h)
h[k] = [el for el in v if not CSP_ILLEGAL_CHARS.search(el)]
def _render_csp(h):
_sanitize_csp(h)
return "; ".join(k + ' ' + ' '.join(v) for k, v in h.items() if v)
@@ -266,7 +243,21 @@ def _merge_csp(a, b):
class SecurityMiddleware(MiddlewareMixin):
CSP_EXEMPT = (
'/api/v1/docs/',
)
def process_response(self, request, resp):
def nested_dict_values(d):
for v in d.values():
if isinstance(v, dict):
yield from nested_dict_values(v)
else:
if isinstance(v, str):
yield v
url = resolve(request.path_info)
if settings.DEBUG and resp.status_code >= 400:
# Don't use CSP on debug error page as it breaks of Django's fancy error
# pages
@@ -277,15 +268,18 @@ class SecurityMiddleware(MiddlewareMixin):
# https://github.com/pretix/pretix/issues/765
resp['P3P'] = 'CP=\"ALL DSP COR CUR ADM TAI OUR IND COM NAV INT\"'
if not getattr(resp, '_csp_ignore', False):
resp['Content-Security-Policy'] = _render_csp(self._build_csp(request, resp))
elif 'Content-Security-Policy' in resp:
del resp['Content-Security-Policy']
img_src = []
gs = global_settings_object(request)
if gs.settings.leaflet_tiles:
img_src.append(gs.settings.leaflet_tiles[:gs.settings.leaflet_tiles.index("/", 10)].replace("{s}", "*"))
return resp
def _build_csp(self, request, resp):
url = resolve(request.path_info)
font_src = set()
if hasattr(request, 'event'):
for font in get_fonts(request.event, pdf_support_required=False).values():
for path in list(nested_dict_values(font)):
font_location = urlparse(path)
if font_location.scheme and font_location.netloc:
font_src.add('{}://{}'.format(font_location.scheme, font_location.netloc))
h = {
'default-src': ["{static}"],
@@ -294,8 +288,8 @@ class SecurityMiddleware(MiddlewareMixin):
'frame-src': ['{static}'],
'style-src': ["{static}", "{media}"],
'connect-src': ["{dynamic}", "{media}"],
'img-src': ["{static}", "{media}", "data:"],
'font-src': ["{static}"],
'img-src': ["{static}", "{media}", "data:"] + img_src,
'font-src': ["{static}"] + list(font_src),
'media-src': ["{static}", "data:"],
# form-action is not only used to match on form actions, but also on URLs
# form-actions redirect to. In the context of e.g. payment providers or
@@ -304,13 +298,6 @@ class SecurityMiddleware(MiddlewareMixin):
'form-action': ["{dynamic}", "https:"] + (['http:'] if settings.SITE_URL.startswith('http://') else []),
}
gs = global_settings_object(request)
if gs.settings.leaflet_tiles:
h['img-src'].append(gs.settings.leaflet_tiles[:gs.settings.leaflet_tiles.index("/", 10)].replace("{s}", "*"))
if hasattr(request, 'event'):
h['font-src'] += list(self._get_font_origins(request.event))
if settings.VITE_DEV_MODE:
h['script-src'] += ["http://localhost:5173", "ws://localhost:5173"]
h['style-src'] += ["'unsafe-inline'"]
@@ -322,7 +309,6 @@ class SecurityMiddleware(MiddlewareMixin):
if not settings.VITE_DEV_MODE:
# can't have 'unsafe-inline' and nonce at the same time
h['style-src'].append(nonce)
# Only include pay.google.com for wallet detection purposes on the Payment selection page
if (
url.url_name == "event.order.pay.change" or
@@ -331,32 +317,27 @@ class SecurityMiddleware(MiddlewareMixin):
h['script-src'].append('https://pay.google.com')
h['frame-src'].append('https://pay.google.com')
h['connect-src'].append('https://google.com/pay')
if settings.LOG_CSP:
h['report-uri'] = ["/csp_report/"]
if 'Content-Security-Policy' in resp:
_merge_csp(h, _parse_csp(resp['Content-Security-Policy']))
if settings.CSP_ADDITIONAL_HEADER:
_merge_csp(h, _parse_csp(settings.CSP_ADDITIONAL_HEADER))
placeholders = {
"{static}": ["'self'"],
"{dynamic}": ["'self'"],
"{media}": ["'self'"],
}
staticdomain = "'self'"
dynamicdomain = "'self'"
mediadomain = "'self'"
if settings.MEDIA_URL.startswith('http'):
placeholders["{media}"].append(settings.MEDIA_URL[:settings.MEDIA_URL.find('/', 9)])
mediadomain += " " + settings.MEDIA_URL[:settings.MEDIA_URL.find('/', 9)]
if settings.STATIC_URL.startswith('http'):
placeholders["{static}"].append(settings.STATIC_URL[:settings.STATIC_URL.find('/', 9)])
staticdomain += " " + settings.STATIC_URL[:settings.STATIC_URL.find('/', 9)]
if settings.SITE_URL.startswith('http'):
if settings.SITE_URL.find('/', 9) > 0:
placeholders["{static}"].append(settings.SITE_URL[:settings.SITE_URL.find('/', 9)])
placeholders["{dynamic}"].append(settings.SITE_URL[:settings.SITE_URL.find('/', 9)])
staticdomain += " " + settings.SITE_URL[:settings.SITE_URL.find('/', 9)]
dynamicdomain += " " + settings.SITE_URL[:settings.SITE_URL.find('/', 9)]
else:
placeholders["{static}"].append(settings.SITE_URL)
placeholders["{dynamic}"].append(settings.SITE_URL)
staticdomain += " " + settings.SITE_URL
dynamicdomain += " " + settings.SITE_URL
if hasattr(request, 'organizer') and request.organizer:
if hasattr(request, 'event') and request.event:
@@ -367,29 +348,18 @@ class SecurityMiddleware(MiddlewareMixin):
siteurlsplit = urlsplit(settings.SITE_URL)
if siteurlsplit.port and siteurlsplit.port not in (80, 443):
domain = '%s:%d' % (domain, siteurlsplit.port)
placeholders["{dynamic}"].append(domain)
dynamicdomain += " " + domain
for k, v in h.items():
h[k] = sorted(set(result for part in v for result in placeholders.get(part, [part])))
if request.path not in self.CSP_EXEMPT and not getattr(resp, '_csp_ignore', False):
resp['Content-Security-Policy'] = _render_csp(h).format(static=staticdomain, dynamic=dynamicdomain,
media=mediadomain)
for k, v in h.items():
h[k] = sorted(set(' '.join(v).format(static=staticdomain, dynamic=dynamicdomain, media=mediadomain).split(' ')))
resp['Content-Security-Policy'] = _render_csp(h)
elif 'Content-Security-Policy' in resp:
del resp['Content-Security-Policy']
return h
def _get_font_origins(self, event):
def nested_dict_values(d):
for v in d.values():
if isinstance(v, dict):
yield from nested_dict_values(v)
else:
if isinstance(v, str):
yield v
font_src = set()
for font in get_fonts(event, pdf_support_required=False).values():
for path in list(nested_dict_values(font)):
font_location = urlparse(path)
if font_location.scheme and font_location.netloc:
font_src.add('{}://{}'.format(font_location.scheme, font_location.netloc))
return font_src
return resp
class RejectInvalidInputMiddleware(MiddlewareMixin):
+9 -6
View File
@@ -647,22 +647,25 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
id__in=self.teams.filter(TeamQuerySet.organizer_permission_q(permission)).values_list('organizer', flat=True)
)
def has_active_staff_session(self, session_key):
def has_active_staff_session(self, session_key=None):
"""
Returns whether or not a user has an active staff session (formerly known as superuser session)
with the given session key.
"""
return self.get_active_staff_session(session_key) is not None
def get_active_staff_session(self, session_key):
if not self.is_staff or not session_key:
def get_active_staff_session(self, session_key=None):
if not self.is_staff:
return None
if not hasattr(self, '_staff_session_cache'):
self._staff_session_cache = {}
if session_key not in self._staff_session_cache:
sess = StaffSession.objects.filter(
user=self, date_end__isnull=True, session_key=session_key
).first()
qs = StaffSession.objects.filter(
user=self, date_end__isnull=True
)
if session_key:
qs = qs.filter(session_key=session_key)
sess = qs.first()
if sess:
if sess.date_start < now() - timedelta(seconds=settings.PRETIX_SESSION_TIMEOUT_ABSOLUTE):
sess.date_end = now()
-513
View File
@@ -1,513 +0,0 @@
from dataclasses import dataclass, field
from decimal import Decimal
from itertools import chain
from typing import (
TYPE_CHECKING, Any, Callable, Dict, List, Literal, Optional, Protocol, Set,
Tuple, TypeAlias,
)
from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.db.models import Prefetch
from django.utils.translation import gettext_lazy as _
from pretix.base.decimal import round_decimal
from pretix.base.models import Event, Item, ItemVariation, Order, OrderPosition
from pretix.base.reldate import ModelRelativeDateTimeField
from pretix.base.signals import self_service_cancellation_checks
"""
Supporting self-service cancellation requires us to do two main things:
1. uphold the business logic of pretix and the installed plugins
2. charge the customer the appropriate fees for their cancellation
Number 1 is a question of bringing enough checks into place and prevent a
cancellation if one of them is violated.
Checks need to subclass `CancellationCheck` and can be provided via the new
`self_service_cancellation_checks` signal.
Number 2 is trickier because organizers will have complex^(TM) cancellation
fee structures and expressing these in an understandable way is a challenge.
Especially when taking support cases into consideration that have to debug
certain behaviour long after.
The cancellation fees are computed via `CancellationRules`.
When a customer triggers a self service cancellation, we will:
1. Positions
a. Evaluate all `CancellationChecks` that are concerned with individual positions
b. Evaluate all `CancellationRules` that are concerned with individual positions and compute the fees
c. Choose for each position the cheapest `CancellationRules` position result available
2. Process
a. Evaluate all `CancellationChecks` that are concerned with the process of cancellation
b. Evaluate all `CancellationRules` that are concerned with the process of cancellation
c. Choose the cheapest `CancellationRules` process result available
3. Return all results for Checks and Rules
Step 1c. and 2c. are kept separate intentionally.
The alternative of finding the cheapest cancellation option overall (process and position) would
require us to check the full combinatorics of possible process and position fees, resulting
in unfeasible runtime behaviour, and if we would optimize it in difficult to explain non-optimal
situations.
"""
class FeeType(models.TextChoices):
"""
Process fees can be added on top of all position fees, or they
can set a floor for the minimum cancellation fee that this will incur.
"""
MINIMUM = "min_process_fee", _("Minimum total fee")
ADDITIONAL = "add_process_fee", _("Additional fee")
POSITION = "position_fee", _("Position fee")
class CheckTypes(models.TextChoices):
POSITION = "position", _("Order Position Cancellation Rule")
PROCESS = "process", _("Cancellation Process Rule")
@dataclass(frozen=True)
class CheckResult:
"""
Result of an individual cancellation check.
The check result only encodes if the check allows or disallows cancellation via
`cancellation_possible`
"""
id: str
reason: str
cancellation_possible: bool
type: Literal['check'] = field(default="check")
@dataclass(frozen=True)
class RuleResult:
"""
Result of evaluating a CancellationRule.
A rule can consist out of multiple different checks, each partial_result is recorded individually.
A RuleResult encodes both the feasibility of a cancellation via `cancellation_possible` and
the resulting consequences in form of fees which can be expressed as:
- absolute position fees of a fixed amount
- relative position fees of a percentage of the position price
- minimum process fees, the total cancellation fee across all positions and the process must be at least this
- additional process fee, an additional processing fee is charged in addition to the per position fees
"""
id: int
partial_results: List[CheckResult]
fee_type: FeeType
fee: Decimal
type: Literal['rule'] = field(default="rule")
@property
def cancellation_possible(self) -> bool:
return all(result.cancellation_possible for result in self.partial_results)
@classmethod
def from_absolute_fee(
cls,
id: int,
partial_results: List[CheckResult],
fee_type: Literal[FeeType.POSITION],
absolute_fee: Decimal
) -> "RuleResult":
return RuleResult(id=id, partial_results=partial_results, fee_type=fee_type, fee=absolute_fee)
@classmethod
def from_relative_fee(
cls,
id: int,
partial_results: List[CheckResult],
fee_type: Literal[FeeType.POSITION],
position_price: Decimal,
percentage: Decimal,
currency: str
) -> "RuleResult":
return RuleResult(id=id, partial_results=partial_results, fee_type=fee_type,
fee=round_decimal(position_price * (percentage / 100), currency))
@classmethod
def from_process_fee(
cls,
id: int,
partial_results: List[CheckResult],
fee_type: Literal[FeeType.MINIMUM, FeeType.ADDITIONAL],
absolute_fee: Decimal,
reference_price: Decimal
) -> "RuleResult":
fee = Decimal(0)
if fee_type == FeeType.MINIMUM:
if reference_price < absolute_fee:
fee = absolute_fee - reference_price
else:
fee = Decimal(0)
elif fee_type == FeeType.ADDITIONAL:
fee = absolute_fee
else:
raise ValueError("Unknown fee type")
return RuleResult(id=id, partial_results=partial_results, fee_type=fee_type, fee=fee)
def __lt__(self, other: object) -> bool:
if not isinstance(other, RuleResult):
return NotImplemented
if self.cancellation_possible == other.cancellation_possible:
return self.fee < other.fee
else:
return self.cancellation_possible and not other.cancellation_possible
@dataclass(frozen=True)
class Checks:
position: List["CancellationCheck"]
process: List["CancellationCheck"]
@property
def prefetches(self) -> List[Callable[[], Prefetch]]:
return list(chain([check.prefetches for check in [*self.position, *self.process]]))
@property
def related_selects(self) -> List[str]:
return list(chain([check.related_selects for check in [*self.position, *self.process]]))
PositionSet: TypeAlias = Set[OrderPosition]
class PositionCheckFn(Protocol):
def __call__(self, order: Order, keep: PositionSet, position: OrderPosition) -> CheckResult:
...
class ProcessCheckFn(Protocol):
def __call__(self, order: Order, keep: PositionSet) -> CheckResult:
...
@dataclass(frozen=True)
class CancellationCheck:
id: str
type: CheckTypes
check_fn: PositionCheckFn | ProcessCheckFn = field(compare=False)
prefetches: List[Callable[[], Prefetch]] = field(default_factory=list)
related_selects: List[str] = field(default_factory=list)
def evaluate(self, order: Order, keep: PositionSet,
position: OrderPosition | None) -> CheckResult:
if position and self.type == CheckTypes.POSITION:
return self.check_fn(order, keep, position)
elif position is None and self.type == CheckTypes.PROCESS:
return self.check_fn(order, keep)
else:
raise ValidationError("Type of the rule doesn't match the check_fn")
@dataclass(frozen=True)
class PositionResult:
position_check_results: Dict[int, List[CheckResult]]
position_rule_results: Dict[int, List[RuleResult]]
@property
def cancellation_possible(self) -> bool:
def ok(results: List[CheckResult] | List[RuleResult]) -> bool:
return all([val.cancellation_possible for val in results]) if results else True
return all(
ok(results)
for d in
(self.position_check_results, {key: [min(pos_res)] for key, pos_res in self.position_rule_results.items()})
for results in d.values()
)
@property
def fee_value(self) -> Decimal:
fee_value = Decimal("0.00")
for pos_id, results in self.position_rule_results.items():
if len(results) > 0:
best_option = min(results)
if best_option.cancellation_possible:
fee_value += best_option.fee
return fee_value
@dataclass(frozen=True)
class ProcessResult:
process_check_results: List[CheckResult]
process_rule_results: List[RuleResult]
@property
def cancellation_possible(self) -> bool:
best_option = min(self.process_rule_results)
return all([res.cancellation_possible for res in [*self.process_check_results, best_option]])
@property
def fee_value(self) -> Decimal:
best_option = min(self.process_rule_results)
if best_option.cancellation_possible:
return best_option.fee
return Decimal("0.00")
@dataclass(frozen=True)
class CancellationResult:
position_result: PositionResult
process_result: ProcessResult
@property
def cancellation_possible(self) -> bool:
return self.position_result.cancellation_possible and self.process_result.cancellation_possible
def remember_cancellation(self):
# TODO: store the cancellation verdict in the session storage for X Minutes
pass
def perform_cancellation(self, order: Order, keep: Set[int]):
# TODO load the cancellation verdict from the session and perform the actions
pass
def _send_self_service_cancellation_checks(event: Event) -> List[Tuple[Any, Any]]:
return self_service_cancellation_checks.send(sender=event)
class CancellationRule(models.Model):
event = models.ForeignKey(
Event,
verbose_name=_("Event"),
related_name="cancellation_rule",
on_delete=models.CASCADE
)
type = models.CharField(
verbose_name=_("Type of the cancellation rule"),
default=CheckTypes.POSITION,
choices=CheckTypes,
max_length=15,
)
allowed_until = ModelRelativeDateTimeField(null=True, blank=True)
except_after = ModelRelativeDateTimeField(null=True, blank=True)
prefetches: List[Callable[[], Prefetch]] = []
related_selects: List[str] = []
@staticmethod
def _collect_checks(event: Event, send_fn: Callable[
[Event], List[Tuple[Any, Any]]
] = _send_self_service_cancellation_checks) -> Checks:
position_checks: List[CancellationCheck] = []
process_checks: List[CancellationCheck] = []
seen = set()
for recv, resp in send_fn(event):
if not isinstance(resp, CancellationCheck):
raise ValueError('self_service_cancellation_checks received response of wrong type')
if resp.id in seen:
raise ValueError('self_service_cancellation_checks received multiple responses with the id')
seen.add(resp.id)
if resp.type == CheckTypes.POSITION:
position_checks.append(resp)
if resp.type == CheckTypes.PROCESS:
process_checks.append(resp)
return Checks(position=position_checks, process=process_checks)
@staticmethod
def evaluate(event: Event, order: Order, keep: Set[OrderPosition]) -> "CancellationResult":
# collect all checks, position_rules and process_rules that are applicable
checks = CancellationRule._collect_checks(event=event)
position_rules = PositionCancellationRule.objects.filter(event=event, type=CheckTypes.POSITION)
process_rules = ProcessCancellationRule.objects.filter(event=event, type=CheckTypes.PROCESS)
order = CancellationRule._prefetch_order(event, order, checks)
# keep track of all decisions so we can explain them in the logs
position_check_results: Dict[int, List[CheckResult]] = {}
position_rule_results: Dict[int, List[RuleResult]] = {}
# perform all position checks and position rules
for position in order.positions.all():
position_check_results[position.id] = []
position_rule_results[position.id] = []
# skip this position if customer doesn't want to cancel
if position.id in keep:
continue
# evaluate the system/plugin checks for the position
for check in checks.position:
position_check_results[position.id].append(check.evaluate(order=order, keep=keep, position=position))
# evaluate all customer specified rules for this position
for rule in position_rules:
result = rule.evaluate_position_rule(order=order, keep=keep, position=position)
if result is not None:
position_rule_results[position.id].append(result)
position_results = PositionResult(position_check_results=position_check_results,
position_rule_results=position_rule_results)
# we need the current fee_value to select the cheapest process rule
temp_position_fees = position_results.fee_value
# again keep track of all decisions so we can explain them in the logs
process_check_results: List[CheckResult] = []
process_rule_results: List[RuleResult] = []
# evaluate all system/plugin provided checks for the cancellation process
for check in checks.process:
process_check_results.append(check.evaluate(order=order, keep=keep, position=None))
# evaluate all customer specified rules for the cancellation process
for rule in process_rules:
result = rule.evaluate_process_rule(order=order, keep=keep, position_fees=temp_position_fees)
if result is not None:
process_rule_results.append(result)
process_result = ProcessResult(process_check_results=process_check_results,
process_rule_results=process_rule_results)
return CancellationResult(position_result=position_results, process_result=process_result)
@staticmethod
def _prefetch_order(event: Event, order: Order, checks: Checks) -> Order:
prefetches = [pref() for pref in [*chain(*checks.prefetches),
*chain(*PositionCancellationRule.prefetches),
*chain(*ProcessCancellationRule.prefetches)]]
related_selects = {*chain(*checks.related_selects),
*chain(*PositionCancellationRule.related_selects),
*chain(*ProcessCancellationRule.related_selects)}
order = Order.objects.prefetch_related(*prefetches).select_related(*related_selects).get(event=event,
id=order.id)
return order
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
class PositionCancellationRule(CancellationRule):
"""
PositionCancellationRules answer the questions:
- Can this position be canceled?
- What is the price for cancelling this position?
"""
class Meta:
abstract = True
fee_percentage_per_position = models.DecimalField(
max_digits=5,
decimal_places=2,
validators=[MinValueValidator("0.00"), MaxValueValidator("100.00")],
verbose_name=_("Fee Percentage per OrderPosition"),
default=Decimal("0.00"),
)
fee_absolute_per_position = models.DecimalField(
max_digits=13,
decimal_places=2,
verbose_name=_("Absolute fee per OrderPosition"),
default=Decimal("0.00"),
)
all_products = models.BooleanField(
verbose_name=_("All products and variations"),
default=True,
)
limit_products = models.ManyToManyField(Item, verbose_name=_("Products"), blank=True)
limit_variations = models.ManyToManyField(
ItemVariation, blank=True, verbose_name=_("Variations")
)
prefetches: List[Callable[[], Prefetch]] = []
related_selects: List[str] = []
if TYPE_CHECKING:
allowed_until = ModelRelativeDateTimeField(null=True, blank=True)
except_after = ModelRelativeDateTimeField(null=True, blank=True)
def evaluate_position_rule(self, order: Order, keep: Set[OrderPosition], position: OrderPosition) -> Optional[
RuleResult
]:
if not self.all_products and position.item_id not in self.limit_products.values_list('pk', flat=True):
return None
if not self.all_products and position.variation_id not in self.limit_variations.values_list('pk', flat=True):
return None
rule_results = [] # TODO really evaluate rules
if self.fee_percentage_per_position and self.fee_absolute_per_position:
raise NotImplementedError(
"Combination of fee_percentage_per position and fee_absolute_per_position is not valid")
elif self.fee_absolute_per_position != Decimal(0.00):
return RuleResult.from_absolute_fee(
id=self.id,
partial_results=rule_results,
fee_type=FeeType.POSITION,
absolute_fee=self.fee_absolute_per_position
)
else:
return RuleResult.from_relative_fee(
id=self.id,
partial_results=rule_results,
fee_type=FeeType.POSITION,
position_price=position.price,
percentage=self.fee_absolute_per_position,
currency=order.event.currency
)
class ProcessCancellationRule(CancellationRule):
"""
ProcessCancellationRules answer the question:
- What is the processing fee for performing this cancellation?
"""
class Meta:
abstract = True
fee_cancellation_process = models.DecimalField(
max_digits=13,
decimal_places=2,
verbose_name=_("Absolute fee per Cancellation"),
default=Decimal("0.00"),
)
fee_mode = models.CharField(
verbose_name=_("Restrict to check-in status"),
default=FeeType.MINIMUM,
choices=[
(FeeType.MINIMUM, FeeType.MINIMUM.label),
(FeeType.ADDITIONAL, FeeType.ADDITIONAL.label),
],
max_length=15,
)
prefetches: List[Callable[[], Prefetch]] = []
related_selects: List[str] = []
if TYPE_CHECKING:
allowed_until = ModelRelativeDateTimeField(null=True, blank=True)
except_after = ModelRelativeDateTimeField(null=True, blank=True)
def evaluate_process_rule(self, order: Order, keep: Set[OrderPosition], position_fees: Decimal) -> \
Optional[RuleResult]:
rule_results = [] # TODO really evaluate rules
fee_type = self.fee_mode
if fee_type not in (FeeType.MINIMUM, FeeType.ADDITIONAL):
raise ValueError(f"Unexpected fee_mode: {fee_type!r}")
return RuleResult.from_process_fee(
id=self.id,
partial_results=rule_results,
fee_type=fee_type,
absolute_fee=self.fee_cancellation_process,
reference_price=position_fees,
)
+1 -8
View File
@@ -40,7 +40,6 @@ import warnings
from collections import Counter, OrderedDict, defaultdict
from datetime import datetime, time, timedelta
from operator import attrgetter
from typing import TYPE_CHECKING
from urllib.parse import urljoin
from zoneinfo import ZoneInfo
@@ -80,16 +79,10 @@ from pretix.helpers.thumb import get_thumbnail
from ..settings import settings_hierarkey
from .organizer import Organizer, Team
if TYPE_CHECKING:
from hierarkey.proxy import HierarkeyProxy
logger = logging.getLogger(__name__)
class EventMixin:
if TYPE_CHECKING:
settings: HierarkeyProxy
def clean(self):
if self.presale_start and self.presale_end and self.presale_start > self.presale_end:
raise ValidationError({'presale_end': _('The end of the presale period has to be later than its start.')})
@@ -906,7 +899,7 @@ class Event(EventMixin, LoggedModel):
self.save()
self.log_action('pretix.object.cloned', data={'source': other.slug, 'source_id': other.pk})
if hasattr(other, 'alternative_domain_assignment') and not is_cross_organizer:
if hasattr(other, 'alternative_domain_assignment'):
other.alternative_domain_assignment.domain.event_assignments.create(event=self)
if not self.all_sales_channels:
-7
View File
@@ -35,7 +35,6 @@ import operator
import string
from datetime import date, datetime, time
from functools import reduce
from typing import TYPE_CHECKING
import pytz_deprecation_shim
from django.conf import settings
@@ -62,9 +61,6 @@ from ...helpers.permission_migration import (
from ..settings import settings_hierarkey
from .auth import User
if TYPE_CHECKING:
from hierarkey.proxy import HierarkeyProxy
@settings_hierarkey.add(cache_namespace='organizer')
class Organizer(LoggedModel):
@@ -82,9 +78,6 @@ class Organizer(LoggedModel):
"""
settings_namespace = 'organizer'
if TYPE_CHECKING:
settings: HierarkeyProxy
name = models.CharField(max_length=200,
verbose_name=_("Name"))
slug = models.CharField(
+1 -1
View File
@@ -834,7 +834,7 @@ class BasePaymentProvider:
"""
raise NotImplementedError() # NOQA
def execute_payment(self, request: HttpRequest, payment: OrderPayment) -> str | None:
def execute_payment(self, request: HttpRequest, payment: OrderPayment) -> str:
"""
After the user has confirmed their purchase, this method will be called to complete
the payment process. This is the place to actually move the money if applicable.
+68 -92
View File
@@ -41,7 +41,6 @@ from collections import Counter, defaultdict, namedtuple
from datetime import datetime, time, timedelta
from decimal import Decimal
from functools import reduce
from time import sleep
from typing import List, Optional
@@ -51,8 +50,8 @@ from django.core.cache import cache
from django.core.exceptions import ValidationError
from django.db import transaction
from django.db.models import (
Count, Exists, F, IntegerField, Max, Min, OuterRef, Prefetch, Q, QuerySet,
Sum, Value,
Count, Exists, F, IntegerField, Max, Min, OuterRef, Q, QuerySet, Sum,
Value,
)
from django.db.models.functions import Coalesce, Greatest
from django.db.transaction import get_connection
@@ -68,13 +67,10 @@ 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,
CartPosition, Device, Event, GiftCard, Item, ItemVariation, LogEntry,
Membership, Order, OrderPayment, OrderPosition, Quota, Seat,
SeatCategoryMapping, User, Voucher,
)
from pretix.base.models.cancellation import (
CancellationCheck, CheckResult, CheckTypes, PositionSet,
)
from pretix.base.models.event import SubEvent
from pretix.base.models.orders import (
BlockedTicketSecret, InvoiceAddress, OrderFee, OrderRefund,
@@ -107,7 +103,7 @@ from pretix.base.signals import (
order_approved, order_canceled, order_changed, order_denied, order_expired,
order_expiry_changed, order_fee_calculation, order_paid, order_placed,
order_reactivated, order_split, order_valid_if_pending, periodic_task,
self_service_cancellation_checks, validate_order,
validate_order,
)
from pretix.base.timemachine import time_machine_now, time_machine_now_assigned
from pretix.celery_app import app
@@ -1623,7 +1619,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'))
'valid_from', 'valid_until', 'is_bundled', 'result', 'count'))
SplitOperation = namedtuple('SplitOperation', ('position',))
FeeValueOperation = namedtuple('FeeValueOperation', ('fee', 'value', 'price_diff'))
AddFeeOperation = namedtuple('AddFeeOperation', ('fee', 'price_diff'))
@@ -1637,16 +1633,24 @@ class OrderChangeManager:
ForceRecomputeOperation = namedtuple('ForceRecomputeOperation', tuple())
class AddPositionResult:
_position: Optional[OrderPosition]
_positions: Optional[List[OrderPosition]]
def __init__(self):
self._position = None
self._positions = None
@property
def position(self) -> OrderPosition:
if self._position is None:
if self._positions is None:
raise RuntimeError("Order position has not been created yet. Call commit() first on OrderChangeManager.")
return self._position
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
def __init__(self, order: Order, user=None, auth=None, notify=True, reissue_invoice=True, allow_blocked_seats=False):
self.order = order
@@ -1853,8 +1857,12 @@ 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) -> 'OrderChangeManager.AddPositionResult':
valid_from: datetime = None, valid_until: datetime = None, count: int = 1) -> 'OrderChangeManager.AddPositionResult':
if count < 1:
raise ValueError("Count must be positive")
if isinstance(seat, str):
if count > 1:
raise ValueError("Cannot combine count > 1 with seat")
if not seat:
seat = None
else:
@@ -1908,14 +1916,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
self._quotadiff.update(new_quotas)
self._totaldiff_guesstimate += price.gross * count
self._quotadiff.update({q: count for q in 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))
valid_from, valid_until, is_bundled, result, count))
return result
def split(self, position: OrderPosition):
@@ -2535,29 +2543,35 @@ class OrderChangeManager:
secret_dirty.remove(position)
position.save(update_fields=['canceled', 'secret'])
elif isinstance(op, self.AddOperation):
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
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)
elif isinstance(op, self.SplitOperation):
position = position_cache.setdefault(op.position.pk, op.position)
split_positions.append(position)
@@ -2882,7 +2896,7 @@ class OrderChangeManager:
return total
def _check_order_size(self):
if (len(self.order.positions.all()) + len([op for op in self._operations if isinstance(op, self.AddOperation)])) > settings.PRETIX_MAX_ORDER_SIZE:
if (len(self.order.positions.all()) + sum([op.count 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,
@@ -2943,7 +2957,7 @@ class OrderChangeManager:
]) + len([
o for o in self._operations if isinstance(o, self.SplitOperation)
])
adds = len([o for o in self._operations if isinstance(o, self.AddOperation)])
adds = sum([o.count 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'])
@@ -2990,17 +3004,18 @@ 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):
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)
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)
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:
@@ -3511,42 +3526,3 @@ def signal_listener_issue_media(sender: Event, order: Order, **kwargs):
'customer': order.customer_id,
}
)
def position_not_used_cancellation_check(order: Order, keep: PositionSet, position: OrderPosition):
for pos in order.all_positions.all():
if pos == position and position not in keep:
for checkin in pos.all_checkins.all():
if checkin.successful and checkin.list.consider_tickets_used:
return CheckResult(
id="pretixbase_position_not_used",
reason=f"Position used in Checkin {checkin}",
cancellation_possible=False,
)
else:
return CheckResult(
id="pretixbase_position_not_used",
reason="Position not up for cancellation",
cancellation_possible=True,
)
return CheckResult(
id="pretixbase_position_not_used",
reason="Ticket not used",
cancellation_possible=True,
)
@receiver(self_service_cancellation_checks, dispatch_uid="pretixbase_position_not_used")
def signal_listener_position_not_used(sender: Event, **kwargs):
return CancellationCheck(id="pretixbase_position_not_used",
type=CheckTypes.POSITION,
check_fn=position_not_used_cancellation_check,
prefetches=[
lambda: Prefetch('all_positions__all_checkins__list', )
])
# TODO weitere System Checks
# OrderPositions mit Item.min_per_order dürfen nur storniert werden, wenn genug übrig bleiben oder alle des gleichen Items storniert werden
# OrderPositions mit addon_to != None dürfen nur über den bestehenden Add-On-Flow storniert werden
# OrderPositions mit is_bundled dürfen nur mit der Parent-Position zusammen storniert werden
+3 -1
View File
@@ -1276,7 +1276,7 @@ DEFAULTS = {
'serializer_class': serializers.BooleanField,
'write_permission': 'event.settings.invoicing:write',
'form_kwargs': dict(
label=_("Allow updating existing invoices"),
label=_("Allow to update existing invoices"),
help_text=_("By default, invoices can never again be changed once they are issued. In most countries, we "
"recommend to leave this option turned off and always issue a new invoice if a change needs "
"to be made."),
@@ -1924,6 +1924,8 @@ DEFAULTS = {
'serializer_class': serializers.BooleanField,
'form_kwargs': dict(
label=_("Hide all past dates from calendar"),
help_text=_("This option currently only affects the calendar of this event series, not the organizer-wide "
"calendar.")
)
},
'allow_modifications': {
-8
View File
@@ -1206,11 +1206,3 @@ This signal is sent out each time the information for a Device is modified.
Both the original and updated versions of the Device are included to allow
receivers to see what has been updated.
"""
self_service_cancellation_checks = EventPluginSignal()
"""
This signal is sent out to collect checks to approve or deny a self service cancellation.
You are expected to return a class instance that implements CancellationCheck.
It is is expected that that the CheckFn will not issue any further queries.
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
"""
@@ -1,28 +0,0 @@
{% extends "error.html" %}
{% load i18n %}
{% load eventurl %}
{% load urlreplace %}
{% load static %}
{% block content %}
<h1>{% trans "Please continue in a new tab" %}</h1>
<p class="larger">
{% blocktrans trimmed %}
For security reasons, the following step is only possible in a new tab.
{% endblocktrans %}
</p>
<p class="larger">
{% blocktrans trimmed %}
If the new tab did not open automatically, please click the following button:
{% endblocktrans %}
</p>
<div class="text-center">
<a href="{{ url }}"
class="btn btn-primary btn-lg" target="_blank">
<span class="fa fa-external-link-square"></span>
{% trans "Continue in new tab" %}
</a>
{{ url|json_script:"framebreak-url" }}
<script type="text/javascript" src="{% static "pretixbase/js/framebreak.js" %}"></script>
</div>
{% endblock %}
+4 -2
View File
@@ -42,6 +42,8 @@ from bleach import DEFAULT_CALLBACKS, html5lib_shim
from bleach.linkifier import build_email_re
from django import template
from django.conf import settings
from django.core import signing
from django.urls import reverse
from django.utils.functional import SimpleLazyObject
from django.utils.html import escape
from django.utils.http import url_has_allowed_host_and_scheme
@@ -52,7 +54,6 @@ from markdown.postprocessors import Postprocessor
from markdown.treeprocessors import UnescapeTreeprocessor
from tlds import tld_set
from pretix.base.views.redirect import safelink
from pretix.helpers.format import SafeFormatter, format_map
register = template.Library()
@@ -157,7 +158,8 @@ def safelink_callback(attrs, new=False):
"""
url = html.unescape(attrs.get((None, 'href'), '/'))
if not url_has_allowed_host_and_scheme(url, allowed_hosts=None) and not url.startswith('mailto:') and not url.startswith('tel:'):
attrs[None, 'href'] = safelink(url)
signer = signing.Signer(salt='safe-redirect')
attrs[None, 'href'] = reverse('redirect') + '?url=' + urllib.parse.quote(signer.sign(url))
attrs[None, 'target'] = '_blank'
attrs[None, 'rel'] = 'noopener'
return attrs
+6 -29
View File
@@ -19,7 +19,6 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
import logging
import urllib.parse
from django.core import signing
@@ -27,8 +26,6 @@ from django.http import HttpResponseBadRequest, HttpResponseRedirect
from django.shortcuts import render
from django.urls import reverse
logger = logging.getLogger(__name__)
def _is_samesite_referer(request):
referer = request.headers.get('referer')
@@ -45,16 +42,11 @@ def _is_samesite_referer(request):
def redir_view(request):
framebreak = "framebreak" in request.GET
salt = 'framebreak-safelink-url' if framebreak else 'safelink-url'
signer = signing.Signer(salt='safe-redirect')
try:
url = signing.Signer(salt=salt).unsign(request.GET.get('url', ''))
url = signer.unsign(request.GET.get('url', ''))
except signing.BadSignature:
try:
# Backwards-compatibility for a change in 2026-06, remove after a while
url = signing.Signer(salt='safe-redirect').unsign(request.GET.get('url', ''))
except signing.BadSignature:
return HttpResponseBadRequest('Invalid parameter')
return HttpResponseBadRequest('Invalid parameter')
if not _is_samesite_referer(request):
u = urllib.parse.urlparse(url)
@@ -63,26 +55,11 @@ def redir_view(request):
'url': url,
})
if framebreak:
r = render(request, 'pretixbase/framebreak.html', {
'url': url,
})
r.xframe_options_exempt = True
return r
r = HttpResponseRedirect(url)
r['X-Robots-Tag'] = 'noindex'
return r
def safelink(url, framebreak=False):
url = str(url)
if not (url.startswith('https://') or url.startswith('http://') or url.startswith("/")):
logger.warning('Invalid URL passed to safelink: %r', url)
return '#invalid-url'
salt = 'framebreak-safelink-url' if framebreak else 'safelink-url'
signer = signing.Signer(salt=salt)
u = reverse('redirect') + '?url=' + urllib.parse.quote(signer.sign(url))
if framebreak:
u += "&framebreak=true"
return u
def safelink(url):
signer = signing.Signer(salt='safe-redirect')
return reverse('redirect') + '?url=' + urllib.parse.quote(signer.sign(url))
+1 -1
View File
@@ -1673,7 +1673,7 @@ class CountriesAndEUAndStates(CountriesAndEU):
class TaxRuleLineForm(I18nForm):
country = LazyTypedChoiceField(
choices=CountriesAndEUAndStates(),
choices=lazy(lambda: CountriesAndEUAndStates(), CountriesAndEUAndStates),
required=False
)
address_type = forms.ChoiceField(
-5
View File
@@ -106,11 +106,6 @@ class VoucherForm(I18nModelForm):
pass
super().__init__(*args, **kwargs)
self.fields['tag'].widget.attrs['data-typeahead-url'] = reverse('control:event.vouchers.tags.typeahead', kwargs={
'event': instance.event.slug,
'organizer': instance.event.organizer.slug,
})
if instance.event.has_subevents:
self.fields['subevent'].queryset = instance.event.subevents.all()
self.fields['subevent'].widget = Select2(
+7 -11
View File
@@ -36,7 +36,6 @@ from urllib.parse import quote, urljoin, urlparse
from django.conf import settings
from django.contrib.auth import REDIRECT_FIELD_NAME, logout
from django.contrib.auth.views import redirect_to_login
from django.http import Http404
from django.shortcuts import get_object_or_404, resolve_url
from django.template.response import TemplateResponse
@@ -213,17 +212,14 @@ class AuditLogMiddleware:
if request.path.startswith(get_script_prefix() + 'control') and request.user.is_authenticated:
if getattr(request.user, "is_hijacked", False):
hijack_history = request.session.get('hijack_history', False)
hijacker = get_object_or_404(User, pk=hijack_history[0]["user"])
hijacker = get_object_or_404(User, pk=hijack_history[0])
ss = hijacker.get_active_staff_session(request.session.get('hijacker_session'))
if not ss:
# Staff session expired or not found
logout(request)
return redirect_to_login(request.get_full_path())
ss.logs.create(
url=request.path,
method=request.method,
impersonating=request.user
)
if ss:
ss.logs.create(
url=request.path,
method=request.method,
impersonating=request.user
)
else:
ss = request.user.get_active_staff_session(request.session.session_key)
if ss:
-1
View File
@@ -370,7 +370,6 @@ urlpatterns = [
re_path(r'^discounts/add$', discounts.DiscountCreate.as_view(), name='event.items.discounts.add'),
re_path(r'^vouchers/$', vouchers.VoucherList.as_view(), name='event.vouchers'),
re_path(r'^vouchers/tags/$', vouchers.VoucherTags.as_view(), name='event.vouchers.tags'),
re_path(r'^vouchers/tags/typeahead$', typeahead.voucher_tag_typeahead, name='event.vouchers.tags.typeahead'),
re_path(r'^vouchers/rng$', vouchers.VoucherRNG.as_view(), name='event.vouchers.rng'),
re_path(r'^vouchers/item_select$', typeahead.itemvarquota_select2, name='event.vouchers.itemselect2'),
re_path(r'^vouchers/(?P<voucher>\d+)/$', vouchers.VoucherUpdate.as_view(), name='event.voucher'),
-15
View File
@@ -975,21 +975,6 @@ def subevent_meta_values(request, organizer, event):
})
@event_permission_required('event.vouchers:read')
def voucher_tag_typeahead(request, **kwargs):
q = request.GET.get('q', '')
tags = request.event.vouchers.filter(
tag__isnull=False,
waitinglistentries__isnull=True,
).filter(
tag__icontains=q,
).values_list('tag', flat=True).distinct().order_by('tag')[:10]
return JsonResponse({
'results': [{'name': t} for t in tags]
})
def item_meta_values(request, organizer, event):
q = request.GET.get('q')
propname = request.GET.get('property')
+16 -56
View File
@@ -19,23 +19,19 @@
# 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 hmac
import json
from contextlib import contextmanager
from django.conf import settings
from django.contrib import messages
from django.contrib.auth import (
BACKEND_SESSION_KEY, HASH_SESSION_KEY, get_user_model, load_backend, login,
logout,
BACKEND_SESSION_KEY, get_user_model, load_backend, login,
)
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.views import redirect_to_login
from django.core.exceptions import PermissionDenied
from django.db import transaction
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse
from django.utils.crypto import get_random_string, salted_hmac
from django.utils.crypto import get_random_string
from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _
from django.views import View
@@ -222,13 +218,11 @@ class UserImpersonateView(AdministratorPermissionRequiredMixin, RecentAuthentica
def post(self, request, *args, **kwargs):
self.object = get_object_or_404(User, pk=self.kwargs.get("id"))
staff_session = request.user.get_active_staff_session(request.session.session_key)
self.request.user.log_action('pretix.control.auth.user.impersonated',
user=request.user,
data={
'other': self.kwargs.get("id"),
'other_email': self.object.email,
'staff_session': staff_session.pk,
'other_email': self.object.email
})
oldkey = request.session.session_key
@@ -236,15 +230,7 @@ class UserImpersonateView(AdministratorPermissionRequiredMixin, RecentAuthentica
hijacked = self.object
hijack_history = request.session.get("hijack_history", [])
hijack_history.append({
"user": request.user.pk,
# We include the auth_hash, because it is unguessable. So should an attacker gain an attack vector to
# modify hijack_history, they can't just insert or change a user that shouldn't be there. We HMAC it
# again, though, since we also do not want the auth_hash of the admin user to be in the session of an
# unprivileged user to contain the risk if there is some leak of session data.
"auth_hash": salted_hmac(key_salt=b"hijack-history-hash", value=request.session[HASH_SESSION_KEY],
algorithm="sha256", secret=settings.SECRET_KEY).hexdigest(),
})
hijack_history.append(request.user._meta.pk.value_to_string(hijacker))
backend = get_used_backend(request)
backend = f"{backend.__module__}.{backend.__class__.__name__}"
@@ -252,12 +238,6 @@ class UserImpersonateView(AdministratorPermissionRequiredMixin, RecentAuthentica
with signals.no_update_last_login(), keep_session_age(request.session):
login(request, hijacked, backend=backend)
request.session.save()
staff_session.logs.create(
method='(NOTE)',
url=f'Begin impersonating user #{hijacked.pk} (request session {oldkey[:8]} -> {request.session.session_key[:8]})',
)
request.session["hijack_history"] = hijack_history
signals.hijack_started.send(
@@ -274,28 +254,13 @@ class UserImpersonateView(AdministratorPermissionRequiredMixin, RecentAuthentica
class UserImpersonateStopView(LoginRequiredMixin, View):
def post(self, request, *args, **kwargs):
staff_session_key = request.session['hijacker_session']
prev_session_key = request.session.session_key
impersonated = request.user
hijs = request.session['hijacker_session']
hijack_history = request.session.get("hijack_history", [])
hijacked = request.user
prev_session = hijack_history.pop()
hijacker = get_object_or_404(get_user_model(), pk=prev_session["user"])
staff_session = hijacker.get_active_staff_session(staff_session_key)
if not staff_session:
raise PermissionDenied
expected_hash = salted_hmac(
key_salt=b"hijack-history-hash",
value=hijacker.get_session_auth_hash(),
algorithm="sha256",
secret=settings.SECRET_KEY
).hexdigest()
if not hmac.compare_digest(expected_hash, prev_session["auth_hash"]):
# Could be an attacker-controlled hijack history, but could also be e.g. a password change of the admin user
# that happened during the hijack session
logout(request)
return redirect_to_login(request.get_full_path())
user_pk = hijack_history.pop()
hijacker = get_object_or_404(get_user_model(), pk=user_pk)
backend = get_used_backend(request)
backend = f"{backend.__module__}.{backend.__class__.__name__}"
with signals.no_update_last_login(), keep_session_age(request.session):
@@ -310,22 +275,17 @@ class UserImpersonateStopView(LoginRequiredMixin, View):
hijacked=hijacked,
)
request.session.save()
staff_session.session_key = request.session.session_key
staff_session.save()
staff_session.logs.create(
method='(NOTE)',
url=f'Stop impersonating user #{hijacked.pk} (request session {prev_session_key[:8]}, '
f'staff session {staff_session_key[:8]} -> {request.session.session_key[:8]})',
)
ss = request.user.get_active_staff_session(hijs)
if ss:
request.session.save()
ss.session_key = request.session.session_key
ss.save()
request.user.log_action('pretix.control.auth.user.impersonate_stopped',
user=request.user,
data={
'other': hijacked.pk,
'other_email': hijacked.email,
'staff_session': staff_session.pk,
'other': impersonated.pk,
'other_email': impersonated.email
})
return redirect(reverse('control:index'))
-21
View File
@@ -20,8 +20,6 @@
# <https://www.gnu.org/licenses/>.
#
import contextlib
import logging
import os
from django.conf import settings
from django.core.exceptions import FieldDoesNotExist, ImproperlyConfigured
@@ -31,8 +29,6 @@ from django.db.models import (
)
from django.utils.functional import lazy
logger = logging.getLogger(__name__)
class DummyRollbackException(Exception):
pass
@@ -289,20 +285,3 @@ def get_deterministic_ordering(model, ordering):
# on the primary key to provide total ordering.
ordering.append("-pk")
return ordering
@contextlib.contextmanager
def ensure_no_queries():
"""
Ensures that no database queries are being made in that context.
Raises a RuntimeError if running in DEBUG mode, otherwise logs
an error.
:return:
"""
def blocker(*args, **kwargs):
if settings.DEBUG or "PYTEST_CURRENT_TEST" in os.environ:
raise RuntimeError(f"Unexpected DB query: {args[1]}")
logger.error("Unexpected DB query: %s", args[1])
with connection.execute_wrapper(blocker):
yield
-7
View File
@@ -46,13 +46,6 @@ class RequestIdFilter(logging.Filter):
return True
class SkipNotFoundFilter(logging.Filter):
# Drop the WARNING "Not Found: ..." records django.request emits for 404s
# We have different access logs for that
def filter(self, record):
return getattr(record, 'status_code', None) != 404
class RequestIdMiddleware:
def __init__(self, get_response):
self.get_response = get_response
+1 -14
View File
@@ -25,19 +25,6 @@ import text_unidecode
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
EPC_QR_ALLOWED_CHARS = set(
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz"
"0123456789/-?:().,'+ "
)
def epc_qr_field(value):
return ''.join(
char for char in text_unidecode.unidecode(str(value or ''))
if char in EPC_QR_ALLOWED_CHARS
)
def dotdecimal(value):
return str(value).replace(",", ".")
@@ -90,7 +77,7 @@ def euro_epc_qr(
return {
"id": "girocode",
"label": "EPC-QR",
"qr_data": "\n".join(epc_qr_field(d) for d in [
"qr_data": "\n".join(text_unidecode.unidecode(str(d or '')) for d in [
"BCD", # Service Tag: BCD
"002", # Version: V2
"2", # Character set: ISO 8859-1
+1 -1
View File
@@ -10045,7 +10045,7 @@ msgid ""
msgstr ""
#: pretix/base/settings.py:1279
msgid "Allow updating existing invoices"
msgid "Allow to update existing invoices"
msgstr ""
#: pretix/base/settings.py:1280
+1 -1
View File
@@ -11440,7 +11440,7 @@ msgstr ""
"في لوحة التحكم."
#: pretix/base/settings.py:1279
msgid "Allow updating existing invoices"
msgid "Allow to update existing invoices"
msgstr ""
#: pretix/base/settings.py:1280
+1 -1
View File
@@ -10047,7 +10047,7 @@ msgid ""
msgstr ""
#: pretix/base/settings.py:1279
msgid "Allow updating existing invoices"
msgid "Allow to update existing invoices"
msgstr ""
#: pretix/base/settings.py:1280
+1 -1
View File
@@ -11564,7 +11564,7 @@ msgid ""
msgstr ""
#: pretix/base/settings.py:1279
msgid "Allow updating existing invoices"
msgid "Allow to update existing invoices"
msgstr ""
#: pretix/base/settings.py:1280
+1 -1
View File
@@ -11019,7 +11019,7 @@ msgstr ""
"provedené prostřednictvím backendu."
#: pretix/base/settings.py:1279
msgid "Allow updating existing invoices"
msgid "Allow to update existing invoices"
msgstr "Umožnit aktualizaci stávajících faktur"
#: pretix/base/settings.py:1280
+1 -1
View File
@@ -10101,7 +10101,7 @@ msgid ""
msgstr ""
#: pretix/base/settings.py:1279
msgid "Allow updating existing invoices"
msgid "Allow to update existing invoices"
msgstr ""
#: pretix/base/settings.py:1280
+1 -1
View File
@@ -11034,7 +11034,7 @@ msgstr ""
"Denne indstilling påvirker ikke ændringer, der foretages via backend'et."
#: pretix/base/settings.py:1279
msgid "Allow updating existing invoices"
msgid "Allow to update existing invoices"
msgstr "Tillad opdatering af eksisterende fakturaer"
#: pretix/base/settings.py:1280
+3 -3
View File
@@ -5,8 +5,8 @@ msgstr ""
"Project-Id-Version: 1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-06-28 14:42+0000\n"
"PO-Revision-Date: 2026-06-29 17:00+0000\n"
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
"PO-Revision-Date: 2026-06-28 15:19+0000\n"
"Last-Translator: Raphael Michel <michel@rami.io>\n"
"Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix/"
"de/>\n"
"Language: de\n"
@@ -11114,7 +11114,7 @@ msgstr ""
"Einstellung betrifft keine Änderungen, die über das Backend getätigt werden."
#: pretix/base/settings.py:1279
msgid "Allow updating existing invoices"
msgid "Allow to update existing invoices"
msgstr "Existierende Rechnungen dürfen neu generiert werden"
#: pretix/base/settings.py:1280
+6 -9
View File
@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-06-28 15:49+0000\n"
"PO-Revision-Date: 2026-07-06 02:00+0000\n"
"Last-Translator: Benedikt Bormann <mail@bbormann.de>\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/"
"de/>\n"
"Language: de\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 2026.6.1\n"
"X-Generator: Weblate 5.16.2\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
@@ -445,11 +445,11 @@ msgstr "Drücken Sie Strg+C zum Kopieren!"
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue:80
msgid "Edit"
msgstr "Bearbeiten"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue:86
msgid "Visualize"
msgstr "Visualisieren"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue:96
msgid ""
@@ -457,13 +457,10 @@ msgid ""
"or variations are not contained in any of your rule parts so people with "
"these tickets will not get in:"
msgstr ""
"Ihre Regeln filtern immer nach Produkt oder Variation, jedoch sind die "
"folgenden Produkte oder Variationen nicht in einer Ihrer Regeln enthalten, "
"sodass Kunden mit diesen Tickets keinen Eintritt erhalten werden:"
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue:99
msgid "Please double-check if this was intentional."
msgstr "Bitte überprüfen Sie, ob dies beabsichtigt war."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:4
msgid "All of the conditions below (AND)"
@@ -11101,7 +11101,7 @@ msgstr ""
"Einstellung betrifft keine Änderungen, die über das Backend getätigt werden."
#: pretix/base/settings.py:1279
msgid "Allow updating existing invoices"
msgid "Allow to update existing invoices"
msgstr "Existierende Rechnungen dürfen neu generiert werden"
#: pretix/base/settings.py:1280
+1 -1
View File
@@ -10046,7 +10046,7 @@ msgid ""
msgstr ""
#: pretix/base/settings.py:1279
msgid "Allow updating existing invoices"
msgid "Allow to update existing invoices"
msgstr ""
#: pretix/base/settings.py:1280
+1 -1
View File
@@ -11944,7 +11944,7 @@ msgid ""
msgstr ""
#: pretix/base/settings.py:1279
msgid "Allow updating existing invoices"
msgid "Allow to update existing invoices"
msgstr ""
#: pretix/base/settings.py:1280
+1 -1
View File
@@ -10045,7 +10045,7 @@ msgid ""
msgstr ""
#: pretix/base/settings.py:1279
msgid "Allow updating existing invoices"
msgid "Allow to update existing invoices"
msgstr ""
#: pretix/base/settings.py:1280
+113 -72
View File
@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-06-28 14:42+0000\n"
"PO-Revision-Date: 2026-06-29 17:00+0000\n"
"PO-Revision-Date: 2026-05-29 17:00+0000\n"
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
"Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/pretix/"
"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 2026.6.1\n"
"X-Generator: Weblate 2026.5\n"
#: htmlcov/d_daa1541d0cbf5e2b_dashboards_py.html:670
#: pretix/control/templates/pretixcontrol/events/index.html:166
@@ -467,8 +467,10 @@ msgid "Medium connected to other event"
msgstr "Medio conectado a otro evento"
#: pretix/api/views/checkin.py:814
#, fuzzy
#| msgid "You cannot change this order."
msgid "You cannot exchange a medium for a medium."
msgstr "No se puede cambiar un medio por otro."
msgstr "No puedes cambiar este pedido."
#: pretix/api/views/oauth.py:107 pretix/control/logdisplay.py:777
#, python-brace-format
@@ -4728,16 +4730,22 @@ msgid "Check-in annulled"
msgstr "Check-in anulado"
#: pretix/base/models/checkin.py:372
#, fuzzy
#| msgid "Ticket already used"
msgid "Ticket already exchanged"
msgstr "Entrada ya canjeada"
msgstr "Esta entrada ya fue utilizada"
#: pretix/base/models/checkin.py:373
#, fuzzy
#| msgid "Reusable media"
msgid "Reusable medium invalid"
msgstr "Soporte reutilizable no válido"
msgstr "Medios reutilizables"
#: pretix/base/models/checkin.py:374
#, fuzzy
#| msgid "Reusable media type"
msgid "Reusable medium already exists"
msgstr "El soporte reutilizable ya existe"
msgstr "Tipo de medio reusable"
#: pretix/base/models/customers.py:63
msgid "Provider name"
@@ -5322,8 +5330,11 @@ msgstr ""
"ambas cosas."
#: pretix/base/models/event.py:1884
#, fuzzy
#| msgid "The bundled item must belong to the same event as the item."
msgid "Property and event must belong to the same organizer."
msgstr "La propiedad y el evento deben pertenecer al mismo organizador."
msgstr ""
"La agrupación de artículos debe pertenecer al mismo evento que el artículo."
#: pretix/base/models/event.py:1928 pretix/base/models/organizer.py:627
msgid "Link text"
@@ -5575,41 +5586,42 @@ msgid "Show product with info on why its unavailable"
msgstr "Mostrar el producto con la razón que no está disponible"
#: pretix/base/models/items.py:458 pretix/base/models/items.py:786
#, fuzzy
#| msgid "Don't use re-usable media, use regular one-off tickets"
msgid "Don't use reusable media, use regular one-off tickets"
msgstr ""
"No utilizar soportes reutilizables, sino billetes normales de un solo uso"
msgstr "No usar medios reutilizables, usar entradas de un solo uso"
#: pretix/base/models/items.py:459
msgid "Require a previously unknown medium to be newly added"
msgstr "Exigir que un medio desconocido anteriormente se adicione de nuevo"
#: pretix/base/models/items.py:460
#, fuzzy
#| msgid "Require an existing medium to be re-used"
msgid "Require an existing medium to be reused, replacing any previous tickets"
msgstr ""
"Necesita que se reutilice un soporte ya existente, sustituyendo cualquier "
"billete anterior"
msgstr "Exigir que se reuse un medio ya existente"
#: pretix/base/models/items.py:461
#, fuzzy
#| msgid "Require either an existing or a new medium to be used"
msgid ""
"Require either an existing or a new medium to be used, replacing any "
"previous tickets"
msgstr ""
"Se debe utilizar un soporte ya existente o uno nuevo, sustituyendo cualquier "
"billete anterior"
msgstr "Exigir que se use un medio existente o uno nuevo"
#: pretix/base/models/items.py:462
#, fuzzy
#| msgid "Require an existing medium to be re-used"
msgid "Require an existing medium to be reused, adding to any previous tickets"
msgstr ""
"Exigir que se reutilice un soporte ya existente, añadiendo esta información "
"a cualquier entrada anterior"
msgstr "Exigir que se reuse un medio ya existente"
#: pretix/base/models/items.py:464
#, fuzzy
#| msgid "Require either an existing or a new medium to be used"
msgid ""
"Require either an existing or a new medium to be used, adding to any "
"previous tickets"
msgstr ""
"Es necesario utilizar un medio ya existente o uno nuevo, que se sumará a los "
"billetes anteriores"
msgstr "Exigir que se use un medio existente o uno nuevo"
#: pretix/base/models/items.py:480 pretix/base/models/items.py:1468
msgid "Category"
@@ -5969,6 +5981,14 @@ msgid "Reusable media policy"
msgstr "Condiciones de utilización de medios"
#: pretix/base/models/items.py:777
#, fuzzy
#| msgid ""
#| "If this product should be stored on a re-usable physical medium, you can "
#| "attach a physical media policy. This is not required for regular tickets, "
#| "which just use a one-time barcode, but only for products like renewable "
#| "season tickets or re-chargeable gift card wristbands. This is an advanced "
#| "feature that also requires specific configuration of ticketing and "
#| "printing settings."
msgid ""
"If this product should be stored on a reusable physical medium, you can "
"attach a physical media policy. This is not required for regular tickets, "
@@ -6032,9 +6052,6 @@ msgid ""
"prior to their usage. Therefore, the selected media policy does not make "
"sense for this media type."
msgstr ""
"El tipo de soporte seleccionado requiere que todos los soportes se registren "
"en el sistema antes de su uso. Por lo tanto, la política de soportes "
"seleccionada no es aplicable a este tipo de soporte."
#: pretix/base/models/items.py:1009
msgid ""
@@ -6609,16 +6626,18 @@ msgstr "rebotado"
#: pretix/base/models/media.py:77
msgctxt "reusable_medium"
msgid "Claim token"
msgstr "Canjear el token"
msgstr ""
#: pretix/base/models/media.py:82
msgctxt "reusable_medium"
msgid "Label"
msgstr "Designación"
msgstr ""
#: pretix/base/models/media.py:105
#, fuzzy
#| msgid "Linked ticket"
msgid "Linked tickets"
msgstr "Entradas vinculadas"
msgstr "Entrada vinculada"
#: pretix/base/models/media.py:107
msgid ""
@@ -6626,9 +6645,6 @@ msgid ""
"validity. If multiple tickets are valid at once, this will lead to failed "
"check-ins."
msgstr ""
"Si enlaza más de un billete, asegúrese de que no haya solapamiento en la "
"validez. Si varios billetes son válidos al mismo tiempo, esto provocará que "
"no se puedan realizar el check-in."
#: pretix/base/models/memberships.py:44
#: pretix/presale/templates/pretixpresale/organizers/customer_memberships.html:28
@@ -8374,10 +8390,14 @@ msgid "Atlantis"
msgstr "Atlántida"
#: pretix/base/pdf.py:376
#, fuzzy
#| msgid "Invoice recipient email"
msgid "Invoice custom recipient field"
msgstr "Campo personalizado de destinatario en la factura"
msgstr "Correo electrónico del destinatario de la factura"
#: pretix/base/pdf.py:377
#, fuzzy
#| msgid "Custom recipient field label"
msgid "Custom recipient field"
msgstr "Campo de destinatario personalizado"
@@ -9395,15 +9415,13 @@ msgstr "Necesitas responder preguntas para terminar el check-in."
#: pretix/base/services/checkin.py:1121
msgid "Ticket needs to be exchanged to a suitable medium."
msgstr "El billete debe canjearse por un soporte adecuado."
msgstr ""
#: pretix/base/services/checkin.py:1128
msgid ""
"This ticket has already been exchanged for a reusable medium that now needs "
"to be used instead."
msgstr ""
"Esta entrada ya se ha canjeado por un soporte reutilizable que ahora hay que "
"utilizar en su lugar."
#: pretix/base/services/checkin.py:1180
msgid "This ticket has already been redeemed."
@@ -9569,46 +9587,64 @@ msgstr ""
"{event}."
#: pretix/base/services/media.py:93 pretix/base/services/media.py:95
#, fuzzy
#| msgid "Invalid input type."
msgid "Invalid medium type."
msgstr "Tipo de soporte no válido."
msgstr "Tipo de entrada no válido."
#: pretix/base/services/media.py:100 pretix/base/services/media.py:102
#, fuzzy
#| msgid "The selected media type is not enabled in your organizer settings."
msgid "Medium type is not enabled for organizer."
msgstr "Este tipo de medio no está habilitado para el organizador."
msgstr ""
"El tipo de medio seleccionado no está activo en tus ajustes de organizador/a/"
"e."
#: pretix/base/services/media.py:107 pretix/base/services/media.py:109
msgid "Incorrect medium type for product."
msgstr "El tipo de soporte no es el adecuado para este producto."
msgstr ""
#: pretix/base/services/media.py:114 pretix/base/services/media.py:116
#, fuzzy
#| msgid "This ticket has already been redeemed."
msgid "Ticket is already exchanged for reusable medium."
msgstr "La entrada ya se ha canjeado por un soporte reutilizable."
msgstr "Esta entrada ya ha sido canjeada."
#: pretix/base/services/media.py:133 pretix/base/services/media.py:135
#, fuzzy
#| msgid "Reusable Medium ID"
msgid "Reusable medium not found."
msgstr "No se ha encontrado el soporte reutilizable."
msgstr "ID mediana reutilizable"
#: pretix/base/services/media.py:140 pretix/base/services/media.py:142
#: pretix/base/services/media.py:168 pretix/base/services/media.py:170
#, fuzzy
#| msgid "The reusable medium has been created."
msgid "Reusable medium is inactive or expired."
msgstr "El medio reutilizable está inactivo o caducado."
msgstr "Se ha creado el medio reutilizable."
#: pretix/base/services/media.py:155 pretix/base/services/media.py:162
#: pretix/base/services/media.py:176
#, fuzzy
#| msgid "The reusable medium has been created."
msgid "Reusable medium not found and could not be created."
msgstr "No se ha encontrado el soporte reutilizable y no se ha podido crear."
msgstr "Se ha creado el medio reutilizable."
#: pretix/base/services/media.py:183
#, fuzzy
#| msgid "Reusable media type"
msgid "Reusable medium already exists."
msgstr "Ya existe un soporte reutilizable."
msgstr "Tipo de medio reusable"
#: pretix/base/services/media.py:189
#, fuzzy
#| msgid "The reusable medium has been created."
msgid "Reusable medium could not be created."
msgstr "No se ha podido crear el soporte reutilizable."
msgstr "Se ha creado el medio reutilizable."
#: pretix/base/services/media.py:195 pretix/base/services/media.py:197
msgid "Product does not support medium exchange."
msgstr "Este producto no admite el cambio de medio."
msgstr ""
#: pretix/base/services/memberships.py:108
#, python-brace-format
@@ -10332,10 +10368,17 @@ msgstr ""
"inició sesión durante la compra."
#: pretix/base/settings.py:214
#, fuzzy
#| msgid "Activate re-usable media"
msgid "Activate reusable media"
msgstr "Activar medios reutilizables"
#: pretix/base/settings.py:215
#, fuzzy
#| msgid ""
#| "The re-usable media feature allows you to connect tickets and gift cards "
#| "with physical media such as wristbands or chip cards that may be re-used "
#| "for different tickets or gift cards later."
msgid ""
"The reusable media feature allows you to connect tickets and gift cards with "
"physical media such as wristbands or chip cards that may be reused for "
@@ -10347,7 +10390,7 @@ msgstr ""
#: pretix/base/settings.py:226
msgid "Enforce the usage of issued reusable media for check-in"
msgstr "Exigir el uso de los soportes reutilizables para el check-in"
msgstr ""
#: pretix/base/settings.py:227
msgid ""
@@ -10355,10 +10398,6 @@ msgid ""
"medium has been created and linked to a ticket. Keeping this option turned "
"off will treat the reusable medium and ticket as equals."
msgstr ""
"Si se activa esta opción, ya no se aceptarán los códigos de barras de los "
"billetes cuando se haya creado un soporte reutilizable y se haya vinculado a "
"un billete. Si se mantiene desactivada esta opción, el soporte reutilizable "
"y el billete se tratarán como iguales."
#: pretix/base/settings.py:254
msgid "Length of barcodes"
@@ -11123,7 +11162,7 @@ msgstr ""
"ajuste no afecta los cambios realizados a través del backend."
#: pretix/base/settings.py:1279
msgid "Allow updating existing invoices"
msgid "Allow to update existing invoices"
msgstr "Permitir actualizar facturas existentes"
#: pretix/base/settings.py:1280
@@ -18509,12 +18548,16 @@ msgid "The reusable medium has been changed."
msgstr "El medio reutilizable ha sido modificado."
#: pretix/control/logdisplay.py:746
#, fuzzy
#| msgid "The new member has been added to the team."
msgid "A new ticket has been added to the medium."
msgstr "Se ha añadido un nuevo billete al medio."
msgstr "El nuevo miembro ha sido añadido al equipo."
#: pretix/control/logdisplay.py:747
#, fuzzy
#| msgid "{user} has been removed from the team."
msgid "A ticket has been removed from the medium."
msgstr "Se ha eliminado un billete del medio."
msgstr "{user} ha sido removido del equipo."
#: pretix/control/logdisplay.py:748
msgid "The medium has been connected to a new ticket."
@@ -18526,8 +18569,6 @@ msgid ""
"The ticket #{positionid} was exchanged for reusable medium "
"{medium_identifier}."
msgstr ""
"El billete n.º {positionid} se ha canjeado por un medio reutilizable "
"{medium_identifier}."
#: pretix/control/logdisplay.py:750
msgid "The medium has been connected to a new gift card."
@@ -20442,8 +20483,10 @@ msgstr ""
"check-in:"
#: pretix/control/templates/pretixcontrol/checkin/simulator.html:85
#, fuzzy
#| msgid "Special attention required"
msgid "Media exchange required"
msgstr "Es necesario intercambiar medios"
msgstr "Atención especial requerida"
#: pretix/control/templates/pretixcontrol/checkin/simulator.html:87
#, python-format
@@ -20451,8 +20494,6 @@ msgid ""
"This ticket needs to be exchanged into a <strong>%(media_type)s</strong> "
"reusable medium. <strong>%(media_policy)s</strong>."
msgstr ""
"Esta entrada debe canjearse por un soporte reutilizable <strong>%(media_type)"
"s</strong>. <strong>%(media_policy)s</strong>."
#: pretix/control/templates/pretixcontrol/checkin/simulator.html:103
msgid "Special attention required"
@@ -26997,9 +27038,6 @@ msgid ""
"Even if a team has no access to a certain category of data, they might still "
"be able to see parts of this data when it is linked to data they can see."
msgstr ""
"Aunque un equipo no tenga acceso a una determinada categoría de datos, es "
"posible que pueda ver parte de esos datos cuando estén vinculados a datos a "
"los que sí tiene acceso."
#: pretix/control/templates/pretixcontrol/organizers/team_edit.html:35
msgid ""
@@ -27007,10 +27045,6 @@ msgid ""
"some information about gift cards linked to a customer account, even if they "
"generally can't see gift cards directly."
msgstr ""
"Por ejemplo, una persona con acceso a las cuentas de los clientes podrá ver "
"cierta información sobre las tarjetas regalo vinculadas a una cuenta de "
"cliente, aunque, por lo general, no pueda ver las tarjetas regalo "
"directamente."
#: pretix/control/templates/pretixcontrol/organizers/team_edit.html:59
msgid ""
@@ -27018,9 +27052,6 @@ msgid ""
"information about vouchers used to create an order, even if they generally "
"can't see vouchers directly."
msgstr ""
"Por ejemplo, una persona con acceso a los pedidos podrá ver cierta "
"información sobre los vales utilizados para crear un pedido, aunque "
"normalmente no pueda ver los vales directamente."
#: pretix/control/templates/pretixcontrol/organizers/team_members.html:21
msgid "Member"
@@ -27881,22 +27912,30 @@ msgstr ""
#: pretix/control/templates/pretixcontrol/subevents/detail.html:9
#: pretix/control/templates/pretixcontrol/subevents/detail.html:13
#, python-format
#, fuzzy, python-format
#| msgid "Quota: %(name)s"
msgctxt "subevent"
msgid "Date: %(name)s"
msgstr "Fecha: %(name)s"
msgstr "Cuota: %(name)s"
#: pretix/control/templates/pretixcontrol/subevents/detail.html:234
#, fuzzy
#| msgid "Partially paid"
msgid "partially canceled"
msgstr "cancelado parcialmente"
msgstr "Pagado parcialmente"
#: pretix/control/templates/pretixcontrol/subevents/detail.html:282
#, fuzzy
#| msgctxt "permission_level"
#| msgid "View all"
msgid "View all"
msgstr "Ver todo"
#: pretix/control/templates/pretixcontrol/subevents/detail.html:289
#, fuzzy
#| msgid "No archived events found."
msgid "No orders found."
msgstr "No se han encontrado pedidos."
msgstr "No se han encontrado eventos archivados."
#: pretix/control/templates/pretixcontrol/subevents/detail.html:302
#: pretix/control/templates/pretixcontrol/subevents/edit.html:279
@@ -30669,8 +30708,10 @@ msgid "Voucher {}"
msgstr "Vale de compra {}"
#: pretix/control/views/typeahead.py:179 pretix/control/views/typeahead.py:180
#, fuzzy
#| msgid "Go to event"
msgid "No event"
msgstr "No hay eventos"
msgstr "Ir al evento"
#: pretix/control/views/user.py:169
msgid "The password you entered was invalid, please try again."
+5 -8
View File
@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-06-28 15:49+0000\n"
"PO-Revision-Date: 2026-06-29 17:00+0000\n"
"PO-Revision-Date: 2026-03-30 03:00+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 2026.6.1\n"
"X-Generator: Weblate 5.16.2\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
@@ -442,11 +442,11 @@ msgstr "¡Presione Control+C para copiar!"
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue:80
msgid "Edit"
msgstr "Editar"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue:86
msgid "Visualize"
msgstr "Visualizar"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue:96
msgid ""
@@ -454,13 +454,10 @@ msgid ""
"or variations are not contained in any of your rule parts so people with "
"these tickets will not get in:"
msgstr ""
"Su regla siempre filtra por producto o variante, pero los siguientes "
"productos o variantes no figuran en ninguna de las partes de su regla, por "
"lo que las personas con estos tickets no podrán acceder:"
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue:99
msgid "Please double-check if this was intentional."
msgstr "Por favor, comprueba bien si esto ha sido a propósito."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:4
msgid "All of the conditions below (AND)"
@@ -10232,7 +10232,7 @@ msgid ""
msgstr ""
#: pretix/base/settings.py:1279
msgid "Allow updating existing invoices"
msgid "Allow to update existing invoices"
msgstr ""
#: pretix/base/settings.py:1280
+1 -1
View File
@@ -10047,7 +10047,7 @@ msgid ""
msgstr ""
#: pretix/base/settings.py:1279
msgid "Allow updating existing invoices"
msgid "Allow to update existing invoices"
msgstr ""
#: pretix/base/settings.py:1280
+1 -1
View File
@@ -11222,7 +11222,7 @@ msgstr ""
"Konfigurazio honek ez die eragiten backend-aren bidez egindako aldaketei."
#: pretix/base/settings.py:1279
msgid "Allow updating existing invoices"
msgid "Allow to update existing invoices"
msgstr "Sortutako fakturak eguneratzeko baimena ematea"
#: pretix/base/settings.py:1280
+1 -1
View File
@@ -11159,7 +11159,7 @@ msgstr ""
"taustajärjestelmän kautta tehtyihin muutoksiin."
#: pretix/base/settings.py:1279
msgid "Allow updating existing invoices"
msgid "Allow to update existing invoices"
msgstr "Salli olemassa olevien laskujen päivittäminen"
#: pretix/base/settings.py:1280
+1 -1
View File
@@ -10117,7 +10117,7 @@ msgid ""
msgstr ""
#: pretix/base/settings.py:1279
msgid "Allow updating existing invoices"
msgid "Allow to update existing invoices"
msgstr ""
#: pretix/base/settings.py:1280
+112 -72
View File
@@ -4,10 +4,10 @@ msgstr ""
"Project-Id-Version: 1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-06-28 14:42+0000\n"
"PO-Revision-Date: 2026-06-29 17:00+0000\n"
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
"Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix/"
"fr/>\n"
"PO-Revision-Date: 2026-06-08 17:00+0000\n"
"Last-Translator: Sébastien BRUNEAU <s.bruneau@beauvaisis.fr>\n"
"Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix/fr/"
">\n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@@ -466,8 +466,10 @@ msgid "Medium connected to other event"
msgstr "Média connecté à un autre événement"
#: pretix/api/views/checkin.py:814
#, fuzzy
#| msgid "You cannot change this order."
msgid "You cannot exchange a medium for a medium."
msgstr "Il n'est pas possible d'échanger un support contre un autre support."
msgstr "Vous ne pouvez pas modifier cette commande."
#: pretix/api/views/oauth.py:107 pretix/control/logdisplay.py:777
#, python-brace-format
@@ -4734,16 +4736,22 @@ msgid "Check-in annulled"
msgstr "Enregistrement annulé"
#: pretix/base/models/checkin.py:372
#, fuzzy
#| msgid "Ticket already used"
msgid "Ticket already exchanged"
msgstr "Billet déjà échangé"
msgstr "Billet déjà utilisé"
#: pretix/base/models/checkin.py:373
#, fuzzy
#| msgid "Reusable media"
msgid "Reusable medium invalid"
msgstr "Support réutilisable non valide"
msgstr "Support réutilisable"
#: pretix/base/models/checkin.py:374
#, fuzzy
#| msgid "Reusable media type"
msgid "Reusable medium already exists"
msgstr "Il existe déjà un support réutilisable"
msgstr "Type de support réutilisable"
#: pretix/base/models/customers.py:63
msgid "Provider name"
@@ -5335,8 +5343,10 @@ msgstr ""
"deux."
#: pretix/base/models/event.py:1884
#, fuzzy
#| msgid "The bundled item must belong to the same event as the item."
msgid "Property and event must belong to the same organizer."
msgstr "La propriété et l'événement doivent appartenir au même organisateur."
msgstr "L’élément groupé doit appartenir au même événement que l’élément."
#: pretix/base/models/event.py:1928 pretix/base/models/organizer.py:627
msgid "Link text"
@@ -5591,6 +5601,8 @@ msgstr ""
"indisponibilité"
#: pretix/base/models/items.py:458 pretix/base/models/items.py:786
#, fuzzy
#| msgid "Don't use re-usable media, use regular one-off tickets"
msgid "Don't use reusable media, use regular one-off tickets"
msgstr ""
"N'utilisez pas de supports réutilisables, mais plutôt des tickets uniques "
@@ -5601,30 +5613,32 @@ msgid "Require a previously unknown medium to be newly added"
msgstr "Exiger l'ajout d'un support inconnu jusqu'alors"
#: pretix/base/models/items.py:460
#, fuzzy
#| msgid "Require an existing medium to be re-used"
msgid "Require an existing medium to be reused, replacing any previous tickets"
msgstr "Exiger la réutilisation d'un support existant"
#: pretix/base/models/items.py:461
#, fuzzy
#| msgid "Require either an existing or a new medium to be used"
msgid ""
"Require either an existing or a new medium to be used, replacing any "
"previous tickets"
msgstr ""
"Exiger l'utilisation d'un support existant ou d'un nouveau support, en "
"remplacement de tout billet antérieur"
msgstr "Nécessiter l'utilisation d'un support existant ou d'un nouveau support"
#: pretix/base/models/items.py:462
#, fuzzy
#| msgid "Require an existing medium to be re-used"
msgid "Require an existing medium to be reused, adding to any previous tickets"
msgstr ""
"Exiger la réutilisation d'un support existant, en ajoutant cette demande à "
"tout ticket précédent"
msgstr "Exiger la réutilisation d'un support existant"
#: pretix/base/models/items.py:464
#, fuzzy
#| msgid "Require either an existing or a new medium to be used"
msgid ""
"Require either an existing or a new medium to be used, adding to any "
"previous tickets"
msgstr ""
"Exiger l'utilisation d'un support existant ou d'un nouveau support, en "
"complément des tickets précédents"
msgstr "Nécessiter l'utilisation d'un support existant ou d'un nouveau support"
#: pretix/base/models/items.py:480 pretix/base/models/items.py:1468
msgid "Category"
@@ -5987,6 +6001,14 @@ msgid "Reusable media policy"
msgstr "Politique relative aux médias réutilisables"
#: pretix/base/models/items.py:777
#, fuzzy
#| msgid ""
#| "If this product should be stored on a re-usable physical medium, you can "
#| "attach a physical media policy. This is not required for regular tickets, "
#| "which just use a one-time barcode, but only for products like renewable "
#| "season tickets or re-chargeable gift card wristbands. This is an advanced "
#| "feature that also requires specific configuration of ticketing and "
#| "printing settings."
msgid ""
"If this product should be stored on a reusable physical medium, you can "
"attach a physical media policy. This is not required for regular tickets, "
@@ -6051,10 +6073,6 @@ msgid ""
"prior to their usage. Therefore, the selected media policy does not make "
"sense for this media type."
msgstr ""
"Le type de support sélectionné exige que tous les supports soient "
"enregistrés dans le système avant leur utilisation. Par conséquent, la "
"politique relative aux supports sélectionnée n'est pas applicable à ce type "
"de support."
#: pretix/base/models/items.py:1009
msgid ""
@@ -6631,16 +6649,18 @@ msgstr "Non distribué"
#: pretix/base/models/media.py:77
msgctxt "reusable_medium"
msgid "Claim token"
msgstr "Réclamer un jeton"
msgstr ""
#: pretix/base/models/media.py:82
msgctxt "reusable_medium"
msgid "Label"
msgstr "Descriptif"
msgstr ""
#: pretix/base/models/media.py:105
#, fuzzy
#| msgid "Linked ticket"
msgid "Linked tickets"
msgstr "Billets liés"
msgstr "Billet lié"
#: pretix/base/models/media.py:107
msgid ""
@@ -6648,9 +6668,6 @@ msgid ""
"validity. If multiple tickets are valid at once, this will lead to failed "
"check-ins."
msgstr ""
"Si vous associez plusieurs billets, assurez-vous qu'il n'y ait pas de "
"chevauchement entre leurs périodes de validité. Si plusieurs billets sont "
"valables en même temps, cela entraînera l'échec de l'enregistrement."
#: pretix/base/models/memberships.py:44
#: pretix/presale/templates/pretixpresale/organizers/customer_memberships.html:28
@@ -8420,12 +8437,16 @@ msgid "Atlantis"
msgstr "Atlantide"
#: pretix/base/pdf.py:376
#, fuzzy
#| msgid "Invoice recipient email"
msgid "Invoice custom recipient field"
msgstr "Champ personnalisé destinataire de la facture"
msgstr "E-mail du destinataire de la facture"
#: pretix/base/pdf.py:377
#, fuzzy
#| msgid "Custom recipient field label"
msgid "Custom recipient field"
msgstr "Champ de destinataire personnalisé"
msgstr "Libellé personnalisé du champ destinataire"
#: pretix/base/pdf.py:381
msgid "List of Add-Ons"
@@ -9450,15 +9471,13 @@ msgstr ""
#: pretix/base/services/checkin.py:1121
msgid "Ticket needs to be exchanged to a suitable medium."
msgstr "Le billet doit être échangé contre un support adapté."
msgstr ""
#: pretix/base/services/checkin.py:1128
msgid ""
"This ticket has already been exchanged for a reusable medium that now needs "
"to be used instead."
msgstr ""
"Ce billet a déjà été échangé contre un support réutilisable qui doit "
"désormais être utilisé à sa place."
#: pretix/base/services/checkin.py:1180
msgid "This ticket has already been redeemed."
@@ -9624,46 +9643,64 @@ msgstr ""
"Vous recevez cet e-mail parce que vous avez passé une commande pour {event}."
#: pretix/base/services/media.py:93 pretix/base/services/media.py:95
#, fuzzy
#| msgid "Invalid input type."
msgid "Invalid medium type."
msgstr "Type de support non valide."
msgstr "Type dentrée non valide."
#: pretix/base/services/media.py:100 pretix/base/services/media.py:102
#, fuzzy
#| msgid "The selected media type is not enabled in your organizer settings."
msgid "Medium type is not enabled for organizer."
msgstr "Ce type de média nest pas activé par l'organisateur."
msgstr ""
"Le type de média sélectionné nest pas activé dans les paramètres de votre "
"organisateur."
#: pretix/base/services/media.py:107 pretix/base/services/media.py:109
msgid "Incorrect medium type for product."
msgstr "Type de support incorrect pour ce produit."
msgstr ""
#: pretix/base/services/media.py:114 pretix/base/services/media.py:116
#, fuzzy
#| msgid "This ticket has already been redeemed."
msgid "Ticket is already exchanged for reusable medium."
msgstr "Le billet a déjà été échangé contre un support réutilisable."
msgstr "Ce billet a déjà été échangé."
#: pretix/base/services/media.py:133 pretix/base/services/media.py:135
#, fuzzy
#| msgid "Reusable Medium ID"
msgid "Reusable medium not found."
msgstr "Support réutilisable introuvable."
msgstr "Identification de support réutilisable"
#: pretix/base/services/media.py:140 pretix/base/services/media.py:142
#: pretix/base/services/media.py:168 pretix/base/services/media.py:170
#, fuzzy
#| msgid "The reusable medium has been created."
msgid "Reusable medium is inactive or expired."
msgstr "Le support réutilisable est inactif ou a expiré."
msgstr "Le support réutilisable a été créé."
#: pretix/base/services/media.py:155 pretix/base/services/media.py:162
#: pretix/base/services/media.py:176
#, fuzzy
#| msgid "The reusable medium has been created."
msgid "Reusable medium not found and could not be created."
msgstr "Support réutilisable introuvable et impossible à créer."
msgstr "Le support réutilisable a été créé."
#: pretix/base/services/media.py:183
#, fuzzy
#| msgid "Reusable media type"
msgid "Reusable medium already exists."
msgstr "Le type de support réutilisable existe déjà."
msgstr "Type de support réutilisable"
#: pretix/base/services/media.py:189
#, fuzzy
#| msgid "The reusable medium has been created."
msgid "Reusable medium could not be created."
msgstr "Impossible de créer le support réutilisable."
msgstr "Le support réutilisable a été créé."
#: pretix/base/services/media.py:195 pretix/base/services/media.py:197
msgid "Product does not support medium exchange."
msgstr "Ce produit ne permet pas de changer de support."
msgstr ""
#: pretix/base/services/memberships.py:108
#, python-brace-format
@@ -10389,10 +10426,17 @@ msgstr ""
"lachat."
#: pretix/base/settings.py:214
#, fuzzy
#| msgid "Activate re-usable media"
msgid "Activate reusable media"
msgstr "Activer les supports réutilisables"
#: pretix/base/settings.py:215
#, fuzzy
#| msgid ""
#| "The re-usable media feature allows you to connect tickets and gift cards "
#| "with physical media such as wristbands or chip cards that may be re-used "
#| "for different tickets or gift cards later."
msgid ""
"The reusable media feature allows you to connect tickets and gift cards with "
"physical media such as wristbands or chip cards that may be reused for "
@@ -10406,8 +10450,6 @@ msgstr ""
#: pretix/base/settings.py:226
msgid "Enforce the usage of issued reusable media for check-in"
msgstr ""
"Imposer l'utilisation de supports réutilisables fournis lors de "
"l'enregistrement"
#: pretix/base/settings.py:227
msgid ""
@@ -10415,10 +10457,6 @@ msgid ""
"medium has been created and linked to a ticket. Keeping this option turned "
"off will treat the reusable medium and ticket as equals."
msgstr ""
"Si cette option est activée, le code-barres d'un billet ne sera plus accepté "
"dès lors qu'un support réutilisable a été créé et associé à ce billet. Si "
"cette option reste désactivée, le support réutilisable et le billet seront "
"considérés comme équivalents."
#: pretix/base/settings.py:254
msgid "Length of barcodes"
@@ -11192,7 +11230,7 @@ msgstr ""
"principal."
#: pretix/base/settings.py:1279
msgid "Allow updating existing invoices"
msgid "Allow to update existing invoices"
msgstr "Autoriser la mise à jour des factures existantes"
#: pretix/base/settings.py:1280
@@ -18653,12 +18691,16 @@ msgid "The reusable medium has been changed."
msgstr "Le support réutilisable a été changé."
#: pretix/control/logdisplay.py:746
#, fuzzy
#| msgid "The new member has been added to the team."
msgid "A new ticket has been added to the medium."
msgstr "Un nouveau billet a été ajouté sur le support."
msgstr "Le nouveau membre a été ajouté à l'équipe."
#: pretix/control/logdisplay.py:747
#, fuzzy
#| msgid "{user} has been removed from the team."
msgid "A ticket has been removed from the medium."
msgstr "Un billet a été retiré du support."
msgstr "{user} a été retiré de l'équipe."
#: pretix/control/logdisplay.py:748
msgid "The medium has been connected to a new ticket."
@@ -18670,8 +18712,6 @@ msgid ""
"The ticket #{positionid} was exchanged for reusable medium "
"{medium_identifier}."
msgstr ""
"Le ticket n° {positionid} a été échangé contre un support réutilisable "
"{medium_identifier}."
#: pretix/control/logdisplay.py:750
msgid "The medium has been connected to a new gift card."
@@ -20587,8 +20627,10 @@ msgstr ""
"lenregistrement :"
#: pretix/control/templates/pretixcontrol/checkin/simulator.html:85
#, fuzzy
#| msgid "Special attention required"
msgid "Media exchange required"
msgstr "Échange de supports requis"
msgstr "Une attention particulière est requise"
#: pretix/control/templates/pretixcontrol/checkin/simulator.html:87
#, python-format
@@ -20596,8 +20638,6 @@ msgid ""
"This ticket needs to be exchanged into a <strong>%(media_type)s</strong> "
"reusable medium. <strong>%(media_policy)s</strong>."
msgstr ""
"Ce billet doit être échangé contre un support réutilisable <strong>%"
"(media_type)s</strong>. <strong>%(media_policy)s</strong>."
#: pretix/control/templates/pretixcontrol/checkin/simulator.html:103
msgid "Special attention required"
@@ -27192,9 +27232,6 @@ msgid ""
"Even if a team has no access to a certain category of data, they might still "
"be able to see parts of this data when it is linked to data they can see."
msgstr ""
"Même si une équipe n'a pas accès à une certaine catégorie de données, elle "
"peut néanmoins être en mesure de consulter certaines parties de ces données "
"lorsque celles-ci sont liées à des données auxquelles elle a accès."
#: pretix/control/templates/pretixcontrol/organizers/team_edit.html:35
msgid ""
@@ -27202,10 +27239,6 @@ msgid ""
"some information about gift cards linked to a customer account, even if they "
"generally can't see gift cards directly."
msgstr ""
"Par exemple, une personne ayant accès aux comptes clients pourra consulter "
"certaines informations concernant les cartes cadeaux associées à un compte "
"client, même si, en règle générale, elle ne peut pas voir directement ces "
"cartes cadeaux."
#: pretix/control/templates/pretixcontrol/organizers/team_edit.html:59
msgid ""
@@ -27213,9 +27246,6 @@ msgid ""
"information about vouchers used to create an order, even if they generally "
"can't see vouchers directly."
msgstr ""
"Par exemple, une personne ayant accès aux commandes pourra consulter "
"certaines informations concernant les bons utilisés pour créer une commande, "
"même si, en règle générale, elle ne peut pas consulter directement ces bons."
#: pretix/control/templates/pretixcontrol/organizers/team_members.html:21
msgid "Member"
@@ -28087,22 +28117,30 @@ msgstr ""
#: pretix/control/templates/pretixcontrol/subevents/detail.html:9
#: pretix/control/templates/pretixcontrol/subevents/detail.html:13
#, python-format
#, fuzzy, python-format
#| msgid "Quota: %(name)s"
msgctxt "subevent"
msgid "Date: %(name)s"
msgstr "Date : %(name)s"
msgstr "Quota : %(name)s"
#: pretix/control/templates/pretixcontrol/subevents/detail.html:234
#, fuzzy
#| msgid "Partially paid"
msgid "partially canceled"
msgstr "partiellement annulé"
msgstr "Partiellement payé"
#: pretix/control/templates/pretixcontrol/subevents/detail.html:282
#, fuzzy
#| msgctxt "permission_level"
#| msgid "View all"
msgid "View all"
msgstr "Tout afficher"
#: pretix/control/templates/pretixcontrol/subevents/detail.html:289
#, fuzzy
#| msgid "No archived events found."
msgid "No orders found."
msgstr "Aucune commande trouvée."
msgstr "Aucun événement archivé trouvé."
#: pretix/control/templates/pretixcontrol/subevents/detail.html:302
#: pretix/control/templates/pretixcontrol/subevents/edit.html:279
@@ -30900,8 +30938,10 @@ msgid "Voucher {}"
msgstr "Bon {}"
#: pretix/control/views/typeahead.py:179 pretix/control/views/typeahead.py:180
#, fuzzy
#| msgid "Go to event"
msgid "No event"
msgstr "Aucun événement"
msgstr "Aller à l'événement"
#: pretix/control/views/user.py:169
msgid "The password you entered was invalid, please try again."
+5 -9
View File
@@ -7,7 +7,7 @@ msgstr ""
"Project-Id-Version: French\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-06-28 15:49+0000\n"
"PO-Revision-Date: 2026-06-29 17:00+0000\n"
"PO-Revision-Date: 2026-03-18 12:23+0000\n"
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
"Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix-js/"
"fr/>\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 2026.6.1\n"
"X-Generator: Weblate 5.16.2\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
@@ -443,11 +443,11 @@ msgstr "Appuyez sur Ctrl-C pour copier !"
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue:80
msgid "Edit"
msgstr "Éditer"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue:86
msgid "Visualize"
msgstr "Visualiser"
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue:96
msgid ""
@@ -455,14 +455,10 @@ msgid ""
"or variations are not contained in any of your rule parts so people with "
"these tickets will not get in:"
msgstr ""
"Votre règle effectue toujours un filtrage par produit ou variante, mais les "
"produits ou variantes suivants ne figurent dans aucune des parties de votre "
"règle; par conséquent, les personnes détenant ces billets ne seront pas "
"admises :"
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue:99
msgid "Please double-check if this was intentional."
msgstr "Veuillez vérifier si cela était intentionnel."
msgstr ""
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:4
msgid "All of the conditions below (AND)"
+1 -1
View File
@@ -11443,7 +11443,7 @@ msgid ""
msgstr ""
#: pretix/base/settings.py:1279
msgid "Allow updating existing invoices"
msgid "Allow to update existing invoices"
msgstr ""
#: pretix/base/settings.py:1280
+1 -1
View File
@@ -10785,7 +10785,7 @@ msgstr ""
"ותונפק חשבונית חדשה. הגדרה זו לא משפיעה על שינויים שנעשו דרך הממשק האחורי."
#: pretix/base/settings.py:1279
msgid "Allow updating existing invoices"
msgid "Allow to update existing invoices"
msgstr "אפשר לעדכן חשבוניות קיימות"
#: pretix/base/settings.py:1280
+1 -1
View File
@@ -10843,7 +10843,7 @@ msgid ""
msgstr ""
#: pretix/base/settings.py:1279
msgid "Allow updating existing invoices"
msgid "Allow to update existing invoices"
msgstr "Dopusti ažuriranje postojećih računa"
#: pretix/base/settings.py:1280
+1 -1
View File
@@ -10427,7 +10427,7 @@ msgid ""
msgstr ""
#: pretix/base/settings.py:1279
msgid "Allow updating existing invoices"
msgid "Allow to update existing invoices"
msgstr ""
#: pretix/base/settings.py:1280
+1 -1
View File
@@ -11318,7 +11318,7 @@ msgstr ""
"Pengaturan ini tidak mempengaruhi perubahan yang dilakukan melalui backend."
#: pretix/base/settings.py:1279
msgid "Allow updating existing invoices"
msgid "Allow to update existing invoices"
msgstr "Izinkan untuk memperbarui faktur yang ada"
#: pretix/base/settings.py:1280
+1 -1
View File
@@ -11229,7 +11229,7 @@ msgstr ""
"backend."
#: pretix/base/settings.py:1279
msgid "Allow updating existing invoices"
msgid "Allow to update existing invoices"
msgstr "Consenti l'aggiornamento delle fatture esistenti"
#: pretix/base/settings.py:1280
+1 -1
View File
@@ -10850,7 +10850,7 @@ msgstr ""
"響しません。"
#: pretix/base/settings.py:1279
msgid "Allow updating existing invoices"
msgid "Allow to update existing invoices"
msgstr "既存の請求書を更新することを許可"
#: pretix/base/settings.py:1280
+1 -1
View File
@@ -10871,7 +10871,7 @@ msgstr ""
"발행됩니다. 이 설정은 백엔드를 통한 변경 사항에 영향을 미치지 않습니다."
#: pretix/base/settings.py:1279
msgid "Allow updating existing invoices"
msgid "Allow to update existing invoices"
msgstr "기존 송장 업데이트 허용"
#: pretix/base/settings.py:1280
+1 -1
View File
@@ -10331,7 +10331,7 @@ msgid ""
msgstr ""
#: pretix/base/settings.py:1279
msgid "Allow updating existing invoices"
msgid "Allow to update existing invoices"
msgstr ""
#: pretix/base/settings.py:1280
+1 -1
View File
@@ -10050,7 +10050,7 @@ msgid ""
msgstr ""
#: pretix/base/settings.py:1279
msgid "Allow updating existing invoices"
msgid "Allow to update existing invoices"
msgstr ""
#: pretix/base/settings.py:1280
+1 -1
View File
@@ -11104,7 +11104,7 @@ msgid ""
msgstr ""
#: pretix/base/settings.py:1279
msgid "Allow updating existing invoices"
msgid "Allow to update existing invoices"
msgstr ""
#: pretix/base/settings.py:1280
+1 -1
View File
@@ -10045,7 +10045,7 @@ msgid ""
msgstr ""
#: pretix/base/settings.py:1279
msgid "Allow updating existing invoices"
msgid "Allow to update existing invoices"
msgstr ""
#: pretix/base/settings.py:1280
@@ -11365,7 +11365,7 @@ msgstr ""
"innstillingen påvirker ikke endringer som gjøres via baksystemet."
#: pretix/base/settings.py:1279
msgid "Allow updating existing invoices"
msgid "Allow to update existing invoices"
msgstr "Tillat oppdatering av eksisterende fakturaer."
#: pretix/base/settings.py:1280
+1 -1
View File
@@ -11133,7 +11133,7 @@ msgstr ""
"backend worden aangebracht."
#: pretix/base/settings.py:1279
msgid "Allow updating existing invoices"
msgid "Allow to update existing invoices"
msgstr "Sta wijziging van bestaande facturen toe"
#: pretix/base/settings.py:1280
@@ -11130,7 +11130,7 @@ msgstr ""
"backend worden aangebracht."
#: pretix/base/settings.py:1279
msgid "Allow updating existing invoices"
msgid "Allow to update existing invoices"
msgstr "Sta wijziging van bestaande facturen toe"
#: pretix/base/settings.py:1280
@@ -11154,7 +11154,7 @@ msgstr ""
"toepassing op wijzigingen die in de backend worden gemaakt."
#: pretix/base/settings.py:1279
msgid "Allow updating existing invoices"
msgid "Allow to update existing invoices"
msgstr "Sta wijziging van bestaande facturen toe"
#: pretix/base/settings.py:1280
+1 -1
View File
@@ -11246,7 +11246,7 @@ msgstr ""
"wewnętrznego."
#: pretix/base/settings.py:1279
msgid "Allow updating existing invoices"
msgid "Allow to update existing invoices"
msgstr "Pozwól na aktualizację istniejących faktur"
#: pretix/base/settings.py:1280
@@ -10422,7 +10422,7 @@ msgid ""
msgstr ""
#: pretix/base/settings.py:1279
msgid "Allow updating existing invoices"
msgid "Allow to update existing invoices"
msgstr ""
#: pretix/base/settings.py:1280
+1 -1
View File
@@ -10337,7 +10337,7 @@ msgid ""
msgstr ""
#: pretix/base/settings.py:1279
msgid "Allow updating existing invoices"
msgid "Allow to update existing invoices"
msgstr ""
#: pretix/base/settings.py:1280
@@ -11081,7 +11081,7 @@ msgstr ""
"configuração não afeta as alterações feitas por meio do back-end."
#: pretix/base/settings.py:1279
msgid "Allow updating existing invoices"
msgid "Allow to update existing invoices"
msgstr "Permitir alterar faturas existentes"
#: pretix/base/settings.py:1280
@@ -11226,7 +11226,7 @@ msgstr ""
"definição não afecta as alterações feitas através do backend."
#: pretix/base/settings.py:1279
msgid "Allow updating existing invoices"
msgid "Allow to update existing invoices"
msgstr "Permitir atualizar as faturas existentes"
#: pretix/base/settings.py:1280
+1 -1
View File
@@ -11595,7 +11595,7 @@ msgstr ""
"Această setare nu afectează modificările efectuate prin backend."
#: pretix/base/settings.py:1279
msgid "Allow updating existing invoices"
msgid "Allow to update existing invoices"
msgstr "Permiteți actualizarea facturilor existente"
#: pretix/base/settings.py:1280
+5 -3
View File
@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-06-28 14:42+0000\n"
"PO-Revision-Date: 2026-06-30 16:52+0000\n"
"PO-Revision-Date: 2026-06-20 17:00+0000\n"
"Last-Translator: Nikita Mitasov <me@ch4og.com>\n"
"Language-Team: Russian <https://translate.pretix.eu/projects/pretix/pretix/"
"ru/>\n"
@@ -11488,7 +11488,7 @@ msgid ""
msgstr ""
#: pretix/base/settings.py:1279
msgid "Allow updating existing invoices"
msgid "Allow to update existing invoices"
msgstr ""
#: pretix/base/settings.py:1280
@@ -35488,8 +35488,10 @@ msgstr "Введите промокод ниже, чтобы купить эт
#: pretix/presale/templates/pretixpresale/event/fragment_availability.html:10
#: pretix/presale/templates/pretixpresale/event/fragment_availability.html:14
#, fuzzy
#| msgid "Quota availabilities"
msgid "Not available yet."
msgstr "Еще недоступно."
msgstr "Наличие квот"
#: pretix/presale/templates/pretixpresale/event/fragment_availability.html:18
msgid "Not available any more."
+1 -1
View File
@@ -10118,7 +10118,7 @@ msgid ""
msgstr ""
#: pretix/base/settings.py:1279
msgid "Allow updating existing invoices"
msgid "Allow to update existing invoices"
msgstr ""
#: pretix/base/settings.py:1280
+1 -1
View File
@@ -11122,7 +11122,7 @@ msgstr ""
"vplyv na zmeny vykonané prostredníctvom backendu."
#: pretix/base/settings.py:1279
msgid "Allow updating existing invoices"
msgid "Allow to update existing invoices"
msgstr "Umožniť aktualizáciu existujúcich faktúr"
#: pretix/base/settings.py:1280
+1 -1
View File
@@ -11467,7 +11467,7 @@ msgstr ""
"opravljene prek zalednega dela."
#: pretix/base/settings.py:1279
msgid "Allow updating existing invoices"
msgid "Allow to update existing invoices"
msgstr ""
#: pretix/base/settings.py:1280
+1 -1
View File
@@ -10057,7 +10057,7 @@ msgid ""
msgstr ""
#: pretix/base/settings.py:1279
msgid "Allow updating existing invoices"
msgid "Allow to update existing invoices"
msgstr ""
#: pretix/base/settings.py:1280
+1 -1
View File
@@ -11223,7 +11223,7 @@ msgstr ""
"inställning påverkar inte ändringar som görs via admin."
#: pretix/base/settings.py:1279
msgid "Allow updating existing invoices"
msgid "Allow to update existing invoices"
msgstr "Tillåt att uppdatera befintliga fakturor"
#: pretix/base/settings.py:1280
+1 -1
View File
@@ -10589,7 +10589,7 @@ msgstr ""
"การตั้งค่านี้ไม่มีผลกับการแก้ไขผ่านระบบหลังบ้าน"
#: pretix/base/settings.py:1279
msgid "Allow updating existing invoices"
msgid "Allow to update existing invoices"
msgstr "อนุญาตให้แก้ไขใบแจ้งหนี้ที่มีอยู่เดิม"
#: pretix/base/settings.py:1280
+1 -1
View File
@@ -11865,7 +11865,7 @@ msgid ""
msgstr ""
#: pretix/base/settings.py:1279
msgid "Allow updating existing invoices"
msgid "Allow to update existing invoices"
msgstr ""
#: pretix/base/settings.py:1280
+1 -1
View File
@@ -11439,7 +11439,7 @@ msgstr ""
"налаштування не впливає на будь-які зміни, що вносяться за допомогою бекенду."
#: pretix/base/settings.py:1279
msgid "Allow updating existing invoices"
msgid "Allow to update existing invoices"
msgstr "Дозволити оновити існуючі рахунки"
#: pretix/base/settings.py:1280
+1 -1
View File
@@ -10844,7 +10844,7 @@ msgstr ""
"hóa đơn sẽ tự động bị hủy và hóa đơn mới sẽ được cấp."
#: pretix/base/settings.py:1279
msgid "Allow updating existing invoices"
msgid "Allow to update existing invoices"
msgstr "Cho phép cập nhật hóa đơn hiện có"
#: pretix/base/settings.py:1280
+1 -1
View File
@@ -10045,7 +10045,7 @@ msgid ""
msgstr ""
#: pretix/base/settings.py:1279
msgid "Allow updating existing invoices"
msgid "Allow to update existing invoices"
msgstr ""
#: pretix/base/settings.py:1280
@@ -11458,7 +11458,7 @@ msgstr ""
"响。"
#: pretix/base/settings.py:1279
msgid "Allow updating existing invoices"
msgid "Allow to update existing invoices"
msgstr ""
#: pretix/base/settings.py:1280
@@ -10633,7 +10633,7 @@ msgstr ""
"過後端所做的更改。"
#: pretix/base/settings.py:1279
msgid "Allow updating existing invoices"
msgid "Allow to update existing invoices"
msgstr "允許更新現有發票"
#: pretix/base/settings.py:1280
+7 -2
View File
@@ -34,6 +34,7 @@
import json
import logging
import urllib.parse
from collections import OrderedDict
from decimal import Decimal
@@ -41,6 +42,7 @@ import paypalrestsdk
import paypalrestsdk.exceptions
from django import forms
from django.contrib import messages
from django.core import signing
from django.http import HttpRequest
from django.template.loader import get_template
from django.urls import reverse
@@ -56,7 +58,6 @@ from pretix.base.forms import SecretKeySettingsField
from pretix.base.models import Event, Order, OrderPayment, OrderRefund, Quota
from pretix.base.payment import BasePaymentProvider, PaymentException
from pretix.base.settings import SettingsSandbox
from pretix.base.views.redirect import safelink
from pretix.multidomain.urlreverse import eventreverse_absolute
from pretix.plugins.paypal.api import Api
from pretix.plugins.paypal.models import ReferencedPayPalObject
@@ -348,7 +349,11 @@ class Paypal(BasePaymentProvider):
for link in payment.links:
if link.method == "REDIRECT" and link.rel == "approval_url":
if request.session.get('iframe_session', False):
return safelink(link.href, framebreak=True)
signer = signing.Signer(salt='safe-redirect')
return (
eventreverse_absolute(request.event, 'plugins:paypal:redirect') + '?url=' +
urllib.parse.quote(signer.sign(link.href))
)
else:
return str(link.href)
else:
@@ -0,0 +1,33 @@
{% load compress %}
{% load i18n %}
{% load static %}
<!DOCTYPE html>
<html>
<head>
<title>{{ settings.PRETIX_INSTANCE_NAME }}</title>
{% compress css %}
<link rel="stylesheet" type="text/x-scss" href="{% static "pretixbase/scss/cachedfiles.scss" %}"/>
{% endcompress %}
{% compress js %}
<script type="text/javascript" src="{% static "jquery/js/jquery-3.6.4.min.js" %}"></script>
{% endcompress %}
</head>
<body>
<div class="container">
<h1>{% trans "The payment process has started in a new window." %}</h1>
<p>
{% trans "The window to enter your payment data was not opened or was closed?" %}
</p>
<p>
<a href="{{ url }}" target="_blank" class="btn btn-default btn-lg">
<span class="fa fa-external-link-square"></span>
{% trans "Click here in order to open the window." %}
</a>
</p>
<script>
window.open('{{ url|escapejs }}');
</script>
</div>
</body>
</html>
+2 -1
View File
@@ -21,12 +21,13 @@
#
from django.urls import include, re_path
from .views import abort, oauth_disconnect, success
from .views import abort, oauth_disconnect, redirect_view, success
event_patterns = [
re_path(r'^paypal/', include([
re_path(r'^abort/$', abort, name='abort'),
re_path(r'^return/$', success, name='return'),
re_path(r'^redirect/$', redirect_view, name='redirect'),
re_path(r'w/(?P<cart_namespace>[a-zA-Z0-9]{16})/abort/', abort, name='abort'),
re_path(r'w/(?P<cart_namespace>[a-zA-Z0-9]{16})/return/', success, name='return'),
+19 -1
View File
@@ -39,10 +39,13 @@ from decimal import Decimal
import paypalrestsdk
import paypalrestsdk.exceptions
from django.contrib import messages
from django.core import signing
from django.db.models import Sum
from django.http import HttpResponse
from django.http import HttpResponse, HttpResponseBadRequest
from django.shortcuts import render
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from django.views.decorators.clickjacking import xframe_options_exempt
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
from django_scopes import scopes_disabled
@@ -58,6 +61,21 @@ from pretix.plugins.paypal.payment import Paypal
logger = logging.getLogger('pretix.plugins.paypal')
@xframe_options_exempt
def redirect_view(request, *args, **kwargs):
signer = signing.Signer(salt='safe-redirect')
try:
url = signer.unsign(request.GET.get('url', ''))
except signing.BadSignature:
return HttpResponseBadRequest('Invalid parameter')
r = render(request, 'pretixplugins/paypal/redirect.html', {
'url': url,
})
r._csp_ignore = True
return r
def success(request, *args, **kwargs):
pid = request.GET.get('paymentId')
token = request.GET.get('token')
@@ -0,0 +1,33 @@
{% load compress %}
{% load i18n %}
{% load static %}
<!DOCTYPE html>
<html>
<head>
<title>{{ settings.PRETIX_INSTANCE_NAME }}</title>
{% compress css %}
<link rel="stylesheet" type="text/x-scss" href="{% static "pretixbase/scss/cachedfiles.scss" %}"/>
{% endcompress %}
{% compress js %}
<script type="text/javascript" src="{% static "jquery/js/jquery-3.6.4.min.js" %}"></script>
{% endcompress %}
</head>
<body>
<div class="container">
<h1>{% trans "The payment process has started in a new window." %}</h1>
<p>
{% trans "The window to enter your payment data was not opened or was closed?" %}
</p>
<p>
<a href="{{ url }}" target="_blank" class="btn btn-default btn-lg">
<span class="fa fa-external-link-square"></span>
{% trans "Click here in order to open the window." %}
</a>
</p>
<script>
window.open('{{ url|escapejs }}');
</script>
</div>
</body>
</html>
+3 -1
View File
@@ -22,13 +22,15 @@
from django.urls import include, re_path
from .views import (
PayView, XHRView, abort, isu_disconnect, isu_return, success, webhook,
PayView, XHRView, abort, isu_disconnect, isu_return, redirect_view,
success, webhook,
)
event_patterns = [
re_path(r'^paypal2/', include([
re_path(r'^abort/$', abort, name='abort'),
re_path(r'^return/$', success, name='return'),
re_path(r'^redirect/$', redirect_view, name='redirect'),
re_path(r'^xhr/$', XHRView.as_view(), name='xhr'),
re_path(r'^pay/(?P<order>[^/]+)/(?P<hash>[^/]+)/(?P<payment>[^/]+)/$', PayView.as_view(), name='pay'),
re_path(r'^(?P<order>[^/][^w]+)/(?P<secret>[A-Za-z0-9]+)/xhr/$', XHRView.as_view(), name='xhr'),
+19 -1
View File
@@ -36,10 +36,13 @@ import logging
from decimal import Decimal
from django.contrib import messages
from django.core import signing
from django.core.cache import cache
from django.db import transaction
from django.db.models import Sum
from django.http import Http404, HttpResponse, JsonResponse
from django.http import (
Http404, HttpResponse, HttpResponseBadRequest, JsonResponse,
)
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.utils.decorators import method_decorator
@@ -101,6 +104,21 @@ class PaypalOrderView:
}) + ('?paid=yes' if self.order.status == Order.STATUS_PAID else ''))
@xframe_options_exempt
def redirect_view(request, *args, **kwargs):
signer = signing.Signer(salt='safe-redirect')
try:
url = signer.unsign(request.GET.get('url', ''))
except signing.BadSignature:
return HttpResponseBadRequest('Invalid parameter')
r = render(request, 'pretixplugins/paypal2/redirect.html', {
'url': url,
})
r._csp_ignore = True
return r
@method_decorator(csrf_exempt, name='dispatch')
@method_decorator(xframe_options_exempt, 'dispatch')
class XHRView(View):
+15 -3
View File
@@ -46,6 +46,7 @@ import stripe
from django import forms
from django.conf import settings
from django.contrib import messages
from django.core import signing
from django.db import transaction
from django.http import HttpRequest
from django.template.loader import get_template
@@ -71,7 +72,6 @@ from pretix.base.payment import (
)
from pretix.base.plugins import get_all_plugins
from pretix.base.settings import SettingsSandbox
from pretix.base.views.redirect import safelink
from pretix.helpers import OF_SELF
from pretix.helpers.countries import CachedCountries
from pretix.helpers.http import get_client_ip
@@ -745,7 +745,15 @@ class StripeMethod(BasePaymentProvider):
def redirect(self, request, url):
if request.session.get('iframe_session', False):
return safelink(url, framebreak=True)
return (
eventreverse_absolute(request.event, 'plugins:stripe:redirect') +
'?data=' + signing.dumps({
'url': url,
'session': {
'payment_stripe_order_secret': request.session['payment_stripe_order_secret'],
},
}, salt='safe-redirect')
)
else:
return str(url)
@@ -1045,7 +1053,11 @@ class StripeMethod(BasePaymentProvider):
'hash': payment.order.tagged_secret('plugins:stripe'),
})
if not self.redirect_in_widget_allowed and request.session.get('iframe_session', False):
return safelink(url, framebreak=True)
return eventreverse_absolute(self.event, 'plugins:stripe:redirect') + '?data=' + signing.dumps({
'url': url,
'session': {},
}, salt='safe-redirect')
return url
def _confirm_payment_intent(self, request, payment):
@@ -0,0 +1,33 @@
{% load compress %}
{% load i18n %}
{% load static %}
<!DOCTYPE html>
<html>
<head>
<title>{{ settings.PRETIX_INSTANCE_NAME }}</title>
{% compress css %}
<link rel="stylesheet" type="text/x-scss" href="{% static "pretixbase/scss/cachedfiles.scss" %}"/>
{% endcompress %}
{% compress js %}
<script type="text/javascript" src="{% static "jquery/js/jquery-3.6.4.min.js" %}"></script>
{% endcompress %}
</head>
<body>
<div class="container">
<h1>{% trans "The payment process has started in a new window." %}</h1>
<p>
{% trans "The window to enter your payment data was not opened or was closed?" %}
</p>
<p>
<a href="{{ url }}" target="_blank" class="btn btn-default btn-lg">
<span class="fa fa-external-link-square"></span>
{% trans "Click here in order to open the window." %}
</a>
</p>
<script>
window.open('{{ url|escapejs }}');
</script>
</div>
</body>
</html>
+2 -1
View File
@@ -25,12 +25,13 @@ from pretix.multidomain import event_url
from .views import (
OrganizerSettingsFormView, ReturnView, ScaReturnView, ScaView,
oauth_disconnect, oauth_return, webhook,
oauth_disconnect, oauth_return, redirect_view, webhook,
)
event_patterns = [
re_path(r'^stripe/', include([
event_url(r'^webhook/$', webhook, name='webhook', require_live=False),
re_path(r'^redirect/$', redirect_view, name='redirect'),
re_path(r'^return/(?P<order>[^/]+)/(?P<hash>[^/]+)/(?P<payment>[0-9]+)/$', ReturnView.as_view(), name='return'),
re_path(r'^sca/(?P<order>[^/]+)/(?P<hash>[^/]+)/(?P<payment>[0-9]+)/$', ScaView.as_view(), name='sca'),
re_path(r'^sca/(?P<order>[^/]+)/(?P<hash>[^/]+)/(?P<payment>[0-9]+)/return/$',
+31 -2
View File
@@ -34,11 +34,13 @@
import json
import logging
import urllib.parse
import requests
from django.contrib import messages
from django.core import signing
from django.db import transaction
from django.http import Http404, HttpResponse
from django.http import Http404, HttpResponse, HttpResponseBadRequest
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.utils.decorators import method_decorator
@@ -62,7 +64,7 @@ from pretix.control.views.event import DecoupleMixin
from pretix.control.views.organizer import OrganizerDetailViewMixin
from pretix.helpers import OF_SELF
from pretix.helpers.http import redirect_to_url
from pretix.multidomain.urlreverse import eventreverse
from pretix.multidomain.urlreverse import eventreverse, eventreverse_absolute
from pretix.plugins.stripe.forms import OrganizerStripeSettingsForm
from pretix.plugins.stripe.models import ReferencedStripeObject
from pretix.plugins.stripe.tasks import (
@@ -72,6 +74,28 @@ from pretix.plugins.stripe.tasks import (
logger = logging.getLogger('pretix.plugins.stripe')
@xframe_options_exempt
def redirect_view(request, *args, **kwargs):
try:
data = signing.loads(request.GET.get('data', ''), salt='safe-redirect')
except signing.BadSignature:
return HttpResponseBadRequest('Invalid parameter')
if 'go' in request.GET:
if 'session' in data:
for k, v in data['session'].items():
request.session[k] = v
return redirect(data['url'])
else:
params = request.GET.copy()
params['go'] = '1'
r = render(request, 'pretixplugins/stripe/redirect.html', {
'url': eventreverse_absolute(request.event, 'plugins:stripe:redirect') + '?' + urllib.parse.urlencode(params),
})
r._csp_ignore = True
return r
@scopes_disabled()
def oauth_return(request, *args, **kwargs):
import stripe
@@ -490,6 +514,11 @@ class StripeOrderView:
return self.request.event.get_payment_providers()[self.payment.provider]
def _redirect_to_order(self):
if self.request.session.get('payment_stripe_order_secret') != self.order.secret and not self.payment.provider.startswith('stripe'):
messages.error(self.request, _('Sorry, there was an error in the payment process. Please check the link '
'in your emails to continue.'))
return redirect_to_url(eventreverse(self.request.event, 'presale:event.index'))
return redirect_to_url(eventreverse(self.request.event, 'presale:event.order', kwargs={
'order': self.order.code,
'secret': self.order.secret
@@ -28,10 +28,9 @@ from django.utils.translation import gettext_lazy as _
from pretix.base.logentrytypes import EventLogEntryType, log_entry_types
from pretix.base.models import Event, SalesChannel
from pretix.base.signals import EventPluginSignal # NOQA: legacy import
from pretix.base.signals import (
event_copy_data, item_copy_data, layout_text_variables, logentry_display,
logentry_object_link, register_data_exporters,
from pretix.base.signals import ( # NOQA: legacy import
EventPluginSignal, event_copy_data, item_copy_data, layout_text_variables,
logentry_display, logentry_object_link, register_data_exporters,
register_multievent_data_exporters, register_ticket_outputs,
)
from pretix.control.signals import item_forms, order_position_buttons
@@ -39,8 +38,9 @@ from pretix.plugins.ticketoutputpdf.forms import TicketLayoutItemForm
from pretix.plugins.ticketoutputpdf.models import (
TicketLayout, TicketLayoutItem,
)
from pretix.presale.style import get_fonts # NOQA: legacy import
from pretix.presale.style import register_event_fonts, register_fonts
from pretix.presale.style import ( # NOQA: legacy import
get_fonts, register_event_fonts, register_fonts,
)
@receiver(register_ticket_outputs, dispatch_uid="output_pdf")
@@ -9,7 +9,7 @@
{% load anonymize_email %}
{% block thetitle %}
{% if messages %}
{{ messages|join:" " }} ::
{{ messages|join:" " }} ::
{% endif %}
{% block title %}{% endblock %}{% if request.resolver_match.url_name != "event.index" %} :: {% endif %}{{ event.name }}
{% endblock %}
@@ -40,7 +40,6 @@
</dd>
</dl>
{% else %}
<div class="alert alert-danger hidden"></div>
<p>
{% blocktrans trimmed with org=request.organizer.name %}
If you created a customer account at {{ org }} before, you can log in now and connect
@@ -2,7 +2,6 @@
{% load i18n %}
{% load eventurl %}
{% load urlreplace %}
{% load static %}
{% block content %}
{% if cart_namespace %}
@@ -24,8 +23,9 @@
class="btn btn-primary btn-lg" target="_blank">
{% trans "Continue in new tab" %}
</a>
{{ url|json_script:"framebreak-url" }}
<script type="text/javascript" src="{% static "pretixbase/js/framebreak.js" %}"></script>
<script>
window.open('{{ url|escapejs }}');
</script>
</div>
{% else %}
<h1>{% trans "Cookies not supported" %}</h1>
@@ -1,7 +1,6 @@
{% extends "pretixpresale/event/base.html" %}
{% load i18n %}
{% load l10n %}
{% load static %}
{% load eventurl %}
{% load cache_large %}
{% load money %}
@@ -40,7 +39,6 @@
{% else %}
<meta property="og:url" content="{% abseventurl request.event "presale:event.index" %}" />
{% endif %}
<script type="text/javascript" src="{% static "pretixpresale/js/csrfcookieretry.js" %}"></script>
{% endblock %}
{% block content %}

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