mirror of
https://github.com/pretix/pretix.git
synced 2026-08-15 11:36:27 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3a07f5b76 |
+2
-3
@@ -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
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -118,7 +118,6 @@ ALL_LANGUAGES = [
|
||||
('sv', _('Swedish')),
|
||||
('es', _('Spanish')),
|
||||
('es-419', _('Spanish (Latin America)')),
|
||||
('th', _('Thai')),
|
||||
('tr', _('Turkish')),
|
||||
('uk', _('Ukrainian')),
|
||||
]
|
||||
|
||||
@@ -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
|
||||
@@ -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):
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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:
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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': {
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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'))
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 it’s 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."
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 d’entré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 n’est pas activé par l'organisateur."
|
||||
msgstr ""
|
||||
"Le type de média sélectionné n’est 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 ""
|
||||
"l’achat."
|
||||
|
||||
#: 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 ""
|
||||
"l’enregistrement :"
|
||||
|
||||
#: 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."
|
||||
|
||||
@@ -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)"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
@@ -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'),
|
||||
|
||||
@@ -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>
|
||||
@@ -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'),
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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>
|
||||
@@ -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/$',
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user