mirror of
https://github.com/pretix/pretix.git
synced 2026-10-01 19:14:43 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3a07f5b76 |
+1
-1
@@ -44,7 +44,7 @@ dependencies = [
|
||||
"django-filter==25.1",
|
||||
"django-formset-js-improved==0.5.0.5",
|
||||
"django-formtools==2.6.1",
|
||||
"django-hierarkey==2.0.*,>=2.0.2",
|
||||
"django-hierarkey==2.0.*,>=2.0.1",
|
||||
"django-hijack==3.7.*",
|
||||
"django-i18nfield==1.11.*",
|
||||
"django-libsass==0.9",
|
||||
|
||||
@@ -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.2"
|
||||
__version__ = "2026.6.0.dev0"
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
def get_session_key_for_api_auth(user, auth):
|
||||
if user.is_authenticated:
|
||||
return f'api-upload-User-{user.pk}'
|
||||
else:
|
||||
return f'api-upload-{str(type(auth))}-{auth.pk}'
|
||||
|
||||
|
||||
def get_session_key_for_api_request(request):
|
||||
return get_session_key_for_api_auth(request.user, request.auth)
|
||||
@@ -101,10 +101,6 @@ class OAuthAccessToken(AbstractAccessToken):
|
||||
self.expires = now() - timedelta(hours=1)
|
||||
self.save(update_fields=['expires'])
|
||||
|
||||
def is_valid(self, scopes=None):
|
||||
# Can maybe be removed after upgrading django-oauth-toolkit to 3.4.1
|
||||
return super().is_valid(scopes) and self.application.is_usable(None)
|
||||
|
||||
|
||||
class OAuthRefreshToken(AbstractRefreshToken):
|
||||
application = models.ForeignKey(
|
||||
|
||||
@@ -37,8 +37,6 @@ from collections import OrderedDict
|
||||
from django.core.exceptions import ValidationError
|
||||
from rest_framework import serializers
|
||||
|
||||
from pretix.api.auth.utils import get_session_key_for_api_request
|
||||
|
||||
|
||||
def remove_duplicates_from_list(data):
|
||||
return list(OrderedDict.fromkeys(data))
|
||||
@@ -85,16 +83,10 @@ class UploadedFileField(serializers.Field):
|
||||
request = self.context.get('request', None)
|
||||
try:
|
||||
cf = CachedFile.objects.get(
|
||||
session_key=f'api-upload-{str(type(request.user or request.auth))}-{(request.user or request.auth).pk}',
|
||||
file__isnull=False,
|
||||
pk=data[len("file:"):],
|
||||
)
|
||||
if cf.session_key == "api-upload-<class 'django.contrib.auth.models.AnonymousUser'>-None":
|
||||
# OK, backwards-compatibility of a security bug fixed 2026-09, delete this at some point, but should
|
||||
# also be harmless because all files with this key are expired one day after deployment of this fix
|
||||
# and no new files with this key are created
|
||||
pass
|
||||
elif cf.session_key != get_session_key_for_api_request(request):
|
||||
self.fail('not_found')
|
||||
except (ValidationError, IndexError): # invalid uuid
|
||||
self.fail('not_found')
|
||||
except CachedFile.DoesNotExist:
|
||||
|
||||
@@ -41,7 +41,6 @@ from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.relations import SlugRelatedField
|
||||
from rest_framework.reverse import reverse
|
||||
|
||||
from pretix.api.auth.utils import get_session_key_for_api_request
|
||||
from pretix.api.serializers import CompatibleJSONField
|
||||
from pretix.api.serializers.event import SubEventSerializer
|
||||
from pretix.api.serializers.forms import form_field_to_serializer_field
|
||||
@@ -258,21 +257,16 @@ class AnswerSerializer(I18nAwareModelSerializer):
|
||||
if data['answer'] == 'file:keep':
|
||||
return data
|
||||
try:
|
||||
ao = self.context["request"].user or self.context["request"].auth
|
||||
cf = CachedFile.objects.get(
|
||||
session_key=f'api-upload-{str(type(ao))}-{ao.pk}',
|
||||
file__isnull=False,
|
||||
pk=data['answer'][len("file:"):],
|
||||
)
|
||||
if cf.session_key == "api-upload-<class 'django.contrib.auth.models.AnonymousUser'>-None":
|
||||
# OK, backwards-compatibility of a security bug fixed 2026-09, delete this at some point, but should
|
||||
# also be harmless because all files with this key are expired one day after deployment of this fix
|
||||
# and no new files with this key are created
|
||||
pass
|
||||
elif cf.session_key != get_session_key_for_api_request(self.context["request"]):
|
||||
raise ValidationError('The submitted file ID "{fid}" was not found.'.format(fid=data['answer']))
|
||||
except (ValidationError, IndexError): # invalid uuid
|
||||
raise ValidationError('The submitted file ID "{fid}" was not found.'.format(fid=data['answer']))
|
||||
raise ValidationError('The submitted file ID "{fid}" was not found.'.format(fid=data))
|
||||
except CachedFile.DoesNotExist:
|
||||
raise ValidationError('The submitted file ID "{fid}" was not found.'.format(fid=data['answer']))
|
||||
raise ValidationError('The submitted file ID "{fid}" was not found.'.format(fid=data))
|
||||
|
||||
allowed_types = (
|
||||
'image/png', 'image/jpeg', 'image/gif', 'application/pdf'
|
||||
|
||||
@@ -50,7 +50,6 @@ from rest_framework.generics import ListAPIView
|
||||
from rest_framework.permissions import SAFE_METHODS
|
||||
from rest_framework.response import Response
|
||||
|
||||
from pretix.api.auth.utils import get_session_key_for_api_auth
|
||||
from pretix.api.serializers.checkin import (
|
||||
CheckinListSerializer, CheckinRPCAnnulInputSerializer,
|
||||
CheckinRPCRedeemInputSerializer, MiniCheckinListSerializer,
|
||||
@@ -330,16 +329,10 @@ with scopes_disabled():
|
||||
def _handle_file_upload(data, user, auth):
|
||||
try:
|
||||
cf = CachedFile.objects.get(
|
||||
session_key=f'api-upload-{str(type(user or auth))}-{(user or auth).pk}',
|
||||
file__isnull=False,
|
||||
pk=data[len("file:"):],
|
||||
)
|
||||
if cf.session_key == "api-upload-<class 'django.contrib.auth.models.AnonymousUser'>-None":
|
||||
# OK, backwards-compatibility of a security bug fixed 2026-09, delete this at some point, but should
|
||||
# also be harmless because all files with this key are expired one day after deployment of this fix
|
||||
# and no new files with this key are created
|
||||
pass
|
||||
elif cf.session_key != get_session_key_for_api_auth(user, auth):
|
||||
raise ValidationError('The submitted file ID "{fid}" was not found.'.format(fid=data))
|
||||
except (ValidationError, BaseValidationError, IndexError): # invalid uuid
|
||||
raise ValidationError('The submitted file ID "{fid}" was not found.'.format(fid=data))
|
||||
except CachedFile.DoesNotExist:
|
||||
|
||||
@@ -33,7 +33,6 @@ from rest_framework.views import APIView
|
||||
from pretix.api.auth.device import DeviceTokenAuthentication
|
||||
from pretix.api.auth.permission import AnyAuthenticatedClientPermission
|
||||
from pretix.api.auth.token import TeamTokenAuthentication
|
||||
from pretix.api.auth.utils import get_session_key_for_api_request
|
||||
from pretix.base.models import CachedFile
|
||||
from pretix.helpers.images import (
|
||||
IMAGE_TYPES, validate_uploaded_file_for_valid_image,
|
||||
@@ -79,7 +78,7 @@ class UploadView(APIView):
|
||||
web_download=False,
|
||||
filename=file_obj.name,
|
||||
type=content_type,
|
||||
session_key=get_session_key_for_api_request(request)
|
||||
session_key=f'api-upload-{str(type(request.user or request.auth))}-{(request.user or request.auth).pk}'
|
||||
)
|
||||
cf.file.save(file_obj.name, file_obj)
|
||||
cf.save()
|
||||
|
||||
@@ -53,6 +53,7 @@ from django.db.models import QuerySet
|
||||
from django.forms import Select, widgets
|
||||
from django.forms.widgets import FILE_INPUT_CONTRADICTION
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.functional import lazy
|
||||
from django.utils.html import escape
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.text import format_lazy
|
||||
@@ -324,16 +325,21 @@ class WrappedPhonePrefixSelect(Select):
|
||||
initial = None
|
||||
|
||||
def __init__(self, initial=None):
|
||||
choices = [("", "---------")]
|
||||
def _get_choices():
|
||||
choices = [("", "---------")]
|
||||
|
||||
if initial:
|
||||
for prefix, values in COUNTRY_CODE_TO_REGION_CODE.items():
|
||||
if all(v == REGION_CODE_FOR_NON_GEO_ENTITY for v in values):
|
||||
continue
|
||||
if initial in values:
|
||||
self.initial = "+%d" % prefix
|
||||
break
|
||||
choices += get_phone_prefixes_sorted_and_localized()
|
||||
return choices
|
||||
|
||||
choices = lazy(_get_choices, list)()
|
||||
|
||||
if initial:
|
||||
for prefix, values in COUNTRY_CODE_TO_REGION_CODE.items():
|
||||
if all(v == REGION_CODE_FOR_NON_GEO_ENTITY for v in values):
|
||||
continue
|
||||
if initial in values:
|
||||
self.initial = "+%d" % prefix
|
||||
break
|
||||
choices += get_phone_prefixes_sorted_and_localized()
|
||||
super().__init__(choices=choices, attrs={
|
||||
'aria-label': pgettext_lazy('phonenumber', 'International area code'),
|
||||
'autocomplete': 'tel-country-code',
|
||||
@@ -587,16 +593,12 @@ class PortraitImageField(SizeValidationMixin, ExtValidationMixin, forms.FileFiel
|
||||
image = ImageOps.exif_transpose(image)
|
||||
|
||||
if f._cropdata:
|
||||
left = int(f._cropdata.get('x', 0))
|
||||
top = int(f._cropdata.get('y', 0))
|
||||
right = left + int(f._cropdata.get('width', image.width))
|
||||
bottom = top + int(f._cropdata.get('height', image.height))
|
||||
if left >= image.width or top >= image.height or right > image.width or bottom > image.height:
|
||||
raise ValidationError(
|
||||
self.error_messages['max_dimension'],
|
||||
code='max_dimension',
|
||||
)
|
||||
image = image.crop((left, top, right, bottom))
|
||||
image = image.crop((
|
||||
f._cropdata.get('x', 0),
|
||||
f._cropdata.get('y', 0),
|
||||
f._cropdata.get('x', 0) + f._cropdata.get('width', image.width),
|
||||
f._cropdata.get('y', 0) + f._cropdata.get('height', image.height),
|
||||
))
|
||||
with BytesIO() as output:
|
||||
# This might use a lot of memory, but temporary files are not a good option since
|
||||
# we don't control the cleanup
|
||||
|
||||
@@ -19,21 +19,11 @@
|
||||
# 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 json
|
||||
|
||||
import pytest
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
|
||||
def test_allowed_json():
|
||||
assert json.loads('{"float":1.5,"int":161,"arr":[]}') == {"float": 1.5, "int": 161, "arr": []}
|
||||
class Command(BaseCommand):
|
||||
help = "Do nothing. Useful for startup performance testing."
|
||||
|
||||
|
||||
def test_disallowed_json_float_consts():
|
||||
with pytest.raises(KeyError):
|
||||
json.loads("Infinity")
|
||||
with pytest.raises(KeyError):
|
||||
json.loads("-Infinity")
|
||||
with pytest.raises(KeyError):
|
||||
json.loads("NaN")
|
||||
with pytest.raises(KeyError):
|
||||
json.loads("[123, NaN, Infinity, -Infinity]")
|
||||
def handle(self, *args, **options):
|
||||
pass
|
||||
@@ -40,7 +40,6 @@ import warnings
|
||||
from collections import Counter, OrderedDict, defaultdict
|
||||
from datetime import datetime, time, timedelta
|
||||
from operator import attrgetter
|
||||
from typing import TYPE_CHECKING
|
||||
from urllib.parse import urljoin
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
@@ -80,16 +79,10 @@ from pretix.helpers.thumb import get_thumbnail
|
||||
from ..settings import settings_hierarkey
|
||||
from .organizer import Organizer, Team
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from hierarkey.proxy import HierarkeyProxy
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EventMixin:
|
||||
if TYPE_CHECKING:
|
||||
settings: HierarkeyProxy
|
||||
|
||||
def clean(self):
|
||||
if self.presale_start and self.presale_end and self.presale_start > self.presale_end:
|
||||
raise ValidationError({'presale_end': _('The end of the presale period has to be later than its start.')})
|
||||
@@ -906,7 +899,7 @@ class Event(EventMixin, LoggedModel):
|
||||
self.save()
|
||||
self.log_action('pretix.object.cloned', data={'source': other.slug, 'source_id': other.pk})
|
||||
|
||||
if hasattr(other, 'alternative_domain_assignment') and not is_cross_organizer:
|
||||
if hasattr(other, 'alternative_domain_assignment'):
|
||||
other.alternative_domain_assignment.domain.event_assignments.create(event=self)
|
||||
|
||||
if not self.all_sales_channels:
|
||||
|
||||
@@ -1070,8 +1070,10 @@ class Item(LoggedModel):
|
||||
|
||||
replace_year = valid_until.year
|
||||
replace_month = valid_until.month + self.validity_dynamic_duration_months
|
||||
replace_year += (replace_month - 1) // 12
|
||||
replace_month = ((replace_month - 1) % 12) + 1
|
||||
|
||||
while replace_month > 12:
|
||||
replace_month -= 12
|
||||
replace_year += 1
|
||||
max_day = calendar.monthrange(replace_year, replace_month)[1]
|
||||
replace_date = date(
|
||||
year=replace_year,
|
||||
|
||||
@@ -35,7 +35,6 @@ import operator
|
||||
import string
|
||||
from datetime import date, datetime, time
|
||||
from functools import reduce
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import pytz_deprecation_shim
|
||||
from django.conf import settings
|
||||
@@ -62,9 +61,6 @@ from ...helpers.permission_migration import (
|
||||
from ..settings import settings_hierarkey
|
||||
from .auth import User
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from hierarkey.proxy import HierarkeyProxy
|
||||
|
||||
|
||||
@settings_hierarkey.add(cache_namespace='organizer')
|
||||
class Organizer(LoggedModel):
|
||||
@@ -82,9 +78,6 @@ class Organizer(LoggedModel):
|
||||
"""
|
||||
|
||||
settings_namespace = 'organizer'
|
||||
if TYPE_CHECKING:
|
||||
settings: HierarkeyProxy
|
||||
|
||||
name = models.CharField(max_length=200,
|
||||
verbose_name=_("Name"))
|
||||
slug = models.CharField(
|
||||
|
||||
@@ -1074,7 +1074,7 @@ class Renderer:
|
||||
fontsize = float(o['fontsize'])
|
||||
height = float(o['height']) * mm
|
||||
width = float(o['width']) * mm
|
||||
for _i in range(25): # try adapting the font size at most 25 times
|
||||
while True:
|
||||
p, ad, lineheight = self._text_paragraph(op, order, o, override_fontsize=fontsize)
|
||||
w, h = p.wrapOn(canvas, width, 1000 * mm)
|
||||
widths = p.getActualLineWidths0()
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
{% extends "error.html" %}
|
||||
{% load i18n %}
|
||||
{% load eventurl %}
|
||||
{% load urlreplace %}
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
<h1>{% trans "Please continue in a new tab" %}</h1>
|
||||
<p class="larger">
|
||||
{% blocktrans trimmed %}
|
||||
For security reasons, the following step is only possible in a new tab.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<p class="larger">
|
||||
{% blocktrans trimmed %}
|
||||
If the new tab did not open automatically, please click the following button:
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<div class="text-center">
|
||||
<a href="{{ url }}"
|
||||
class="btn btn-primary btn-lg" target="_blank">
|
||||
<span class="fa fa-external-link-square"></span>
|
||||
{% trans "Continue in new tab" %}
|
||||
</a>
|
||||
{{ url|json_script:"framebreak-url" }}
|
||||
<script type="text/javascript" src="{% static "pretixbase/js/framebreak.js" %}"></script>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -54,7 +54,6 @@ from markdown.postprocessors import Postprocessor
|
||||
from markdown.treeprocessors import UnescapeTreeprocessor
|
||||
from tlds import tld_set
|
||||
|
||||
from pretix.base.views.redirect import safelink
|
||||
from pretix.helpers.format import SafeFormatter, format_map
|
||||
|
||||
register = template.Library()
|
||||
@@ -159,7 +158,8 @@ def safelink_callback(attrs, new=False):
|
||||
"""
|
||||
url = html.unescape(attrs.get((None, 'href'), '/'))
|
||||
if not url_has_allowed_host_and_scheme(url, allowed_hosts=None) and not url.startswith('mailto:') and not url.startswith('tel:'):
|
||||
attrs[None, 'href'] = safelink(url)
|
||||
signer = signing.Signer(salt='safe-redirect')
|
||||
attrs[None, 'href'] = reverse('redirect') + '?url=' + urllib.parse.quote(signer.sign(url))
|
||||
attrs[None, 'target'] = '_blank'
|
||||
attrs[None, 'rel'] = 'noopener'
|
||||
return attrs
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import logging
|
||||
import urllib.parse
|
||||
|
||||
from django.core import signing
|
||||
@@ -27,8 +26,6 @@ from django.http import HttpResponseBadRequest, HttpResponseRedirect
|
||||
from django.shortcuts import render
|
||||
from django.urls import reverse
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _is_samesite_referer(request):
|
||||
referer = request.headers.get('referer')
|
||||
@@ -45,16 +42,11 @@ def _is_samesite_referer(request):
|
||||
|
||||
|
||||
def redir_view(request):
|
||||
framebreak = "framebreak" in request.GET
|
||||
salt = 'framebreak-safelink-url' if framebreak else 'safelink-url'
|
||||
signer = signing.Signer(salt='safe-redirect')
|
||||
try:
|
||||
url = signing.Signer(salt=salt).unsign(request.GET.get('url', ''))
|
||||
url = signer.unsign(request.GET.get('url', ''))
|
||||
except signing.BadSignature:
|
||||
try:
|
||||
# Backwards-compatibility for a change in 2026-06, remove after a while
|
||||
url = signing.Signer(salt='safe-redirect').unsign(request.GET.get('url', ''))
|
||||
except signing.BadSignature:
|
||||
return HttpResponseBadRequest('Invalid parameter')
|
||||
return HttpResponseBadRequest('Invalid parameter')
|
||||
|
||||
if not _is_samesite_referer(request):
|
||||
u = urllib.parse.urlparse(url)
|
||||
@@ -63,26 +55,11 @@ def redir_view(request):
|
||||
'url': url,
|
||||
})
|
||||
|
||||
if framebreak:
|
||||
r = render(request, 'pretixbase/framebreak.html', {
|
||||
'url': url,
|
||||
})
|
||||
r.xframe_options_exempt = True
|
||||
return r
|
||||
|
||||
r = HttpResponseRedirect(url)
|
||||
r['X-Robots-Tag'] = 'noindex'
|
||||
return r
|
||||
|
||||
|
||||
def safelink(url, framebreak=False):
|
||||
url = str(url)
|
||||
if not (url.startswith('https://') or url.startswith('http://') or url.startswith("/")):
|
||||
logger.warning('Invalid URL passed to safelink: %r', url)
|
||||
return '#invalid-url'
|
||||
salt = 'framebreak-safelink-url' if framebreak else 'safelink-url'
|
||||
signer = signing.Signer(salt=salt)
|
||||
u = reverse('redirect') + '?url=' + urllib.parse.quote(signer.sign(url))
|
||||
if framebreak:
|
||||
u += "&framebreak=true"
|
||||
return u
|
||||
def safelink(url):
|
||||
signer = signing.Signer(salt='safe-redirect')
|
||||
return reverse('redirect') + '?url=' + urllib.parse.quote(signer.sign(url))
|
||||
|
||||
@@ -830,10 +830,9 @@ class CancelSettingsForm(SettingsForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if self.obj.settings.giftcard_expiry_years is not None:
|
||||
self.fields['cancel_allow_user_paid_refund_as_giftcard'].help_text = format_html(
|
||||
gettext('You have configured gift cards to be valid {} years plus the year the gift card is issued in.'),
|
||||
self.obj.settings.giftcard_expiry_years
|
||||
)
|
||||
self.fields['cancel_allow_user_paid_refund_as_giftcard'].help_text = gettext(
|
||||
'You have configured gift cards to be valid {} years plus the year the gift card is issued in.'
|
||||
).format(self.obj.settings.giftcard_expiry_years)
|
||||
|
||||
|
||||
class PaymentSettingsForm(EventSettingsValidationMixin, SettingsForm):
|
||||
@@ -1674,7 +1673,7 @@ class CountriesAndEUAndStates(CountriesAndEU):
|
||||
|
||||
class TaxRuleLineForm(I18nForm):
|
||||
country = LazyTypedChoiceField(
|
||||
choices=CountriesAndEUAndStates(),
|
||||
choices=lazy(lambda: CountriesAndEUAndStates(), CountriesAndEUAndStates),
|
||||
required=False
|
||||
)
|
||||
address_type = forms.ChoiceField(
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.functional import lazy
|
||||
from django.utils.html import conditional_escape, format_html
|
||||
from django.utils.html import format_html
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from pretix.base.modelimport_orders import get_order_import_columns
|
||||
@@ -66,7 +66,7 @@ class ProcessForm(forms.Form):
|
||||
widget=forms.Select(
|
||||
attrs={'data-static': 'true'}
|
||||
),
|
||||
help_text=conditional_escape(c.help_text),
|
||||
help_text=c.help_text,
|
||||
)
|
||||
|
||||
def get_columns(self):
|
||||
|
||||
@@ -364,7 +364,7 @@ class TeamForm(forms.ModelForm):
|
||||
for opt in pg.options
|
||||
],
|
||||
label=pg.label,
|
||||
help_text=conditional_escape(pg.help_text),
|
||||
help_text=pg.help_text,
|
||||
initial=initial,
|
||||
widget=forms.RadioSelect,
|
||||
)
|
||||
@@ -389,7 +389,7 @@ class TeamForm(forms.ModelForm):
|
||||
for opt in pg.options
|
||||
],
|
||||
label=pg.label,
|
||||
help_text=conditional_escape(pg.help_text),
|
||||
help_text=pg.help_text,
|
||||
initial=initial,
|
||||
widget=forms.RadioSelect,
|
||||
)
|
||||
|
||||
@@ -41,7 +41,6 @@ from django.core.exceptions import ObjectDoesNotExist, ValidationError
|
||||
from django.core.validators import EmailValidator
|
||||
from django.db.models.functions import Upper
|
||||
from django.urls import reverse
|
||||
from django.utils.html import escape
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_scopes.forms import SafeModelChoiceField
|
||||
|
||||
@@ -162,7 +161,7 @@ class VoucherForm(I18nModelForm):
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={'data-seat-guid-field': '1'}),
|
||||
initial=self.instance.seat.seat_guid if self.instance.seat else '',
|
||||
help_text=escape(str(self.instance.seat) if self.instance.seat else ''),
|
||||
help_text=str(self.instance.seat) if self.instance.seat else '',
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
|
||||
@@ -212,7 +212,7 @@ class AuditLogMiddleware:
|
||||
if request.path.startswith(get_script_prefix() + 'control') and request.user.is_authenticated:
|
||||
if getattr(request.user, "is_hijacked", False):
|
||||
hijack_history = request.session.get('hijack_history', False)
|
||||
hijacker = get_object_or_404(User, pk=hijack_history[0]["user"])
|
||||
hijacker = get_object_or_404(User, pk=hijack_history[0])
|
||||
ss = hijacker.get_active_staff_session(request.session.get('hijacker_session'))
|
||||
if ss:
|
||||
ss.logs.create(
|
||||
|
||||
@@ -414,12 +414,9 @@ def event_index_widgets_lazy(request, organizer, event):
|
||||
except SubEvent.DoesNotExist:
|
||||
pass
|
||||
|
||||
can_view_orders = request.user.has_event_permission(request.organizer, request.event, 'event.orders:read',
|
||||
request=request)
|
||||
widgets = []
|
||||
if can_view_orders:
|
||||
for r, result in event_dashboard_widgets.send(sender=request.event, subevent=subevent, lazy=False):
|
||||
widgets.extend(result)
|
||||
for r, result in event_dashboard_widgets.send(sender=request.event, subevent=subevent, lazy=False):
|
||||
widgets.extend(result)
|
||||
|
||||
return JsonResponse({'widgets': widgets})
|
||||
|
||||
|
||||
@@ -1558,7 +1558,7 @@ class WidgetSettings(EventSettingsViewMixin, EventPermissionRequiredMixin, FormV
|
||||
return ctx
|
||||
|
||||
|
||||
class QuickSetupView(EventPermissionRequiredMixin, FormView):
|
||||
class QuickSetupView(FormView):
|
||||
template_name = 'pretixcontrol/event/quick_setup.html'
|
||||
permission = 'event.settings.general:write'
|
||||
form_class = QuickSetupForm
|
||||
|
||||
@@ -19,22 +19,19 @@
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import hmac
|
||||
import json
|
||||
from contextlib import contextmanager
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth import (
|
||||
BACKEND_SESSION_KEY, HASH_SESSION_KEY, get_user_model, load_backend, login,
|
||||
logout,
|
||||
BACKEND_SESSION_KEY, get_user_model, load_backend, login,
|
||||
)
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.contrib.auth.views import redirect_to_login
|
||||
from django.db import transaction
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.urls import reverse
|
||||
from django.utils.crypto import get_random_string, salted_hmac
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views import View
|
||||
@@ -233,15 +230,7 @@ class UserImpersonateView(AdministratorPermissionRequiredMixin, RecentAuthentica
|
||||
hijacked = self.object
|
||||
|
||||
hijack_history = request.session.get("hijack_history", [])
|
||||
hijack_history.append({
|
||||
"user": request.user.pk,
|
||||
# We include the auth_hash, because it is unguessable. So should an attacker gain an attack vector to
|
||||
# modify hijack_history, they can't just insert or change a user that shouldn't be there. We HMAC it
|
||||
# again, though, since we also do not want the auth_hash of the admin user to be in the session of an
|
||||
# unprivileged user to contain the risk if there is some leak of session data.
|
||||
"auth_hash": salted_hmac(key_salt=b"hijack-history-hash", value=request.session[HASH_SESSION_KEY],
|
||||
algorithm="sha256", secret=settings.SECRET_KEY).hexdigest(),
|
||||
})
|
||||
hijack_history.append(request.user._meta.pk.value_to_string(hijacker))
|
||||
|
||||
backend = get_used_backend(request)
|
||||
backend = f"{backend.__module__}.{backend.__class__.__name__}"
|
||||
@@ -270,21 +259,8 @@ class UserImpersonateStopView(LoginRequiredMixin, View):
|
||||
hijs = request.session['hijacker_session']
|
||||
hijack_history = request.session.get("hijack_history", [])
|
||||
hijacked = request.user
|
||||
prev_session = hijack_history.pop()
|
||||
hijacker = get_object_or_404(get_user_model(), pk=prev_session["user"])
|
||||
|
||||
expected_hash = salted_hmac(
|
||||
key_salt=b"hijack-history-hash",
|
||||
value=hijacker.get_session_auth_hash(),
|
||||
algorithm="sha256",
|
||||
secret=settings.SECRET_KEY
|
||||
).hexdigest()
|
||||
if not hmac.compare_digest(expected_hash, prev_session["auth_hash"]):
|
||||
# Could be an attacker-controlled hijack history, but could also be e.g. a password change of the admin user
|
||||
# that happened during the hijack session
|
||||
logout(request)
|
||||
return redirect_to_login(request.get_full_path())
|
||||
|
||||
user_pk = hijack_history.pop()
|
||||
hijacker = get_object_or_404(get_user_model(), pk=user_pk)
|
||||
backend = get_used_backend(request)
|
||||
backend = f"{backend.__module__}.{backend.__class__.__name__}"
|
||||
with signals.no_update_last_login(), keep_session_age(request.session):
|
||||
|
||||
@@ -46,13 +46,6 @@ class RequestIdFilter(logging.Filter):
|
||||
return True
|
||||
|
||||
|
||||
class SkipNotFoundFilter(logging.Filter):
|
||||
# Drop the WARNING "Not Found: ..." records django.request emits for 404s
|
||||
# We have different access logs for that
|
||||
def filter(self, record):
|
||||
return getattr(record, 'status_code', None) != 404
|
||||
|
||||
|
||||
class RequestIdMiddleware:
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
|
||||
@@ -251,13 +251,6 @@ def monkeypatch_reportlab_imagereader():
|
||||
utils.ImageReader.__init__ = new_init
|
||||
|
||||
|
||||
def monkeypatch_json_constants():
|
||||
from json.decoder import _CONSTANTS # noqa
|
||||
del _CONSTANTS['-Infinity']
|
||||
del _CONSTANTS['Infinity']
|
||||
del _CONSTANTS['NaN']
|
||||
|
||||
|
||||
def monkeypatch_all_at_ready():
|
||||
monkeypatch_vobject_performance()
|
||||
monkeypatch_pillow_safer()
|
||||
@@ -265,4 +258,3 @@ def monkeypatch_all_at_ready():
|
||||
monkeypatch_urllib3_ssrf_protection()
|
||||
monkeypatch_cookie_morsel()
|
||||
monkeypatch_reportlab_imagereader()
|
||||
monkeypatch_json_constants()
|
||||
|
||||
@@ -5,8 +5,8 @@ msgstr ""
|
||||
"Project-Id-Version: 1\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-06-28 14:42+0000\n"
|
||||
"PO-Revision-Date: 2026-06-29 17:00+0000\n"
|
||||
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
|
||||
"PO-Revision-Date: 2026-06-28 15:19+0000\n"
|
||||
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
||||
"Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix/"
|
||||
"de/>\n"
|
||||
"Language: de\n"
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-06-28 14:42+0000\n"
|
||||
"PO-Revision-Date: 2026-06-29 17:00+0000\n"
|
||||
"PO-Revision-Date: 2026-05-29 17:00+0000\n"
|
||||
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
|
||||
"Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/pretix/"
|
||||
"es/>\n"
|
||||
@@ -17,7 +17,7 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Weblate 2026.6.1\n"
|
||||
"X-Generator: Weblate 2026.5\n"
|
||||
|
||||
#: htmlcov/d_daa1541d0cbf5e2b_dashboards_py.html:670
|
||||
#: pretix/control/templates/pretixcontrol/events/index.html:166
|
||||
@@ -467,8 +467,10 @@ msgid "Medium connected to other event"
|
||||
msgstr "Medio conectado a otro evento"
|
||||
|
||||
#: pretix/api/views/checkin.py:814
|
||||
#, fuzzy
|
||||
#| msgid "You cannot change this order."
|
||||
msgid "You cannot exchange a medium for a medium."
|
||||
msgstr "No se puede cambiar un medio por otro."
|
||||
msgstr "No puedes cambiar este pedido."
|
||||
|
||||
#: pretix/api/views/oauth.py:107 pretix/control/logdisplay.py:777
|
||||
#, python-brace-format
|
||||
@@ -4728,16 +4730,22 @@ msgid "Check-in annulled"
|
||||
msgstr "Check-in anulado"
|
||||
|
||||
#: pretix/base/models/checkin.py:372
|
||||
#, fuzzy
|
||||
#| msgid "Ticket already used"
|
||||
msgid "Ticket already exchanged"
|
||||
msgstr "Entrada ya canjeada"
|
||||
msgstr "Esta entrada ya fue utilizada"
|
||||
|
||||
#: pretix/base/models/checkin.py:373
|
||||
#, fuzzy
|
||||
#| msgid "Reusable media"
|
||||
msgid "Reusable medium invalid"
|
||||
msgstr "Soporte reutilizable no válido"
|
||||
msgstr "Medios reutilizables"
|
||||
|
||||
#: pretix/base/models/checkin.py:374
|
||||
#, fuzzy
|
||||
#| msgid "Reusable media type"
|
||||
msgid "Reusable medium already exists"
|
||||
msgstr "El soporte reutilizable ya existe"
|
||||
msgstr "Tipo de medio reusable"
|
||||
|
||||
#: pretix/base/models/customers.py:63
|
||||
msgid "Provider name"
|
||||
@@ -5322,8 +5330,11 @@ msgstr ""
|
||||
"ambas cosas."
|
||||
|
||||
#: pretix/base/models/event.py:1884
|
||||
#, fuzzy
|
||||
#| msgid "The bundled item must belong to the same event as the item."
|
||||
msgid "Property and event must belong to the same organizer."
|
||||
msgstr "La propiedad y el evento deben pertenecer al mismo organizador."
|
||||
msgstr ""
|
||||
"La agrupación de artículos debe pertenecer al mismo evento que el artículo."
|
||||
|
||||
#: pretix/base/models/event.py:1928 pretix/base/models/organizer.py:627
|
||||
msgid "Link text"
|
||||
@@ -5575,41 +5586,42 @@ msgid "Show product with info on why it’s unavailable"
|
||||
msgstr "Mostrar el producto con la razón que no está disponible"
|
||||
|
||||
#: pretix/base/models/items.py:458 pretix/base/models/items.py:786
|
||||
#, fuzzy
|
||||
#| msgid "Don't use re-usable media, use regular one-off tickets"
|
||||
msgid "Don't use reusable media, use regular one-off tickets"
|
||||
msgstr ""
|
||||
"No utilizar soportes reutilizables, sino billetes normales de un solo uso"
|
||||
msgstr "No usar medios reutilizables, usar entradas de un solo uso"
|
||||
|
||||
#: pretix/base/models/items.py:459
|
||||
msgid "Require a previously unknown medium to be newly added"
|
||||
msgstr "Exigir que un medio desconocido anteriormente se adicione de nuevo"
|
||||
|
||||
#: pretix/base/models/items.py:460
|
||||
#, fuzzy
|
||||
#| msgid "Require an existing medium to be re-used"
|
||||
msgid "Require an existing medium to be reused, replacing any previous tickets"
|
||||
msgstr ""
|
||||
"Necesita que se reutilice un soporte ya existente, sustituyendo cualquier "
|
||||
"billete anterior"
|
||||
msgstr "Exigir que se reuse un medio ya existente"
|
||||
|
||||
#: pretix/base/models/items.py:461
|
||||
#, fuzzy
|
||||
#| msgid "Require either an existing or a new medium to be used"
|
||||
msgid ""
|
||||
"Require either an existing or a new medium to be used, replacing any "
|
||||
"previous tickets"
|
||||
msgstr ""
|
||||
"Se debe utilizar un soporte ya existente o uno nuevo, sustituyendo cualquier "
|
||||
"billete anterior"
|
||||
msgstr "Exigir que se use un medio existente o uno nuevo"
|
||||
|
||||
#: pretix/base/models/items.py:462
|
||||
#, fuzzy
|
||||
#| msgid "Require an existing medium to be re-used"
|
||||
msgid "Require an existing medium to be reused, adding to any previous tickets"
|
||||
msgstr ""
|
||||
"Exigir que se reutilice un soporte ya existente, añadiendo esta información "
|
||||
"a cualquier entrada anterior"
|
||||
msgstr "Exigir que se reuse un medio ya existente"
|
||||
|
||||
#: pretix/base/models/items.py:464
|
||||
#, fuzzy
|
||||
#| msgid "Require either an existing or a new medium to be used"
|
||||
msgid ""
|
||||
"Require either an existing or a new medium to be used, adding to any "
|
||||
"previous tickets"
|
||||
msgstr ""
|
||||
"Es necesario utilizar un medio ya existente o uno nuevo, que se sumará a los "
|
||||
"billetes anteriores"
|
||||
msgstr "Exigir que se use un medio existente o uno nuevo"
|
||||
|
||||
#: pretix/base/models/items.py:480 pretix/base/models/items.py:1468
|
||||
msgid "Category"
|
||||
@@ -5969,6 +5981,14 @@ msgid "Reusable media policy"
|
||||
msgstr "Condiciones de utilización de medios"
|
||||
|
||||
#: pretix/base/models/items.py:777
|
||||
#, fuzzy
|
||||
#| msgid ""
|
||||
#| "If this product should be stored on a re-usable physical medium, you can "
|
||||
#| "attach a physical media policy. This is not required for regular tickets, "
|
||||
#| "which just use a one-time barcode, but only for products like renewable "
|
||||
#| "season tickets or re-chargeable gift card wristbands. This is an advanced "
|
||||
#| "feature that also requires specific configuration of ticketing and "
|
||||
#| "printing settings."
|
||||
msgid ""
|
||||
"If this product should be stored on a reusable physical medium, you can "
|
||||
"attach a physical media policy. This is not required for regular tickets, "
|
||||
@@ -6032,9 +6052,6 @@ msgid ""
|
||||
"prior to their usage. Therefore, the selected media policy does not make "
|
||||
"sense for this media type."
|
||||
msgstr ""
|
||||
"El tipo de soporte seleccionado requiere que todos los soportes se registren "
|
||||
"en el sistema antes de su uso. Por lo tanto, la política de soportes "
|
||||
"seleccionada no es aplicable a este tipo de soporte."
|
||||
|
||||
#: pretix/base/models/items.py:1009
|
||||
msgid ""
|
||||
@@ -6609,16 +6626,18 @@ msgstr "rebotado"
|
||||
#: pretix/base/models/media.py:77
|
||||
msgctxt "reusable_medium"
|
||||
msgid "Claim token"
|
||||
msgstr "Canjear el token"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/models/media.py:82
|
||||
msgctxt "reusable_medium"
|
||||
msgid "Label"
|
||||
msgstr "Designación"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/models/media.py:105
|
||||
#, fuzzy
|
||||
#| msgid "Linked ticket"
|
||||
msgid "Linked tickets"
|
||||
msgstr "Entradas vinculadas"
|
||||
msgstr "Entrada vinculada"
|
||||
|
||||
#: pretix/base/models/media.py:107
|
||||
msgid ""
|
||||
@@ -6626,9 +6645,6 @@ msgid ""
|
||||
"validity. If multiple tickets are valid at once, this will lead to failed "
|
||||
"check-ins."
|
||||
msgstr ""
|
||||
"Si enlaza más de un billete, asegúrese de que no haya solapamiento en la "
|
||||
"validez. Si varios billetes son válidos al mismo tiempo, esto provocará que "
|
||||
"no se puedan realizar el check-in."
|
||||
|
||||
#: pretix/base/models/memberships.py:44
|
||||
#: pretix/presale/templates/pretixpresale/organizers/customer_memberships.html:28
|
||||
@@ -8374,10 +8390,14 @@ msgid "Atlantis"
|
||||
msgstr "Atlántida"
|
||||
|
||||
#: pretix/base/pdf.py:376
|
||||
#, fuzzy
|
||||
#| msgid "Invoice recipient email"
|
||||
msgid "Invoice custom recipient field"
|
||||
msgstr "Campo personalizado de destinatario en la factura"
|
||||
msgstr "Correo electrónico del destinatario de la factura"
|
||||
|
||||
#: pretix/base/pdf.py:377
|
||||
#, fuzzy
|
||||
#| msgid "Custom recipient field label"
|
||||
msgid "Custom recipient field"
|
||||
msgstr "Campo de destinatario personalizado"
|
||||
|
||||
@@ -9395,15 +9415,13 @@ msgstr "Necesitas responder preguntas para terminar el check-in."
|
||||
|
||||
#: pretix/base/services/checkin.py:1121
|
||||
msgid "Ticket needs to be exchanged to a suitable medium."
|
||||
msgstr "El billete debe canjearse por un soporte adecuado."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/services/checkin.py:1128
|
||||
msgid ""
|
||||
"This ticket has already been exchanged for a reusable medium that now needs "
|
||||
"to be used instead."
|
||||
msgstr ""
|
||||
"Esta entrada ya se ha canjeado por un soporte reutilizable que ahora hay que "
|
||||
"utilizar en su lugar."
|
||||
|
||||
#: pretix/base/services/checkin.py:1180
|
||||
msgid "This ticket has already been redeemed."
|
||||
@@ -9569,46 +9587,64 @@ msgstr ""
|
||||
"{event}."
|
||||
|
||||
#: pretix/base/services/media.py:93 pretix/base/services/media.py:95
|
||||
#, fuzzy
|
||||
#| msgid "Invalid input type."
|
||||
msgid "Invalid medium type."
|
||||
msgstr "Tipo de soporte no válido."
|
||||
msgstr "Tipo de entrada no válido."
|
||||
|
||||
#: pretix/base/services/media.py:100 pretix/base/services/media.py:102
|
||||
#, fuzzy
|
||||
#| msgid "The selected media type is not enabled in your organizer settings."
|
||||
msgid "Medium type is not enabled for organizer."
|
||||
msgstr "Este tipo de medio no está habilitado para el organizador."
|
||||
msgstr ""
|
||||
"El tipo de medio seleccionado no está activo en tus ajustes de organizador/a/"
|
||||
"e."
|
||||
|
||||
#: pretix/base/services/media.py:107 pretix/base/services/media.py:109
|
||||
msgid "Incorrect medium type for product."
|
||||
msgstr "El tipo de soporte no es el adecuado para este producto."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/services/media.py:114 pretix/base/services/media.py:116
|
||||
#, fuzzy
|
||||
#| msgid "This ticket has already been redeemed."
|
||||
msgid "Ticket is already exchanged for reusable medium."
|
||||
msgstr "La entrada ya se ha canjeado por un soporte reutilizable."
|
||||
msgstr "Esta entrada ya ha sido canjeada."
|
||||
|
||||
#: pretix/base/services/media.py:133 pretix/base/services/media.py:135
|
||||
#, fuzzy
|
||||
#| msgid "Reusable Medium ID"
|
||||
msgid "Reusable medium not found."
|
||||
msgstr "No se ha encontrado el soporte reutilizable."
|
||||
msgstr "ID mediana reutilizable"
|
||||
|
||||
#: pretix/base/services/media.py:140 pretix/base/services/media.py:142
|
||||
#: pretix/base/services/media.py:168 pretix/base/services/media.py:170
|
||||
#, fuzzy
|
||||
#| msgid "The reusable medium has been created."
|
||||
msgid "Reusable medium is inactive or expired."
|
||||
msgstr "El medio reutilizable está inactivo o caducado."
|
||||
msgstr "Se ha creado el medio reutilizable."
|
||||
|
||||
#: pretix/base/services/media.py:155 pretix/base/services/media.py:162
|
||||
#: pretix/base/services/media.py:176
|
||||
#, fuzzy
|
||||
#| msgid "The reusable medium has been created."
|
||||
msgid "Reusable medium not found and could not be created."
|
||||
msgstr "No se ha encontrado el soporte reutilizable y no se ha podido crear."
|
||||
msgstr "Se ha creado el medio reutilizable."
|
||||
|
||||
#: pretix/base/services/media.py:183
|
||||
#, fuzzy
|
||||
#| msgid "Reusable media type"
|
||||
msgid "Reusable medium already exists."
|
||||
msgstr "Ya existe un soporte reutilizable."
|
||||
msgstr "Tipo de medio reusable"
|
||||
|
||||
#: pretix/base/services/media.py:189
|
||||
#, fuzzy
|
||||
#| msgid "The reusable medium has been created."
|
||||
msgid "Reusable medium could not be created."
|
||||
msgstr "No se ha podido crear el soporte reutilizable."
|
||||
msgstr "Se ha creado el medio reutilizable."
|
||||
|
||||
#: pretix/base/services/media.py:195 pretix/base/services/media.py:197
|
||||
msgid "Product does not support medium exchange."
|
||||
msgstr "Este producto no admite el cambio de medio."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/services/memberships.py:108
|
||||
#, python-brace-format
|
||||
@@ -10332,10 +10368,17 @@ msgstr ""
|
||||
"inició sesión durante la compra."
|
||||
|
||||
#: pretix/base/settings.py:214
|
||||
#, fuzzy
|
||||
#| msgid "Activate re-usable media"
|
||||
msgid "Activate reusable media"
|
||||
msgstr "Activar medios reutilizables"
|
||||
|
||||
#: pretix/base/settings.py:215
|
||||
#, fuzzy
|
||||
#| msgid ""
|
||||
#| "The re-usable media feature allows you to connect tickets and gift cards "
|
||||
#| "with physical media such as wristbands or chip cards that may be re-used "
|
||||
#| "for different tickets or gift cards later."
|
||||
msgid ""
|
||||
"The reusable media feature allows you to connect tickets and gift cards with "
|
||||
"physical media such as wristbands or chip cards that may be reused for "
|
||||
@@ -10347,7 +10390,7 @@ msgstr ""
|
||||
|
||||
#: pretix/base/settings.py:226
|
||||
msgid "Enforce the usage of issued reusable media for check-in"
|
||||
msgstr "Exigir el uso de los soportes reutilizables para el check-in"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/settings.py:227
|
||||
msgid ""
|
||||
@@ -10355,10 +10398,6 @@ msgid ""
|
||||
"medium has been created and linked to a ticket. Keeping this option turned "
|
||||
"off will treat the reusable medium and ticket as equals."
|
||||
msgstr ""
|
||||
"Si se activa esta opción, ya no se aceptarán los códigos de barras de los "
|
||||
"billetes cuando se haya creado un soporte reutilizable y se haya vinculado a "
|
||||
"un billete. Si se mantiene desactivada esta opción, el soporte reutilizable "
|
||||
"y el billete se tratarán como iguales."
|
||||
|
||||
#: pretix/base/settings.py:254
|
||||
msgid "Length of barcodes"
|
||||
@@ -18509,12 +18548,16 @@ msgid "The reusable medium has been changed."
|
||||
msgstr "El medio reutilizable ha sido modificado."
|
||||
|
||||
#: pretix/control/logdisplay.py:746
|
||||
#, fuzzy
|
||||
#| msgid "The new member has been added to the team."
|
||||
msgid "A new ticket has been added to the medium."
|
||||
msgstr "Se ha añadido un nuevo billete al medio."
|
||||
msgstr "El nuevo miembro ha sido añadido al equipo."
|
||||
|
||||
#: pretix/control/logdisplay.py:747
|
||||
#, fuzzy
|
||||
#| msgid "{user} has been removed from the team."
|
||||
msgid "A ticket has been removed from the medium."
|
||||
msgstr "Se ha eliminado un billete del medio."
|
||||
msgstr "{user} ha sido removido del equipo."
|
||||
|
||||
#: pretix/control/logdisplay.py:748
|
||||
msgid "The medium has been connected to a new ticket."
|
||||
@@ -18526,8 +18569,6 @@ msgid ""
|
||||
"The ticket #{positionid} was exchanged for reusable medium "
|
||||
"{medium_identifier}."
|
||||
msgstr ""
|
||||
"El billete n.º {positionid} se ha canjeado por un medio reutilizable "
|
||||
"{medium_identifier}."
|
||||
|
||||
#: pretix/control/logdisplay.py:750
|
||||
msgid "The medium has been connected to a new gift card."
|
||||
@@ -20442,8 +20483,10 @@ msgstr ""
|
||||
"check-in:"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/checkin/simulator.html:85
|
||||
#, fuzzy
|
||||
#| msgid "Special attention required"
|
||||
msgid "Media exchange required"
|
||||
msgstr "Es necesario intercambiar medios"
|
||||
msgstr "Atención especial requerida"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/checkin/simulator.html:87
|
||||
#, python-format
|
||||
@@ -20451,8 +20494,6 @@ msgid ""
|
||||
"This ticket needs to be exchanged into a <strong>%(media_type)s</strong> "
|
||||
"reusable medium. <strong>%(media_policy)s</strong>."
|
||||
msgstr ""
|
||||
"Esta entrada debe canjearse por un soporte reutilizable <strong>%(media_type)"
|
||||
"s</strong>. <strong>%(media_policy)s</strong>."
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/checkin/simulator.html:103
|
||||
msgid "Special attention required"
|
||||
@@ -26997,9 +27038,6 @@ msgid ""
|
||||
"Even if a team has no access to a certain category of data, they might still "
|
||||
"be able to see parts of this data when it is linked to data they can see."
|
||||
msgstr ""
|
||||
"Aunque un equipo no tenga acceso a una determinada categoría de datos, es "
|
||||
"posible que pueda ver parte de esos datos cuando estén vinculados a datos a "
|
||||
"los que sí tiene acceso."
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/organizers/team_edit.html:35
|
||||
msgid ""
|
||||
@@ -27007,10 +27045,6 @@ msgid ""
|
||||
"some information about gift cards linked to a customer account, even if they "
|
||||
"generally can't see gift cards directly."
|
||||
msgstr ""
|
||||
"Por ejemplo, una persona con acceso a las cuentas de los clientes podrá ver "
|
||||
"cierta información sobre las tarjetas regalo vinculadas a una cuenta de "
|
||||
"cliente, aunque, por lo general, no pueda ver las tarjetas regalo "
|
||||
"directamente."
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/organizers/team_edit.html:59
|
||||
msgid ""
|
||||
@@ -27018,9 +27052,6 @@ msgid ""
|
||||
"information about vouchers used to create an order, even if they generally "
|
||||
"can't see vouchers directly."
|
||||
msgstr ""
|
||||
"Por ejemplo, una persona con acceso a los pedidos podrá ver cierta "
|
||||
"información sobre los vales utilizados para crear un pedido, aunque "
|
||||
"normalmente no pueda ver los vales directamente."
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/organizers/team_members.html:21
|
||||
msgid "Member"
|
||||
@@ -27881,22 +27912,30 @@ msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/subevents/detail.html:9
|
||||
#: pretix/control/templates/pretixcontrol/subevents/detail.html:13
|
||||
#, python-format
|
||||
#, fuzzy, python-format
|
||||
#| msgid "Quota: %(name)s"
|
||||
msgctxt "subevent"
|
||||
msgid "Date: %(name)s"
|
||||
msgstr "Fecha: %(name)s"
|
||||
msgstr "Cuota: %(name)s"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/subevents/detail.html:234
|
||||
#, fuzzy
|
||||
#| msgid "Partially paid"
|
||||
msgid "partially canceled"
|
||||
msgstr "cancelado parcialmente"
|
||||
msgstr "Pagado parcialmente"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/subevents/detail.html:282
|
||||
#, fuzzy
|
||||
#| msgctxt "permission_level"
|
||||
#| msgid "View all"
|
||||
msgid "View all"
|
||||
msgstr "Ver todo"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/subevents/detail.html:289
|
||||
#, fuzzy
|
||||
#| msgid "No archived events found."
|
||||
msgid "No orders found."
|
||||
msgstr "No se han encontrado pedidos."
|
||||
msgstr "No se han encontrado eventos archivados."
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/subevents/detail.html:302
|
||||
#: pretix/control/templates/pretixcontrol/subevents/edit.html:279
|
||||
@@ -30669,8 +30708,10 @@ msgid "Voucher {}"
|
||||
msgstr "Vale de compra {}"
|
||||
|
||||
#: pretix/control/views/typeahead.py:179 pretix/control/views/typeahead.py:180
|
||||
#, fuzzy
|
||||
#| msgid "Go to event"
|
||||
msgid "No event"
|
||||
msgstr "No hay eventos"
|
||||
msgstr "Ir al evento"
|
||||
|
||||
#: pretix/control/views/user.py:169
|
||||
msgid "The password you entered was invalid, please try again."
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-06-28 15:49+0000\n"
|
||||
"PO-Revision-Date: 2026-06-29 17:00+0000\n"
|
||||
"PO-Revision-Date: 2026-03-30 03:00+0000\n"
|
||||
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
|
||||
"Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/pretix-"
|
||||
"js/es/>\n"
|
||||
@@ -17,7 +17,7 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Weblate 2026.6.1\n"
|
||||
"X-Generator: Weblate 5.16.2\n"
|
||||
|
||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
|
||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
|
||||
@@ -442,11 +442,11 @@ msgstr "¡Presione Control+C para copiar!"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue:80
|
||||
msgid "Edit"
|
||||
msgstr "Editar"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue:86
|
||||
msgid "Visualize"
|
||||
msgstr "Visualizar"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue:96
|
||||
msgid ""
|
||||
@@ -454,13 +454,10 @@ msgid ""
|
||||
"or variations are not contained in any of your rule parts so people with "
|
||||
"these tickets will not get in:"
|
||||
msgstr ""
|
||||
"Su regla siempre filtra por producto o variante, pero los siguientes "
|
||||
"productos o variantes no figuran en ninguna de las partes de su regla, por "
|
||||
"lo que las personas con estos tickets no podrán acceder:"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue:99
|
||||
msgid "Please double-check if this was intentional."
|
||||
msgstr "Por favor, comprueba bien si esto ha sido a propósito."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:4
|
||||
msgid "All of the conditions below (AND)"
|
||||
|
||||
@@ -4,10 +4,10 @@ msgstr ""
|
||||
"Project-Id-Version: 1\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-06-28 14:42+0000\n"
|
||||
"PO-Revision-Date: 2026-06-29 17:00+0000\n"
|
||||
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
|
||||
"Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix/"
|
||||
"fr/>\n"
|
||||
"PO-Revision-Date: 2026-06-08 17:00+0000\n"
|
||||
"Last-Translator: Sébastien BRUNEAU <s.bruneau@beauvaisis.fr>\n"
|
||||
"Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix/fr/"
|
||||
">\n"
|
||||
"Language: fr\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
@@ -466,8 +466,10 @@ msgid "Medium connected to other event"
|
||||
msgstr "Média connecté à un autre événement"
|
||||
|
||||
#: pretix/api/views/checkin.py:814
|
||||
#, fuzzy
|
||||
#| msgid "You cannot change this order."
|
||||
msgid "You cannot exchange a medium for a medium."
|
||||
msgstr "Il n'est pas possible d'échanger un support contre un autre support."
|
||||
msgstr "Vous ne pouvez pas modifier cette commande."
|
||||
|
||||
#: pretix/api/views/oauth.py:107 pretix/control/logdisplay.py:777
|
||||
#, python-brace-format
|
||||
@@ -4734,16 +4736,22 @@ msgid "Check-in annulled"
|
||||
msgstr "Enregistrement annulé"
|
||||
|
||||
#: pretix/base/models/checkin.py:372
|
||||
#, fuzzy
|
||||
#| msgid "Ticket already used"
|
||||
msgid "Ticket already exchanged"
|
||||
msgstr "Billet déjà échangé"
|
||||
msgstr "Billet déjà utilisé"
|
||||
|
||||
#: pretix/base/models/checkin.py:373
|
||||
#, fuzzy
|
||||
#| msgid "Reusable media"
|
||||
msgid "Reusable medium invalid"
|
||||
msgstr "Support réutilisable non valide"
|
||||
msgstr "Support réutilisable"
|
||||
|
||||
#: pretix/base/models/checkin.py:374
|
||||
#, fuzzy
|
||||
#| msgid "Reusable media type"
|
||||
msgid "Reusable medium already exists"
|
||||
msgstr "Il existe déjà un support réutilisable"
|
||||
msgstr "Type de support réutilisable"
|
||||
|
||||
#: pretix/base/models/customers.py:63
|
||||
msgid "Provider name"
|
||||
@@ -5335,8 +5343,10 @@ msgstr ""
|
||||
"deux."
|
||||
|
||||
#: pretix/base/models/event.py:1884
|
||||
#, fuzzy
|
||||
#| msgid "The bundled item must belong to the same event as the item."
|
||||
msgid "Property and event must belong to the same organizer."
|
||||
msgstr "La propriété et l'événement doivent appartenir au même organisateur."
|
||||
msgstr "L’élément groupé doit appartenir au même événement que l’élément."
|
||||
|
||||
#: pretix/base/models/event.py:1928 pretix/base/models/organizer.py:627
|
||||
msgid "Link text"
|
||||
@@ -5591,6 +5601,8 @@ msgstr ""
|
||||
"indisponibilité"
|
||||
|
||||
#: pretix/base/models/items.py:458 pretix/base/models/items.py:786
|
||||
#, fuzzy
|
||||
#| msgid "Don't use re-usable media, use regular one-off tickets"
|
||||
msgid "Don't use reusable media, use regular one-off tickets"
|
||||
msgstr ""
|
||||
"N'utilisez pas de supports réutilisables, mais plutôt des tickets uniques "
|
||||
@@ -5601,30 +5613,32 @@ msgid "Require a previously unknown medium to be newly added"
|
||||
msgstr "Exiger l'ajout d'un support inconnu jusqu'alors"
|
||||
|
||||
#: pretix/base/models/items.py:460
|
||||
#, fuzzy
|
||||
#| msgid "Require an existing medium to be re-used"
|
||||
msgid "Require an existing medium to be reused, replacing any previous tickets"
|
||||
msgstr "Exiger la réutilisation d'un support existant"
|
||||
|
||||
#: pretix/base/models/items.py:461
|
||||
#, fuzzy
|
||||
#| msgid "Require either an existing or a new medium to be used"
|
||||
msgid ""
|
||||
"Require either an existing or a new medium to be used, replacing any "
|
||||
"previous tickets"
|
||||
msgstr ""
|
||||
"Exiger l'utilisation d'un support existant ou d'un nouveau support, en "
|
||||
"remplacement de tout billet antérieur"
|
||||
msgstr "Nécessiter l'utilisation d'un support existant ou d'un nouveau support"
|
||||
|
||||
#: pretix/base/models/items.py:462
|
||||
#, fuzzy
|
||||
#| msgid "Require an existing medium to be re-used"
|
||||
msgid "Require an existing medium to be reused, adding to any previous tickets"
|
||||
msgstr ""
|
||||
"Exiger la réutilisation d'un support existant, en ajoutant cette demande à "
|
||||
"tout ticket précédent"
|
||||
msgstr "Exiger la réutilisation d'un support existant"
|
||||
|
||||
#: pretix/base/models/items.py:464
|
||||
#, fuzzy
|
||||
#| msgid "Require either an existing or a new medium to be used"
|
||||
msgid ""
|
||||
"Require either an existing or a new medium to be used, adding to any "
|
||||
"previous tickets"
|
||||
msgstr ""
|
||||
"Exiger l'utilisation d'un support existant ou d'un nouveau support, en "
|
||||
"complément des tickets précédents"
|
||||
msgstr "Nécessiter l'utilisation d'un support existant ou d'un nouveau support"
|
||||
|
||||
#: pretix/base/models/items.py:480 pretix/base/models/items.py:1468
|
||||
msgid "Category"
|
||||
@@ -5987,6 +6001,14 @@ msgid "Reusable media policy"
|
||||
msgstr "Politique relative aux médias réutilisables"
|
||||
|
||||
#: pretix/base/models/items.py:777
|
||||
#, fuzzy
|
||||
#| msgid ""
|
||||
#| "If this product should be stored on a re-usable physical medium, you can "
|
||||
#| "attach a physical media policy. This is not required for regular tickets, "
|
||||
#| "which just use a one-time barcode, but only for products like renewable "
|
||||
#| "season tickets or re-chargeable gift card wristbands. This is an advanced "
|
||||
#| "feature that also requires specific configuration of ticketing and "
|
||||
#| "printing settings."
|
||||
msgid ""
|
||||
"If this product should be stored on a reusable physical medium, you can "
|
||||
"attach a physical media policy. This is not required for regular tickets, "
|
||||
@@ -6051,10 +6073,6 @@ msgid ""
|
||||
"prior to their usage. Therefore, the selected media policy does not make "
|
||||
"sense for this media type."
|
||||
msgstr ""
|
||||
"Le type de support sélectionné exige que tous les supports soient "
|
||||
"enregistrés dans le système avant leur utilisation. Par conséquent, la "
|
||||
"politique relative aux supports sélectionnée n'est pas applicable à ce type "
|
||||
"de support."
|
||||
|
||||
#: pretix/base/models/items.py:1009
|
||||
msgid ""
|
||||
@@ -6631,16 +6649,18 @@ msgstr "Non distribué"
|
||||
#: pretix/base/models/media.py:77
|
||||
msgctxt "reusable_medium"
|
||||
msgid "Claim token"
|
||||
msgstr "Réclamer un jeton"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/models/media.py:82
|
||||
msgctxt "reusable_medium"
|
||||
msgid "Label"
|
||||
msgstr "Descriptif"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/models/media.py:105
|
||||
#, fuzzy
|
||||
#| msgid "Linked ticket"
|
||||
msgid "Linked tickets"
|
||||
msgstr "Billets liés"
|
||||
msgstr "Billet lié"
|
||||
|
||||
#: pretix/base/models/media.py:107
|
||||
msgid ""
|
||||
@@ -6648,9 +6668,6 @@ msgid ""
|
||||
"validity. If multiple tickets are valid at once, this will lead to failed "
|
||||
"check-ins."
|
||||
msgstr ""
|
||||
"Si vous associez plusieurs billets, assurez-vous qu'il n'y ait pas de "
|
||||
"chevauchement entre leurs périodes de validité. Si plusieurs billets sont "
|
||||
"valables en même temps, cela entraînera l'échec de l'enregistrement."
|
||||
|
||||
#: pretix/base/models/memberships.py:44
|
||||
#: pretix/presale/templates/pretixpresale/organizers/customer_memberships.html:28
|
||||
@@ -8420,12 +8437,16 @@ msgid "Atlantis"
|
||||
msgstr "Atlantide"
|
||||
|
||||
#: pretix/base/pdf.py:376
|
||||
#, fuzzy
|
||||
#| msgid "Invoice recipient email"
|
||||
msgid "Invoice custom recipient field"
|
||||
msgstr "Champ personnalisé destinataire de la facture"
|
||||
msgstr "E-mail du destinataire de la facture"
|
||||
|
||||
#: pretix/base/pdf.py:377
|
||||
#, fuzzy
|
||||
#| msgid "Custom recipient field label"
|
||||
msgid "Custom recipient field"
|
||||
msgstr "Champ de destinataire personnalisé"
|
||||
msgstr "Libellé personnalisé du champ destinataire"
|
||||
|
||||
#: pretix/base/pdf.py:381
|
||||
msgid "List of Add-Ons"
|
||||
@@ -9450,15 +9471,13 @@ msgstr ""
|
||||
|
||||
#: pretix/base/services/checkin.py:1121
|
||||
msgid "Ticket needs to be exchanged to a suitable medium."
|
||||
msgstr "Le billet doit être échangé contre un support adapté."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/services/checkin.py:1128
|
||||
msgid ""
|
||||
"This ticket has already been exchanged for a reusable medium that now needs "
|
||||
"to be used instead."
|
||||
msgstr ""
|
||||
"Ce billet a déjà été échangé contre un support réutilisable qui doit "
|
||||
"désormais être utilisé à sa place."
|
||||
|
||||
#: pretix/base/services/checkin.py:1180
|
||||
msgid "This ticket has already been redeemed."
|
||||
@@ -9624,46 +9643,64 @@ msgstr ""
|
||||
"Vous recevez cet e-mail parce que vous avez passé une commande pour {event}."
|
||||
|
||||
#: pretix/base/services/media.py:93 pretix/base/services/media.py:95
|
||||
#, fuzzy
|
||||
#| msgid "Invalid input type."
|
||||
msgid "Invalid medium type."
|
||||
msgstr "Type de support non valide."
|
||||
msgstr "Type d’entrée non valide."
|
||||
|
||||
#: pretix/base/services/media.py:100 pretix/base/services/media.py:102
|
||||
#, fuzzy
|
||||
#| msgid "The selected media type is not enabled in your organizer settings."
|
||||
msgid "Medium type is not enabled for organizer."
|
||||
msgstr "Ce type de média n’est pas activé par l'organisateur."
|
||||
msgstr ""
|
||||
"Le type de média sélectionné n’est pas activé dans les paramètres de votre "
|
||||
"organisateur."
|
||||
|
||||
#: pretix/base/services/media.py:107 pretix/base/services/media.py:109
|
||||
msgid "Incorrect medium type for product."
|
||||
msgstr "Type de support incorrect pour ce produit."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/services/media.py:114 pretix/base/services/media.py:116
|
||||
#, fuzzy
|
||||
#| msgid "This ticket has already been redeemed."
|
||||
msgid "Ticket is already exchanged for reusable medium."
|
||||
msgstr "Le billet a déjà été échangé contre un support réutilisable."
|
||||
msgstr "Ce billet a déjà été échangé."
|
||||
|
||||
#: pretix/base/services/media.py:133 pretix/base/services/media.py:135
|
||||
#, fuzzy
|
||||
#| msgid "Reusable Medium ID"
|
||||
msgid "Reusable medium not found."
|
||||
msgstr "Support réutilisable introuvable."
|
||||
msgstr "Identification de support réutilisable"
|
||||
|
||||
#: pretix/base/services/media.py:140 pretix/base/services/media.py:142
|
||||
#: pretix/base/services/media.py:168 pretix/base/services/media.py:170
|
||||
#, fuzzy
|
||||
#| msgid "The reusable medium has been created."
|
||||
msgid "Reusable medium is inactive or expired."
|
||||
msgstr "Le support réutilisable est inactif ou a expiré."
|
||||
msgstr "Le support réutilisable a été créé."
|
||||
|
||||
#: pretix/base/services/media.py:155 pretix/base/services/media.py:162
|
||||
#: pretix/base/services/media.py:176
|
||||
#, fuzzy
|
||||
#| msgid "The reusable medium has been created."
|
||||
msgid "Reusable medium not found and could not be created."
|
||||
msgstr "Support réutilisable introuvable et impossible à créer."
|
||||
msgstr "Le support réutilisable a été créé."
|
||||
|
||||
#: pretix/base/services/media.py:183
|
||||
#, fuzzy
|
||||
#| msgid "Reusable media type"
|
||||
msgid "Reusable medium already exists."
|
||||
msgstr "Le type de support réutilisable existe déjà."
|
||||
msgstr "Type de support réutilisable"
|
||||
|
||||
#: pretix/base/services/media.py:189
|
||||
#, fuzzy
|
||||
#| msgid "The reusable medium has been created."
|
||||
msgid "Reusable medium could not be created."
|
||||
msgstr "Impossible de créer le support réutilisable."
|
||||
msgstr "Le support réutilisable a été créé."
|
||||
|
||||
#: pretix/base/services/media.py:195 pretix/base/services/media.py:197
|
||||
msgid "Product does not support medium exchange."
|
||||
msgstr "Ce produit ne permet pas de changer de support."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/services/memberships.py:108
|
||||
#, python-brace-format
|
||||
@@ -10389,10 +10426,17 @@ msgstr ""
|
||||
"l’achat."
|
||||
|
||||
#: pretix/base/settings.py:214
|
||||
#, fuzzy
|
||||
#| msgid "Activate re-usable media"
|
||||
msgid "Activate reusable media"
|
||||
msgstr "Activer les supports réutilisables"
|
||||
|
||||
#: pretix/base/settings.py:215
|
||||
#, fuzzy
|
||||
#| msgid ""
|
||||
#| "The re-usable media feature allows you to connect tickets and gift cards "
|
||||
#| "with physical media such as wristbands or chip cards that may be re-used "
|
||||
#| "for different tickets or gift cards later."
|
||||
msgid ""
|
||||
"The reusable media feature allows you to connect tickets and gift cards with "
|
||||
"physical media such as wristbands or chip cards that may be reused for "
|
||||
@@ -10406,8 +10450,6 @@ msgstr ""
|
||||
#: pretix/base/settings.py:226
|
||||
msgid "Enforce the usage of issued reusable media for check-in"
|
||||
msgstr ""
|
||||
"Imposer l'utilisation de supports réutilisables fournis lors de "
|
||||
"l'enregistrement"
|
||||
|
||||
#: pretix/base/settings.py:227
|
||||
msgid ""
|
||||
@@ -10415,10 +10457,6 @@ msgid ""
|
||||
"medium has been created and linked to a ticket. Keeping this option turned "
|
||||
"off will treat the reusable medium and ticket as equals."
|
||||
msgstr ""
|
||||
"Si cette option est activée, le code-barres d'un billet ne sera plus accepté "
|
||||
"dès lors qu'un support réutilisable a été créé et associé à ce billet. Si "
|
||||
"cette option reste désactivée, le support réutilisable et le billet seront "
|
||||
"considérés comme équivalents."
|
||||
|
||||
#: pretix/base/settings.py:254
|
||||
msgid "Length of barcodes"
|
||||
@@ -18653,12 +18691,16 @@ msgid "The reusable medium has been changed."
|
||||
msgstr "Le support réutilisable a été changé."
|
||||
|
||||
#: pretix/control/logdisplay.py:746
|
||||
#, fuzzy
|
||||
#| msgid "The new member has been added to the team."
|
||||
msgid "A new ticket has been added to the medium."
|
||||
msgstr "Un nouveau billet a été ajouté sur le support."
|
||||
msgstr "Le nouveau membre a été ajouté à l'équipe."
|
||||
|
||||
#: pretix/control/logdisplay.py:747
|
||||
#, fuzzy
|
||||
#| msgid "{user} has been removed from the team."
|
||||
msgid "A ticket has been removed from the medium."
|
||||
msgstr "Un billet a été retiré du support."
|
||||
msgstr "{user} a été retiré de l'équipe."
|
||||
|
||||
#: pretix/control/logdisplay.py:748
|
||||
msgid "The medium has been connected to a new ticket."
|
||||
@@ -18670,8 +18712,6 @@ msgid ""
|
||||
"The ticket #{positionid} was exchanged for reusable medium "
|
||||
"{medium_identifier}."
|
||||
msgstr ""
|
||||
"Le ticket n° {positionid} a été échangé contre un support réutilisable "
|
||||
"{medium_identifier}."
|
||||
|
||||
#: pretix/control/logdisplay.py:750
|
||||
msgid "The medium has been connected to a new gift card."
|
||||
@@ -20587,8 +20627,10 @@ msgstr ""
|
||||
"l’enregistrement :"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/checkin/simulator.html:85
|
||||
#, fuzzy
|
||||
#| msgid "Special attention required"
|
||||
msgid "Media exchange required"
|
||||
msgstr "Échange de supports requis"
|
||||
msgstr "Une attention particulière est requise"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/checkin/simulator.html:87
|
||||
#, python-format
|
||||
@@ -20596,8 +20638,6 @@ msgid ""
|
||||
"This ticket needs to be exchanged into a <strong>%(media_type)s</strong> "
|
||||
"reusable medium. <strong>%(media_policy)s</strong>."
|
||||
msgstr ""
|
||||
"Ce billet doit être échangé contre un support réutilisable <strong>%"
|
||||
"(media_type)s</strong>. <strong>%(media_policy)s</strong>."
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/checkin/simulator.html:103
|
||||
msgid "Special attention required"
|
||||
@@ -27192,9 +27232,6 @@ msgid ""
|
||||
"Even if a team has no access to a certain category of data, they might still "
|
||||
"be able to see parts of this data when it is linked to data they can see."
|
||||
msgstr ""
|
||||
"Même si une équipe n'a pas accès à une certaine catégorie de données, elle "
|
||||
"peut néanmoins être en mesure de consulter certaines parties de ces données "
|
||||
"lorsque celles-ci sont liées à des données auxquelles elle a accès."
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/organizers/team_edit.html:35
|
||||
msgid ""
|
||||
@@ -27202,10 +27239,6 @@ msgid ""
|
||||
"some information about gift cards linked to a customer account, even if they "
|
||||
"generally can't see gift cards directly."
|
||||
msgstr ""
|
||||
"Par exemple, une personne ayant accès aux comptes clients pourra consulter "
|
||||
"certaines informations concernant les cartes cadeaux associées à un compte "
|
||||
"client, même si, en règle générale, elle ne peut pas voir directement ces "
|
||||
"cartes cadeaux."
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/organizers/team_edit.html:59
|
||||
msgid ""
|
||||
@@ -27213,9 +27246,6 @@ msgid ""
|
||||
"information about vouchers used to create an order, even if they generally "
|
||||
"can't see vouchers directly."
|
||||
msgstr ""
|
||||
"Par exemple, une personne ayant accès aux commandes pourra consulter "
|
||||
"certaines informations concernant les bons utilisés pour créer une commande, "
|
||||
"même si, en règle générale, elle ne peut pas consulter directement ces bons."
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/organizers/team_members.html:21
|
||||
msgid "Member"
|
||||
@@ -28087,22 +28117,30 @@ msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/subevents/detail.html:9
|
||||
#: pretix/control/templates/pretixcontrol/subevents/detail.html:13
|
||||
#, python-format
|
||||
#, fuzzy, python-format
|
||||
#| msgid "Quota: %(name)s"
|
||||
msgctxt "subevent"
|
||||
msgid "Date: %(name)s"
|
||||
msgstr "Date : %(name)s"
|
||||
msgstr "Quota : %(name)s"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/subevents/detail.html:234
|
||||
#, fuzzy
|
||||
#| msgid "Partially paid"
|
||||
msgid "partially canceled"
|
||||
msgstr "partiellement annulé"
|
||||
msgstr "Partiellement payé"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/subevents/detail.html:282
|
||||
#, fuzzy
|
||||
#| msgctxt "permission_level"
|
||||
#| msgid "View all"
|
||||
msgid "View all"
|
||||
msgstr "Tout afficher"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/subevents/detail.html:289
|
||||
#, fuzzy
|
||||
#| msgid "No archived events found."
|
||||
msgid "No orders found."
|
||||
msgstr "Aucune commande trouvée."
|
||||
msgstr "Aucun événement archivé trouvé."
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/subevents/detail.html:302
|
||||
#: pretix/control/templates/pretixcontrol/subevents/edit.html:279
|
||||
@@ -30900,8 +30938,10 @@ msgid "Voucher {}"
|
||||
msgstr "Bon {}"
|
||||
|
||||
#: pretix/control/views/typeahead.py:179 pretix/control/views/typeahead.py:180
|
||||
#, fuzzy
|
||||
#| msgid "Go to event"
|
||||
msgid "No event"
|
||||
msgstr "Aucun événement"
|
||||
msgstr "Aller à l'événement"
|
||||
|
||||
#: pretix/control/views/user.py:169
|
||||
msgid "The password you entered was invalid, please try again."
|
||||
|
||||
@@ -7,7 +7,7 @@ msgstr ""
|
||||
"Project-Id-Version: French\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-06-28 15:49+0000\n"
|
||||
"PO-Revision-Date: 2026-06-29 17:00+0000\n"
|
||||
"PO-Revision-Date: 2026-03-18 12:23+0000\n"
|
||||
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
|
||||
"Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||
"fr/>\n"
|
||||
@@ -16,7 +16,7 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n > 1;\n"
|
||||
"X-Generator: Weblate 2026.6.1\n"
|
||||
"X-Generator: Weblate 5.16.2\n"
|
||||
|
||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
|
||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
|
||||
@@ -443,11 +443,11 @@ msgstr "Appuyez sur Ctrl-C pour copier !"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue:80
|
||||
msgid "Edit"
|
||||
msgstr "Éditer"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue:86
|
||||
msgid "Visualize"
|
||||
msgstr "Visualiser"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue:96
|
||||
msgid ""
|
||||
@@ -455,14 +455,10 @@ msgid ""
|
||||
"or variations are not contained in any of your rule parts so people with "
|
||||
"these tickets will not get in:"
|
||||
msgstr ""
|
||||
"Votre règle effectue toujours un filtrage par produit ou variante, mais les "
|
||||
"produits ou variantes suivants ne figurent dans aucune des parties de votre "
|
||||
"règle ; par conséquent, les personnes détenant ces billets ne seront pas "
|
||||
"admises :"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/App.vue:99
|
||||
msgid "Please double-check if this was intentional."
|
||||
msgstr "Veuillez vérifier si cela était intentionnel."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules/constants.ts:4
|
||||
msgid "All of the conditions below (AND)"
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-06-28 14:42+0000\n"
|
||||
"PO-Revision-Date: 2026-06-30 16:52+0000\n"
|
||||
"PO-Revision-Date: 2026-06-20 17:00+0000\n"
|
||||
"Last-Translator: Nikita Mitasov <me@ch4og.com>\n"
|
||||
"Language-Team: Russian <https://translate.pretix.eu/projects/pretix/pretix/"
|
||||
"ru/>\n"
|
||||
@@ -35488,8 +35488,10 @@ msgstr "Введите промокод ниже, чтобы купить эт
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_availability.html:10
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_availability.html:14
|
||||
#, fuzzy
|
||||
#| msgid "Quota availabilities"
|
||||
msgid "Not available yet."
|
||||
msgstr "Еще недоступно."
|
||||
msgstr "Наличие квот"
|
||||
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_availability.html:18
|
||||
msgid "Not available any more."
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
|
||||
import json
|
||||
import logging
|
||||
import urllib.parse
|
||||
from collections import OrderedDict
|
||||
from decimal import Decimal
|
||||
|
||||
@@ -41,6 +42,7 @@ import paypalrestsdk
|
||||
import paypalrestsdk.exceptions
|
||||
from django import forms
|
||||
from django.contrib import messages
|
||||
from django.core import signing
|
||||
from django.http import HttpRequest
|
||||
from django.template.loader import get_template
|
||||
from django.urls import reverse
|
||||
@@ -56,7 +58,6 @@ from pretix.base.forms import SecretKeySettingsField
|
||||
from pretix.base.models import Event, Order, OrderPayment, OrderRefund, Quota
|
||||
from pretix.base.payment import BasePaymentProvider, PaymentException
|
||||
from pretix.base.settings import SettingsSandbox
|
||||
from pretix.base.views.redirect import safelink
|
||||
from pretix.multidomain.urlreverse import eventreverse_absolute
|
||||
from pretix.plugins.paypal.api import Api
|
||||
from pretix.plugins.paypal.models import ReferencedPayPalObject
|
||||
@@ -348,7 +349,11 @@ class Paypal(BasePaymentProvider):
|
||||
for link in payment.links:
|
||||
if link.method == "REDIRECT" and link.rel == "approval_url":
|
||||
if request.session.get('iframe_session', False):
|
||||
return safelink(link.href, framebreak=True)
|
||||
signer = signing.Signer(salt='safe-redirect')
|
||||
return (
|
||||
eventreverse_absolute(request.event, 'plugins:paypal:redirect') + '?url=' +
|
||||
urllib.parse.quote(signer.sign(link.href))
|
||||
)
|
||||
else:
|
||||
return str(link.href)
|
||||
else:
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
{% load compress %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{{ settings.PRETIX_INSTANCE_NAME }}</title>
|
||||
{% compress css %}
|
||||
<link rel="stylesheet" type="text/x-scss" href="{% static "pretixbase/scss/cachedfiles.scss" %}"/>
|
||||
{% endcompress %}
|
||||
{% compress js %}
|
||||
<script type="text/javascript" src="{% static "jquery/js/jquery-3.6.4.min.js" %}"></script>
|
||||
{% endcompress %}
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>{% trans "The payment process has started in a new window." %}</h1>
|
||||
|
||||
<p>
|
||||
{% trans "The window to enter your payment data was not opened or was closed?" %}
|
||||
</p>
|
||||
<p>
|
||||
<a href="{{ url }}" target="_blank" class="btn btn-default btn-lg">
|
||||
<span class="fa fa-external-link-square"></span>
|
||||
{% trans "Click here in order to open the window." %}
|
||||
</a>
|
||||
</p>
|
||||
<script>
|
||||
window.open('{{ url|escapejs }}');
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -21,12 +21,13 @@
|
||||
#
|
||||
from django.urls import include, re_path
|
||||
|
||||
from .views import abort, oauth_disconnect, success
|
||||
from .views import abort, oauth_disconnect, redirect_view, success
|
||||
|
||||
event_patterns = [
|
||||
re_path(r'^paypal/', include([
|
||||
re_path(r'^abort/$', abort, name='abort'),
|
||||
re_path(r'^return/$', success, name='return'),
|
||||
re_path(r'^redirect/$', redirect_view, name='redirect'),
|
||||
|
||||
re_path(r'w/(?P<cart_namespace>[a-zA-Z0-9]{16})/abort/', abort, name='abort'),
|
||||
re_path(r'w/(?P<cart_namespace>[a-zA-Z0-9]{16})/return/', success, name='return'),
|
||||
|
||||
@@ -39,10 +39,13 @@ from decimal import Decimal
|
||||
import paypalrestsdk
|
||||
import paypalrestsdk.exceptions
|
||||
from django.contrib import messages
|
||||
from django.core import signing
|
||||
from django.db.models import Sum
|
||||
from django.http import HttpResponse
|
||||
from django.http import HttpResponse, HttpResponseBadRequest
|
||||
from django.shortcuts import render
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.decorators.clickjacking import xframe_options_exempt
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.views.decorators.http import require_POST
|
||||
from django_scopes import scopes_disabled
|
||||
@@ -58,6 +61,21 @@ from pretix.plugins.paypal.payment import Paypal
|
||||
logger = logging.getLogger('pretix.plugins.paypal')
|
||||
|
||||
|
||||
@xframe_options_exempt
|
||||
def redirect_view(request, *args, **kwargs):
|
||||
signer = signing.Signer(salt='safe-redirect')
|
||||
try:
|
||||
url = signer.unsign(request.GET.get('url', ''))
|
||||
except signing.BadSignature:
|
||||
return HttpResponseBadRequest('Invalid parameter')
|
||||
|
||||
r = render(request, 'pretixplugins/paypal/redirect.html', {
|
||||
'url': url,
|
||||
})
|
||||
r._csp_ignore = True
|
||||
return r
|
||||
|
||||
|
||||
def success(request, *args, **kwargs):
|
||||
pid = request.GET.get('paymentId')
|
||||
token = request.GET.get('token')
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
{% load compress %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{{ settings.PRETIX_INSTANCE_NAME }}</title>
|
||||
{% compress css %}
|
||||
<link rel="stylesheet" type="text/x-scss" href="{% static "pretixbase/scss/cachedfiles.scss" %}"/>
|
||||
{% endcompress %}
|
||||
{% compress js %}
|
||||
<script type="text/javascript" src="{% static "jquery/js/jquery-3.6.4.min.js" %}"></script>
|
||||
{% endcompress %}
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>{% trans "The payment process has started in a new window." %}</h1>
|
||||
|
||||
<p>
|
||||
{% trans "The window to enter your payment data was not opened or was closed?" %}
|
||||
</p>
|
||||
<p>
|
||||
<a href="{{ url }}" target="_blank" class="btn btn-default btn-lg">
|
||||
<span class="fa fa-external-link-square"></span>
|
||||
{% trans "Click here in order to open the window." %}
|
||||
</a>
|
||||
</p>
|
||||
<script>
|
||||
window.open('{{ url|escapejs }}');
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -22,13 +22,15 @@
|
||||
from django.urls import include, re_path
|
||||
|
||||
from .views import (
|
||||
PayView, XHRView, abort, isu_disconnect, isu_return, success, webhook,
|
||||
PayView, XHRView, abort, isu_disconnect, isu_return, redirect_view,
|
||||
success, webhook,
|
||||
)
|
||||
|
||||
event_patterns = [
|
||||
re_path(r'^paypal2/', include([
|
||||
re_path(r'^abort/$', abort, name='abort'),
|
||||
re_path(r'^return/$', success, name='return'),
|
||||
re_path(r'^redirect/$', redirect_view, name='redirect'),
|
||||
re_path(r'^xhr/$', XHRView.as_view(), name='xhr'),
|
||||
re_path(r'^pay/(?P<order>[^/]+)/(?P<hash>[^/]+)/(?P<payment>[^/]+)/$', PayView.as_view(), name='pay'),
|
||||
re_path(r'^(?P<order>[^/][^w]+)/(?P<secret>[A-Za-z0-9]+)/xhr/$', XHRView.as_view(), name='xhr'),
|
||||
|
||||
@@ -36,10 +36,13 @@ import logging
|
||||
from decimal import Decimal
|
||||
|
||||
from django.contrib import messages
|
||||
from django.core import signing
|
||||
from django.core.cache import cache
|
||||
from django.db import transaction
|
||||
from django.db.models import Sum
|
||||
from django.http import Http404, HttpResponse, JsonResponse
|
||||
from django.http import (
|
||||
Http404, HttpResponse, HttpResponseBadRequest, JsonResponse,
|
||||
)
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
from django.utils.decorators import method_decorator
|
||||
@@ -101,6 +104,21 @@ class PaypalOrderView:
|
||||
}) + ('?paid=yes' if self.order.status == Order.STATUS_PAID else ''))
|
||||
|
||||
|
||||
@xframe_options_exempt
|
||||
def redirect_view(request, *args, **kwargs):
|
||||
signer = signing.Signer(salt='safe-redirect')
|
||||
try:
|
||||
url = signer.unsign(request.GET.get('url', ''))
|
||||
except signing.BadSignature:
|
||||
return HttpResponseBadRequest('Invalid parameter')
|
||||
|
||||
r = render(request, 'pretixplugins/paypal2/redirect.html', {
|
||||
'url': url,
|
||||
})
|
||||
r._csp_ignore = True
|
||||
return r
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name='dispatch')
|
||||
@method_decorator(xframe_options_exempt, 'dispatch')
|
||||
class XHRView(View):
|
||||
|
||||
@@ -46,6 +46,7 @@ import stripe
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.core import signing
|
||||
from django.db import transaction
|
||||
from django.http import HttpRequest
|
||||
from django.template.loader import get_template
|
||||
@@ -71,7 +72,6 @@ from pretix.base.payment import (
|
||||
)
|
||||
from pretix.base.plugins import get_all_plugins
|
||||
from pretix.base.settings import SettingsSandbox
|
||||
from pretix.base.views.redirect import safelink
|
||||
from pretix.helpers import OF_SELF
|
||||
from pretix.helpers.countries import CachedCountries
|
||||
from pretix.helpers.http import get_client_ip
|
||||
@@ -745,7 +745,15 @@ class StripeMethod(BasePaymentProvider):
|
||||
|
||||
def redirect(self, request, url):
|
||||
if request.session.get('iframe_session', False):
|
||||
return safelink(url, framebreak=True)
|
||||
return (
|
||||
eventreverse_absolute(request.event, 'plugins:stripe:redirect') +
|
||||
'?data=' + signing.dumps({
|
||||
'url': url,
|
||||
'session': {
|
||||
'payment_stripe_order_secret': request.session['payment_stripe_order_secret'],
|
||||
},
|
||||
}, salt='safe-redirect')
|
||||
)
|
||||
else:
|
||||
return str(url)
|
||||
|
||||
@@ -1045,7 +1053,11 @@ class StripeMethod(BasePaymentProvider):
|
||||
'hash': payment.order.tagged_secret('plugins:stripe'),
|
||||
})
|
||||
if not self.redirect_in_widget_allowed and request.session.get('iframe_session', False):
|
||||
return safelink(url, framebreak=True)
|
||||
return eventreverse_absolute(self.event, 'plugins:stripe:redirect') + '?data=' + signing.dumps({
|
||||
'url': url,
|
||||
'session': {},
|
||||
}, salt='safe-redirect')
|
||||
|
||||
return url
|
||||
|
||||
def _confirm_payment_intent(self, request, payment):
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
{% load compress %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{{ settings.PRETIX_INSTANCE_NAME }}</title>
|
||||
{% compress css %}
|
||||
<link rel="stylesheet" type="text/x-scss" href="{% static "pretixbase/scss/cachedfiles.scss" %}"/>
|
||||
{% endcompress %}
|
||||
{% compress js %}
|
||||
<script type="text/javascript" src="{% static "jquery/js/jquery-3.6.4.min.js" %}"></script>
|
||||
{% endcompress %}
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>{% trans "The payment process has started in a new window." %}</h1>
|
||||
|
||||
<p>
|
||||
{% trans "The window to enter your payment data was not opened or was closed?" %}
|
||||
</p>
|
||||
<p>
|
||||
<a href="{{ url }}" target="_blank" class="btn btn-default btn-lg">
|
||||
<span class="fa fa-external-link-square"></span>
|
||||
{% trans "Click here in order to open the window." %}
|
||||
</a>
|
||||
</p>
|
||||
<script>
|
||||
window.open('{{ url|escapejs }}');
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -25,12 +25,13 @@ from pretix.multidomain import event_url
|
||||
|
||||
from .views import (
|
||||
OrganizerSettingsFormView, ReturnView, ScaReturnView, ScaView,
|
||||
oauth_disconnect, oauth_return, webhook,
|
||||
oauth_disconnect, oauth_return, redirect_view, webhook,
|
||||
)
|
||||
|
||||
event_patterns = [
|
||||
re_path(r'^stripe/', include([
|
||||
event_url(r'^webhook/$', webhook, name='webhook', require_live=False),
|
||||
re_path(r'^redirect/$', redirect_view, name='redirect'),
|
||||
re_path(r'^return/(?P<order>[^/]+)/(?P<hash>[^/]+)/(?P<payment>[0-9]+)/$', ReturnView.as_view(), name='return'),
|
||||
re_path(r'^sca/(?P<order>[^/]+)/(?P<hash>[^/]+)/(?P<payment>[0-9]+)/$', ScaView.as_view(), name='sca'),
|
||||
re_path(r'^sca/(?P<order>[^/]+)/(?P<hash>[^/]+)/(?P<payment>[0-9]+)/return/$',
|
||||
|
||||
@@ -34,11 +34,13 @@
|
||||
|
||||
import json
|
||||
import logging
|
||||
import urllib.parse
|
||||
|
||||
import requests
|
||||
from django.contrib import messages
|
||||
from django.core import signing
|
||||
from django.db import transaction
|
||||
from django.http import Http404, HttpResponse
|
||||
from django.http import Http404, HttpResponse, HttpResponseBadRequest
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
from django.utils.decorators import method_decorator
|
||||
@@ -62,7 +64,7 @@ from pretix.control.views.event import DecoupleMixin
|
||||
from pretix.control.views.organizer import OrganizerDetailViewMixin
|
||||
from pretix.helpers import OF_SELF
|
||||
from pretix.helpers.http import redirect_to_url
|
||||
from pretix.multidomain.urlreverse import eventreverse
|
||||
from pretix.multidomain.urlreverse import eventreverse, eventreverse_absolute
|
||||
from pretix.plugins.stripe.forms import OrganizerStripeSettingsForm
|
||||
from pretix.plugins.stripe.models import ReferencedStripeObject
|
||||
from pretix.plugins.stripe.tasks import (
|
||||
@@ -72,6 +74,28 @@ from pretix.plugins.stripe.tasks import (
|
||||
logger = logging.getLogger('pretix.plugins.stripe')
|
||||
|
||||
|
||||
@xframe_options_exempt
|
||||
def redirect_view(request, *args, **kwargs):
|
||||
try:
|
||||
data = signing.loads(request.GET.get('data', ''), salt='safe-redirect')
|
||||
except signing.BadSignature:
|
||||
return HttpResponseBadRequest('Invalid parameter')
|
||||
|
||||
if 'go' in request.GET:
|
||||
if 'session' in data:
|
||||
for k, v in data['session'].items():
|
||||
request.session[k] = v
|
||||
return redirect(data['url'])
|
||||
else:
|
||||
params = request.GET.copy()
|
||||
params['go'] = '1'
|
||||
r = render(request, 'pretixplugins/stripe/redirect.html', {
|
||||
'url': eventreverse_absolute(request.event, 'plugins:stripe:redirect') + '?' + urllib.parse.urlencode(params),
|
||||
})
|
||||
r._csp_ignore = True
|
||||
return r
|
||||
|
||||
|
||||
@scopes_disabled()
|
||||
def oauth_return(request, *args, **kwargs):
|
||||
import stripe
|
||||
@@ -490,6 +514,11 @@ class StripeOrderView:
|
||||
return self.request.event.get_payment_providers()[self.payment.provider]
|
||||
|
||||
def _redirect_to_order(self):
|
||||
if self.request.session.get('payment_stripe_order_secret') != self.order.secret and not self.payment.provider.startswith('stripe'):
|
||||
messages.error(self.request, _('Sorry, there was an error in the payment process. Please check the link '
|
||||
'in your emails to continue.'))
|
||||
return redirect_to_url(eventreverse(self.request.event, 'presale:event.index'))
|
||||
|
||||
return redirect_to_url(eventreverse(self.request.event, 'presale:event.order', kwargs={
|
||||
'order': self.order.code,
|
||||
'secret': self.order.secret
|
||||
|
||||
@@ -33,7 +33,7 @@ from django.contrib.auth.password_validation import (
|
||||
from django.contrib.auth.tokens import PasswordResetTokenGenerator
|
||||
from django.core import signing
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.html import escape, format_html
|
||||
from django.utils.html import escape
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from phonenumber_field.formfields import PhoneNumberField
|
||||
|
||||
@@ -83,8 +83,7 @@ class AuthenticationForm(forms.Form):
|
||||
self.request = request
|
||||
self.customer_cache = None
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['password'].help_text = format_html(
|
||||
"<a target='_blank' href='{}'>{}</a>",
|
||||
self.fields['password'].help_text = "<a target='_blank' href='{}'>{}</a>".format(
|
||||
eventreverse_absolute(False, 'presale:organizer.customer.resetpw', kwargs={
|
||||
'organizer': request.organizer.slug,
|
||||
}),
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
{% load i18n %}
|
||||
{% load eventurl %}
|
||||
{% load urlreplace %}
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
{% if cart_namespace %}
|
||||
@@ -24,8 +23,9 @@
|
||||
class="btn btn-primary btn-lg" target="_blank">
|
||||
{% trans "Continue in new tab" %}
|
||||
</a>
|
||||
{{ url|json_script:"framebreak-url" }}
|
||||
<script type="text/javascript" src="{% static "pretixbase/js/framebreak.js" %}"></script>
|
||||
<script>
|
||||
window.open('{{ url|escapejs }}');
|
||||
</script>
|
||||
</div>
|
||||
{% else %}
|
||||
<h1>{% trans "Cookies not supported" %}</h1>
|
||||
|
||||
@@ -14,23 +14,10 @@
|
||||
{% trans "Log out" %}
|
||||
</a>
|
||||
{% else %}
|
||||
<form
|
||||
{% if request.event_domain %}
|
||||
action="{% abseventurl request.event "presale:event.customer.loginstart" %}" method="post"
|
||||
{% else %}
|
||||
action="{% abseventurl request.organizer "presale:organizer.customer.login" %}" method="get"
|
||||
{% endif %}
|
||||
class="helper-display-inline">
|
||||
{% if request.event_domain %}
|
||||
{% csrf_token %}
|
||||
{% endif %}
|
||||
{% if request.resolver_match.url_name != "organizer.customer.login" %}
|
||||
<input type="hidden" name="next" value="{% if request.event_domain %}{{ request.scheme }}://{{ request.get_host }}{% endif %}{{ request.path }}?{{ request.META.QUERY_STRING }}">
|
||||
{% endif %}
|
||||
<button class="btn btn-link" type="submit">
|
||||
<span class="fa fa-sign-in" aria-hidden="true"></span>
|
||||
{% trans "Log in" %}</button>
|
||||
</form>
|
||||
<a href="{% abseventurl request.organizer "presale:organizer.customer.login" %}{% if request.resolver_match.url_name != "organizer.customer.login" %}?next={% if request.event_domain %}{{ request.scheme }}://{{ request.get_host }}{% endif %}{{ request.path|urlencode }}%3F{{ request.META.QUERY_STRING|urlencode }}{% endif %}{% if request.event_domain %}&request_cross_domain_customer_auth=true{% endif %}">
|
||||
<span class="fa fa-sign-in" aria-hidden="true"></span>
|
||||
{% trans "Log in" %}</a>
|
||||
|
||||
{% endif %}
|
||||
</nav>
|
||||
{% endif %}
|
||||
|
||||
@@ -98,7 +98,6 @@ event_patterns = [
|
||||
re_path(r'unlock/(?P<hash>[a-z0-9]{64})/$', pretix.presale.views.user.UnlockHashView.as_view(),
|
||||
name='event.payment.unlock'),
|
||||
re_path(r'resend/$', pretix.presale.views.user.ResendLinkView.as_view(), name='event.resend_link'),
|
||||
re_path(r'^account/loginstart$', pretix.presale.views.customer.LoginStartView.as_view(), name='event.customer.loginstart'),
|
||||
|
||||
re_path(r'^favicon.ico/?$',
|
||||
pretix.presale.views.organizer.OrganizerFavicon.as_view(),
|
||||
|
||||
@@ -151,9 +151,7 @@ def add_customer_to_request(request):
|
||||
else:
|
||||
parent_session_key = otpstore.get(f'customer_cross_domain_auth_{request.organizer.pk}')
|
||||
|
||||
expected_nonce = request.session.pop('cross_domain_customer_auth_nonce', None)
|
||||
found_nonce = request.GET.get("cross_domain_customer_auth_nonce")
|
||||
if parent_session_key and expected_nonce and expected_nonce == found_nonce: # not already invalidated, expired, …
|
||||
if parent_session_key: # not already invalidated, expired, …
|
||||
# Make sure the OTP can't be used again
|
||||
otpstore.delete()
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
#
|
||||
from urllib.parse import quote, urlencode
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.http import Http404
|
||||
from django.utils.decorators import method_decorator
|
||||
@@ -30,7 +29,6 @@ from django.views.generic import View
|
||||
|
||||
from pretix.base.services.cart import CartError
|
||||
from pretix.base.signals import validate_cart
|
||||
from pretix.base.views.tasks import AsyncAction
|
||||
from pretix.helpers.http import redirect_to_url
|
||||
from pretix.multidomain.urlreverse import eventreverse
|
||||
from pretix.presale.checkoutflow import get_checkout_flow
|
||||
@@ -53,12 +51,7 @@ class CheckoutView(View):
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
self.request = request
|
||||
|
||||
is_asyncaction_call = (
|
||||
request.method == "GET" and
|
||||
'async_id' in request.GET and
|
||||
settings.HAS_CELERY
|
||||
)
|
||||
if not cart_exists(request) and not is_asyncaction_call:
|
||||
if not cart_exists(request) and "async_id" not in request.GET:
|
||||
messages.error(request, _("Your cart is empty"))
|
||||
return self.redirect(self.get_index_url(self.request))
|
||||
|
||||
@@ -85,9 +78,7 @@ class CheckoutView(View):
|
||||
utm_params = {k: v for k, v in request.GET.items() if k.startswith("utm_")}
|
||||
return self.redirect(step.get_step_url(request) + '?' + urlencode(utm_params))
|
||||
is_selected = (step.identifier == kwargs.get('step', ''))
|
||||
|
||||
is_valid_asyncaction_call = is_asyncaction_call and isinstance(step, AsyncAction)
|
||||
if not is_valid_asyncaction_call and not is_selected and not step.is_completed(request, warn=not is_selected):
|
||||
if "async_id" not in request.GET and not is_selected and not step.is_completed(request, warn=not is_selected):
|
||||
return self.redirect(step.get_step_url(request))
|
||||
if is_selected:
|
||||
if request.method.lower() in self.http_method_names:
|
||||
|
||||
@@ -146,7 +146,6 @@ class LoginView(RedirectBackMixin, FormView):
|
||||
u = urlparse(url)
|
||||
qsl = parse_qs(u.query)
|
||||
qsl['cross_domain_customer_auth'] = otp
|
||||
qsl['cross_domain_customer_auth_nonce'] = self.request.GET.get("request_cross_domain_customer_auth_nonce", "")
|
||||
url = urlunparse((u.scheme, u.netloc, u.path, u.params, urlencode(qsl, doseq=True), u.fragment))
|
||||
|
||||
return url
|
||||
@@ -706,7 +705,6 @@ class SSOLoginView(RedirectBackMixin, View):
|
||||
request.session[f'pretix_customerauth_{self.provider.pk}_nonce'] = nonce
|
||||
request.session[f'pretix_customerauth_{self.provider.pk}_popup_origin'] = popup_origin
|
||||
request.session[f'pretix_customerauth_{self.provider.pk}_cross_domain_requested'] = self.request.GET.get("request_cross_domain_customer_auth") == "true"
|
||||
request.session[f'pretix_customerauth_{self.provider.pk}_cross_domain_nonce'] = self.request.GET.get("request_cross_domain_customer_auth_nonce")
|
||||
redirect_uri = eventreverse_absolute(self.request.organizer, 'presale:organizer.customer.login.return', kwargs={
|
||||
'provider': self.provider.pk
|
||||
})
|
||||
@@ -957,28 +955,6 @@ class SSOLoginReturnView(RedirectBackMixin, View):
|
||||
u = urlparse(url)
|
||||
qsl = parse_qs(u.query)
|
||||
qsl['cross_domain_customer_auth'] = otp
|
||||
qsl['cross_domain_customer_auth_nonce'] = self.request.session.get(f'pretix_customerauth_{self.provider.pk}_cross_domain_nonce', '')
|
||||
url = urlunparse((u.scheme, u.netloc, u.path, u.params, urlencode(qsl, doseq=True), u.fragment))
|
||||
|
||||
return url
|
||||
|
||||
|
||||
class LoginStartView(View):
|
||||
# When a login is initiated on a event-domain-level view, we need to carry the user to the organizer domain through
|
||||
# this POST request to be able to set a nonce on their current session. We can't just use a link, since then we'd
|
||||
# need to create sessions for every anonymous user of the ticketshop, which is too expensive.
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
if getattr(self.request, 'domain_mode', 'system') not in (KnownDomain.MODE_ORG_ALT_DOMAIN, KnownDomain.MODE_EVENT_DOMAIN):
|
||||
raise Http404("Only active on event-level domains")
|
||||
|
||||
nonce = get_random_string(32)
|
||||
request.session['cross_domain_customer_auth_nonce'] = nonce
|
||||
query = {
|
||||
"next": request.POST.get("next", ""),
|
||||
"request_cross_domain_customer_auth_nonce": nonce,
|
||||
"request_cross_domain_customer_auth": "true",
|
||||
}
|
||||
return redirect_to_url(
|
||||
eventreverse_absolute(self.request.organizer, "presale:organizer.customer.login") + "?" + urlencode(query)
|
||||
)
|
||||
|
||||
@@ -536,6 +536,7 @@ 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()):
|
||||
|
||||
@@ -42,7 +42,7 @@ import os
|
||||
import re
|
||||
from collections import Counter, OrderedDict, defaultdict
|
||||
from decimal import Decimal
|
||||
from urllib.parse import quote, urlencode
|
||||
from urllib.parse import quote
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
@@ -55,7 +55,6 @@ from django.http import (
|
||||
FileResponse, Http404, HttpResponseRedirect, JsonResponse,
|
||||
)
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.timezone import now
|
||||
@@ -118,14 +117,8 @@ class OrderDetailMixin(NoSearchIndexViewMixin):
|
||||
login_url = eventreverse(self.request.organizer, 'presale:organizer.customer.login', kwargs={})
|
||||
|
||||
if hasattr(self.request, "event_domain") and self.request.event_domain:
|
||||
nonce = get_random_string(32)
|
||||
self.request.session['cross_domain_customer_auth_nonce'] = nonce
|
||||
query = {
|
||||
"next": self.request.scheme + "://" + self.request.get_host() + self.request.get_full_path(),
|
||||
"request_cross_domain_customer_auth_nonce": nonce,
|
||||
"request_cross_domain_customer_auth": "true",
|
||||
}
|
||||
return redirect_to_url(f'{login_url}?{urlencode(query)}')
|
||||
next_url = quote(self.request.scheme + "://" + self.request.get_host() + self.request.get_full_path())
|
||||
return redirect_to_url(f'{login_url}?next={next_url}&request_cross_domain_customer_auth=true')
|
||||
|
||||
else:
|
||||
next_url = quote(self.request.get_full_path())
|
||||
|
||||
@@ -125,6 +125,7 @@ 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:
|
||||
|
||||
@@ -624,9 +624,6 @@ LOGGING = {
|
||||
'request_id': {
|
||||
'()': 'pretix.helpers.logs.RequestIdFilter'
|
||||
},
|
||||
'skip_not_found': {
|
||||
'()': 'pretix.helpers.logs.SkipNotFoundFilter',
|
||||
}
|
||||
},
|
||||
'handlers': {
|
||||
'console': {
|
||||
@@ -668,7 +665,6 @@ LOGGING = {
|
||||
'handlers': ['file', 'console', 'mail_admins'],
|
||||
'level': loglevel,
|
||||
'propagate': True,
|
||||
'filters': ['skip_not_found'],
|
||||
},
|
||||
'pretix.security.csp': {
|
||||
'handlers': ['csp_file'],
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
// 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)
|
||||
@@ -221,11 +221,6 @@ footer nav .btn-link {
|
||||
/*border-bottom: 2px solid $brand-primary;*/
|
||||
font-weight: bold;
|
||||
}
|
||||
.btn-link {
|
||||
padding: 0;
|
||||
margin-left: 5px;
|
||||
vertical-align: top;
|
||||
}
|
||||
img {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
@@ -658,35 +653,6 @@ h2 .label {
|
||||
}
|
||||
|
||||
|
||||
.helper-position-relative {
|
||||
position: relative;
|
||||
}
|
||||
.helper-display-block {
|
||||
display: block !important;
|
||||
}
|
||||
.helper-display-inline {
|
||||
display: inline !important;
|
||||
}
|
||||
.helper-display-inline-block {
|
||||
display: inline-block !important;
|
||||
}
|
||||
.helper-display-none-soft {
|
||||
display: none;
|
||||
}
|
||||
.helper-display-none {
|
||||
display: none !important;
|
||||
}
|
||||
.helper-width-auto {
|
||||
width: auto;
|
||||
}
|
||||
.helper-width-100 {
|
||||
width: 100%;
|
||||
}
|
||||
.helper-space-below {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
|
||||
@import "_iframe.scss";
|
||||
@import "_a11y.scss";
|
||||
@import "_print.scss";
|
||||
|
||||
@@ -252,7 +252,7 @@ TEST_HISTORY_RES = {
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
@pytest.mark.django_db
|
||||
def test_list_list(token_client, organizer, event, clist, item, subevent, django_assert_num_queries):
|
||||
res = dict(TEST_LIST_RES)
|
||||
res["id"] = clist.pk
|
||||
@@ -422,7 +422,7 @@ def test_list_update(token_client, organizer, event, clist):
|
||||
assert cl.name == "VIP"
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
@pytest.mark.django_db
|
||||
def test_list_all_items_positions(token_client, organizer, event, clist, clist_all, item, other_item, order, django_assert_num_queries):
|
||||
with scopes_disabled():
|
||||
p1 = dict(TEST_ORDERPOSITION1_RES)
|
||||
@@ -680,7 +680,7 @@ def _redeem(token_client, org, clist, p, body=None):
|
||||
), body or {}, format='json')
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
@pytest.mark.django_db
|
||||
def test_query_load(token_client, organizer, clist, event, order, django_assert_max_num_queries):
|
||||
with scopes_disabled():
|
||||
p = order.positions.first().pk
|
||||
@@ -1367,7 +1367,7 @@ def test_redeem_addon_if_match_and_revoked_force(token_client, organizer, clist,
|
||||
assert ci.position == p
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
@pytest.mark.django_db
|
||||
def test_search(token_client, organizer, event, clist, clist_all, item, other_item, order, django_assert_max_num_queries):
|
||||
with scopes_disabled():
|
||||
p1 = dict(TEST_ORDERPOSITION1_RES)
|
||||
@@ -1399,7 +1399,7 @@ def test_checkin_pdf_data_requires_permission(token_client, event, team, organiz
|
||||
assert not resp.data['results'][0].get('pdf_data')
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
@pytest.mark.django_db
|
||||
def test_expand(token_client, organizer, event, clist, clist_all, item, other_item, order, django_assert_max_num_queries):
|
||||
with scopes_disabled():
|
||||
op = order.positions.first()
|
||||
|
||||
@@ -214,7 +214,7 @@ def _redeem(token_client, org, clist, p, body=None, query='', headers={}):
|
||||
), body, format='json', headers={})
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
@pytest.mark.django_db
|
||||
def test_query_load(token_client, organizer, clist, event, order, django_assert_max_num_queries):
|
||||
with scopes_disabled():
|
||||
p = order.positions.first()
|
||||
@@ -996,7 +996,7 @@ def test_redeem_conflicting_lists(token_client, organizer, clist, clist_all, eve
|
||||
assert resp.data == ['Selecting two check-in lists from the same event is unsupported.']
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
@pytest.mark.django_db
|
||||
def test_search(token_client, organizer, event, clist, clist_all, item, other_item, order,
|
||||
django_assert_max_num_queries):
|
||||
with scopes_disabled():
|
||||
|
||||
@@ -1826,7 +1826,7 @@ def test_event_block_unblock_seat_bulk(token_client, organizer, event, seatingpl
|
||||
assert not s2.blocked
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
@pytest.mark.django_db
|
||||
def test_event_expand_seat_filter_and_querycount(token_client, organizer, event, seatingplan, item):
|
||||
event.settings.seating_minimal_distance = 2
|
||||
|
||||
|
||||
@@ -70,27 +70,14 @@ def admin_user(admin_team):
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def app_developer():
|
||||
return User.objects.create_user('app-developer@example.org', 'app-developer')
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client2():
|
||||
# We need a second test client instance to log in as the app developer user
|
||||
from django.test import Client
|
||||
return Client()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def application(app_developer):
|
||||
def application():
|
||||
secret = get_random_string(32)
|
||||
a = OAuthApplication.objects.create(
|
||||
name="pretalx",
|
||||
redirect_uris="https://pretalx.com",
|
||||
client_type='confidential',
|
||||
client_secret=secret,
|
||||
authorization_grant_type='authorization-code',
|
||||
user=app_developer,
|
||||
authorization_grant_type='authorization-code'
|
||||
)
|
||||
a._cached_secret = secret
|
||||
a.save()
|
||||
@@ -716,47 +703,6 @@ def test_token_revoke_access_token(client, admin_user, organizer, application: O
|
||||
assert list(grant.organizers.all()) == [organizer]
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_token_app_disabled(client, client2, admin_user, organizer, application: OAuthApplication, app_developer):
|
||||
client.login(email='dummy@dummy.dummy', password='dummy')
|
||||
session = client.session
|
||||
session['pretix_auth_login_time'] = int(time.time())
|
||||
session.save()
|
||||
resp = client.get('/api/v1/oauth/authorize?client_id=%s&redirect_uri=%s&response_type=code' % (
|
||||
application.client_id, quote(application.redirect_uris)
|
||||
))
|
||||
assert resp.status_code == 200
|
||||
resp = client.post('/api/v1/oauth/authorize', data={
|
||||
'organizers': str(organizer.pk),
|
||||
'redirect_uri': application.redirect_uris,
|
||||
'scope': 'read write',
|
||||
'client_id': application.client_id,
|
||||
'response_type': 'code',
|
||||
'allow': 'Authorize',
|
||||
})
|
||||
assert resp.status_code == 302
|
||||
assert resp['Location'].startswith('https://pretalx.com?code=')
|
||||
code = resp['Location'].split("=")[1]
|
||||
client.logout()
|
||||
resp = client.post('/api/v1/oauth/token', data={
|
||||
'code': code,
|
||||
'redirect_uri': application.redirect_uris,
|
||||
'grant_type': 'authorization_code',
|
||||
}, HTTP_AUTHORIZATION='Basic ' + base64.b64encode(
|
||||
('%s:%s' % (application.client_id, application._cached_secret)).encode()).decode())
|
||||
assert resp.status_code == 200
|
||||
data = json.loads(resp.content.decode())
|
||||
access_token = data['access_token']
|
||||
resp = client.get('/api/v1/organizers/dummy/events/', HTTP_AUTHORIZATION='Bearer %s' % access_token)
|
||||
assert resp.status_code == 200
|
||||
|
||||
client2.login(email='app-developer@example.org', password='app-developer')
|
||||
client2.post(f'/control/settings/oauth/apps/{application.pk}/disable', {})
|
||||
|
||||
resp = client.get('/api/v1/organizers/dummy/events/', HTTP_AUTHORIZATION='Bearer %s' % access_token)
|
||||
assert resp.status_code == 401
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_user_revoke(client, admin_user, organizer, application: OAuthApplication):
|
||||
client.login(email='dummy@dummy.dummy', password='dummy')
|
||||
|
||||
@@ -1058,7 +1058,7 @@ def test_orderposition_list_limited_read(
|
||||
('/api/v1/organizers/{}/orderpositions/', "organizer")
|
||||
],
|
||||
)
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
@pytest.mark.django_db
|
||||
def test_orderposition_list(
|
||||
endpoint_template,
|
||||
endpoint_type,
|
||||
@@ -2036,7 +2036,7 @@ def test_blocked_secret_list(token_client, organizer, event):
|
||||
assert [res] == resp.data['results']
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
@pytest.mark.django_db
|
||||
def test_pdf_data(token_client, organizer, event, order, django_assert_max_num_queries):
|
||||
# order detail
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/orders/{}/?pdf_data=true'.format(
|
||||
|
||||
@@ -732,7 +732,7 @@ def test_five_tickets_one_free(event):
|
||||
|
||||
|
||||
@scopes_disabled()
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize("itemcount", [3, 10, 50])
|
||||
def test_query_count_many_items(event, itemcount):
|
||||
setup_items(event, 'Tickets', 'both', 'discounts',
|
||||
@@ -784,7 +784,7 @@ def test_query_count_many_items(event, itemcount):
|
||||
|
||||
|
||||
@scopes_disabled()
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize("catcount", [1, 10, 50])
|
||||
def test_query_count_many_categories_and_discounts(event, catcount):
|
||||
for n in range(1, catcount + 1):
|
||||
@@ -838,7 +838,7 @@ def test_query_count_many_categories_and_discounts(event, catcount):
|
||||
|
||||
|
||||
@scopes_disabled()
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize("catcount", [2, 10, 50])
|
||||
def test_query_count_many_cartpos(event, catcount):
|
||||
for n in range(1, catcount + 1):
|
||||
|
||||
@@ -210,7 +210,7 @@ def test_validate_membership_required(event, customer, membership, requiring_tic
|
||||
assert "requires an active" in str(excinfo.value)
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
@pytest.mark.django_db
|
||||
def test_validate_membership_ensure_locking(event, customer, membership, requiring_ticket, membership_type, django_assert_num_queries):
|
||||
with django_assert_num_queries(4) as captured:
|
||||
validate_memberships_in_order(
|
||||
|
||||
@@ -119,7 +119,7 @@ def test_linkify_abs(link):
|
||||
assert markdown_compile_email(input) == f"<p>{output}</p>"
|
||||
|
||||
|
||||
signer = signing.Signer(salt='safelink-url')
|
||||
signer = signing.Signer(salt='safe-redirect')
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
||||
@@ -28,7 +28,6 @@ from django.test import override_settings
|
||||
from django.utils import translation
|
||||
from django_scopes import scopes_disabled
|
||||
from fakeredis import FakeRedisConnection
|
||||
from hierarkey.proxy import dirty_cache_keys
|
||||
from xdist.dsession import DSession
|
||||
|
||||
from pretix.testutils.mock import get_redis_connection
|
||||
@@ -83,11 +82,6 @@ def reset_locale():
|
||||
translation.activate("en")
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def reset_hierarkey_cache_state():
|
||||
dirty_cache_keys.set(set())
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fakeredis_client(monkeypatch):
|
||||
worker_id = os.environ.get("PYTEST_XDIST_WORKER")
|
||||
|
||||
@@ -90,7 +90,6 @@ event_urls = [
|
||||
"delete/",
|
||||
"dangerzone/",
|
||||
"cancel/",
|
||||
"quickstart/",
|
||||
"settings/",
|
||||
"settings/plugins",
|
||||
"settings/payment",
|
||||
@@ -312,7 +311,6 @@ event_permission_urls = [
|
||||
("event.settings.general:write", "live/", 200, HTTP_GET),
|
||||
("event.settings.general:write", "delete/", 200, HTTP_GET),
|
||||
("event.settings.general:write", "dangerzone/", 200, HTTP_GET),
|
||||
("event.settings.general:write", "quickstart/", 200, HTTP_GET),
|
||||
("event.settings.general:write", "settings/", 200, HTTP_GET),
|
||||
# ("event.settings.payment:write", "settings/payment", 200, HTTP_GET), GET allowed also with other permissions
|
||||
("event.settings.payment:write", "settings/payment", 200, HTTP_POST),
|
||||
|
||||
@@ -5959,14 +5959,3 @@ class CustomerCheckoutTestCase(BaseCheckoutTestCase, TestCase):
|
||||
response = self.client.get('/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug), follow=True)
|
||||
assert 'Gift card' in response.content.decode()
|
||||
assert '(1 available)' in response.content.decode()
|
||||
|
||||
def test_validation_bypass_error_async_id_is_fixed(self):
|
||||
with scopes_disabled():
|
||||
CartPosition.objects.create(
|
||||
event=self.event, cart_id=self.session_key, item=self.ticket,
|
||||
price=0, listed_price=0, price_after_voucher=0, expires=now() + timedelta(minutes=10)
|
||||
)
|
||||
|
||||
response = self.client.post('/%s/%s/checkout/confirm/?async_id=1' % (self.orga.slug, self.event.slug), follow=False)
|
||||
self.assertRedirects(response, '/%s/%s/checkout/customer/' % (self.orga.slug, self.event.slug),
|
||||
target_status_code=200)
|
||||
|
||||
@@ -721,21 +721,9 @@ def _cross_domain_login(env, client, client2, org_alt=False):
|
||||
else:
|
||||
KnownDomain.objects.create(domainname='event.test', organizer=env[0], event=env[1])
|
||||
|
||||
# Start session on event domain
|
||||
path = '/conf/' if org_alt else '/'
|
||||
r = client2.post(f'{path}account/loginstart', {
|
||||
'next': f'https://event.test{path}redeem',
|
||||
}, HTTP_HOST='event.test')
|
||||
assert r.status_code == 302
|
||||
u = urlparse(r.headers['Location'])
|
||||
assert u.netloc == 'org.test'
|
||||
assert u.path == '/account/login'
|
||||
assert 'request_cross_domain_customer_auth=' in u.query
|
||||
assert 'request_cross_domain_customer_auth_nonce=' in u.query
|
||||
assert 'next=' in u.query
|
||||
|
||||
# Log in on org domain
|
||||
r = client.post(f'{u.path}?{u.query}', {
|
||||
path = '/conf/' if org_alt else '/'
|
||||
r = client.post(f'/account/login?next=https://event.test{path}redeem&request_cross_domain_customer_auth=true', {
|
||||
'email': 'john@example.org',
|
||||
'password': 'foo',
|
||||
}, HTTP_HOST='org.test')
|
||||
@@ -746,7 +734,6 @@ def _cross_domain_login(env, client, client2, org_alt=False):
|
||||
assert u.path == path + 'redeem'
|
||||
q = parse_qs(u.query)
|
||||
assert 'cross_domain_customer_auth' in q
|
||||
assert 'cross_domain_customer_auth_nonce' in q
|
||||
|
||||
# Take session over to event domain
|
||||
r = client2.get(f'{path}?{u.query}', HTTP_HOST='event.test')
|
||||
@@ -758,7 +745,7 @@ def _cross_domain_login(env, client, client2, org_alt=False):
|
||||
def test_cross_domain_login(env, client, client2):
|
||||
_cross_domain_login(env, client, client2)
|
||||
|
||||
# Logged in on event domain
|
||||
# Logged in on evnet domain
|
||||
r = client.get('/', HTTP_HOST='event.test')
|
||||
assert r.status_code == 200
|
||||
assert b'john@example.org' in r.content
|
||||
@@ -909,14 +896,7 @@ def test_cross_domain_login_with_sso(env, client, client2, provider):
|
||||
},
|
||||
)
|
||||
|
||||
r = client2.post('/account/loginstart', {"next": "https://event.test/redeem"}, follow=False, HTTP_HOST='event.test')
|
||||
assert r.status_code == 302
|
||||
assert "/account/login" in r['Location']
|
||||
|
||||
u = urlparse(r.headers['Location'])
|
||||
nonce = parse_qs(u.query)['request_cross_domain_customer_auth_nonce'][0]
|
||||
url = (f'/account/login/{provider.pk}/?next=https://event.test/redeem&request_cross_domain_customer_auth=true&'
|
||||
f'request_cross_domain_customer_auth_nonce={nonce}')
|
||||
url = f'/account/login/{provider.pk}/?next=https://event.test/redeem&request_cross_domain_customer_auth=true'
|
||||
r = client.get(url, follow=False, HTTP_HOST='org.test')
|
||||
assert r.status_code == 302
|
||||
assert "/authorize" in r['Location']
|
||||
@@ -930,7 +910,6 @@ def test_cross_domain_login_with_sso(env, client, client2, provider):
|
||||
assert u.path == '/redeem'
|
||||
q = parse_qs(u.query)
|
||||
assert 'cross_domain_customer_auth' in q
|
||||
assert 'cross_domain_customer_auth_nonce' in q
|
||||
|
||||
# Take session over to event domain
|
||||
r = client2.get(f'/?{u.query}', HTTP_HOST='event.test')
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
#
|
||||
# This file is part of pretix (Community Edition).
|
||||
#
|
||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||
#
|
||||
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||||
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||||
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||||
# this file, see <https://pretix.eu/about/en/license>.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
|
||||
def test_start_with_redis_down():
|
||||
"""
|
||||
This is a test that ensures that pretix is able to start without a running redis server,
|
||||
even if one is configured.
|
||||
"""
|
||||
with tempfile.NamedTemporaryFile(suffix="cfg") as f:
|
||||
f.write(b"[redis]\nlocation=redis://127.0.0.99:65534/2\n")
|
||||
f.flush()
|
||||
|
||||
assert subprocess.check_call(
|
||||
[
|
||||
sys.executable,
|
||||
os.path.join(os.path.dirname(__file__), '../manage.py'),
|
||||
"noop",
|
||||
],
|
||||
env={
|
||||
"PRETIX_CONFIG_FILE": f.name,
|
||||
}
|
||||
) == 0
|
||||
Reference in New Issue
Block a user