mirror of
https://github.com/pretix/pretix.git
synced 2026-07-04 05:01:54 +00:00
Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| faa97026e1 | |||
| e1736e8d2a | |||
| 120317a8f2 | |||
| 7d5b00a610 | |||
| 83612c7d65 | |||
| 7a5f96369a | |||
| 7fd6bf41f9 | |||
| 458c3d4b83 | |||
| d10d061e45 | |||
| d30bca50f7 | |||
| 3903aca7c9 | |||
| 493c920aba | |||
| 09b7bc00b0 | |||
| 2e195c0274 | |||
| 18cb9c1816 | |||
| c3e0120f9f | |||
| 67f7fec134 | |||
| c2c97f31ca | |||
| fd565ecdb2 | |||
| f35b13b686 | |||
| 550bb675f5 | |||
| adc9c9d514 | |||
| 8441c4bc7a | |||
| 97ff252c09 | |||
| d3ca2ac1e5 | |||
| 6eebaaa563 | |||
| c9781f012b | |||
| 000bf54105 | |||
| e42d3d632f | |||
| 3bf5a5e478 | |||
| a6f31df0d4 |
+2
-1
@@ -53,6 +53,7 @@ dependencies = [
|
||||
"django-oauth-toolkit==2.3.*",
|
||||
"django-otp==1.7.*",
|
||||
"django-phonenumber-field==8.4.*",
|
||||
"django-querytagger==0.0.2",
|
||||
"django-redis==6.0.*",
|
||||
"django-scopes==2.0.*",
|
||||
"django-statici18n==2.7.*",
|
||||
@@ -93,7 +94,7 @@ dependencies = [
|
||||
"redis==7.4.*",
|
||||
"reportlab==4.5.*",
|
||||
"requests==2.32.*",
|
||||
"sentry-sdk==2.63.*",
|
||||
"sentry-sdk==2.64.*",
|
||||
"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 --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)
|
||||
./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)
|
||||
|
||||
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.6.0.dev0"
|
||||
__version__ = "2026.7.0.dev0"
|
||||
|
||||
@@ -118,6 +118,7 @@ ALL_LANGUAGES = [
|
||||
('sv', _('Swedish')),
|
||||
('es', _('Spanish')),
|
||||
('es-419', _('Spanish (Latin America)')),
|
||||
('th', _('Thai')),
|
||||
('tr', _('Turkish')),
|
||||
('uk', _('Ukrainian')),
|
||||
]
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
# 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
|
||||
@@ -43,6 +45,8 @@ from pretix.multidomain.urlreverse import (
|
||||
)
|
||||
from pretix.presale.style import get_fonts
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_supported = None
|
||||
|
||||
|
||||
@@ -223,7 +227,26 @@ 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)
|
||||
|
||||
|
||||
@@ -243,21 +266,7 @@ 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
|
||||
@@ -268,18 +277,15 @@ class SecurityMiddleware(MiddlewareMixin):
|
||||
# https://github.com/pretix/pretix/issues/765
|
||||
resp['P3P'] = 'CP=\"ALL DSP COR CUR ADM TAI OUR IND COM NAV INT\"'
|
||||
|
||||
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}", "*"))
|
||||
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']
|
||||
|
||||
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))
|
||||
return resp
|
||||
|
||||
def _build_csp(self, request, resp):
|
||||
url = resolve(request.path_info)
|
||||
|
||||
h = {
|
||||
'default-src': ["{static}"],
|
||||
@@ -288,8 +294,8 @@ class SecurityMiddleware(MiddlewareMixin):
|
||||
'frame-src': ['{static}'],
|
||||
'style-src': ["{static}", "{media}"],
|
||||
'connect-src': ["{dynamic}", "{media}"],
|
||||
'img-src': ["{static}", "{media}", "data:"] + img_src,
|
||||
'font-src': ["{static}"] + list(font_src),
|
||||
'img-src': ["{static}", "{media}", "data:"],
|
||||
'font-src': ["{static}"],
|
||||
'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
|
||||
@@ -298,6 +304,13 @@ 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'"]
|
||||
@@ -309,6 +322,7 @@ 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
|
||||
@@ -317,27 +331,32 @@ 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))
|
||||
|
||||
staticdomain = "'self'"
|
||||
dynamicdomain = "'self'"
|
||||
mediadomain = "'self'"
|
||||
placeholders = {
|
||||
"{static}": ["'self'"],
|
||||
"{dynamic}": ["'self'"],
|
||||
"{media}": ["'self'"],
|
||||
}
|
||||
if settings.MEDIA_URL.startswith('http'):
|
||||
mediadomain += " " + settings.MEDIA_URL[:settings.MEDIA_URL.find('/', 9)]
|
||||
placeholders["{media}"].append(settings.MEDIA_URL[:settings.MEDIA_URL.find('/', 9)])
|
||||
if settings.STATIC_URL.startswith('http'):
|
||||
staticdomain += " " + settings.STATIC_URL[:settings.STATIC_URL.find('/', 9)]
|
||||
placeholders["{static}"].append(settings.STATIC_URL[:settings.STATIC_URL.find('/', 9)])
|
||||
if settings.SITE_URL.startswith('http'):
|
||||
if settings.SITE_URL.find('/', 9) > 0:
|
||||
staticdomain += " " + settings.SITE_URL[:settings.SITE_URL.find('/', 9)]
|
||||
dynamicdomain += " " + settings.SITE_URL[:settings.SITE_URL.find('/', 9)]
|
||||
placeholders["{static}"].append(settings.SITE_URL[:settings.SITE_URL.find('/', 9)])
|
||||
placeholders["{dynamic}"].append(settings.SITE_URL[:settings.SITE_URL.find('/', 9)])
|
||||
else:
|
||||
staticdomain += " " + settings.SITE_URL
|
||||
dynamicdomain += " " + settings.SITE_URL
|
||||
placeholders["{static}"].append(settings.SITE_URL)
|
||||
placeholders["{dynamic}"].append(settings.SITE_URL)
|
||||
|
||||
if hasattr(request, 'organizer') and request.organizer:
|
||||
if hasattr(request, 'event') and request.event:
|
||||
@@ -348,18 +367,29 @@ class SecurityMiddleware(MiddlewareMixin):
|
||||
siteurlsplit = urlsplit(settings.SITE_URL)
|
||||
if siteurlsplit.port and siteurlsplit.port not in (80, 443):
|
||||
domain = '%s:%d' % (domain, siteurlsplit.port)
|
||||
dynamicdomain += " " + domain
|
||||
placeholders["{dynamic}"].append(domain)
|
||||
|
||||
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']
|
||||
for k, v in h.items():
|
||||
h[k] = sorted(set(result for part in v for result in placeholders.get(part, [part])))
|
||||
|
||||
return resp
|
||||
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
|
||||
|
||||
|
||||
class RejectInvalidInputMiddleware(MiddlewareMixin):
|
||||
|
||||
@@ -647,25 +647,22 @@ 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=None):
|
||||
def has_active_staff_session(self, session_key):
|
||||
"""
|
||||
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=None):
|
||||
if not self.is_staff:
|
||||
def get_active_staff_session(self, session_key):
|
||||
if not self.is_staff or not session_key:
|
||||
return None
|
||||
if not hasattr(self, '_staff_session_cache'):
|
||||
self._staff_session_cache = {}
|
||||
if session_key not in self._staff_session_cache:
|
||||
qs = StaffSession.objects.filter(
|
||||
user=self, date_end__isnull=True
|
||||
)
|
||||
if session_key:
|
||||
qs = qs.filter(session_key=session_key)
|
||||
sess = qs.first()
|
||||
sess = StaffSession.objects.filter(
|
||||
user=self, date_end__isnull=True, session_key=session_key
|
||||
).first()
|
||||
if sess:
|
||||
if sess.date_start < now() - timedelta(seconds=settings.PRETIX_SESSION_TIMEOUT_ABSOLUTE):
|
||||
sess.date_end = now()
|
||||
|
||||
@@ -40,6 +40,7 @@ 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
|
||||
|
||||
@@ -79,10 +80,16 @@ 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.')})
|
||||
@@ -899,7 +906,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'):
|
||||
if hasattr(other, 'alternative_domain_assignment') and not is_cross_organizer:
|
||||
other.alternative_domain_assignment.domain.event_assignments.create(event=self)
|
||||
|
||||
if not self.all_sales_channels:
|
||||
|
||||
@@ -35,6 +35,7 @@ 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
|
||||
@@ -61,6 +62,9 @@ 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):
|
||||
@@ -78,6 +82,9 @@ class Organizer(LoggedModel):
|
||||
"""
|
||||
|
||||
settings_namespace = 'organizer'
|
||||
if TYPE_CHECKING:
|
||||
settings: HierarkeyProxy
|
||||
|
||||
name = models.CharField(max_length=200,
|
||||
verbose_name=_("Name"))
|
||||
slug = models.CharField(
|
||||
|
||||
@@ -936,7 +936,7 @@ class BasePaymentProvider:
|
||||
"""
|
||||
Will be called if the *event administrator* views the details of a payment.
|
||||
|
||||
It should return a SafeString containing HTML code, with information regarding the current payment
|
||||
It should return HTML code containing information regarding the current payment
|
||||
status and, if applicable, next steps.
|
||||
|
||||
The default implementation returns an empty string.
|
||||
@@ -961,7 +961,7 @@ class BasePaymentProvider:
|
||||
"""
|
||||
Will be called if the *event administrator* views the details of a refund.
|
||||
|
||||
It should return a SafeString containing HTML code, with information regarding the current refund
|
||||
It should return HTML code containing information regarding the current refund
|
||||
status and, if applicable, next steps.
|
||||
|
||||
The default implementation returns an empty string.
|
||||
|
||||
@@ -1924,8 +1924,6 @@ 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': {
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
{% 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,8 +42,6 @@ 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
|
||||
@@ -54,6 +52,7 @@ 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()
|
||||
@@ -158,8 +157,7 @@ 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:'):
|
||||
signer = signing.Signer(salt='safe-redirect')
|
||||
attrs[None, 'href'] = reverse('redirect') + '?url=' + urllib.parse.quote(signer.sign(url))
|
||||
attrs[None, 'href'] = safelink(url)
|
||||
attrs[None, 'target'] = '_blank'
|
||||
attrs[None, 'rel'] = 'noopener'
|
||||
return attrs
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import logging
|
||||
import urllib.parse
|
||||
|
||||
from django.core import signing
|
||||
@@ -26,6 +27,8 @@ 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')
|
||||
@@ -42,11 +45,16 @@ def _is_samesite_referer(request):
|
||||
|
||||
|
||||
def redir_view(request):
|
||||
signer = signing.Signer(salt='safe-redirect')
|
||||
framebreak = "framebreak" in request.GET
|
||||
salt = 'framebreak-safelink-url' if framebreak else 'safelink-url'
|
||||
try:
|
||||
url = signer.unsign(request.GET.get('url', ''))
|
||||
url = signing.Signer(salt=salt).unsign(request.GET.get('url', ''))
|
||||
except signing.BadSignature:
|
||||
return HttpResponseBadRequest('Invalid parameter')
|
||||
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')
|
||||
|
||||
if not _is_samesite_referer(request):
|
||||
u = urllib.parse.urlparse(url)
|
||||
@@ -55,11 +63,26 @@ 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):
|
||||
signer = signing.Signer(salt='safe-redirect')
|
||||
return reverse('redirect') + '?url=' + urllib.parse.quote(signer.sign(url))
|
||||
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
|
||||
|
||||
@@ -36,6 +36,7 @@ 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
|
||||
@@ -212,14 +213,17 @@ 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])
|
||||
hijacker = get_object_or_404(User, pk=hijack_history[0]["user"])
|
||||
ss = hijacker.get_active_staff_session(request.session.get('hijacker_session'))
|
||||
if ss:
|
||||
ss.logs.create(
|
||||
url=request.path,
|
||||
method=request.method,
|
||||
impersonating=request.user
|
||||
)
|
||||
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
|
||||
)
|
||||
else:
|
||||
ss = request.user.get_active_staff_session(request.session.session_key)
|
||||
if ss:
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% load compress %}
|
||||
{% load escapejson %}
|
||||
{% block content %}
|
||||
<form class="form-signin" action="" method="post" id="webauthn-form">
|
||||
{% csrf_token %}
|
||||
@@ -31,7 +30,8 @@
|
||||
</form>
|
||||
{% if jsondata %}
|
||||
<script type="text/json" id="webauthn-login">
|
||||
{{ jsondata|escapejson }}
|
||||
{{ jsondata|safe }}
|
||||
|
||||
</script>
|
||||
{% endif %}
|
||||
{% compress js %}
|
||||
|
||||
@@ -50,6 +50,46 @@
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if dkim_warning %}
|
||||
<div class="alert alert-danger">
|
||||
<p>
|
||||
{{ dkim_warning }}
|
||||
</p>
|
||||
<p>
|
||||
{% trans "Your new DKIM record should be set up as a CNAME record like this:" %}
|
||||
</p>
|
||||
<pre><code>{{ dkim_hostname }} CNAME {{ dkim_cname }}</code></pre>
|
||||
<p>
|
||||
{% trans "Please keep in mind that updates to DNS might require multiple hours to take effect." %}
|
||||
</p>
|
||||
</div>
|
||||
{% elif dkim_cname %}
|
||||
<div class="alert alert-success">
|
||||
{% blocktrans trimmed %}
|
||||
We found a DKIM record on your domain for this system. Great!
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if dmarc_warning %}
|
||||
<div class="alert alert-danger">
|
||||
<p>
|
||||
{{ dmarc_warning }}
|
||||
</p>
|
||||
<p>
|
||||
{% trans "Your new DMARC record could look like this:" %}
|
||||
</p>
|
||||
<pre><code>_dmarc.{{ hostname }} TXT "v=DMARC1; p=quarantine; sp=none; adkim=r; aspf=r;"</code></pre>
|
||||
<p>
|
||||
{% trans "Please keep in mind that updates to DNS might require multiple hours to take effect." %}
|
||||
</p>
|
||||
</div>
|
||||
{% elif dkim_cname %}
|
||||
<div class="alert alert-success">
|
||||
{% blocktrans trimmed %}
|
||||
We found a DMARC record on your domain for this system. Great!
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if verification %}
|
||||
<h3>{% trans "Verification" %}</h3>
|
||||
<p>
|
||||
@@ -70,7 +110,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if spf_warning %}
|
||||
{% if spf_warning or dkim_warning or dmarc_warning %}
|
||||
<div class="form-group submit-group">
|
||||
<a href="" class="btn btn-default btn-save">
|
||||
{% trans "Cancel" %}
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
{% if form.cancellation_fee %}
|
||||
{% if fee %}
|
||||
{% with fee|money:request.event.currency as f %}
|
||||
<p>{% blocktrans trimmed with fee=f|wrap_in:"strong" %}
|
||||
<p>{% blocktrans trimmed with fee="<strong>"|add:f|add:"</strong>"|safe %}
|
||||
The configured cancellation fee for a self-service cancellation would be {{ fee }} for this
|
||||
order, but for a cancellation performed by you, you need to set the cancellation fee here:
|
||||
{% endblocktrans %}</p>
|
||||
|
||||
@@ -903,7 +903,7 @@
|
||||
<tr>
|
||||
<td colspan="1"></td>
|
||||
<td colspan="5">
|
||||
{{ p.html_info }}
|
||||
{{ p.html_info|safe }}
|
||||
{% if staff_session %}
|
||||
<p>
|
||||
<a href="" class="btn btn-default btn-xs admin-only" data-expandpayment data-id="{{ p.pk }}">
|
||||
@@ -1018,7 +1018,7 @@
|
||||
</dl>
|
||||
{% endif %}
|
||||
{% if r.html_info %}
|
||||
{{ r.html_info }}
|
||||
{{ r.html_info|safe }}
|
||||
{% endif %}
|
||||
{% if staff_session %}
|
||||
<p>
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% load bootstrap3 %}
|
||||
{% load escapejson_dumps %}
|
||||
{% block inner %}
|
||||
<h1>{% trans "Connect to device:" %} {{ device.name }}</h1>
|
||||
|
||||
@@ -19,7 +18,7 @@
|
||||
{% trans "Open the app that you want to connect and optionally reset it to the original state." %}
|
||||
</li>
|
||||
<li>{% trans "Scan the following configuration code:" %}<br><br>
|
||||
<script type="application/json" data-replace-with-qr>{{ qrdata|escapejson_dumps }}</script><br>
|
||||
<script type="text/json" data-replace-with-qr>{{ qrdata|safe }}</script><br>
|
||||
{% trans "If your app/device does not support scanning a QR code, you can also enter the following information:" %}
|
||||
<br>
|
||||
<strong>{% trans "System URL:" %}</strong> <code id="system_url">{{ settings.SITE_URL }}</code>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{% extends "pretixcontrol/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% load escapejson %}
|
||||
{% block title %}{% trans "Add a two-factor authentication device" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h1>{% trans "Add a two-factor authentication device" %}</h1>
|
||||
@@ -33,7 +32,7 @@
|
||||
</li>
|
||||
<li>
|
||||
{% trans "Add a new account to the app by scanning the following barcode:" %}
|
||||
<script type="application/json" data-replace-with-qr>{{ qrdata|escapejson_dumps }}</script>
|
||||
<div class="qrcode-canvas" data-qrdata="#qrdata"></div>
|
||||
<p>
|
||||
<a data-toggle="collapse" href="#no_scan">
|
||||
{% trans "Can't scan the barcode?" %}
|
||||
@@ -82,4 +81,9 @@
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<script type="text/json" id="qrdata">
|
||||
{{ qrdata|safe }}
|
||||
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
{% load bootstrap3 %}
|
||||
{% load static %}
|
||||
{% load compress %}
|
||||
{% load escapejson %}
|
||||
{% block title %}{% trans "Add a two-factor authentication device" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h1>{% trans "Add a two-factor authentication device" %}</h1>
|
||||
@@ -27,7 +26,9 @@
|
||||
{% trans "Device registration failed." %}
|
||||
</div>
|
||||
<script type="text/json" id="webauthn-enroll">
|
||||
{{ jsondata|escapejson }}
|
||||
{{ jsondata|safe }}
|
||||
|
||||
|
||||
</script>
|
||||
{% compress js %}
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/base64js.js" %}"></script>
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
{% load bootstrap3 %}
|
||||
{% load compress %}
|
||||
{% load static %}
|
||||
{% load escapejson %}
|
||||
{% block content %}
|
||||
<form class="form-signin" id="webauthn-form" action="" method="post">
|
||||
{% csrf_token %}
|
||||
@@ -44,7 +43,7 @@
|
||||
|
||||
{% if jsondata %}
|
||||
<script type="text/json" id="webauthn-login">
|
||||
{{ jsondata|escapejson }}
|
||||
{{ jsondata|safe }}
|
||||
</script>
|
||||
{% endif %}
|
||||
{% compress js %}
|
||||
|
||||
@@ -41,6 +41,30 @@ from pretix.control.forms.mailsetup import SimpleMailForm, SMTPMailForm
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_cname_record(hostname):
|
||||
try:
|
||||
r = dns.resolver.Resolver()
|
||||
answers = r.resolve(hostname, 'CNAME')
|
||||
answers = list(answers)
|
||||
if len(answers) != 1:
|
||||
logger.exception('Found multiple CNAME records for {}'.format(hostname))
|
||||
return
|
||||
return str(answers[0].target).lower()
|
||||
except:
|
||||
logger.exception('Could not fetch CNAME record for {}'.format(hostname))
|
||||
|
||||
|
||||
def get_dmarc_record(hostname):
|
||||
try:
|
||||
r = dns.resolver.Resolver()
|
||||
for resp in r.resolve("_dmarc." + hostname, 'TXT'):
|
||||
data = b''.join(resp.strings).decode()
|
||||
if 'DMARC1' in data.strip():
|
||||
return data
|
||||
except:
|
||||
logger.exception("Could not fetch DMARC record for {}".format(hostname))
|
||||
|
||||
|
||||
def get_spf_record(hostname):
|
||||
try:
|
||||
r = dns.resolver.Resolver()
|
||||
@@ -49,7 +73,7 @@ def get_spf_record(hostname):
|
||||
if data.lower().strip().startswith('v=spf1 '): # RFC7208, section 4.5
|
||||
return data
|
||||
except:
|
||||
logger.exception("Could not fetch SPF record")
|
||||
logger.exception("Could not fetch SPF record for {}".format(hostname))
|
||||
|
||||
|
||||
def _check_spf_record(not_found_lookup_parts, spf_record, depth):
|
||||
@@ -168,10 +192,15 @@ class MailSettingsSetupView(TemplateView):
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
session_key = f'sender_mail_verification_code_{self.request.path}_{self.simple_form.cleaned_data.get("mail_from")}'
|
||||
verify_dns = (
|
||||
settings.MAIL_CUSTOM_SENDER_SPF_STRING or
|
||||
(settings.MAIL_CUSTOM_SENDER_DKIM_CNAME and settings.MAIL_CUSTOM_SENDER_DMARC_REQUIRED) or
|
||||
settings.MAIL_CUSTOM_SENDER_DMARC_REQUIRED
|
||||
)
|
||||
allow_save = (
|
||||
(not settings.MAIL_CUSTOM_SENDER_VERIFICATION_REQUIRED or
|
||||
('verification' in self.request.POST and self.request.POST.get('verification', '') == self.request.session.get(session_key, None))) and
|
||||
(not settings.MAIL_CUSTOM_SENDER_SPF_STRING or self.request.POST.get('state') == 'save')
|
||||
(not verify_dns or self.request.POST.get('state') == 'save')
|
||||
)
|
||||
|
||||
if allow_save:
|
||||
@@ -192,8 +221,8 @@ class MailSettingsSetupView(TemplateView):
|
||||
|
||||
spf_warning = None
|
||||
spf_record = None
|
||||
hostname = self.simple_form.cleaned_data['mail_from'].split('@')[-1]
|
||||
if settings.MAIL_CUSTOM_SENDER_SPF_STRING:
|
||||
hostname = self.simple_form.cleaned_data['mail_from'].split('@')[-1]
|
||||
spf_record = get_spf_record(hostname)
|
||||
if not spf_record:
|
||||
spf_warning = _(
|
||||
@@ -210,7 +239,43 @@ class MailSettingsSetupView(TemplateView):
|
||||
'this system in the SPF record.'
|
||||
)
|
||||
|
||||
verification = settings.MAIL_CUSTOM_SENDER_VERIFICATION_REQUIRED and not spf_warning
|
||||
dkim_warning = None
|
||||
dkim_hostname = None
|
||||
dkim_cname = None
|
||||
if settings.MAIL_CUSTOM_SENDER_DKIM_CNAME and settings.MAIL_CUSTOM_SENDER_DKIM_SELECTOR:
|
||||
dkim_hostname = settings.MAIL_CUSTOM_SENDER_DKIM_SELECTOR + '._domainkey.' + hostname
|
||||
cname_target = get_cname_record(dkim_hostname)
|
||||
dkim_cname = settings.MAIL_CUSTOM_SENDER_DKIM_CNAME
|
||||
if not dkim_cname.endswith("."):
|
||||
dkim_cname += "."
|
||||
if "%s" in dkim_cname:
|
||||
dkim_cname = dkim_cname.replace("%s", hostname.replace(".", "-").lower())
|
||||
if not cname_target:
|
||||
dkim_warning = _(
|
||||
'We could not find a CNAME record pointing to our DKIM key for domain you are trying to use. '
|
||||
'This means that there is a very high change most of the emails will be rejected or marked as '
|
||||
'spam. We strongly recommend setting up DKIM through a CNAME record. You can do so through the '
|
||||
'DNS settings at the provider you registered your domain with.'
|
||||
)
|
||||
elif cname_target != dkim_cname:
|
||||
dkim_warning = _(
|
||||
'We found a CNAME record for a DKIM key, but it is not pointing to the right location. '
|
||||
'This means that there is a very high chance most of the emails will be rejected or marked as '
|
||||
'spam. You should update the DNS settings of your domain.'
|
||||
)
|
||||
|
||||
dmarc_warning = None
|
||||
dmarc_record = None
|
||||
if settings.MAIL_CUSTOM_SENDER_DMARC_REQUIRED:
|
||||
dmarc_record = get_dmarc_record(hostname)
|
||||
if not dmarc_record:
|
||||
spf_warning = _(
|
||||
'We did not find DMARC record for your domain. This means that there is a very high chance '
|
||||
'most of the emails will be rejected or marked as spam. You should update the DNS settings '
|
||||
'of your domain.'
|
||||
)
|
||||
|
||||
verification = settings.MAIL_CUSTOM_SENDER_VERIFICATION_REQUIRED and not spf_warning and not dkim_warning and not dmarc_warning
|
||||
if verification:
|
||||
if 'verification' in self.request.POST:
|
||||
messages.error(request, _('The verification code was incorrect, please try again.'))
|
||||
@@ -240,6 +305,12 @@ class MailSettingsSetupView(TemplateView):
|
||||
'spf_warning': spf_warning,
|
||||
'spf_record': spf_record,
|
||||
'spf_key': settings.MAIL_CUSTOM_SENDER_SPF_STRING,
|
||||
'dkim_warning': dkim_warning,
|
||||
'dkim_hostname': dkim_hostname,
|
||||
'dkim_cname': dkim_cname,
|
||||
'dmarc_warning': dmarc_warning,
|
||||
'dmarc_record': dmarc_record,
|
||||
'hostname': hostname,
|
||||
'recp': self.simple_form.cleaned_data.get('mail_from')
|
||||
},
|
||||
using=self.template_engine,
|
||||
|
||||
@@ -551,10 +551,10 @@ class OrderDetail(OrderView):
|
||||
ctx['refunds'] = self.order.refunds.select_related('payment').order_by('-created')
|
||||
for p in ctx['payments']:
|
||||
if p.payment_provider:
|
||||
p.html_info = p.payment_provider.payment_control_render(self.request, p) or ""
|
||||
p.html_info = (p.payment_provider.payment_control_render(self.request, p) or "").strip()
|
||||
for r in ctx['refunds']:
|
||||
if r.payment_provider:
|
||||
r.html_info = r.payment_provider.refund_control_render(self.request, r) or ""
|
||||
r.html_info = (r.payment_provider.refund_control_render(self.request, r) or "").strip()
|
||||
ctx['invoices'] = list(self.order.invoices.all().select_related('event'))
|
||||
ctx['comment_form'] = CommentForm(initial={
|
||||
'comment': self.order.comment,
|
||||
|
||||
@@ -19,19 +19,23 @@
|
||||
# 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, get_user_model, load_backend, login,
|
||||
BACKEND_SESSION_KEY, HASH_SESSION_KEY, get_user_model, load_backend, login,
|
||||
logout,
|
||||
)
|
||||
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
|
||||
from django.utils.crypto import get_random_string, salted_hmac
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views import View
|
||||
@@ -218,11 +222,13 @@ 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
|
||||
'other_email': self.object.email,
|
||||
'staff_session': staff_session.pk,
|
||||
})
|
||||
oldkey = request.session.session_key
|
||||
|
||||
@@ -230,7 +236,15 @@ class UserImpersonateView(AdministratorPermissionRequiredMixin, RecentAuthentica
|
||||
hijacked = self.object
|
||||
|
||||
hijack_history = request.session.get("hijack_history", [])
|
||||
hijack_history.append(request.user._meta.pk.value_to_string(hijacker))
|
||||
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(),
|
||||
})
|
||||
|
||||
backend = get_used_backend(request)
|
||||
backend = f"{backend.__module__}.{backend.__class__.__name__}"
|
||||
@@ -238,6 +252,12 @@ 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(
|
||||
@@ -254,13 +274,28 @@ class UserImpersonateView(AdministratorPermissionRequiredMixin, RecentAuthentica
|
||||
class UserImpersonateStopView(LoginRequiredMixin, View):
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
impersonated = request.user
|
||||
|
||||
hijs = request.session['hijacker_session']
|
||||
staff_session_key = request.session['hijacker_session']
|
||||
prev_session_key = request.session.session_key
|
||||
hijack_history = request.session.get("hijack_history", [])
|
||||
hijacked = request.user
|
||||
user_pk = hijack_history.pop()
|
||||
hijacker = get_object_or_404(get_user_model(), pk=user_pk)
|
||||
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())
|
||||
|
||||
backend = get_used_backend(request)
|
||||
backend = f"{backend.__module__}.{backend.__class__.__name__}"
|
||||
with signals.no_update_last_login(), keep_session_age(request.session):
|
||||
@@ -275,17 +310,22 @@ class UserImpersonateStopView(LoginRequiredMixin, View):
|
||||
hijacked=hijacked,
|
||||
)
|
||||
|
||||
ss = request.user.get_active_staff_session(hijs)
|
||||
if ss:
|
||||
request.session.save()
|
||||
ss.session_key = request.session.session_key
|
||||
ss.save()
|
||||
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]})',
|
||||
)
|
||||
|
||||
request.user.log_action('pretix.control.auth.user.impersonate_stopped',
|
||||
user=request.user,
|
||||
data={
|
||||
'other': impersonated.pk,
|
||||
'other_email': impersonated.email
|
||||
'other': hijacked.pk,
|
||||
'other_email': hijacked.email,
|
||||
'staff_session': staff_session.pk,
|
||||
})
|
||||
return redirect(reverse('control:index'))
|
||||
|
||||
|
||||
@@ -47,5 +47,5 @@ def escapejson(value):
|
||||
|
||||
@keep_lazy(str, SafeText)
|
||||
def escapejson_attr(value):
|
||||
"""Hex encodes characters for use in a html attribute."""
|
||||
"""Hex encodes characters for use in a html attributw script."""
|
||||
return mark_safe(force_str(value).translate(_json_escapes_attr))
|
||||
|
||||
@@ -46,6 +46,13 @@ 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
|
||||
|
||||
@@ -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-28 15:19+0000\n"
|
||||
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
||||
"PO-Revision-Date: 2026-06-29 17:00+0000\n"
|
||||
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
|
||||
"Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix/"
|
||||
"de/>\n"
|
||||
"Language: de\n"
|
||||
|
||||
@@ -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-05-29 17:00+0000\n"
|
||||
"PO-Revision-Date: 2026-06-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.5\n"
|
||||
"X-Generator: Weblate 2026.6.1\n"
|
||||
|
||||
#: htmlcov/d_daa1541d0cbf5e2b_dashboards_py.html:670
|
||||
#: pretix/control/templates/pretixcontrol/events/index.html:166
|
||||
@@ -467,10 +467,8 @@ 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 puedes cambiar este pedido."
|
||||
msgstr "No se puede cambiar un medio por otro."
|
||||
|
||||
#: pretix/api/views/oauth.py:107 pretix/control/logdisplay.py:777
|
||||
#, python-brace-format
|
||||
@@ -4730,22 +4728,16 @@ msgid "Check-in annulled"
|
||||
msgstr "Check-in anulado"
|
||||
|
||||
#: pretix/base/models/checkin.py:372
|
||||
#, fuzzy
|
||||
#| msgid "Ticket already used"
|
||||
msgid "Ticket already exchanged"
|
||||
msgstr "Esta entrada ya fue utilizada"
|
||||
msgstr "Entrada ya canjeada"
|
||||
|
||||
#: pretix/base/models/checkin.py:373
|
||||
#, fuzzy
|
||||
#| msgid "Reusable media"
|
||||
msgid "Reusable medium invalid"
|
||||
msgstr "Medios reutilizables"
|
||||
msgstr "Soporte reutilizable no válido"
|
||||
|
||||
#: pretix/base/models/checkin.py:374
|
||||
#, fuzzy
|
||||
#| msgid "Reusable media type"
|
||||
msgid "Reusable medium already exists"
|
||||
msgstr "Tipo de medio reusable"
|
||||
msgstr "El soporte reutilizable ya existe"
|
||||
|
||||
#: pretix/base/models/customers.py:63
|
||||
msgid "Provider name"
|
||||
@@ -5330,11 +5322,8 @@ 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 agrupación de artículos debe pertenecer al mismo evento que el artículo."
|
||||
msgstr "La propiedad y el evento deben pertenecer al mismo organizador."
|
||||
|
||||
#: pretix/base/models/event.py:1928 pretix/base/models/organizer.py:627
|
||||
msgid "Link text"
|
||||
@@ -5586,42 +5575,41 @@ 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 usar medios reutilizables, usar entradas de un solo uso"
|
||||
msgstr ""
|
||||
"No utilizar soportes reutilizables, sino billetes normales 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 "Exigir que se reuse un medio ya existente"
|
||||
msgstr ""
|
||||
"Necesita que se reutilice un soporte ya existente, sustituyendo cualquier "
|
||||
"billete anterior"
|
||||
|
||||
#: 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 "Exigir que se use un medio existente o uno nuevo"
|
||||
msgstr ""
|
||||
"Se debe utilizar un soporte ya existente o uno nuevo, sustituyendo cualquier "
|
||||
"billete anterior"
|
||||
|
||||
#: 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 reuse un medio ya existente"
|
||||
msgstr ""
|
||||
"Exigir que se reutilice un soporte ya existente, añadiendo esta información "
|
||||
"a cualquier entrada anterior"
|
||||
|
||||
#: 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 "Exigir que se use un medio existente o uno nuevo"
|
||||
msgstr ""
|
||||
"Es necesario utilizar un medio ya existente o uno nuevo, que se sumará a los "
|
||||
"billetes anteriores"
|
||||
|
||||
#: pretix/base/models/items.py:480 pretix/base/models/items.py:1468
|
||||
msgid "Category"
|
||||
@@ -5981,14 +5969,6 @@ 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, "
|
||||
@@ -6052,6 +6032,9 @@ 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 ""
|
||||
@@ -6626,18 +6609,16 @@ msgstr "rebotado"
|
||||
#: pretix/base/models/media.py:77
|
||||
msgctxt "reusable_medium"
|
||||
msgid "Claim token"
|
||||
msgstr ""
|
||||
msgstr "Canjear el token"
|
||||
|
||||
#: pretix/base/models/media.py:82
|
||||
msgctxt "reusable_medium"
|
||||
msgid "Label"
|
||||
msgstr ""
|
||||
msgstr "Designación"
|
||||
|
||||
#: pretix/base/models/media.py:105
|
||||
#, fuzzy
|
||||
#| msgid "Linked ticket"
|
||||
msgid "Linked tickets"
|
||||
msgstr "Entrada vinculada"
|
||||
msgstr "Entradas vinculadas"
|
||||
|
||||
#: pretix/base/models/media.py:107
|
||||
msgid ""
|
||||
@@ -6645,6 +6626,9 @@ 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
|
||||
@@ -8390,14 +8374,10 @@ msgid "Atlantis"
|
||||
msgstr "Atlántida"
|
||||
|
||||
#: pretix/base/pdf.py:376
|
||||
#, fuzzy
|
||||
#| msgid "Invoice recipient email"
|
||||
msgid "Invoice custom recipient field"
|
||||
msgstr "Correo electrónico del destinatario de la factura"
|
||||
msgstr "Campo personalizado de destinatario en la factura"
|
||||
|
||||
#: pretix/base/pdf.py:377
|
||||
#, fuzzy
|
||||
#| msgid "Custom recipient field label"
|
||||
msgid "Custom recipient field"
|
||||
msgstr "Campo de destinatario personalizado"
|
||||
|
||||
@@ -9415,13 +9395,15 @@ 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 ""
|
||||
msgstr "El billete debe canjearse por un soporte adecuado."
|
||||
|
||||
#: 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."
|
||||
@@ -9587,64 +9569,46 @@ 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 entrada no válido."
|
||||
msgstr "Tipo de soporte 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 ""
|
||||
"El tipo de medio seleccionado no está activo en tus ajustes de organizador/a/"
|
||||
"e."
|
||||
msgstr "Este tipo de medio no está habilitado para el organizador."
|
||||
|
||||
#: pretix/base/services/media.py:107 pretix/base/services/media.py:109
|
||||
msgid "Incorrect medium type for product."
|
||||
msgstr ""
|
||||
msgstr "El tipo de soporte no es el adecuado para este producto."
|
||||
|
||||
#: 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 "Esta entrada ya ha sido canjeada."
|
||||
msgstr "La entrada ya se ha canjeado por un soporte reutilizable."
|
||||
|
||||
#: pretix/base/services/media.py:133 pretix/base/services/media.py:135
|
||||
#, fuzzy
|
||||
#| msgid "Reusable Medium ID"
|
||||
msgid "Reusable medium not found."
|
||||
msgstr "ID mediana reutilizable"
|
||||
msgstr "No se ha encontrado el soporte 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 "Se ha creado el medio reutilizable."
|
||||
msgstr "El medio reutilizable está inactivo o caducado."
|
||||
|
||||
#: 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 "Se ha creado el medio reutilizable."
|
||||
msgstr "No se ha encontrado el soporte reutilizable y no se ha podido crear."
|
||||
|
||||
#: pretix/base/services/media.py:183
|
||||
#, fuzzy
|
||||
#| msgid "Reusable media type"
|
||||
msgid "Reusable medium already exists."
|
||||
msgstr "Tipo de medio reusable"
|
||||
msgstr "Ya existe un soporte reutilizable."
|
||||
|
||||
#: pretix/base/services/media.py:189
|
||||
#, fuzzy
|
||||
#| msgid "The reusable medium has been created."
|
||||
msgid "Reusable medium could not be created."
|
||||
msgstr "Se ha creado el medio reutilizable."
|
||||
msgstr "No se ha podido crear el soporte reutilizable."
|
||||
|
||||
#: pretix/base/services/media.py:195 pretix/base/services/media.py:197
|
||||
msgid "Product does not support medium exchange."
|
||||
msgstr ""
|
||||
msgstr "Este producto no admite el cambio de medio."
|
||||
|
||||
#: pretix/base/services/memberships.py:108
|
||||
#, python-brace-format
|
||||
@@ -10368,17 +10332,10 @@ 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 "
|
||||
@@ -10390,7 +10347,7 @@ msgstr ""
|
||||
|
||||
#: pretix/base/settings.py:226
|
||||
msgid "Enforce the usage of issued reusable media for check-in"
|
||||
msgstr ""
|
||||
msgstr "Exigir el uso de los soportes reutilizables para el check-in"
|
||||
|
||||
#: pretix/base/settings.py:227
|
||||
msgid ""
|
||||
@@ -10398,6 +10355,10 @@ 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"
|
||||
@@ -18548,16 +18509,12 @@ 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 "El nuevo miembro ha sido añadido al equipo."
|
||||
msgstr "Se ha añadido un nuevo billete al medio."
|
||||
|
||||
#: pretix/control/logdisplay.py:747
|
||||
#, fuzzy
|
||||
#| msgid "{user} has been removed from the team."
|
||||
msgid "A ticket has been removed from the medium."
|
||||
msgstr "{user} ha sido removido del equipo."
|
||||
msgstr "Se ha eliminado un billete del medio."
|
||||
|
||||
#: pretix/control/logdisplay.py:748
|
||||
msgid "The medium has been connected to a new ticket."
|
||||
@@ -18569,6 +18526,8 @@ 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."
|
||||
@@ -20483,10 +20442,8 @@ msgstr ""
|
||||
"check-in:"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/checkin/simulator.html:85
|
||||
#, fuzzy
|
||||
#| msgid "Special attention required"
|
||||
msgid "Media exchange required"
|
||||
msgstr "Atención especial requerida"
|
||||
msgstr "Es necesario intercambiar medios"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/checkin/simulator.html:87
|
||||
#, python-format
|
||||
@@ -20494,6 +20451,8 @@ 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"
|
||||
@@ -27038,6 +26997,9 @@ 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 ""
|
||||
@@ -27045,6 +27007,10 @@ 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 ""
|
||||
@@ -27052,6 +27018,9 @@ 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"
|
||||
@@ -27912,30 +27881,22 @@ msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/subevents/detail.html:9
|
||||
#: pretix/control/templates/pretixcontrol/subevents/detail.html:13
|
||||
#, fuzzy, python-format
|
||||
#| msgid "Quota: %(name)s"
|
||||
#, python-format
|
||||
msgctxt "subevent"
|
||||
msgid "Date: %(name)s"
|
||||
msgstr "Cuota: %(name)s"
|
||||
msgstr "Fecha: %(name)s"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/subevents/detail.html:234
|
||||
#, fuzzy
|
||||
#| msgid "Partially paid"
|
||||
msgid "partially canceled"
|
||||
msgstr "Pagado parcialmente"
|
||||
msgstr "cancelado 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 eventos archivados."
|
||||
msgstr "No se han encontrado pedidos."
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/subevents/detail.html:302
|
||||
#: pretix/control/templates/pretixcontrol/subevents/edit.html:279
|
||||
@@ -30708,10 +30669,8 @@ 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 "Ir al evento"
|
||||
msgstr "No hay eventos"
|
||||
|
||||
#: 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-03-30 03:00+0000\n"
|
||||
"PO-Revision-Date: 2026-06-29 17: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 5.16.2\n"
|
||||
"X-Generator: Weblate 2026.6.1\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 ""
|
||||
msgstr "Editar"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue:86
|
||||
msgid "Visualize"
|
||||
msgstr ""
|
||||
msgstr "Visualizar"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue:96
|
||||
msgid ""
|
||||
@@ -454,10 +454,13 @@ 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 ""
|
||||
msgstr "Por favor, comprueba bien si esto ha sido a propósito."
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:4
|
||||
msgid "All of the conditions below (AND)"
|
||||
|
||||
@@ -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-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"
|
||||
"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"
|
||||
"Language: fr\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
@@ -466,10 +466,8 @@ 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 "Vous ne pouvez pas modifier cette commande."
|
||||
msgstr "Il n'est pas possible d'échanger un support contre un autre support."
|
||||
|
||||
#: pretix/api/views/oauth.py:107 pretix/control/logdisplay.py:777
|
||||
#, python-brace-format
|
||||
@@ -4736,22 +4734,16 @@ 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à utilisé"
|
||||
msgstr "Billet déjà échangé"
|
||||
|
||||
#: pretix/base/models/checkin.py:373
|
||||
#, fuzzy
|
||||
#| msgid "Reusable media"
|
||||
msgid "Reusable medium invalid"
|
||||
msgstr "Support réutilisable"
|
||||
msgstr "Support réutilisable non valide"
|
||||
|
||||
#: pretix/base/models/checkin.py:374
|
||||
#, fuzzy
|
||||
#| msgid "Reusable media type"
|
||||
msgid "Reusable medium already exists"
|
||||
msgstr "Type de support réutilisable"
|
||||
msgstr "Il existe déjà un support réutilisable"
|
||||
|
||||
#: pretix/base/models/customers.py:63
|
||||
msgid "Provider name"
|
||||
@@ -5343,10 +5335,8 @@ 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 "L’élément groupé doit appartenir au même événement que l’élément."
|
||||
msgstr "La propriété et l'événement doivent appartenir au même organisateur."
|
||||
|
||||
#: pretix/base/models/event.py:1928 pretix/base/models/organizer.py:627
|
||||
msgid "Link text"
|
||||
@@ -5601,8 +5591,6 @@ 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 "
|
||||
@@ -5613,32 +5601,30 @@ 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 "Nécessiter l'utilisation d'un support existant ou d'un nouveau support"
|
||||
msgstr ""
|
||||
"Exiger l'utilisation d'un support existant ou d'un nouveau support, en "
|
||||
"remplacement de tout billet antérieur"
|
||||
|
||||
#: 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"
|
||||
msgstr ""
|
||||
"Exiger la réutilisation d'un support existant, en ajoutant cette demande à "
|
||||
"tout ticket précédent"
|
||||
|
||||
#: 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 "Nécessiter l'utilisation d'un support existant ou d'un nouveau support"
|
||||
msgstr ""
|
||||
"Exiger l'utilisation d'un support existant ou d'un nouveau support, en "
|
||||
"complément des tickets précédents"
|
||||
|
||||
#: pretix/base/models/items.py:480 pretix/base/models/items.py:1468
|
||||
msgid "Category"
|
||||
@@ -6001,14 +5987,6 @@ 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, "
|
||||
@@ -6073,6 +6051,10 @@ 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 ""
|
||||
@@ -6649,18 +6631,16 @@ msgstr "Non distribué"
|
||||
#: pretix/base/models/media.py:77
|
||||
msgctxt "reusable_medium"
|
||||
msgid "Claim token"
|
||||
msgstr ""
|
||||
msgstr "Réclamer un jeton"
|
||||
|
||||
#: pretix/base/models/media.py:82
|
||||
msgctxt "reusable_medium"
|
||||
msgid "Label"
|
||||
msgstr ""
|
||||
msgstr "Descriptif"
|
||||
|
||||
#: pretix/base/models/media.py:105
|
||||
#, fuzzy
|
||||
#| msgid "Linked ticket"
|
||||
msgid "Linked tickets"
|
||||
msgstr "Billet lié"
|
||||
msgstr "Billets liés"
|
||||
|
||||
#: pretix/base/models/media.py:107
|
||||
msgid ""
|
||||
@@ -6668,6 +6648,9 @@ 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
|
||||
@@ -8437,16 +8420,12 @@ msgid "Atlantis"
|
||||
msgstr "Atlantide"
|
||||
|
||||
#: pretix/base/pdf.py:376
|
||||
#, fuzzy
|
||||
#| msgid "Invoice recipient email"
|
||||
msgid "Invoice custom recipient field"
|
||||
msgstr "E-mail du destinataire de la facture"
|
||||
msgstr "Champ personnalisé destinataire de la facture"
|
||||
|
||||
#: pretix/base/pdf.py:377
|
||||
#, fuzzy
|
||||
#| msgid "Custom recipient field label"
|
||||
msgid "Custom recipient field"
|
||||
msgstr "Libellé personnalisé du champ destinataire"
|
||||
msgstr "Champ de destinataire personnalisé"
|
||||
|
||||
#: pretix/base/pdf.py:381
|
||||
msgid "List of Add-Ons"
|
||||
@@ -9471,13 +9450,15 @@ msgstr ""
|
||||
|
||||
#: pretix/base/services/checkin.py:1121
|
||||
msgid "Ticket needs to be exchanged to a suitable medium."
|
||||
msgstr ""
|
||||
msgstr "Le billet doit être échangé contre un support adapté."
|
||||
|
||||
#: 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."
|
||||
@@ -9643,64 +9624,46 @@ 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 d’entrée non valide."
|
||||
msgstr "Type de support 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 ""
|
||||
"Le type de média sélectionné n’est pas activé dans les paramètres de votre "
|
||||
"organisateur."
|
||||
msgstr "Ce type de média n’est pas activé par l'organisateur."
|
||||
|
||||
#: pretix/base/services/media.py:107 pretix/base/services/media.py:109
|
||||
msgid "Incorrect medium type for product."
|
||||
msgstr ""
|
||||
msgstr "Type de support incorrect pour ce produit."
|
||||
|
||||
#: 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 "Ce billet a déjà été échangé."
|
||||
msgstr "Le billet a déjà été échangé contre un support réutilisable."
|
||||
|
||||
#: pretix/base/services/media.py:133 pretix/base/services/media.py:135
|
||||
#, fuzzy
|
||||
#| msgid "Reusable Medium ID"
|
||||
msgid "Reusable medium not found."
|
||||
msgstr "Identification de support réutilisable"
|
||||
msgstr "Support réutilisable introuvable."
|
||||
|
||||
#: 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 a été créé."
|
||||
msgstr "Le support réutilisable est inactif ou a expiré."
|
||||
|
||||
#: 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 "Le support réutilisable a été créé."
|
||||
msgstr "Support réutilisable introuvable et impossible à créer."
|
||||
|
||||
#: pretix/base/services/media.py:183
|
||||
#, fuzzy
|
||||
#| msgid "Reusable media type"
|
||||
msgid "Reusable medium already exists."
|
||||
msgstr "Type de support réutilisable"
|
||||
msgstr "Le type de support réutilisable existe déjà."
|
||||
|
||||
#: pretix/base/services/media.py:189
|
||||
#, fuzzy
|
||||
#| msgid "The reusable medium has been created."
|
||||
msgid "Reusable medium could not be created."
|
||||
msgstr "Le support réutilisable a été créé."
|
||||
msgstr "Impossible de créer le support réutilisable."
|
||||
|
||||
#: pretix/base/services/media.py:195 pretix/base/services/media.py:197
|
||||
msgid "Product does not support medium exchange."
|
||||
msgstr ""
|
||||
msgstr "Ce produit ne permet pas de changer de support."
|
||||
|
||||
#: pretix/base/services/memberships.py:108
|
||||
#, python-brace-format
|
||||
@@ -10426,17 +10389,10 @@ 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 "
|
||||
@@ -10450,6 +10406,8 @@ 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 ""
|
||||
@@ -10457,6 +10415,10 @@ 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"
|
||||
@@ -18691,16 +18653,12 @@ 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 "Le nouveau membre a été ajouté à l'équipe."
|
||||
msgstr "Un nouveau billet a été ajouté sur le support."
|
||||
|
||||
#: pretix/control/logdisplay.py:747
|
||||
#, fuzzy
|
||||
#| msgid "{user} has been removed from the team."
|
||||
msgid "A ticket has been removed from the medium."
|
||||
msgstr "{user} a été retiré de l'équipe."
|
||||
msgstr "Un billet a été retiré du support."
|
||||
|
||||
#: pretix/control/logdisplay.py:748
|
||||
msgid "The medium has been connected to a new ticket."
|
||||
@@ -18712,6 +18670,8 @@ 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."
|
||||
@@ -20627,10 +20587,8 @@ msgstr ""
|
||||
"l’enregistrement :"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/checkin/simulator.html:85
|
||||
#, fuzzy
|
||||
#| msgid "Special attention required"
|
||||
msgid "Media exchange required"
|
||||
msgstr "Une attention particulière est requise"
|
||||
msgstr "Échange de supports requis"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/checkin/simulator.html:87
|
||||
#, python-format
|
||||
@@ -20638,6 +20596,8 @@ 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"
|
||||
@@ -27232,6 +27192,9 @@ 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 ""
|
||||
@@ -27239,6 +27202,10 @@ 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 ""
|
||||
@@ -27246,6 +27213,9 @@ 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"
|
||||
@@ -28117,30 +28087,22 @@ msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/subevents/detail.html:9
|
||||
#: pretix/control/templates/pretixcontrol/subevents/detail.html:13
|
||||
#, fuzzy, python-format
|
||||
#| msgid "Quota: %(name)s"
|
||||
#, python-format
|
||||
msgctxt "subevent"
|
||||
msgid "Date: %(name)s"
|
||||
msgstr "Quota : %(name)s"
|
||||
msgstr "Date : %(name)s"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/subevents/detail.html:234
|
||||
#, fuzzy
|
||||
#| msgid "Partially paid"
|
||||
msgid "partially canceled"
|
||||
msgstr "Partiellement payé"
|
||||
msgstr "partiellement annulé"
|
||||
|
||||
#: 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 "Aucun événement archivé trouvé."
|
||||
msgstr "Aucune commande trouvée."
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/subevents/detail.html:302
|
||||
#: pretix/control/templates/pretixcontrol/subevents/edit.html:279
|
||||
@@ -30938,10 +30900,8 @@ 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 "Aller à l'événement"
|
||||
msgstr "Aucun é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-03-18 12:23+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-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 5.16.2\n"
|
||||
"X-Generator: Weblate 2026.6.1\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 ""
|
||||
msgstr "Éditer"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue:86
|
||||
msgid "Visualize"
|
||||
msgstr ""
|
||||
msgstr "Visualiser"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue:96
|
||||
msgid ""
|
||||
@@ -455,10 +455,14 @@ 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 ""
|
||||
msgstr "Veuillez vérifier si cela était intentionnel."
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:4
|
||||
msgid "All of the conditions below (AND)"
|
||||
|
||||
@@ -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-20 17:00+0000\n"
|
||||
"PO-Revision-Date: 2026-06-30 16:52+0000\n"
|
||||
"Last-Translator: Nikita Mitasov <me@ch4og.com>\n"
|
||||
"Language-Team: Russian <https://translate.pretix.eu/projects/pretix/pretix/"
|
||||
"ru/>\n"
|
||||
@@ -35488,10 +35488,8 @@ 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."
|
||||
|
||||
@@ -34,7 +34,6 @@
|
||||
|
||||
import json
|
||||
import logging
|
||||
import urllib.parse
|
||||
from collections import OrderedDict
|
||||
from decimal import Decimal
|
||||
|
||||
@@ -42,7 +41,6 @@ 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
|
||||
@@ -58,6 +56,7 @@ 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
|
||||
@@ -349,11 +348,7 @@ class Paypal(BasePaymentProvider):
|
||||
for link in payment.links:
|
||||
if link.method == "REDIRECT" and link.rel == "approval_url":
|
||||
if request.session.get('iframe_session', False):
|
||||
signer = signing.Signer(salt='safe-redirect')
|
||||
return (
|
||||
eventreverse_absolute(request.event, 'plugins:paypal:redirect') + '?url=' +
|
||||
urllib.parse.quote(signer.sign(link.href))
|
||||
)
|
||||
return safelink(link.href, framebreak=True)
|
||||
else:
|
||||
return str(link.href)
|
||||
else:
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
{% 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,13 +21,12 @@
|
||||
#
|
||||
from django.urls import include, re_path
|
||||
|
||||
from .views import abort, oauth_disconnect, redirect_view, success
|
||||
from .views import abort, oauth_disconnect, 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,13 +39,10 @@ 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, HttpResponseBadRequest
|
||||
from django.shortcuts import render
|
||||
from django.http import HttpResponse
|
||||
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
|
||||
@@ -61,21 +58,6 @@ 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')
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
{% 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,15 +22,13 @@
|
||||
from django.urls import include, re_path
|
||||
|
||||
from .views import (
|
||||
PayView, XHRView, abort, isu_disconnect, isu_return, redirect_view,
|
||||
success, webhook,
|
||||
PayView, XHRView, abort, isu_disconnect, isu_return, 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,13 +36,10 @@ 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, HttpResponseBadRequest, JsonResponse,
|
||||
)
|
||||
from django.http import Http404, HttpResponse, JsonResponse
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
from django.utils.decorators import method_decorator
|
||||
@@ -104,21 +101,6 @@ 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,7 +46,6 @@ 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
|
||||
@@ -72,6 +71,7 @@ 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,15 +745,7 @@ class StripeMethod(BasePaymentProvider):
|
||||
|
||||
def redirect(self, request, url):
|
||||
if request.session.get('iframe_session', False):
|
||||
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')
|
||||
)
|
||||
return safelink(url, framebreak=True)
|
||||
else:
|
||||
return str(url)
|
||||
|
||||
@@ -1053,11 +1045,7 @@ 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 eventreverse_absolute(self.event, 'plugins:stripe:redirect') + '?data=' + signing.dumps({
|
||||
'url': url,
|
||||
'session': {},
|
||||
}, salt='safe-redirect')
|
||||
|
||||
return safelink(url, framebreak=True)
|
||||
return url
|
||||
|
||||
def _confirm_payment_intent(self, request, payment):
|
||||
|
||||
@@ -323,7 +323,7 @@ $(function () {
|
||||
}
|
||||
}
|
||||
} else if ($("#stripe_payment_intent_next_action_redirect_url").length) {
|
||||
let payment_intent_next_action_redirect_url = JSON.parse($("#stripe_payment_intent_next_action_redirect_url").html());
|
||||
let payment_intent_next_action_redirect_url = $.trim($("#stripe_payment_intent_next_action_redirect_url").html());
|
||||
pretixstripe.handlePaymentRedirectAction(payment_intent_next_action_redirect_url);
|
||||
} else if ($.trim($("#stripe_payment_intent_action_type").html()) === "promptpay_display_qr_code") {
|
||||
waitingDialog.hide();
|
||||
@@ -432,4 +432,4 @@ $(function () {
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,33 +0,0 @@
|
||||
{% 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>
|
||||
@@ -9,7 +9,7 @@
|
||||
<script type="text/plain" id="stripe_payment_intent_action_type">{{ payment_intent_action_type }}</script>
|
||||
<script type="text/plain" id="stripe_payment_intent_client_secret">{{ payment_intent_client_secret }}</script>
|
||||
{% if payment_intent_next_action_redirect_url %}
|
||||
{{ payment_intent_next_action_redirect_url|json_script:"stripe_payment_intent_next_action_redirect_url" }}
|
||||
<script type="text/plain" id="stripe_payment_intent_next_action_redirect_url">{{ payment_intent_next_action_redirect_url|safe }}</script>
|
||||
{% endif %}
|
||||
{% if payment_intent_redirect_action_handling %}
|
||||
<script type="text/plain" id="stripe_payment_intent_redirect_action_handling">{{ payment_intent_redirect_action_handling }}</script>
|
||||
|
||||
@@ -25,13 +25,12 @@ from pretix.multidomain import event_url
|
||||
|
||||
from .views import (
|
||||
OrganizerSettingsFormView, ReturnView, ScaReturnView, ScaView,
|
||||
oauth_disconnect, oauth_return, redirect_view, webhook,
|
||||
oauth_disconnect, oauth_return, 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,13 +34,11 @@
|
||||
|
||||
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, HttpResponseBadRequest
|
||||
from django.http import Http404, HttpResponse
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
from django.utils.decorators import method_decorator
|
||||
@@ -64,7 +62,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, eventreverse_absolute
|
||||
from pretix.multidomain.urlreverse import eventreverse
|
||||
from pretix.plugins.stripe.forms import OrganizerStripeSettingsForm
|
||||
from pretix.plugins.stripe.models import ReferencedStripeObject
|
||||
from pretix.plugins.stripe.tasks import (
|
||||
@@ -74,28 +72,6 @@ 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
|
||||
@@ -514,11 +490,6 @@ 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
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
{% load i18n %}
|
||||
{% load eventurl %}
|
||||
{% load urlreplace %}
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
{% if cart_namespace %}
|
||||
@@ -23,9 +24,8 @@
|
||||
class="btn btn-primary btn-lg" target="_blank">
|
||||
{% trans "Continue in new tab" %}
|
||||
</a>
|
||||
<script>
|
||||
window.open('{{ url|escapejs }}');
|
||||
</script>
|
||||
{{ url|json_script:"framebreak-url" }}
|
||||
<script type="text/javascript" src="{% static "pretixbase/js/framebreak.js" %}"></script>
|
||||
</div>
|
||||
{% else %}
|
||||
<h1>{% trans "Cookies not supported" %}</h1>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
{% if code_info.link %}<a aria-label="{{ code_info.link_aria_label }}" href="{{ code_info.link }}">{% endif %}
|
||||
<div class="{{ code_info.css_class }}" role="figure" aria-labelledby="banktransfer_qrcodes_{{ code_info.id }}_tab banktransfer_qrcodes_label">
|
||||
{{ code_info.html_prefix }}
|
||||
<script type="application/json" data-size="150" data-replace-with-qr data-desc="{% trans 'Scan this image with your banking app’s QR-Reader to start the payment process.' %}">{{ code_info.qr_data|escapejson_dumps }}</script>
|
||||
<script type="text/plain" data-size="150" data-replace-with-qr data-desc="{% trans 'Scan this image with your banking app’s QR-Reader to start the payment process.' %}">{{ code_info.qr_data }}</script>
|
||||
</div>
|
||||
{% if code_info.link %}</a>{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -21,22 +21,21 @@
|
||||
data-date="{{ day.date|date_fast:"SHORT_DATE_FORMAT" }}">
|
||||
<p>
|
||||
{% if day.events %}
|
||||
<a href="#selected-day" class="day-label event hidden-sm hidden-md hidden-lg">
|
||||
<a href="#selected-day" class="day-label event hidden-sm hidden-md hidden-lg" aria-describedby="nr-of-events-{{ day.date|date_fast:"Y-m-d" }}">
|
||||
<b aria-hidden="true">{{ day.day }}</b>
|
||||
<time datetime="{{ day.date|date_fast:"Y-m-d" }}" class="sr-only">
|
||||
{{ day.date|date_fast:"SHORT_DATE_FORMAT" }}
|
||||
</time>
|
||||
<span class="sr-only">
|
||||
({% blocktrans trimmed count count=day.events|length %}
|
||||
{{ count }} event
|
||||
{% plural %}
|
||||
{{ count }} events
|
||||
{% endblocktrans %})
|
||||
</span>
|
||||
<span class="sr-only" id="nr-of-events-{{ day.date|date_fast:"Y-m-d" }}">{% blocktrans trimmed count count=day.events|length %}
|
||||
{{ count }} event
|
||||
{% plural %}
|
||||
{{ count }} events
|
||||
{% endblocktrans %}</span>
|
||||
</a>
|
||||
<time datetime="{{ day.date|date_fast:"Y-m-d" }}" class="hidden-xs">{{ day.day }}</time>
|
||||
{% else %}
|
||||
<time datetime="{{ day.date|date_fast:"Y-m-d" }}" class="day-label">{{ day.day }}</time>
|
||||
<span class="sr-only">{% trans "No events" %}</span>
|
||||
{% endif %}
|
||||
</p>
|
||||
<ul class="events">
|
||||
|
||||
@@ -536,7 +536,6 @@ class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView):
|
||||
**pass_through_url_params,
|
||||
})
|
||||
})
|
||||
r._csp_ignore = True
|
||||
return r
|
||||
|
||||
if not request.event.all_sales_channels and request.sales_channel.identifier not in (s.identifier for s in request.event.limit_sales_channels.all()):
|
||||
|
||||
@@ -650,6 +650,10 @@ def add_subevents_for_days(qs, before, after, ebd, timezones, sales_channel, eve
|
||||
if hide:
|
||||
continue
|
||||
|
||||
if s.event_calendar_future_only:
|
||||
if (se.date_to or se.date_from) < time_machine_now():
|
||||
continue
|
||||
|
||||
timezones.add(s.timezone)
|
||||
tz = ZoneInfo(s.timezone)
|
||||
datetime_from = se.date_from.astimezone(tz)
|
||||
|
||||
@@ -125,7 +125,6 @@ class WaitingView(EventViewMixin, FormView):
|
||||
request.event, "presale:event.waitinglist", kwargs={'cart_namespace': kwargs.get('cart_namespace')}
|
||||
) + '?' + url_replace(request, 'require_cookie', '', 'iframe', '', 'locale', request.GET.get('locale', get_language_without_region()))
|
||||
})
|
||||
r._csp_ignore = True
|
||||
return r
|
||||
|
||||
if not self.itemvars:
|
||||
|
||||
@@ -257,6 +257,9 @@ MAIL_FROM_NOTIFICATIONS = config.get('mail', 'from_notifications', fallback=MAIL
|
||||
MAIL_FROM_ORGANIZERS = config.get('mail', 'from_organizers', fallback=MAIL_FROM)
|
||||
MAIL_CUSTOM_SENDER_VERIFICATION_REQUIRED = config.getboolean('mail', 'custom_sender_verification_required', fallback=True)
|
||||
MAIL_CUSTOM_SENDER_SPF_STRING = config.get('mail', 'custom_sender_spf_string', fallback='')
|
||||
MAIL_CUSTOM_SENDER_DKIM_SELECTOR = config.get('mail', 'custom_sender_dkim_selector', fallback='')
|
||||
MAIL_CUSTOM_SENDER_DKIM_CNAME = config.get('mail', 'custom_sender_dkim_cname', fallback='')
|
||||
MAIL_CUSTOM_SENDER_DMARC_REQUIRED = config.getboolean('mail', 'custom_sender_dmarc_required', fallback=False)
|
||||
MAIL_CUSTOM_SMTP_ALLOW_PRIVATE_NETWORKS = config.getboolean('mail', 'custom_smtp_allow_private_networks', fallback=DEBUG)
|
||||
EMAIL_HOST = config.get('mail', 'host', fallback='localhost')
|
||||
EMAIL_PORT = config.getint('mail', 'port', fallback=25)
|
||||
@@ -440,6 +443,7 @@ CSRF_COOKIE_NAME = 'pretix_csrftoken'
|
||||
SESSION_COOKIE_HTTPONLY = True
|
||||
|
||||
INSTALLED_APPS += [ # noqa
|
||||
'django_querytagger',
|
||||
'django_filters',
|
||||
'django_markup',
|
||||
'django_otp',
|
||||
@@ -505,6 +509,7 @@ MIDDLEWARE = [
|
||||
'pretix.helpers.logs.RequestIdMiddleware',
|
||||
'pretix.api.middleware.IdempotencyMiddleware',
|
||||
'pretix.multidomain.middlewares.MultiDomainMiddleware',
|
||||
'django_querytagger.middleware.SetTagMiddleware', # after MultiDomainMiddleware for correct url resolving
|
||||
'pretix.base.middleware.CustomCommonMiddleware',
|
||||
'pretix.multidomain.middlewares.SessionMiddleware',
|
||||
'pretix.multidomain.middlewares.CsrfViewMiddleware',
|
||||
@@ -624,6 +629,9 @@ LOGGING = {
|
||||
'request_id': {
|
||||
'()': 'pretix.helpers.logs.RequestIdFilter'
|
||||
},
|
||||
'skip_not_found': {
|
||||
'()': 'pretix.helpers.logs.SkipNotFoundFilter',
|
||||
}
|
||||
},
|
||||
'handlers': {
|
||||
'console': {
|
||||
@@ -665,6 +673,7 @@ LOGGING = {
|
||||
'handlers': ['file', 'console', 'mail_admins'],
|
||||
'level': loglevel,
|
||||
'propagate': True,
|
||||
'filters': ['skip_not_found'],
|
||||
},
|
||||
'pretix.security.csp': {
|
||||
'handlers': ['csp_file'],
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
// Attempt to auto-open page in new tab. Will be ignored by most browser's popup blockers anyways, though.
|
||||
var url = JSON.parse(document.getElementById('framebreak-url').innerText)
|
||||
window.open(url)
|
||||
@@ -667,10 +667,9 @@ var form_handlers = function (el) {
|
||||
el.find("script[data-replace-with-qr]").each(function () {
|
||||
var $div = $("<div>");
|
||||
$div.insertBefore($(this));
|
||||
var text = (this.getAttribute("type") || "").indexOf("json") !== -1 ? JSON.parse($(this).html()) : $(this).html();
|
||||
$div.qrcode(
|
||||
{
|
||||
text: text,
|
||||
text: $(this).html(),
|
||||
correctLevel: 0, // M
|
||||
width: $(this).attr("data-size") ? parseInt($(this).attr("data-size")) : 256,
|
||||
height: $(this).attr("data-size") ? parseInt($(this).attr("data-size")) : 256,
|
||||
@@ -872,6 +871,14 @@ function setup_basics(el) {
|
||||
});
|
||||
});
|
||||
|
||||
el.find(".qrcode-canvas").each(function () {
|
||||
$(this).qrcode(
|
||||
{
|
||||
text: $.trim($($(this).attr("data-qrdata")).html())
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
el.find(".propagated-settings-box").find("input, textarea, select").not("[readonly]")
|
||||
.attr("data-propagated-locked", "true").prop("readonly", true);
|
||||
|
||||
|
||||
@@ -132,10 +132,9 @@ var form_handlers = function (el) {
|
||||
el.find("script[data-replace-with-qr]").each(function () {
|
||||
var $div = $("<div>");
|
||||
$div.insertBefore($(this));
|
||||
var text = (this.getAttribute("type") || "").indexOf("json") !== -1 ? JSON.parse($(this).html()) : $(this).html();
|
||||
$div.qrcode(
|
||||
{
|
||||
text: text,
|
||||
text: $(this).html(),
|
||||
correctLevel: 0, // M
|
||||
width: $(this).attr("data-size") ? parseInt($(this).attr("data-size")) : 256,
|
||||
height: $(this).attr("data-size") ? parseInt($(this).attr("data-size")) : 256,
|
||||
@@ -346,7 +345,7 @@ function setup_basics(el) {
|
||||
}).on('click', function (event) {
|
||||
setCurrentTab(this);
|
||||
});
|
||||
|
||||
|
||||
var firstTab = tabs.first().get(0);
|
||||
var lastTab = tabs.last().get(0);
|
||||
setCurrentTab(tabs.filter('[aria-selected=true]').get(0));
|
||||
@@ -659,7 +658,7 @@ $(function () {
|
||||
var currentTimeDisplayParts = [];
|
||||
timeFormatParts.forEach(function(format) {
|
||||
currentTimeDisplayParts.push([format, $("<span></span>").appendTo(currentTimeDisplay)])
|
||||
});
|
||||
});
|
||||
var duration = this.getAttribute("data-duration").split(":").reduce(function(previousValue, currentValue, currentIndex) {
|
||||
return previousValue + (currentIndex ? parseInt(currentValue, 10) * 60 : parseInt(currentValue, 10) * 60 * 60);
|
||||
}, 0);
|
||||
@@ -672,7 +671,7 @@ $(function () {
|
||||
currentTimeBar.remove();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
var offset = thisCalendar.querySelector("h3").getBoundingClientRect().width;
|
||||
var dx = Math.round(offset + (thisCalendar.scrollWidth-offset)*(currentTimeDelta/duration));
|
||||
currentTimeDisplayParts.forEach(function(part) {
|
||||
|
||||
@@ -126,3 +126,81 @@ class LocaleDeterminationTest(TestCase):
|
||||
response = c.get('/dummy/dummy/')
|
||||
language = response['Content-Language']
|
||||
self.assertEqual(language, 'en')
|
||||
|
||||
|
||||
def test_render_csp():
|
||||
from pretix.base.middleware import _render_csp
|
||||
|
||||
assert _render_csp({}) == ""
|
||||
assert _render_csp({'default-src': ["'self'"]}) == "default-src 'self'"
|
||||
|
||||
h = {
|
||||
'default-src': ["'self'"],
|
||||
'script-src': ["'self'"],
|
||||
'object-src': ["'none'"],
|
||||
'frame-src': ["'self'"],
|
||||
'style-src': ["'self'", "'self'"],
|
||||
'connect-src': ["'self'", "'self'"],
|
||||
'img-src': ["'self'", "'self'", "data:"],
|
||||
'font-src': ["'self'"],
|
||||
'media-src': ["'self'", "data:"],
|
||||
'form-action': ["'self'", "https:"],
|
||||
}
|
||||
assert _render_csp(h) == (
|
||||
"default-src 'self'; script-src 'self'; object-src 'none'; frame-src 'self'; style-src 'self' 'self'; "
|
||||
"connect-src 'self' 'self'; img-src 'self' 'self' data:; font-src 'self'; media-src 'self' data:; form-action 'self' https:"
|
||||
)
|
||||
|
||||
|
||||
def test_merge_csp():
|
||||
from pretix.base.middleware import _merge_csp, _parse_csp, _render_csp
|
||||
|
||||
h = {
|
||||
'default-src': ["'self'"],
|
||||
'script-src': ["'self'"],
|
||||
'style-src': ["'self'", "'self'"],
|
||||
'form-action': ["'self'", "https:"],
|
||||
'connect-src': ["'self'", "'self'"],
|
||||
}
|
||||
assert _render_csp(h) == (
|
||||
"default-src 'self'; script-src 'self'; style-src 'self' 'self'; form-action 'self' https:; connect-src 'self' 'self'"
|
||||
)
|
||||
|
||||
_merge_csp(h, _parse_csp("style-src 'unsafe-inline'; connect-src https://example.com"))
|
||||
assert _render_csp(h) == (
|
||||
"default-src 'self'; script-src 'self'; style-src 'self' 'self' 'unsafe-inline'; form-action 'self' "
|
||||
"https:; connect-src 'self' 'self' https://example.com"
|
||||
)
|
||||
|
||||
|
||||
def test_roundtrip_csp():
|
||||
from pretix.base.middleware import _merge_csp, _parse_csp, _render_csp
|
||||
|
||||
prod_csp = ("default-src 'self' https://pretix.eu https://static.pretix.cloud; script-src 'self' "
|
||||
"'sha256-+tmFggeXIPOAC2UgcQ3LW/gPHTkwyWg3/D6FOJ5BHGo=' 'unsafe-eval' https://matomo.rami.io "
|
||||
"https://pretix.eu https://static.pretix.cloud https://support.rami.io; object-src 'none'; "
|
||||
"frame-src 'self' https://matomo.rami.io https://pretix.eu https://static.pretix.cloud "
|
||||
"https://support.rami.io https://www.youtube-nocookie.com; style-src 'self' 'unsafe-inline' "
|
||||
"data: https://cdn.pretix.cloud https://pretix.eu https://static.pretix…rt.rami.io; connect-src "
|
||||
"'self' https://cdn.pretix.cloud https://matomo.rami.io https://pretix.eu https://static.pretix.cloud "
|
||||
"https://support.rami.io ws://support.rami.io; img-src 'self' data: https://cdn.pretix.cloud "
|
||||
"https://matomo.rami.io https://pretix.eu https://static.pretix.cloud https://support.rami.io; "
|
||||
"font-src 'self' https://pretix.eu https://static.pretix.cloud; media-src 'self' data: "
|
||||
"https://cdn.pretix.cloud https://pretix.eu https://static.pretix.cloud; form-action 'self' "
|
||||
"https: https://pretix.eu")
|
||||
h = _parse_csp(prod_csp)
|
||||
_merge_csp(h, _parse_csp(prod_csp))
|
||||
assert _render_csp(h) == prod_csp
|
||||
|
||||
|
||||
def test_sanitize_csp():
|
||||
from pretix.base.middleware import _render_csp
|
||||
|
||||
h = {
|
||||
'style-src': ["'self'", "https://example.com", "https://example.org https://attack.example.net", "https://\fexample.org",
|
||||
"https://\texample.org", "https://example.org;script-src https://example.org", ],
|
||||
'script-src': ["'self'"],
|
||||
}
|
||||
assert _render_csp(h) == (
|
||||
"style-src 'self' https://example.com; script-src 'self'"
|
||||
)
|
||||
|
||||
@@ -119,7 +119,7 @@ def test_linkify_abs(link):
|
||||
assert markdown_compile_email(input) == f"<p>{output}</p>"
|
||||
|
||||
|
||||
signer = signing.Signer(salt='safe-redirect')
|
||||
signer = signing.Signer(salt='safelink-url')
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
||||
@@ -159,6 +159,22 @@ class OrganizerTest(SoupTest):
|
||||
self.orga1.settings.flush()
|
||||
assert "mail_from" not in self.orga1.settings._cache()
|
||||
|
||||
@staticmethod
|
||||
def _fake_dmarc_record(hostname):
|
||||
return {
|
||||
'test.pretix.dev': 'v=DMARC1; p=quarantine; sp=none; adkim=r; aspf=r;',
|
||||
'bad.pretix.dev': 'BLA',
|
||||
'none.pretix.dev': None,
|
||||
}[hostname]
|
||||
|
||||
@staticmethod
|
||||
def _fake_cname_record(hostname):
|
||||
return {
|
||||
'pretix._domainkey.test.pretix.dev': 'test-pretix-dev.dkim.pretix.eu.',
|
||||
'pretix._domainkey.bad.pretix.dev': 'example.org',
|
||||
'pretix._domainkey.none.pretix.dev': None,
|
||||
}[hostname]
|
||||
|
||||
@staticmethod
|
||||
def _fake_spf_record(hostname):
|
||||
return {
|
||||
@@ -172,9 +188,17 @@ class OrganizerTest(SoupTest):
|
||||
'spftest.pretix.dev': None,
|
||||
}[hostname]
|
||||
|
||||
@override_settings(MAIL_CUSTOM_SENDER_VERIFICATION_REQUIRED=False, MAIL_CUSTOM_SENDER_SPF_STRING="include:spftest.pretix.dev include:test2.pretix.dev")
|
||||
def test_email_setup_no_verification_spf_success(self):
|
||||
@override_settings(
|
||||
MAIL_CUSTOM_SENDER_VERIFICATION_REQUIRED=False,
|
||||
MAIL_CUSTOM_SENDER_SPF_STRING="include:spftest.pretix.dev include:test2.pretix.dev",
|
||||
MAIL_CUSTOM_SENDER_DKIM_SELECTOR="pretix",
|
||||
MAIL_CUSTOM_SENDER_DKIM_CNAME="dkim.pretix.eu.",
|
||||
MAIL_CUSTOM_SENDER_DMARC_REQUIRED=True,
|
||||
)
|
||||
def test_email_setup_no_verification_spf_dmarc_dkim_success(self):
|
||||
self.monkeypatch.setattr("pretix.control.views.mailsetup.get_spf_record", OrganizerTest._fake_spf_record)
|
||||
self.monkeypatch.setattr("pretix.control.views.mailsetup.get_cname_record", OrganizerTest._fake_cname_record)
|
||||
self.monkeypatch.setattr("pretix.control.views.mailsetup.get_dmarc_record", OrganizerTest._fake_dmarc_record)
|
||||
doc = self.post_doc(
|
||||
'/control/organizer/%s/settings/email/setup' % self.orga1.slug,
|
||||
{
|
||||
@@ -213,6 +237,43 @@ class OrganizerTest(SoupTest):
|
||||
# not yet saved
|
||||
assert "mail_from" not in self.orga1.settings._cache()
|
||||
|
||||
@override_settings(MAIL_CUSTOM_SENDER_VERIFICATION_REQUIRED=False,
|
||||
MAIL_CUSTOM_SENDER_DKIM_SELECTOR="pretix",
|
||||
MAIL_CUSTOM_SENDER_DKIM_CNAME="dkim.pretix.eu.",
|
||||
MAIL_CUSTOM_SENDER_SPF_STRING="")
|
||||
def test_email_setup_no_verification_dkim_warning(self):
|
||||
self.monkeypatch.setattr("pretix.control.views.mailsetup.get_cname_record", OrganizerTest._fake_cname_record)
|
||||
doc = self.post_doc(
|
||||
'/control/organizer/%s/settings/email/setup' % self.orga1.slug,
|
||||
{
|
||||
'mode': 'simple',
|
||||
'simple-mail_from': 'test@bad.pretix.dev',
|
||||
},
|
||||
follow=True
|
||||
)
|
||||
assert doc.select('.alert-danger')
|
||||
self.orga1.settings.flush()
|
||||
# not yet saved
|
||||
assert "mail_from" not in self.orga1.settings._cache()
|
||||
|
||||
@override_settings(MAIL_CUSTOM_SENDER_VERIFICATION_REQUIRED=False,
|
||||
MAIL_CUSTOM_SENDER_DMARC_REQUIRED=True,
|
||||
MAIL_CUSTOM_SENDER_SPF_STRING="")
|
||||
def test_email_setup_no_verification_dmarc_warning(self):
|
||||
self.monkeypatch.setattr("pretix.control.views.mailsetup.get_dmarc_record", OrganizerTest._fake_dmarc_record)
|
||||
doc = self.post_doc(
|
||||
'/control/organizer/%s/settings/email/setup' % self.orga1.slug,
|
||||
{
|
||||
'mode': 'simple',
|
||||
'simple-mail_from': 'test@bad.pretix.dev',
|
||||
},
|
||||
follow=True
|
||||
)
|
||||
assert doc.select('.alert-danger')
|
||||
self.orga1.settings.flush()
|
||||
# not yet saved
|
||||
assert "mail_from" not in self.orga1.settings._cache()
|
||||
|
||||
def test_email_setup_smtp(self):
|
||||
self.monkeypatch.setattr("pretix.base.email.test_custom_smtp_backend", lambda b, a: None)
|
||||
self.monkeypatch.setattr("socket.gethostbyname", lambda h: "8.8.8.8")
|
||||
|
||||
Reference in New Issue
Block a user