mirror of
https://github.com/pretix/pretix.git
synced 2026-05-16 17:03:58 +00:00
Compare commits
34 Commits
fix-csv-im
...
pajowu/wid
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7cd54c9c5c | ||
|
|
4530d864d3 | ||
|
|
b968266611 | ||
|
|
640518c1b3 | ||
|
|
0715144a31 | ||
|
|
58ea7c8656 | ||
|
|
a8fe6f505e | ||
|
|
baeec92203 | ||
|
|
2f9ac05184 | ||
|
|
4beea63b49 | ||
|
|
5e49df0ef6 | ||
|
|
b3bb9fccb5 | ||
|
|
e3ffd66691 | ||
|
|
0f2ebb8687 | ||
|
|
efd887b439 | ||
|
|
8690d65e99 | ||
|
|
5682d3ed56 | ||
|
|
059ff6c99b | ||
|
|
f46fc7fa69 | ||
|
|
3473fa738d | ||
|
|
6c7163406e | ||
|
|
49729d2c87 | ||
|
|
e80b4b560b | ||
|
|
0bb04ca8f0 | ||
|
|
f50548cd02 | ||
|
|
bb450e1be9 | ||
|
|
6d07530d2b | ||
|
|
5d7ee584d9 | ||
|
|
58cce4b85e | ||
|
|
aa420d4353 | ||
|
|
d2ca217cd8 | ||
|
|
cb6d3967a0 | ||
|
|
221cbd15ab | ||
|
|
5c7104634e |
@@ -31,7 +31,9 @@ from pretix.api.serializers.order import OrderPositionSerializer
|
||||
from pretix.api.serializers.organizer import (
|
||||
CustomerSerializer, GiftCardSerializer,
|
||||
)
|
||||
from pretix.base.models import Order, OrderPosition, ReusableMedium
|
||||
from pretix.base.models import (
|
||||
Device, Order, OrderPosition, ReusableMedium, TeamAPIToken,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -80,8 +82,7 @@ class ReusableMediaSerializer(I18nAwareModelSerializer):
|
||||
)
|
||||
|
||||
if 'linked_orderposition' in self.context['request'].query_params.getlist('expand'):
|
||||
# No additional permission check performed, documented limitation of the permission system
|
||||
# Would get to complex/unusable otherwise since the permission depends on the event
|
||||
# Permission Check performed in to_representation
|
||||
self.fields['linked_orderposition'] = NestedOrderPositionSerializer(read_only=True)
|
||||
else:
|
||||
self.fields['linked_orderposition'] = serializers.PrimaryKeyRelatedField(
|
||||
@@ -117,6 +118,27 @@ class ReusableMediaSerializer(I18nAwareModelSerializer):
|
||||
)
|
||||
return data
|
||||
|
||||
def to_representation(self, instance):
|
||||
r = super().to_representation(instance)
|
||||
request = self.context.get('request')
|
||||
# late permission evaluations for checks that depend on the actual linked events
|
||||
expand_nested = self.context['request'].query_params.getlist('expand')
|
||||
perm_holder = request.auth if isinstance(request.auth, (Device, TeamAPIToken)) else request.user
|
||||
if 'linked_orderposition' in expand_nested:
|
||||
if instance.linked_orderposition is not None:
|
||||
event = instance.linked_orderposition.order.event
|
||||
if not perm_holder.has_event_permission(event.organizer, event, 'event.orders:read', request):
|
||||
r['linked_orderposition'] = {'id': instance.linked_orderposition.id}
|
||||
|
||||
if 'linked_giftcard.owner_ticket' in expand_nested:
|
||||
gc = instance.linked_giftcard
|
||||
if gc is not None and gc.owner_ticket is not None:
|
||||
event = gc.owner_ticket.order.event
|
||||
if not perm_holder.has_event_permission(event.organizer, event, 'event.orders:read', request):
|
||||
r['linked_giftcard']['owner_ticket'] = {'id': instance.linked_giftcard.owner_ticket.id}
|
||||
|
||||
return r
|
||||
|
||||
class Meta:
|
||||
model = ReusableMedium
|
||||
fields = (
|
||||
|
||||
@@ -769,7 +769,11 @@ class PaymentDetailsField(serializers.Field):
|
||||
pp = value.payment_provider
|
||||
if not pp:
|
||||
return {}
|
||||
return pp.api_payment_details(value)
|
||||
try:
|
||||
return pp.api_payment_details(value)
|
||||
except Exception:
|
||||
logger.exception("Failed to retrieve payment_details")
|
||||
return {}
|
||||
|
||||
|
||||
class OrderPaymentSerializer(I18nAwareModelSerializer):
|
||||
|
||||
@@ -286,6 +286,19 @@ class GiftCardSerializer(I18nAwareModelSerializer):
|
||||
)
|
||||
return data
|
||||
|
||||
def to_representation(self, instance):
|
||||
r = super().to_representation(instance)
|
||||
request = self.context.get('request')
|
||||
# late permission evaluations for checks that depend on the actual linked events
|
||||
if 'owner_ticket' in self.context['request'].query_params.getlist('expand'):
|
||||
owner_ticket = instance.owner_ticket
|
||||
if owner_ticket:
|
||||
event = owner_ticket.order.event
|
||||
perm_holder = request.auth if isinstance(request.auth, (Device, TeamAPIToken)) else request.user
|
||||
if not perm_holder.has_event_permission(event.organizer, event, 'event.orders:read', request):
|
||||
r['owner_ticket'] = {'id': instance.owner_ticket.id}
|
||||
return r
|
||||
|
||||
class Meta:
|
||||
model = GiftCard
|
||||
fields = ('id', 'secret', 'issuance', 'value', 'currency', 'testmode', 'expires', 'conditions', 'owner_ticket',
|
||||
|
||||
@@ -1122,7 +1122,7 @@ class CheckinViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
permission = 'event.orders:read'
|
||||
|
||||
def get_queryset(self):
|
||||
qs = Checkin.all.filter().select_related(
|
||||
qs = Checkin.all.filter(list__event=self.request.event).select_related(
|
||||
"position",
|
||||
"device",
|
||||
)
|
||||
|
||||
@@ -381,12 +381,15 @@ class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
|
||||
resp = HttpResponse(ct.file.file.read(), content_type='text/uri-list')
|
||||
return resp
|
||||
else:
|
||||
resp = FileResponse(ct.file.file, content_type=ct.type)
|
||||
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}{}"'.format(
|
||||
self.request.event.slug.upper(), order.code,
|
||||
provider.identifier, ct.extension
|
||||
return FileResponse(
|
||||
ct.file.file,
|
||||
filename='{}-{}-{}{}'.format(
|
||||
self.request.event.slug.upper(), order.code,
|
||||
provider.identifier, ct.extension
|
||||
),
|
||||
as_attachment=True,
|
||||
content_type=ct.type
|
||||
)
|
||||
return resp
|
||||
|
||||
@action(detail=True, methods=['POST'])
|
||||
def mark_paid(self, request, **kwargs):
|
||||
@@ -1303,14 +1306,17 @@ class EventOrderPositionViewSet(OrderPositionViewSetMixin, viewsets.ModelViewSet
|
||||
raise NotFound()
|
||||
|
||||
ftype, ignored = mimetypes.guess_type(answer.file.name)
|
||||
resp = FileResponse(answer.file, content_type=ftype or 'application/binary')
|
||||
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}-{}"'.format(
|
||||
self.request.event.slug.upper(),
|
||||
pos.order.code,
|
||||
pos.positionid,
|
||||
os.path.basename(answer.file.name).split('.', 1)[1]
|
||||
return FileResponse(
|
||||
answer.file,
|
||||
filename='{}-{}-{}-{}'.format(
|
||||
self.request.event.slug.upper(),
|
||||
pos.order.code,
|
||||
pos.positionid,
|
||||
os.path.basename(answer.file.name).split('.', 1)[1]
|
||||
),
|
||||
as_attachment=True,
|
||||
content_type=ftype or 'application/binary'
|
||||
)
|
||||
return resp
|
||||
|
||||
@action(detail=True, url_name="printlog", url_path="printlog", methods=["POST"])
|
||||
def printlog(self, request, **kwargs):
|
||||
@@ -1365,15 +1371,18 @@ class EventOrderPositionViewSet(OrderPositionViewSetMixin, viewsets.ModelViewSet
|
||||
if hasattr(image_file, 'seek'):
|
||||
image_file.seek(0)
|
||||
|
||||
resp = FileResponse(image_file, content_type=ftype or 'application/binary')
|
||||
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}-{}.{}"'.format(
|
||||
self.request.event.slug.upper(),
|
||||
pos.order.code,
|
||||
pos.positionid,
|
||||
key,
|
||||
extension,
|
||||
return FileResponse(
|
||||
image_file,
|
||||
filename='{}-{}-{}-{}.{}'.format(
|
||||
self.request.event.slug.upper(),
|
||||
pos.order.code,
|
||||
pos.positionid,
|
||||
key,
|
||||
extension,
|
||||
),
|
||||
as_attachment=True,
|
||||
content_type=ftype or 'application/binary'
|
||||
)
|
||||
return resp
|
||||
|
||||
@action(detail=True, url_name='download', url_path='download/(?P<output>[^/]+)')
|
||||
def download(self, request, output, **kwargs):
|
||||
@@ -1399,12 +1408,15 @@ class EventOrderPositionViewSet(OrderPositionViewSetMixin, viewsets.ModelViewSet
|
||||
resp = HttpResponse(ct.file.file.read(), content_type='text/uri-list')
|
||||
return resp
|
||||
else:
|
||||
resp = FileResponse(ct.file.file, content_type=ct.type)
|
||||
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}-{}{}"'.format(
|
||||
self.request.event.slug.upper(), pos.order.code, pos.positionid,
|
||||
provider.identifier, ct.extension
|
||||
return FileResponse(
|
||||
ct.file.file,
|
||||
filename='{}-{}-{}-{}{}'.format(
|
||||
self.request.event.slug.upper(), pos.order.code, pos.positionid,
|
||||
provider.identifier, ct.extension
|
||||
),
|
||||
as_attachment=True,
|
||||
content_type=ct.type
|
||||
)
|
||||
return resp
|
||||
|
||||
@action(detail=True, methods=['POST'])
|
||||
def regenerate_secrets(self, request, **kwargs):
|
||||
@@ -1986,9 +1998,12 @@ class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
if not invoice.file:
|
||||
raise RetryException()
|
||||
|
||||
resp = FileResponse(invoice.file.file, content_type='application/pdf')
|
||||
resp['Content-Disposition'] = 'attachment; filename="{}.pdf"'.format(invoice.number)
|
||||
return resp
|
||||
return FileResponse(
|
||||
invoice.file.file,
|
||||
filename='{}.pdf'.format(invoice.number),
|
||||
as_attachment=True,
|
||||
content_type='application/pdf'
|
||||
)
|
||||
|
||||
@action(detail=True, methods=['POST'])
|
||||
def transmit(self, request, **kwargs):
|
||||
|
||||
@@ -19,7 +19,10 @@
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import ipaddress
|
||||
import logging
|
||||
import smtplib
|
||||
import socket
|
||||
from itertools import groupby
|
||||
from smtplib import SMTPResponseException
|
||||
from typing import TypeVar
|
||||
@@ -237,3 +240,80 @@ def base_renderers(sender, **kwargs):
|
||||
|
||||
def get_email_context(**kwargs):
|
||||
return PlaceholderContext(**kwargs).render_all()
|
||||
|
||||
|
||||
def create_connection(address, timeout=socket.getdefaulttimeout(),
|
||||
source_address=None, *, all_errors=False):
|
||||
# Taken from the python stdlib, extended with a check for local ips
|
||||
|
||||
host, port = address
|
||||
exceptions = []
|
||||
for res in socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM):
|
||||
af, socktype, proto, canonname, sa = res
|
||||
|
||||
if not getattr(settings, "MAIL_CUSTOM_SMTP_ALLOW_PRIVATE_NETWORKS", False):
|
||||
ip_addr = ipaddress.ip_address(sa[0])
|
||||
if ip_addr.is_multicast:
|
||||
raise socket.error(f"Request to multicast address {sa[0]} blocked")
|
||||
if ip_addr.is_loopback or ip_addr.is_link_local:
|
||||
raise socket.error(f"Request to local address {sa[0]} blocked")
|
||||
if ip_addr.is_private:
|
||||
raise socket.error(f"Request to private address {sa[0]} blocked")
|
||||
|
||||
sock = None
|
||||
try:
|
||||
sock = socket.socket(af, socktype, proto)
|
||||
if timeout is not socket.getdefaulttimeout():
|
||||
sock.settimeout(timeout)
|
||||
if source_address:
|
||||
sock.bind(source_address)
|
||||
sock.connect(sa)
|
||||
# Break explicitly a reference cycle
|
||||
exceptions.clear()
|
||||
return sock
|
||||
|
||||
except socket.error as exc:
|
||||
if not all_errors:
|
||||
exceptions.clear() # raise only the last error
|
||||
exceptions.append(exc)
|
||||
if sock is not None:
|
||||
sock.close()
|
||||
|
||||
if len(exceptions):
|
||||
try:
|
||||
if not all_errors:
|
||||
raise exceptions[0]
|
||||
raise ExceptionGroup("create_connection failed", exceptions)
|
||||
finally:
|
||||
# Break explicitly a reference cycle
|
||||
exceptions.clear()
|
||||
else:
|
||||
raise socket.error("getaddrinfo returns an empty list")
|
||||
|
||||
|
||||
class CheckPrivateNetworkMixin:
|
||||
# _get_socket taken 1:1 from smtplib, just with a call to our own create_connection
|
||||
def _get_socket(self, host, port, timeout):
|
||||
# This makes it simpler for SMTP_SSL to use the SMTP connect code
|
||||
# and just alter the socket connection bit.
|
||||
if timeout is not None and not timeout:
|
||||
raise ValueError('Non-blocking socket (timeout=0) is not supported')
|
||||
if self.debuglevel > 0:
|
||||
self._print_debug('connect: to', (host, port), self.source_address)
|
||||
return create_connection((host, port), timeout, self.source_address)
|
||||
|
||||
|
||||
class SMTP(CheckPrivateNetworkMixin, smtplib.SMTP):
|
||||
pass
|
||||
|
||||
|
||||
# SMTP used here instead of mixin, because smtp.SMTP_SSL._get_socket calls super()._get_socket and then wraps this socket
|
||||
# super()._get_socket needs to be our version from the mixin
|
||||
class SMTP_SSL(smtplib.SMTP_SSL, SMTP): # noqa: N801
|
||||
pass
|
||||
|
||||
|
||||
class CheckPrivateNetworkSmtpBackend(EmailBackend):
|
||||
@property
|
||||
def connection_class(self):
|
||||
return SMTP_SSL if self.use_ssl else SMTP
|
||||
|
||||
@@ -58,6 +58,7 @@ from pretix.base.invoicing.transmission import (
|
||||
from pretix.base.models import (
|
||||
ExchangeRate, Invoice, InvoiceAddress, InvoiceLine, Order, OrderFee,
|
||||
)
|
||||
from pretix.base.models.orders import OrderPayment
|
||||
from pretix.base.models.tax import EU_CURRENCIES
|
||||
from pretix.base.services.tasks import (
|
||||
TransactionAwareProfiledEventTask, TransactionAwareTask,
|
||||
@@ -102,7 +103,7 @@ def build_invoice(invoice: Invoice) -> Invoice:
|
||||
introductory = invoice.event.settings.get('invoice_introductory_text', as_type=LazyI18nString)
|
||||
additional = invoice.event.settings.get('invoice_additional_text', as_type=LazyI18nString)
|
||||
footer = invoice.event.settings.get('invoice_footer_text', as_type=LazyI18nString)
|
||||
if lp and lp.payment_provider:
|
||||
if lp and lp.payment_provider and lp.state not in (OrderPayment.PAYMENT_STATE_FAILED, OrderPayment.PAYMENT_STATE_CANCELED):
|
||||
if 'payment' in inspect.signature(lp.payment_provider.render_invoice_text).parameters:
|
||||
payment = str(lp.payment_provider.render_invoice_text(invoice.order, lp))
|
||||
else:
|
||||
|
||||
@@ -763,12 +763,7 @@ class InvoicePreview(EventPermissionRequiredMixin, View):
|
||||
def get(self, request, *args, **kwargs):
|
||||
fname, ftype, fcontent = build_preview_invoice_pdf(request.event)
|
||||
resp = HttpResponse(fcontent, content_type=ftype)
|
||||
if settings.DEBUG:
|
||||
# attachment is more secure as we're dealing with user-generated stuff here, but inline is much more convenient during debugging
|
||||
resp['Content-Disposition'] = 'inline; filename="{}"'.format(fname)
|
||||
resp._csp_ignore = True
|
||||
else:
|
||||
resp['Content-Disposition'] = 'attachment; filename="{}"'.format(fname)
|
||||
resp['Content-Disposition'] = 'inline; filename="{}"'.format(fname)
|
||||
return resp
|
||||
|
||||
|
||||
|
||||
@@ -300,5 +300,4 @@ class SysReportView(AdministratorPermissionRequiredMixin, TemplateView):
|
||||
resp = HttpResponse(data)
|
||||
resp['Content-Type'] = mime
|
||||
resp['Content-Disposition'] = 'inline; filename="{}"'.format(name)
|
||||
resp._csp_ignore = True
|
||||
return resp
|
||||
|
||||
@@ -710,22 +710,26 @@ class OrderDownload(AsyncAction, OrderView):
|
||||
resp = HttpResponseRedirect(value.file.file.read())
|
||||
return resp
|
||||
else:
|
||||
resp = FileResponse(value.file.file, content_type=value.type)
|
||||
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}-{}{}"'.format(
|
||||
self.request.event.slug.upper(), self.order.code, self.order_position.positionid,
|
||||
self.output.identifier, value.extension
|
||||
return FileResponse(
|
||||
value.file.file,
|
||||
filename='{}-{}-{}-{}{}'.format(
|
||||
self.request.event.slug.upper(), self.order.code, self.order_position.positionid,
|
||||
self.output.identifier, value.extension
|
||||
),
|
||||
content_type=value.type
|
||||
)
|
||||
return resp
|
||||
elif isinstance(value, CachedCombinedTicket):
|
||||
if value.type == 'text/uri-list':
|
||||
resp = HttpResponseRedirect(value.file.file.read())
|
||||
return resp
|
||||
else:
|
||||
resp = FileResponse(value.file.file, content_type=value.type)
|
||||
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}{}"'.format(
|
||||
self.request.event.slug.upper(), self.order.code, self.output.identifier, value.extension
|
||||
return FileResponse(
|
||||
value.file.file,
|
||||
filename='{}-{}-{}{}'.format(
|
||||
self.request.event.slug.upper(), self.order.code, self.output.identifier, value.extension
|
||||
),
|
||||
content_type=value.type
|
||||
)
|
||||
return resp
|
||||
else:
|
||||
return redirect(self.get_self_url())
|
||||
|
||||
@@ -1831,15 +1835,15 @@ class InvoiceDownload(EventPermissionRequiredMixin, View):
|
||||
return redirect(self.get_order_url())
|
||||
|
||||
try:
|
||||
resp = FileResponse(self.invoice.file.file, content_type='application/pdf')
|
||||
return FileResponse(
|
||||
self.invoice.file.file,
|
||||
filename='{}.pdf'.format(re.sub("[^a-zA-Z0-9-_.]+", "_", self.invoice.number)),
|
||||
content_type='application/pdf'
|
||||
)
|
||||
except FileNotFoundError:
|
||||
invoice_pdf_task.apply(args=(self.invoice.pk,))
|
||||
return self.get(request, *args, **kwargs)
|
||||
|
||||
resp['Content-Disposition'] = 'inline; filename="{}.pdf"'.format(re.sub("[^a-zA-Z0-9-_.]+", "_", self.invoice.number))
|
||||
resp._csp_ignore = True # Some browser's PDF readers do not work with CSP
|
||||
return resp
|
||||
|
||||
|
||||
class OrderExtend(OrderView):
|
||||
permission = 'event.orders:write'
|
||||
|
||||
@@ -263,12 +263,7 @@ class BaseEditorView(EventPermissionRequiredMixin, TemplateView):
|
||||
|
||||
resp = HttpResponse(data, content_type=mimet)
|
||||
ftype = fname.split(".")[-1]
|
||||
if settings.DEBUG:
|
||||
# attachment is more secure as we're dealing with user-generated stuff here, but inline is much more convenient during debugging
|
||||
resp['Content-Disposition'] = 'inline; filename="ticket-preview.{}"'.format(ftype)
|
||||
resp._csp_ignore = True
|
||||
else:
|
||||
resp['Content-Disposition'] = 'attachment; filename="ticket-preview.{}"'.format(ftype)
|
||||
resp['Content-Disposition'] = 'inline; filename="ticket-preview.{}"'.format(ftype)
|
||||
return resp
|
||||
elif "data" in request.POST:
|
||||
if cf:
|
||||
@@ -309,6 +304,5 @@ class FontsCSSView(TemplateView):
|
||||
class PdfView(TemplateView):
|
||||
def get(self, request, *args, **kwargs):
|
||||
cf = get_object_or_404(CachedFile, id=kwargs.get("filename"), filename="background_preview.pdf")
|
||||
resp = FileResponse(cf.file, content_type='application/pdf')
|
||||
resp['Content-Disposition'] = 'attachment; filename="{}"'.format(cf.filename)
|
||||
resp = FileResponse(cf.file, filename=cf.filename, content_type='application/pdf')
|
||||
return resp
|
||||
|
||||
@@ -19,12 +19,26 @@
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import ipaddress
|
||||
import socket
|
||||
import sys
|
||||
import types
|
||||
from datetime import datetime
|
||||
from http import cookies
|
||||
|
||||
from django.conf import settings
|
||||
from PIL import Image
|
||||
from requests.adapters import HTTPAdapter
|
||||
from urllib3.connection import HTTPConnection, HTTPSConnection
|
||||
from urllib3.connectionpool import HTTPConnectionPool, HTTPSConnectionPool
|
||||
from urllib3.exceptions import (
|
||||
ConnectTimeoutError, HTTPError, LocationParseError, NameResolutionError,
|
||||
NewConnectionError,
|
||||
)
|
||||
from urllib3.util.connection import (
|
||||
_TYPE_SOCKET_OPTIONS, _set_socket_options, allowed_gai_family,
|
||||
)
|
||||
from urllib3.util.timeout import _DEFAULT_TIMEOUT
|
||||
|
||||
|
||||
def monkeypatch_vobject_performance():
|
||||
@@ -89,6 +103,123 @@ def monkeypatch_requests_timeout():
|
||||
HTTPAdapter.send = httpadapter_send
|
||||
|
||||
|
||||
def monkeypatch_urllib3_ssrf_protection():
|
||||
"""
|
||||
pretix allows HTTP requests to untrusted URLs, e.g. through webhooks or external API URLs. This is dangerous since
|
||||
it can allow access to private networks that should not be reachable by users ("server-side request forgery", SSRF).
|
||||
Validating URLs at submission is not sufficient, since with DNS rebinding an attacker can make a domain name pass
|
||||
validation and then resolve to a private IP address on actual execution. Unfortunately, there seems no clean solution
|
||||
to this in Python land, so we monkeypatch urllib3's connection management to check the IP address to be external
|
||||
*after* the DNS resolution.
|
||||
|
||||
This does not work when a global http(s) proxy is used, but in that scenario the proxy can perform the validation.
|
||||
"""
|
||||
if getattr(settings, "ALLOW_HTTP_TO_PRIVATE_NETWORKS", False):
|
||||
# Settings are not supposed to change during runtime, so we can optimize performance and complexity by skipping
|
||||
# this if not needed.
|
||||
return
|
||||
|
||||
def create_connection(
|
||||
address: tuple[str, int],
|
||||
timeout=_DEFAULT_TIMEOUT,
|
||||
source_address: tuple[str, int] | None = None,
|
||||
socket_options: _TYPE_SOCKET_OPTIONS | None = None,
|
||||
) -> socket.socket:
|
||||
# This is copied from urllib3.util.connection v2.3.0
|
||||
host, port = address
|
||||
if host.startswith("["):
|
||||
host = host.strip("[]")
|
||||
err = None
|
||||
|
||||
# Using the value from allowed_gai_family() in the context of getaddrinfo lets
|
||||
# us select whether to work with IPv4 DNS records, IPv6 records, or both.
|
||||
# The original create_connection function always returns all records.
|
||||
family = allowed_gai_family()
|
||||
|
||||
try:
|
||||
host.encode("idna")
|
||||
except UnicodeError:
|
||||
raise LocationParseError(f"'{host}', label empty or too long") from None
|
||||
|
||||
for res in socket.getaddrinfo(host, port, family, socket.SOCK_STREAM):
|
||||
af, socktype, proto, canonname, sa = res
|
||||
|
||||
if not getattr(settings, "ALLOW_HTTP_TO_PRIVATE_NETWORKS", False):
|
||||
ip_addr = ipaddress.ip_address(sa[0])
|
||||
if ip_addr.is_multicast:
|
||||
raise HTTPError(f"Request to multicast address {sa[0]} blocked")
|
||||
if ip_addr.is_loopback or ip_addr.is_link_local:
|
||||
raise HTTPError(f"Request to local address {sa[0]} blocked")
|
||||
if ip_addr.is_private:
|
||||
raise HTTPError(f"Request to private address {sa[0]} blocked")
|
||||
|
||||
sock = None
|
||||
try:
|
||||
sock = socket.socket(af, socktype, proto)
|
||||
|
||||
# If provided, set socket level options before connecting.
|
||||
_set_socket_options(sock, socket_options)
|
||||
|
||||
if timeout is not _DEFAULT_TIMEOUT:
|
||||
sock.settimeout(timeout)
|
||||
if source_address:
|
||||
sock.bind(source_address)
|
||||
sock.connect(sa)
|
||||
# Break explicitly a reference cycle
|
||||
err = None
|
||||
return sock
|
||||
|
||||
except OSError as _:
|
||||
err = _
|
||||
if sock is not None:
|
||||
sock.close()
|
||||
|
||||
if err is not None:
|
||||
try:
|
||||
raise err
|
||||
finally:
|
||||
# Break explicitly a reference cycle
|
||||
err = None
|
||||
else:
|
||||
raise OSError("getaddrinfo returns an empty list")
|
||||
|
||||
class ProtectionMixin:
|
||||
def _new_conn(self) -> socket.socket:
|
||||
# This is 1:1 the version from urllib3.connection.HTTPConnection._new_conn v2.3.0
|
||||
# just with a call to our own create_connection
|
||||
try:
|
||||
sock = create_connection(
|
||||
(self._dns_host, self.port),
|
||||
self.timeout,
|
||||
source_address=self.source_address,
|
||||
socket_options=self.socket_options,
|
||||
)
|
||||
except socket.gaierror as e:
|
||||
raise NameResolutionError(self.host, self, e) from e
|
||||
except socket.timeout as e:
|
||||
raise ConnectTimeoutError(
|
||||
self,
|
||||
f"Connection to {self.host} timed out. (connect timeout={self.timeout})",
|
||||
) from e
|
||||
|
||||
except OSError as e:
|
||||
raise NewConnectionError(
|
||||
self, f"Failed to establish a new connection: {e}"
|
||||
) from e
|
||||
|
||||
sys.audit("http.client.connect", self, self.host, self.port)
|
||||
return sock
|
||||
|
||||
class ProtectedHTTPConnection(ProtectionMixin, HTTPConnection):
|
||||
pass
|
||||
|
||||
class ProtectedHTTPSConnection(ProtectionMixin, HTTPSConnection):
|
||||
pass
|
||||
|
||||
HTTPConnectionPool.ConnectionCls = ProtectedHTTPConnection
|
||||
HTTPSConnectionPool.ConnectionCls = ProtectedHTTPSConnection
|
||||
|
||||
|
||||
def monkeypatch_cookie_morsel():
|
||||
# See https://code.djangoproject.com/ticket/34613
|
||||
cookies.Morsel._flags.add("partitioned")
|
||||
@@ -99,4 +230,5 @@ def monkeypatch_all_at_ready():
|
||||
monkeypatch_vobject_performance()
|
||||
monkeypatch_pillow_safer()
|
||||
monkeypatch_requests_timeout()
|
||||
monkeypatch_urllib3_ssrf_protection()
|
||||
monkeypatch_cookie_morsel()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,8 +8,8 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-03-30 11:22+0000\n"
|
||||
"PO-Revision-Date: 2026-03-31 17:00+0000\n"
|
||||
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
|
||||
"PO-Revision-Date: 2026-04-17 03:00+0000\n"
|
||||
"Last-Translator: Tim <plicnetwork@gmail.com>\n"
|
||||
"Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/pretix/"
|
||||
"es/>\n"
|
||||
"Language: es\n"
|
||||
@@ -17,7 +17,7 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Weblate 5.16.2\n"
|
||||
"X-Generator: Weblate 5.17\n"
|
||||
|
||||
#: pretix/_base_settings.py:87
|
||||
msgid "English"
|
||||
@@ -4150,7 +4150,7 @@ msgstr "Se encontraron varios productos coincidentes."
|
||||
#: pretix/base/modelimport_vouchers.py:205 pretix/base/models/items.py:1257
|
||||
#: pretix/base/models/vouchers.py:266 pretix/base/models/waitinglist.py:99
|
||||
msgid "Product variation"
|
||||
msgstr "Variación del producto"
|
||||
msgstr "Variante de producto"
|
||||
|
||||
#: pretix/base/modelimport_orders.py:161
|
||||
msgid "The variation can be specified by its internal ID or full name."
|
||||
@@ -4312,7 +4312,7 @@ msgstr "Ya existe un vale de compra con este código."
|
||||
#: pretix/base/models/vouchers.py:199 pretix/control/views/vouchers.py:121
|
||||
#: pretix/presale/templates/pretixpresale/organizers/customer_membership.html:52
|
||||
msgid "Maximum usages"
|
||||
msgstr "Usos máximos"
|
||||
msgstr "Número máximo de usos"
|
||||
|
||||
#: pretix/base/modelimport_vouchers.py:79
|
||||
msgid "The maximum number of usages must be set."
|
||||
@@ -4333,14 +4333,14 @@ msgstr "Reservar entrada con cargo a la cuota"
|
||||
|
||||
#: pretix/base/modelimport_vouchers.py:127 pretix/base/models/vouchers.py:236
|
||||
msgid "Allow to bypass quota"
|
||||
msgstr "Permitir que se anule la cuota"
|
||||
msgstr "Permitir omitir la cuota"
|
||||
|
||||
#: pretix/base/modelimport_vouchers.py:135 pretix/base/models/vouchers.py:242
|
||||
#: pretix/control/templates/pretixcontrol/vouchers/bulk.html:44
|
||||
#: pretix/control/templates/pretixcontrol/vouchers/detail.html:70
|
||||
#: pretix/control/views/vouchers.py:121
|
||||
msgid "Price effect"
|
||||
msgstr "Efecto sobre los precios"
|
||||
msgstr "Efecto en el precio"
|
||||
|
||||
#: pretix/base/modelimport_vouchers.py:150
|
||||
#, python-brace-format
|
||||
@@ -4351,7 +4351,7 @@ msgstr ""
|
||||
|
||||
#: pretix/base/modelimport_vouchers.py:160 pretix/base/models/vouchers.py:248
|
||||
msgid "Voucher value"
|
||||
msgstr "Valor del vale de compra"
|
||||
msgstr "Valor del vale"
|
||||
|
||||
#: pretix/base/modelimport_vouchers.py:165
|
||||
msgid "It is pointless to set a value without a price effect."
|
||||
@@ -4394,7 +4394,7 @@ msgstr "Etiqueta"
|
||||
|
||||
#: pretix/base/modelimport_vouchers.py:334 pretix/base/models/vouchers.py:300
|
||||
msgid "Shows hidden products that match this voucher"
|
||||
msgstr "Mostrar los ocultados productos válidos con este vale de compra"
|
||||
msgstr "Muestra los productos ocultos vinculados a este vale"
|
||||
|
||||
#: pretix/base/modelimport_vouchers.py:343 pretix/base/models/vouchers.py:304
|
||||
msgid "Offer all add-on products for free when redeeming this voucher"
|
||||
@@ -7322,8 +7322,8 @@ msgid ""
|
||||
"If activated, a holder of this voucher code can buy tickets, even if there "
|
||||
"are none left."
|
||||
msgstr ""
|
||||
"Si se activa, un titular de este vale de compra puede comprar entradas, "
|
||||
"incluso si no queda ninguna."
|
||||
"Si se activa, el poseedor de este código de vale podrá comprar entradas "
|
||||
"incluso si no quedan existencias."
|
||||
|
||||
#: pretix/base/models/vouchers.py:257 pretix/control/forms/vouchers.py:69
|
||||
msgid ""
|
||||
@@ -7338,14 +7338,14 @@ msgstr ""
|
||||
|
||||
#: pretix/base/models/vouchers.py:268
|
||||
msgid "This variation of the product select above is being used."
|
||||
msgstr "Esta variación del producto seleccionado arriba está siendo utilizada."
|
||||
msgstr "Se aplica a la variante del producto seleccionado arriba."
|
||||
|
||||
#: pretix/base/models/vouchers.py:277
|
||||
msgid ""
|
||||
"If enabled, the voucher is valid for any product affected by this quota."
|
||||
msgstr ""
|
||||
"Si está habilitado, el vale de compra es válido para cualquier producto "
|
||||
"afectado por esta cuota."
|
||||
"Si se activa, el vale será válido para cualquier producto incluido en esta "
|
||||
"cuota."
|
||||
|
||||
#: pretix/base/models/vouchers.py:284
|
||||
msgid "Specific seat"
|
||||
@@ -7357,16 +7357,16 @@ msgid ""
|
||||
"same value for multiple vouchers, you can get statistics on how many of them "
|
||||
"have been redeemed etc."
|
||||
msgstr ""
|
||||
"Puede utilizar este campo para agrupar múltiples vales de compra. Si "
|
||||
"introduce el mismo valor para varios vales de compra, puede obtener "
|
||||
"estadísticas sobre cuántos de ellos se han canjeado, etc."
|
||||
"Puedes usar este campo para agrupar varios vales. Si introduces el mismo "
|
||||
"valor en distintos vales, podrás obtener estadísticas sobre cuántos se han "
|
||||
"canjeado, etc."
|
||||
|
||||
#: pretix/base/models/vouchers.py:316 pretix/base/permissions.py:242
|
||||
#: pretix/control/navigation.py:289
|
||||
#: pretix/control/templates/pretixcontrol/vouchers/index.html:6
|
||||
#: pretix/control/templates/pretixcontrol/vouchers/index.html:8
|
||||
msgid "Vouchers"
|
||||
msgstr "Vales de compra"
|
||||
msgstr "Vales"
|
||||
|
||||
#: pretix/base/models/vouchers.py:342
|
||||
msgid "You cannot select a quota that belongs to a different event."
|
||||
@@ -13365,7 +13365,7 @@ msgstr "Esto eliminará todos los números de teléfono de los pedidos."
|
||||
|
||||
#: pretix/base/shredder.py:290
|
||||
msgid "Emails"
|
||||
msgstr "Correos electrónicos"
|
||||
msgstr "Correos"
|
||||
|
||||
#: pretix/base/shredder.py:292
|
||||
msgid ""
|
||||
@@ -15170,7 +15170,7 @@ msgstr "Todos los productos"
|
||||
#: pretix/control/views/typeahead.py:780
|
||||
#, python-brace-format
|
||||
msgid "{product} – Any variation"
|
||||
msgstr "{product} - Cualquier variación"
|
||||
msgstr "{product} – Cualquier variación"
|
||||
|
||||
#: pretix/control/forms/filter.py:566 pretix/control/forms/orders.py:862
|
||||
msgctxt "subevent"
|
||||
@@ -15469,7 +15469,7 @@ msgstr "Buscar vale de compra"
|
||||
#: pretix/control/views/vouchers.py:133
|
||||
#, python-brace-format
|
||||
msgid "Any product in quota \"{quota}\""
|
||||
msgstr "Cualquier producto del contingente \"{quota}\""
|
||||
msgstr "Cualquier producto en la cuota \"{quota}\""
|
||||
|
||||
#: pretix/control/forms/filter.py:2440
|
||||
msgid "Refund status"
|
||||
@@ -17068,15 +17068,15 @@ msgstr "ID de butaca específico"
|
||||
|
||||
#: pretix/control/forms/vouchers.py:200 pretix/presale/forms/waitinglist.py:103
|
||||
msgid "Invalid product selected."
|
||||
msgstr "Producto no válido seleccionado."
|
||||
msgstr "Se ha seleccionado un producto no válido."
|
||||
|
||||
#: pretix/control/forms/vouchers.py:225
|
||||
msgid ""
|
||||
"The voucher only matches hidden products but you have not selected that it "
|
||||
"should show them."
|
||||
msgstr ""
|
||||
"El vale de compra solo coincide con productos ocultos pero no has "
|
||||
"seleccionado que los muestre."
|
||||
"El vale solo coincide con productos ocultos, pero no has seleccionado que "
|
||||
"deba mostrarlos."
|
||||
|
||||
#: pretix/control/forms/vouchers.py:271
|
||||
msgid "Codes"
|
||||
@@ -21474,7 +21474,7 @@ msgstr ""
|
||||
#: pretix/plugins/ticketoutputpdf/views.py:172
|
||||
#: pretix/presale/views/customer.py:544 pretix/presale/views/customer.py:597
|
||||
msgid "Your changes have been saved."
|
||||
msgstr "Los cambios se han guardado."
|
||||
msgstr "Se han guardado los cambios."
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/event/plugins.html:34
|
||||
#: pretix/control/templates/pretixcontrol/organizers/plugins.html:34
|
||||
@@ -28698,7 +28698,7 @@ msgstr "Se ha creado la nueva lista de asistentes."
|
||||
#: pretix/plugins/ticketoutputpdf/views.py:132
|
||||
msgid "We could not save your changes. See below for details."
|
||||
msgstr ""
|
||||
"No hemos podido guardar los cambios. Consulte los detalles a continuación."
|
||||
"No se pudieron guardar los cambios. Consulta los detalles a continuación."
|
||||
|
||||
#: pretix/control/views/checkin.py:421 pretix/control/views/checkin.py:458
|
||||
msgid "The requested list does not exist."
|
||||
@@ -30864,7 +30864,7 @@ msgstr ""
|
||||
|
||||
#: pretix/plugins/badges/forms.py:33
|
||||
msgid "Template"
|
||||
msgstr "Plantilla"
|
||||
msgstr "Template"
|
||||
|
||||
#: pretix/plugins/badges/forms.py:34
|
||||
msgid ""
|
||||
|
||||
@@ -8,8 +8,8 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-03-30 11:22+0000\n"
|
||||
"PO-Revision-Date: 2026-03-23 21:00+0000\n"
|
||||
"Last-Translator: Hijiri Umemoto <hijiri@umemoto.org>\n"
|
||||
"PO-Revision-Date: 2026-04-20 08:07+0000\n"
|
||||
"Last-Translator: Yasunobu YesNo Kawaguchi <kawaguti@gmail.com>\n"
|
||||
"Language-Team: Japanese <https://translate.pretix.eu/projects/pretix/pretix/"
|
||||
"ja/>\n"
|
||||
"Language: ja\n"
|
||||
@@ -17,7 +17,7 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
"X-Generator: Weblate 5.16.2\n"
|
||||
"X-Generator: Weblate 5.17\n"
|
||||
|
||||
#: pretix/_base_settings.py:87
|
||||
msgid "English"
|
||||
@@ -664,7 +664,7 @@ msgstr "ギフトカードを取引で使用済み"
|
||||
#: pretix/plugins/banktransfer/payment.py:483
|
||||
#: pretix/presale/forms/customer.py:152
|
||||
msgid "This field is required."
|
||||
msgstr "この項目は必須です。"
|
||||
msgstr "このフィールドは必須です。"
|
||||
|
||||
#: pretix/base/addressvalidation.py:213
|
||||
msgid "Enter a postal code in the format XXX."
|
||||
@@ -3800,7 +3800,7 @@ msgstr "単価:{net_price} 税抜 / {gross_price} 税込"
|
||||
#, python-brace-format
|
||||
msgctxt "invoice"
|
||||
msgid "Single price: {price}"
|
||||
msgstr "単価:{price}"
|
||||
msgstr "単価: {price}"
|
||||
|
||||
#: pretix/base/invoicing/pdf.py:947 pretix/base/invoicing/pdf.py:952
|
||||
msgctxt "invoice"
|
||||
@@ -8206,7 +8206,7 @@ msgstr "参加者の呼びかけに使う名前"
|
||||
#: pretix/base/services/placeholders.py:732
|
||||
#: pretix/control/forms/organizer.py:799
|
||||
msgid "Mr Doe"
|
||||
msgstr "山田様"
|
||||
msgstr "山田 太郎"
|
||||
|
||||
#: pretix/base/pdf.py:672 pretix/base/pdf.py:679
|
||||
#: pretix/plugins/badges/exporters.py:501
|
||||
@@ -11001,7 +11001,7 @@ msgstr ""
|
||||
#: pretix/base/settings.py:1869 pretix/base/settings.py:1877
|
||||
#: pretix/presale/templates/pretixpresale/fragment_calendar_nav.html:8
|
||||
msgid "List"
|
||||
msgstr "リスト"
|
||||
msgstr "一覧"
|
||||
|
||||
#: pretix/base/settings.py:1870 pretix/base/settings.py:1878
|
||||
msgid "Week calendar"
|
||||
@@ -12855,7 +12855,7 @@ msgstr "Dr"
|
||||
|
||||
#: pretix/base/settings.py:3819 pretix/base/settings.py:3836
|
||||
msgid "First name"
|
||||
msgstr "名(First Name)"
|
||||
msgstr "名"
|
||||
|
||||
#: pretix/base/settings.py:3820 pretix/base/settings.py:3837
|
||||
msgid "Middle name"
|
||||
@@ -12939,7 +12939,7 @@ msgstr "企業名を必須にするには、請求先住所を必須にする必
|
||||
#: pretix/base/settings.py:4157
|
||||
#, python-brace-format
|
||||
msgid "VAT-ID is not supported for \"{}\"."
|
||||
msgstr ""
|
||||
msgstr "VAT-IDは「{}」に対してサポートされていません。"
|
||||
|
||||
#: pretix/base/settings.py:4164
|
||||
msgid "The last payment date cannot be before the end of presale."
|
||||
@@ -19423,7 +19423,7 @@ msgstr "カスタムチェックインルール"
|
||||
#: pretix/control/templates/pretixcontrol/vouchers/bulk.html:117
|
||||
#: pretix/plugins/sendmail/templates/pretixplugins/sendmail/send_form.html:85
|
||||
msgid "Edit"
|
||||
msgstr "編集する"
|
||||
msgstr "編集"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/checkin/list_edit.html:89
|
||||
msgid "Visualize"
|
||||
@@ -20280,7 +20280,7 @@ msgstr "地理座標"
|
||||
#: pretix/control/templates/pretixcontrol/subevents/bulk.html:271
|
||||
#: pretix/control/templates/pretixcontrol/subevents/bulk.html:275
|
||||
msgid "Optional"
|
||||
msgstr "オプション(必須でない項目)"
|
||||
msgstr "任意"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/event/fragment_geodata.html:22
|
||||
#: pretix/control/templates/pretixcontrol/subevents/bulk_edit.html:58
|
||||
@@ -23636,8 +23636,8 @@ msgid ""
|
||||
"this product was part of the discount calculation for a different product in "
|
||||
"this order."
|
||||
msgstr ""
|
||||
"この製品の価格は自動割引により減額されたか、この製品がこの注文の別の製品の割"
|
||||
"引計算の一部になっています。"
|
||||
"自動割引によりこの商品の価格が引き下げられたか、同じ注文内の別の商品に対する"
|
||||
"割引計算の対象になっています。"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html:496
|
||||
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html:103
|
||||
@@ -25008,7 +25008,7 @@ msgstr "デバイスの概要"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/organizers/device_edit.html:6
|
||||
msgid "Device:"
|
||||
msgstr "デバイス:"
|
||||
msgstr "デバイス:"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/organizers/device_edit.html:8
|
||||
msgid "Connect a new device"
|
||||
@@ -25897,7 +25897,7 @@ msgstr "二要素認証が無効です"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/organizers/team_members.html:57
|
||||
msgid "invited, pending response"
|
||||
msgstr "招待済み、返答待ち"
|
||||
msgstr "招待済み、応答待ち"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/organizers/team_members.html:59
|
||||
msgid "resend invite"
|
||||
@@ -26796,8 +26796,6 @@ msgid "Add a two-factor authentication device"
|
||||
msgstr "2要素認証デバイスを追加してください"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/user/2fa_add.html:19
|
||||
#, fuzzy
|
||||
#| msgid "Smartphone with the Authenticator application"
|
||||
msgid "Smartphone with Authenticator app"
|
||||
msgstr "Authenticatorアプリを搭載したスマートフォン"
|
||||
|
||||
@@ -26806,18 +26804,20 @@ msgid ""
|
||||
"Use your smartphone with any Time-based One-Time-Password app like freeOTP, "
|
||||
"Google Authenticator or Proton Authenticator."
|
||||
msgstr ""
|
||||
"freeOTP、Google Authenticator、Proton Authenticator などの時間ベースの"
|
||||
"ワンタイムパスワードアプリをスマートフォンでご利用ください。"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/user/2fa_add.html:30
|
||||
#, fuzzy
|
||||
#| msgid "WebAuthn-compatible hardware token (e.g. Yubikey)"
|
||||
msgid "WebAuthn-compatible hardware token"
|
||||
msgstr "WebAuthn対応のハードウェアトークン(例:Yubikey)"
|
||||
msgstr "WebAuthn対応のハードウェアトークン"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/user/2fa_add.html:32
|
||||
msgid ""
|
||||
"Use a hardware token like the Yubikey, or other biometric authentication "
|
||||
"like fingerprint or face recognition."
|
||||
msgstr ""
|
||||
"Yubikey などのハードウェアトークンや、指紋や顔認識などの生体認証を使用してく"
|
||||
"ださい。"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/user/2fa_confirm_totp.html:8
|
||||
msgid "To set up this device, please follow the following steps:"
|
||||
@@ -33303,7 +33303,7 @@ msgstr "本当にStripeアカウントを切断しますか?"
|
||||
|
||||
#: pretix/plugins/stripe/templates/pretixplugins/stripe/oauth_disconnect.html:16
|
||||
msgid "Disconnect"
|
||||
msgstr "切断します"
|
||||
msgstr "切断"
|
||||
|
||||
#: pretix/plugins/stripe/templates/pretixplugins/stripe/pending.html:6
|
||||
msgid "Payment instructions"
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-03-30 11:22+0000\n"
|
||||
"PO-Revision-Date: 2026-04-01 17:00+0000\n"
|
||||
"PO-Revision-Date: 2026-04-08 18:00+0000\n"
|
||||
"Last-Translator: Ruud Hendrickx <ruud@leckxicon.eu>\n"
|
||||
"Language-Team: Dutch (Belgium) <https://translate.pretix.eu/projects/pretix/"
|
||||
"pretix/nl_BE/>\n"
|
||||
@@ -31346,7 +31346,7 @@ msgstr "We zullen u een e-mail sturen zodra we uw betaling ontvangen hebben."
|
||||
#: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/refund_export.html:7
|
||||
#: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/sepa_export.html:7
|
||||
msgid "Export bank transfer refunds"
|
||||
msgstr ""
|
||||
msgstr "Terugbetalingen per bankoverschrijving exporteren"
|
||||
|
||||
#: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/refund_export.html:9
|
||||
#, python-format
|
||||
@@ -31354,6 +31354,8 @@ msgid ""
|
||||
"<strong>%(num_new)s</strong> Bank transfer refunds have been placed and are "
|
||||
"not yet part of an export."
|
||||
msgstr ""
|
||||
"<strong>%(num_new)s</strong> terugbetalingen per bankoverschrijving zijn "
|
||||
"aangemaakt en nog niet geëxporteerd."
|
||||
|
||||
#: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/refund_export.html:15
|
||||
msgid "In test mode, your exports will only contain test mode orders."
|
||||
@@ -31366,6 +31368,8 @@ msgid ""
|
||||
"If you want, you can now also create these exports for multiple events "
|
||||
"combined."
|
||||
msgstr ""
|
||||
"Als u dat wilt, kunt u deze exportbestanden nu ook voor meerdere evenementen "
|
||||
"tegelijk aanmaken."
|
||||
|
||||
#: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/refund_export.html:22
|
||||
msgid "Go to organizer-level exports"
|
||||
@@ -31377,7 +31381,7 @@ msgstr "Nieuw exportbestand aanmaken"
|
||||
|
||||
#: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/refund_export.html:38
|
||||
msgid "Aggregate transactions to the same bank account"
|
||||
msgstr ""
|
||||
msgstr "Overschrijvingen naar hetzelfde rekeningnummer samenvoegen"
|
||||
|
||||
#: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/refund_export.html:43
|
||||
msgid ""
|
||||
|
||||
@@ -216,7 +216,7 @@ class PayView(PaypalOrderView, TemplateView):
|
||||
|
||||
|
||||
@scopes_disabled()
|
||||
@event_permission_required('event.settings.general:write')
|
||||
@event_permission_required('event.settings.payment:write')
|
||||
def isu_return(request, *args, **kwargs):
|
||||
getparams = ['merchantId', 'merchantIdInPayPal', 'permissionsGranted', 'accountStatus', 'consentStatus', 'productIntentID', 'isEmailConfirmed']
|
||||
sessionparams = ['payment_paypal_isu_event', 'payment_paypal_isu_tracking_id']
|
||||
@@ -526,7 +526,7 @@ def webhook(request, *args, **kwargs):
|
||||
return HttpResponse(status=200)
|
||||
|
||||
|
||||
@event_permission_required('event.settings.general:write')
|
||||
@event_permission_required('event.settings.payment:write')
|
||||
@require_POST
|
||||
def isu_disconnect(request, **kwargs):
|
||||
del request.event.settings.payment_paypal_connect_refresh_token
|
||||
|
||||
@@ -83,7 +83,7 @@ class AuthenticationForm(forms.Form):
|
||||
self.request = request
|
||||
self.customer_cache = None
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['password'].help_text = "<a href='{}'>{}</a>".format(
|
||||
self.fields['password'].help_text = "<a target='_blank' href='{}'>{}</a>".format(
|
||||
build_absolute_uri(False, 'presale:organizer.customer.resetpw', kwargs={
|
||||
'organizer': request.organizer.slug,
|
||||
}),
|
||||
|
||||
@@ -91,6 +91,9 @@ event_patterns = [
|
||||
re_path(r'w/(?P<cart_namespace>[a-zA-Z0-9]{16})/cart/add',
|
||||
csrf_exempt(pretix.presale.views.cart.CartAdd.as_view()),
|
||||
name='event.cart.add'),
|
||||
re_path(r'w/(?P<cart_namespace>[a-zA-Z0-9]{16})/cart/create',
|
||||
csrf_exempt(pretix.presale.views.cart.CartCreate.as_view()),
|
||||
name='event.cart.create'),
|
||||
|
||||
re_path(r'unlock/(?P<hash>[a-z0-9]{64})/$', pretix.presale.views.user.UnlockHashView.as_view(),
|
||||
name='event.payment.unlock'),
|
||||
|
||||
@@ -555,6 +555,18 @@ class CartClear(EventViewMixin, CartActionMixin, AsyncAction, View):
|
||||
request.sales_channel.identifier, time_machine_now(default=None))
|
||||
|
||||
|
||||
@method_decorator(allow_cors_if_namespaced, 'dispatch')
|
||||
class CartCreate(EventViewMixin, CartActionMixin, View):
|
||||
def get(self, request, *args, **kwargs):
|
||||
if 'ajax' in self.request.GET:
|
||||
cart_id = get_or_create_cart_id(self.request, create=True)
|
||||
return JsonResponse({
|
||||
'cart_id': cart_id,
|
||||
})
|
||||
else:
|
||||
return redirect_to_url(self.get_success_url())
|
||||
|
||||
|
||||
@method_decorator(allow_frame_if_namespaced, 'dispatch')
|
||||
class CartExtendReservation(EventViewMixin, CartActionMixin, AsyncAction, View):
|
||||
task = extend_cart_reservation
|
||||
@@ -843,9 +855,13 @@ class AnswerDownload(EventViewMixin, View):
|
||||
return Http404()
|
||||
|
||||
ftype, _ = mimetypes.guess_type(answer.file.name)
|
||||
resp = FileResponse(answer.file, content_type=ftype or 'application/binary')
|
||||
resp['Content-Disposition'] = 'attachment; filename="{}-cart-{}"'.format(
|
||||
filename = '{}-cart-{}'.format(
|
||||
self.request.event.slug.upper(),
|
||||
os.path.basename(answer.file.name).split('.', 1)[1]
|
||||
).encode("ascii", "ignore")
|
||||
)
|
||||
resp = FileResponse(
|
||||
answer.file,
|
||||
filename=filename,
|
||||
content_type=ftype or 'application/binary'
|
||||
)
|
||||
return resp
|
||||
|
||||
@@ -681,8 +681,6 @@ class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView):
|
||||
context = {}
|
||||
context['list_type'] = self.request.GET.get("style", self.request.event.settings.event_list_type)
|
||||
if context['list_type'] not in ("calendar", "week") and self.request.event.subevents.filter(date_from__gt=time_machine_now()).count() > 50:
|
||||
if self.request.event.settings.event_list_type not in ("calendar", "week"):
|
||||
self.request.event.settings.event_list_type = "calendar"
|
||||
context['list_type'] = "calendar"
|
||||
|
||||
if context['list_type'] == "calendar":
|
||||
|
||||
@@ -1220,30 +1220,26 @@ class OrderDownloadMixin:
|
||||
resp = HttpResponseRedirect(value.file.file.read())
|
||||
return resp
|
||||
else:
|
||||
resp = FileResponse(value.file.file, content_type=value.type)
|
||||
if self.order_position.subevent:
|
||||
# Subevent date in filename improves accessibility e.g. for screen reader users
|
||||
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}-{}-{}{}"'.format(
|
||||
self.request.event.slug.upper(), self.order.code, self.order_position.positionid,
|
||||
self.order_position.subevent.date_from.strftime('%Y_%m_%d'),
|
||||
self.output.identifier, value.extension
|
||||
)
|
||||
else:
|
||||
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}-{}{}"'.format(
|
||||
self.request.event.slug.upper(), self.order.code, self.order_position.positionid,
|
||||
self.output.identifier, value.extension
|
||||
)
|
||||
return resp
|
||||
name_parts = (
|
||||
self.request.event.slug.upper(),
|
||||
self.order.code,
|
||||
str(self.order_position.positionid),
|
||||
self.order_position.subevent.date_from.strftime('%Y_%m_%d') if self.order_position.subevent else None,
|
||||
self.output.identifier
|
||||
)
|
||||
filename = "-".join(filter(None, name_parts)) + value.extension
|
||||
return FileResponse(value.file.file, filename=filename, content_type=value.type)
|
||||
elif isinstance(value, CachedCombinedTicket):
|
||||
if value.type == 'text/uri-list':
|
||||
resp = HttpResponseRedirect(value.file.file.read())
|
||||
return resp
|
||||
else:
|
||||
resp = FileResponse(value.file.file, content_type=value.type)
|
||||
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}{}"'.format(
|
||||
self.request.event.slug.upper(), self.order.code, self.output.identifier, value.extension
|
||||
return FileResponse(
|
||||
value.file.file,
|
||||
filename="{}-{}-{}{}".format(
|
||||
self.request.event.slug.upper(), self.order.code, self.output.identifier, value.extension),
|
||||
content_type=value.type
|
||||
)
|
||||
return resp
|
||||
else:
|
||||
return redirect(self.get_self_url())
|
||||
|
||||
@@ -1383,13 +1379,14 @@ class InvoiceDownload(EventViewMixin, OrderDetailMixin, View):
|
||||
return redirect(self.get_order_url())
|
||||
|
||||
try:
|
||||
resp = FileResponse(invoice.file.file, content_type='application/pdf')
|
||||
return FileResponse(
|
||||
invoice.file.file,
|
||||
filename='{}.pdf'.format(re.sub("[^a-zA-Z0-9-_.]+", "_", invoice.number)),
|
||||
content_type='application/pdf'
|
||||
)
|
||||
except FileNotFoundError:
|
||||
invoice_pdf_task.apply(args=(invoice.pk,))
|
||||
return self.get(request, *args, **kwargs)
|
||||
resp['Content-Disposition'] = 'inline; filename="{}.pdf"'.format(re.sub("[^a-zA-Z0-9-_.]+", "_", invoice.number))
|
||||
resp._csp_ignore = True # Some browser's PDF readers do not work with CSP
|
||||
return resp
|
||||
|
||||
|
||||
class OrderChangeMixin:
|
||||
|
||||
@@ -66,22 +66,27 @@ class WaitingView(EventViewMixin, FormView):
|
||||
if customer else None
|
||||
),
|
||||
)
|
||||
choices = []
|
||||
groups = {}
|
||||
for i in items:
|
||||
if not i.allow_waitinglist:
|
||||
continue
|
||||
|
||||
category_name = str(i.category.name) if i.category else ''
|
||||
group = groups.setdefault(category_name, [])
|
||||
|
||||
if i.has_variations:
|
||||
for v in i.available_variations:
|
||||
if v.cached_availability[0] == Quota.AVAILABILITY_OK:
|
||||
continue
|
||||
choices.append((f'{i.pk}-{v.pk}', f'{i.name} – {v.value}'))
|
||||
group.append((f'{i.pk}-{v.pk}', f'{i.name} – {v.value}'))
|
||||
|
||||
else:
|
||||
if i.cached_availability[0] == Quota.AVAILABILITY_OK:
|
||||
continue
|
||||
choices.append((f'{i.pk}', f'{i.name}'))
|
||||
return choices
|
||||
group.append((f'{i.pk}', f'{i.name}'))
|
||||
|
||||
# Remove categories where all items were available (no waiting list choices)
|
||||
return [(cat, choices) for cat, choices in groups.items() if choices]
|
||||
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super().get_form_kwargs()
|
||||
|
||||
@@ -530,12 +530,10 @@ class WidgetAPIProductList(EventListMixin, View):
|
||||
]
|
||||
|
||||
if hasattr(self.request, 'event') and data['list_type'] not in ("calendar", "week"):
|
||||
# only allow list-view of more than 50 subevents if ordering is by data as this can be done in the database
|
||||
# only allow list-view of more than 50 subevents if ordering is by date as this can be done in the database
|
||||
# ordering by name is currently not supported in database due to I18NField-JSON
|
||||
ordering = self.request.event.settings.get('frontpage_subevent_ordering', default='date_ascending', as_type=str)
|
||||
if ordering not in ("date_ascending", "date_descending") and self.request.event.subevents.filter(date_from__gt=now()).count() > 50:
|
||||
if self.request.event.settings.event_list_type not in ("calendar", "week"):
|
||||
self.request.event.settings.event_list_type = "calendar"
|
||||
data['list_type'] = list_type = 'calendar'
|
||||
|
||||
if hasattr(self.request, 'event'):
|
||||
|
||||
@@ -223,6 +223,7 @@ CSRF_TRUSTED_ORIGINS = [urlparse(SITE_URL).scheme + '://' + urlparse(SITE_URL).h
|
||||
|
||||
TRUST_X_FORWARDED_FOR = config.getboolean('pretix', 'trust_x_forwarded_for', fallback=False)
|
||||
USE_X_FORWARDED_HOST = config.getboolean('pretix', 'trust_x_forwarded_host', fallback=False)
|
||||
ALLOW_HTTP_TO_PRIVATE_NETWORKS = config.getboolean('pretix', 'allow_http_to_private_networks', fallback=False)
|
||||
|
||||
|
||||
REQUEST_ID_HEADER = config.get('pretix', 'request_id_header', fallback=False)
|
||||
@@ -263,7 +264,8 @@ EMAIL_HOST_PASSWORD = config.get('mail', 'password', fallback='')
|
||||
EMAIL_USE_TLS = config.getboolean('mail', 'tls', fallback=False)
|
||||
EMAIL_USE_SSL = config.getboolean('mail', 'ssl', fallback=False)
|
||||
EMAIL_SUBJECT_PREFIX = '[pretix] '
|
||||
EMAIL_BACKEND = EMAIL_CUSTOM_SMTP_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
||||
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
||||
EMAIL_CUSTOM_SMTP_BACKEND = 'pretixbase.email.CheckPrivateNetworkSmtpBackend'
|
||||
EMAIL_TIMEOUT = 60
|
||||
|
||||
ADMINS = [('Admin', n) for n in config.get('mail', 'admins', fallback='').split(",") if n]
|
||||
|
||||
25
src/pretix/static/npm_dir/package-lock.json
generated
25
src/pretix/static/npm_dir/package-lock.json
generated
@@ -1835,10 +1835,9 @@
|
||||
"integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"license": "MIT",
|
||||
"version": "1.1.13",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
|
||||
"integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
@@ -2879,9 +2878,9 @@
|
||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
},
|
||||
@@ -4936,9 +4935,9 @@
|
||||
"integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"version": "1.1.13",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
|
||||
"integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
@@ -5715,9 +5714,9 @@
|
||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
|
||||
},
|
||||
"picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="
|
||||
},
|
||||
"pify": {
|
||||
"version": "4.0.1",
|
||||
|
||||
@@ -110,6 +110,10 @@ var setCookie = function (cname, cvalue, exdays) {
|
||||
var d = new Date();
|
||||
d.setTime(d.getTime() + (exdays * 24 * 60 * 60 * 1000));
|
||||
var expires = "expires=" + d.toUTCString();
|
||||
if (!cvalue) {
|
||||
var expires = "expires=Thu, 01 Jan 1970 00:00:00 GMT";
|
||||
cvalue = "";
|
||||
}
|
||||
document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/";
|
||||
};
|
||||
var getCookie = function (name) {
|
||||
@@ -726,17 +730,16 @@ var shared_methods = {
|
||||
buy_callback: function (data) {
|
||||
if (data.redirect) {
|
||||
if (data.cart_id) {
|
||||
this.$root.cart_id = data.cart_id;
|
||||
setCookie(this.$root.cookieName, data.cart_id, 30);
|
||||
this.$root.set_cart_id(data.cart_id);
|
||||
}
|
||||
if (data.redirect.substr(0, 1) === '/') {
|
||||
data.redirect = this.$root.target_url.replace(/^([^\/]+:\/\/[^\/]+)\/.*$/, "$1") + data.redirect;
|
||||
}
|
||||
var url = data.redirect;
|
||||
if (url.indexOf('?')) {
|
||||
url = url + '&iframe=1&locale=' + lang + '&take_cart_id=' + this.$root.cart_id;
|
||||
url = url + '&iframe=1&locale=' + lang + '&take_cart_id=' + encodeURIComponent(this.$root.get_cart_id());
|
||||
} else {
|
||||
url = url + '?iframe=1&locale=' + lang + '&take_cart_id=' + this.$root.cart_id;
|
||||
url = url + '?iframe=1&locale=' + lang + '&take_cart_id=' + encodeURIComponent(this.$root.get_cart_id());
|
||||
}
|
||||
url += this.$root.consent_parameter;
|
||||
if (this.$root.additionalURLParams) {
|
||||
@@ -779,15 +782,24 @@ var shared_methods = {
|
||||
}
|
||||
},
|
||||
resume: function () {
|
||||
if (!this.$root.get_cart_id() && this.$root.keep_cart) {
|
||||
// create an empty cart whose id we can persist
|
||||
this.$root.create_cart(this.resume)
|
||||
return;
|
||||
}
|
||||
var redirect_url;
|
||||
redirect_url = this.$root.target_url + 'w/' + widget_id + '/';
|
||||
if (this.$root.subevent && !this.$root.cart_id) {
|
||||
if (this.$root.subevent && this.$root.is_button && this.$root.items.length === 0) {
|
||||
// button with subevent but no items
|
||||
redirect_url += this.$root.subevent + '/';
|
||||
}
|
||||
redirect_url += '?iframe=1&locale=' + lang;
|
||||
if (this.$root.cart_id) {
|
||||
redirect_url += '&take_cart_id=' + this.$root.cart_id;
|
||||
if (this.$root.get_cart_id()) {
|
||||
redirect_url += '&take_cart_id=' + encodeURIComponent(this.$root.get_cart_id());
|
||||
if (this.$root.keep_cart) {
|
||||
// make sure the cart-id is used, even if the cart is currently empty
|
||||
redirect_url += '&ajax=1'
|
||||
}
|
||||
}
|
||||
if (this.$root.widget_data) {
|
||||
redirect_url += '&widget_data=' + encodeURIComponent(this.$root.widget_data_json);
|
||||
@@ -1864,12 +1876,11 @@ var shared_root_methods = {
|
||||
if (this.$root.variation_filter) {
|
||||
url += '&variations=' + encodeURIComponent(this.$root.variation_filter);
|
||||
}
|
||||
var cart_id = getCookie(this.cookieName);
|
||||
if (this.$root.voucher_code) {
|
||||
url += '&voucher=' + encodeURIComponent(this.$root.voucher_code);
|
||||
}
|
||||
if (cart_id) {
|
||||
url += "&cart_id=" + encodeURIComponent(cart_id);
|
||||
if (this.$root.get_cart_id()) {
|
||||
url += "&cart_id=" + encodeURIComponent(this.$root.get_cart_id());
|
||||
}
|
||||
if (this.$root.date !== null) {
|
||||
url += "&date=" + this.$root.date.substr(0, 7);
|
||||
@@ -1939,7 +1950,6 @@ var shared_root_methods = {
|
||||
root.display_add_to_cart = data.display_add_to_cart;
|
||||
root.waiting_list_enabled = data.waiting_list_enabled;
|
||||
root.show_variations_expanded = data.show_variations_expanded || !!root.variation_filter;
|
||||
root.cart_id = cart_id;
|
||||
root.cart_exists = data.cart_exists;
|
||||
root.vouchers_exist = data.vouchers_exist;
|
||||
root.has_seating_plan = data.has_seating_plan;
|
||||
@@ -2004,8 +2014,8 @@ var shared_root_methods = {
|
||||
if (this.$root.voucher_code) {
|
||||
redirect_url += '&voucher=' + encodeURIComponent(this.$root.voucher_code);
|
||||
}
|
||||
if (this.$root.cart_id) {
|
||||
redirect_url += '&take_cart_id=' + this.$root.cart_id;
|
||||
if (this.$root.get_cart_id()) {
|
||||
redirect_url += '&take_cart_id=' + encodeURIComponent(this.$root.get_cart_id());
|
||||
}
|
||||
if (this.$root.widget_data) {
|
||||
redirect_url += '&widget_data=' + encodeURIComponent(this.$root.widget_data_json);
|
||||
@@ -2027,7 +2037,28 @@ var shared_root_methods = {
|
||||
this.$root.subevent = event.subevent;
|
||||
this.$root.loading++;
|
||||
this.$root.reload();
|
||||
}
|
||||
},
|
||||
create_cart: function(callback) {
|
||||
var url = this.$root.target_url + 'w/' + widget_id + '/cart/create?ajax=1';
|
||||
|
||||
this.$root.overlay.frame_loading = true;
|
||||
api._getJSON(url, (data) => {
|
||||
this.$root.set_cart_id(data.cart_id);
|
||||
this.$root.overlay.frame_loading = false;
|
||||
callback()
|
||||
}, () => {
|
||||
this.$root.overlay.error_message = strings['cart_error'];
|
||||
this.$root.overlay.frame_loading = false;
|
||||
})
|
||||
},
|
||||
get_cart_id: function() {
|
||||
if (this.$root.keep_cart) {
|
||||
return getCookie(this.$root.cookieName);
|
||||
}
|
||||
},
|
||||
set_cart_id: function(newValue) {
|
||||
setCookie(this.$root.cookieName, newValue, 30);
|
||||
},
|
||||
};
|
||||
|
||||
var shared_root_computed = {
|
||||
@@ -2049,9 +2080,8 @@ var shared_root_computed = {
|
||||
},
|
||||
voucherFormTarget: function () {
|
||||
var form_target = this.target_url + 'w/' + widget_id + '/redeem?iframe=1&locale=' + lang;
|
||||
var cookie = getCookie(this.cookieName);
|
||||
if (cookie) {
|
||||
form_target += "&take_cart_id=" + cookie;
|
||||
if (this.get_cart_id()) {
|
||||
form_target += "&take_cart_id=" + encodeURIComponent(this.get_cart_id());
|
||||
}
|
||||
if (this.subevent) {
|
||||
form_target += "&subevent=" + this.subevent;
|
||||
@@ -2091,9 +2121,8 @@ var shared_root_computed = {
|
||||
checkout_url += '?' + this.$root.additionalURLParams;
|
||||
}
|
||||
var form_target = this.target_url + 'w/' + widget_id + '/cart/add?iframe=1&next=' + encodeURIComponent(checkout_url);
|
||||
var cookie = getCookie(this.cookieName);
|
||||
if (cookie) {
|
||||
form_target += "&take_cart_id=" + cookie;
|
||||
if (this.get_cart_id()) {
|
||||
form_target += "&take_cart_id=" + encodeURIComponent(this.get_cart_id());
|
||||
}
|
||||
form_target += this.$root.consent_parameter
|
||||
return form_target
|
||||
@@ -2329,6 +2358,7 @@ var create_widget = function (element, html_id=null) {
|
||||
has_seating_plan: false,
|
||||
has_seating_plan_waitinglist: false,
|
||||
meta_filter_fields: [],
|
||||
keep_cart: true,
|
||||
}
|
||||
},
|
||||
created: function () {
|
||||
@@ -2366,6 +2396,7 @@ var create_button = function (element, html_id=null) {
|
||||
var raw_items = element.attributes.items ? element.attributes.items.value : "";
|
||||
var skip_ssl = element.attributes["skip-ssl-check"] ? true : false;
|
||||
var disable_iframe = element.attributes["disable-iframe"] ? true : false;
|
||||
var keep_cart = element.attributes["keep-cart"] ? true : false;
|
||||
var button_text = element.innerHTML;
|
||||
var widget_data = JSON.parse(JSON.stringify(window.PretixWidget.widget_data));
|
||||
for (var i = 0; i < element.attributes.length; i++) {
|
||||
@@ -2417,7 +2448,8 @@ var create_button = function (element, html_id=null) {
|
||||
widget_data: widget_data,
|
||||
widget_id: 'pretix-widget-' + widget_id,
|
||||
html_id: html_id,
|
||||
button_text: button_text
|
||||
button_text: button_text,
|
||||
keep_cart: keep_cart || items.length > 0,
|
||||
}
|
||||
},
|
||||
created: function () {
|
||||
@@ -2426,7 +2458,7 @@ var create_button = function (element, html_id=null) {
|
||||
observer.observe(this.$el, observerOptions);
|
||||
},
|
||||
computed: shared_root_computed,
|
||||
methods: shared_root_methods
|
||||
methods: shared_root_methods,
|
||||
});
|
||||
create_overlay(app);
|
||||
return app;
|
||||
@@ -2492,13 +2524,14 @@ window.PretixWidget.open = function (target_url, voucher, subevent, items, widge
|
||||
frame_dismissed: false,
|
||||
widget_data: all_widget_data,
|
||||
widget_id: 'pretix-widget-' + widget_id,
|
||||
button_text: ""
|
||||
button_text: "",
|
||||
keep_cart: true
|
||||
}
|
||||
},
|
||||
created: function () {
|
||||
},
|
||||
computed: shared_root_computed,
|
||||
methods: shared_root_methods
|
||||
methods: shared_root_methods,
|
||||
});
|
||||
create_overlay(app);
|
||||
app.$nextTick(function () {
|
||||
|
||||
@@ -966,6 +966,7 @@ $table-bg-accent: rgba(128, 128, 128, 0.05);
|
||||
width: 80vw;
|
||||
max-width: 1080px;
|
||||
height: 80vh;
|
||||
max-height: 100dvh;
|
||||
}
|
||||
.pretix-widget-frame-inner iframe {
|
||||
width: 100% !important;
|
||||
|
||||
@@ -94,6 +94,9 @@ class DisableMigrations(object):
|
||||
def __getitem__(self, item):
|
||||
return None
|
||||
|
||||
def setdefault(self, key, default=None):
|
||||
return
|
||||
|
||||
|
||||
if not os.environ.get("GITHUB_WORKFLOW", ""):
|
||||
MIGRATION_MODULES = DisableMigrations()
|
||||
|
||||
@@ -171,6 +171,35 @@ def test_giftcard_detail_expand(token_client, organizer, event, giftcard):
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_giftcard_detail_expand_without_permissions(team, token_client, organizer, event, giftcard):
|
||||
with scopes_disabled():
|
||||
o = Order.objects.create(
|
||||
code='FOO', event=event, email='dummy@dummy.test',
|
||||
status=Order.STATUS_PENDING, datetime=now(), expires=now() + timedelta(days=10),
|
||||
sales_channel=event.organizer.sales_channels.get(identifier="web"),
|
||||
total=14, locale='en'
|
||||
)
|
||||
ticket = event.items.create(name='Early-bird ticket', category=None, default_price=23, admission=True,
|
||||
personalized=True)
|
||||
op = o.positions.create(item=ticket, price=Decimal("14"))
|
||||
giftcard.owner_ticket = op
|
||||
giftcard.save()
|
||||
|
||||
team.all_event_permissions = False
|
||||
team.save()
|
||||
|
||||
res = dict(TEST_GC_RES)
|
||||
res["id"] = giftcard.pk
|
||||
res["issuance"] = giftcard.issuance.isoformat().replace('+00:00', 'Z')
|
||||
resp = token_client.get('/api/v1/organizers/{}/giftcards/{}/?expand=owner_ticket'.format(organizer.slug, giftcard.pk))
|
||||
assert resp.status_code == 200
|
||||
|
||||
assert resp.data["owner_ticket"] == {
|
||||
"id": op.pk,
|
||||
}
|
||||
|
||||
|
||||
TEST_GIFTCARD_CREATE_PAYLOAD = {
|
||||
"secret": "DEFABC",
|
||||
"value": "12.00",
|
||||
|
||||
@@ -252,6 +252,76 @@ def test_medium_detail(token_client, organizer, event, medium, giftcard, custome
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_medium_detail_event_permission_missing(token_client, organizer, event, medium, giftcard, customer, team):
|
||||
team.all_organizer_permissions = False
|
||||
team.limit_organizer_permissions = {
|
||||
"organizer.reusablemedia:read": True,
|
||||
"organizer.customers:read": True,
|
||||
"organizer.giftcards:read": True,
|
||||
}
|
||||
team.all_event_permissions = False
|
||||
team.save()
|
||||
|
||||
with scopes_disabled():
|
||||
o = Order.objects.create(
|
||||
code='FOO', event=event, email='dummy@dummy.test',
|
||||
status=Order.STATUS_PENDING, datetime=now(), expires=now() + timedelta(days=10),
|
||||
sales_channel=event.organizer.sales_channels.get(identifier="web"),
|
||||
total=14, locale='en'
|
||||
)
|
||||
ticket = event.items.create(name='Early-bird ticket', category=None, default_price=23, admission=True,
|
||||
personalized=True)
|
||||
op = o.positions.create(item=ticket, price=Decimal("14"))
|
||||
medium.linked_orderposition = op
|
||||
medium.linked_giftcard = giftcard
|
||||
medium.customer = customer
|
||||
medium.save()
|
||||
giftcard.owner_ticket = op
|
||||
giftcard.save()
|
||||
|
||||
resp = token_client.get(
|
||||
'/api/v1/organizers/{}/reusablemedia/{}/?expand=linked_giftcard&expand='
|
||||
'linked_giftcard.owner_ticket&expand=linked_orderposition&expand=customer'.format(
|
||||
organizer.slug, medium.pk
|
||||
)
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
|
||||
assert resp.data["linked_orderposition"] == {
|
||||
"id": op.pk,
|
||||
}
|
||||
|
||||
assert resp.data["linked_giftcard"] == {
|
||||
"id": giftcard.pk,
|
||||
"secret": "ABCDEF",
|
||||
"issuance": giftcard.issuance.isoformat().replace("+00:00", "Z"),
|
||||
"value": "23.00",
|
||||
"currency": "EUR",
|
||||
"testmode": False,
|
||||
"expires": None,
|
||||
"conditions": None,
|
||||
"owner_ticket": {"id": op.pk},
|
||||
"issuer": "dummy",
|
||||
}
|
||||
|
||||
assert resp.data["customer"] == {
|
||||
"identifier": customer.identifier,
|
||||
"external_identifier": None,
|
||||
"email": "foo@example.org",
|
||||
"phone": None,
|
||||
"name": "Foo",
|
||||
"name_parts": {"_legacy": "Foo"},
|
||||
"is_active": True,
|
||||
"is_verified": False,
|
||||
"last_login": None,
|
||||
"date_joined": customer.date_joined.isoformat().replace("+00:00", "Z"),
|
||||
"locale": "en",
|
||||
"last_modified": customer.last_modified.isoformat().replace("+00:00", "Z"),
|
||||
"notes": None
|
||||
}
|
||||
|
||||
|
||||
TEST_MEDIUM_CREATE_PAYLOAD = {
|
||||
"type": "barcode",
|
||||
"identifier": "FOOBAR",
|
||||
|
||||
@@ -35,8 +35,11 @@
|
||||
import datetime
|
||||
import os
|
||||
import re
|
||||
import socket
|
||||
from contextlib import contextmanager
|
||||
from decimal import Decimal
|
||||
from email.mime.text import MIMEText
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
from django.conf import settings
|
||||
@@ -591,3 +594,117 @@ def test_attached_ical_localization(env, order):
|
||||
assert len(djmail.outbox) == 1
|
||||
assert len(djmail.outbox[0].attachments) == 1
|
||||
assert description in djmail.outbox[0].attachments[0][1]
|
||||
|
||||
|
||||
PRIVATE_IPS_RES = [
|
||||
[(socket.AF_INET, socket.SOCK_STREAM, 6, '', ('10.0.0.3', 443))],
|
||||
[(socket.AF_INET, socket.SOCK_STREAM, 6, '', ('0.0.0.0', 443))],
|
||||
[(socket.AF_INET, socket.SOCK_STREAM, 6, '', ('127.1.1.1', 443))],
|
||||
[(socket.AF_INET, socket.SOCK_STREAM, 6, '', ('192.168.5.3', 443))],
|
||||
[(socket.AF_INET, socket.SOCK_STREAM, 6, '', ('224.0.0.1', 443))],
|
||||
[(socket.AF_INET6, socket.SOCK_STREAM, 6, '', ('::1', 443, 0, 0))],
|
||||
[(socket.AF_INET6, socket.SOCK_STREAM, 6, '', ('fe80::1', 443, 0, 0))],
|
||||
[(socket.AF_INET6, socket.SOCK_STREAM, 6, '', ('ff00::1', 443, 0, 0))],
|
||||
[(socket.AF_INET6, socket.SOCK_STREAM, 6, '', ('fc00::1', 443, 0, 0))],
|
||||
]
|
||||
|
||||
|
||||
@contextmanager
|
||||
def assert_mail_connection(res, should_connect, use_ssl):
|
||||
with (
|
||||
mock.patch('socket.socket') as mock_socket,
|
||||
mock.patch('socket.getaddrinfo', return_value=res),
|
||||
mock.patch('smtplib.SMTP.getreply', return_value=(220, "")),
|
||||
mock.patch('smtplib.SMTP.sendmail'),
|
||||
mock.patch('ssl.SSLContext.wrap_socket') as mock_ssl
|
||||
):
|
||||
yield
|
||||
|
||||
if should_connect:
|
||||
mock_socket.assert_called_once()
|
||||
mock_socket.return_value.connect.assert_called_once_with(res[0][-1])
|
||||
if use_ssl:
|
||||
mock_ssl.assert_called_once()
|
||||
else:
|
||||
mock_socket.assert_not_called()
|
||||
mock_socket.return_value.connect.assert_not_called()
|
||||
mock_ssl.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("res", PRIVATE_IPS_RES)
|
||||
@pytest.mark.parametrize("use_ssl", [
|
||||
True, False
|
||||
])
|
||||
def test_private_smtp_ip(res, use_ssl, settings):
|
||||
settings.EMAIL_CUSTOM_SMTP_BACKEND = 'pretix.base.email.CheckPrivateNetworkSmtpBackend'
|
||||
settings.MAIL_CUSTOM_SMTP_ALLOW_PRIVATE_NETWORKS = False
|
||||
with assert_mail_connection(res=res, should_connect=False, use_ssl=use_ssl), pytest.raises(match="Request to .* blocked"):
|
||||
connection = djmail.get_connection(backend=settings.EMAIL_CUSTOM_SMTP_BACKEND,
|
||||
host="localhost",
|
||||
use_ssl=use_ssl)
|
||||
connection.open()
|
||||
|
||||
settings.MAIL_CUSTOM_SMTP_ALLOW_PRIVATE_NETWORKS = True
|
||||
with assert_mail_connection(res=res, should_connect=True, use_ssl=use_ssl):
|
||||
connection = djmail.get_connection(backend=settings.EMAIL_CUSTOM_SMTP_BACKEND,
|
||||
host="localhost",
|
||||
use_ssl=use_ssl)
|
||||
connection.open()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("use_ssl", [
|
||||
True, False
|
||||
])
|
||||
@pytest.mark.parametrize("allow_private", [
|
||||
True, False
|
||||
])
|
||||
def test_public_smtp_ip(use_ssl, allow_private, settings):
|
||||
settings.EMAIL_CUSTOM_SMTP_BACKEND = 'pretix.base.email.CheckPrivateNetworkSmtpBackend'
|
||||
settings.MAIL_CUSTOM_SMTP_ALLOW_PRIVATE_NETWORKS = allow_private
|
||||
|
||||
with assert_mail_connection(res=[(socket.AF_INET, socket.SOCK_STREAM, 6, '', ('8.8.8.8', 443))], should_connect=True, use_ssl=use_ssl):
|
||||
connection = djmail.get_connection(backend=settings.EMAIL_CUSTOM_SMTP_BACKEND,
|
||||
host="localhost",
|
||||
use_ssl=use_ssl)
|
||||
connection.open()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize("use_ssl", [
|
||||
True, False
|
||||
])
|
||||
@pytest.mark.parametrize("allow_private_networks", [
|
||||
True, False
|
||||
])
|
||||
@pytest.mark.parametrize("res", PRIVATE_IPS_RES)
|
||||
def test_send_mail_private_ip(res, use_ssl, allow_private_networks, env):
|
||||
settings.EMAIL_CUSTOM_SMTP_BACKEND = 'pretix.base.email.CheckPrivateNetworkSmtpBackend'
|
||||
settings.MAIL_CUSTOM_SMTP_ALLOW_PRIVATE_NETWORKS = allow_private_networks
|
||||
|
||||
event, user, organizer = env
|
||||
event.settings.smtp_use_custom = True
|
||||
event.settings.smtp_host = "example.com"
|
||||
event.settings.smtp_use_ssl = use_ssl
|
||||
event.settings.smtp_use_tls = False
|
||||
|
||||
def send_mail():
|
||||
m = OutgoingMail.objects.create(
|
||||
to=['recipient@example.com'],
|
||||
subject='Test',
|
||||
body_plain='Test',
|
||||
sender='sender@example.com',
|
||||
event=event
|
||||
)
|
||||
assert m.status == OutgoingMail.STATUS_QUEUED
|
||||
mail_send_task.apply(kwargs={
|
||||
'outgoing_mail': m.pk,
|
||||
}, max_retries=0)
|
||||
m.refresh_from_db()
|
||||
return m
|
||||
|
||||
with assert_mail_connection(res=res, should_connect=allow_private_networks, use_ssl=use_ssl):
|
||||
m = send_mail()
|
||||
if allow_private_networks:
|
||||
assert m.status == OutgoingMail.STATUS_SENT
|
||||
else:
|
||||
assert m.status == OutgoingMail.STATUS_FAILED
|
||||
|
||||
93
src/tests/helpers/test_urllib.py
Normal file
93
src/tests/helpers/test_urllib.py
Normal file
@@ -0,0 +1,93 @@
|
||||
#
|
||||
# This file is part of pretix (Community Edition).
|
||||
#
|
||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||
# Copyright (C) 2020-today pretix GmbH and contributors
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||
#
|
||||
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||||
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||||
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||||
# this file, see <https://pretix.eu/about/en/license>.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
from socket import AF_INET, SOCK_STREAM
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
from django.test import override_settings
|
||||
from dns.inet import AF_INET6
|
||||
from urllib3.exceptions import HTTPError
|
||||
|
||||
|
||||
def test_local_blocked():
|
||||
with pytest.raises(HTTPError, match="Request to local address.*"):
|
||||
requests.get("http://localhost", timeout=0.1)
|
||||
with pytest.raises(HTTPError, match="Request to local address.*"):
|
||||
requests.get("https://localhost", timeout=0.1)
|
||||
|
||||
|
||||
def test_private_ip_blocked():
|
||||
with pytest.raises(HTTPError, match="Request to private address.*"):
|
||||
requests.get("http://10.0.0.1", timeout=0.1)
|
||||
with pytest.raises(HTTPError, match="Request to private address.*"):
|
||||
requests.get("https://10.0.0.1", timeout=0.1)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize("res", [
|
||||
[(AF_INET, SOCK_STREAM, 6, '', ('10.0.0.3', 443))],
|
||||
[(AF_INET, SOCK_STREAM, 6, '', ('0.0.0.0', 443))],
|
||||
[(AF_INET, SOCK_STREAM, 6, '', ('127.1.1.1', 443))],
|
||||
[(AF_INET, SOCK_STREAM, 6, '', ('192.168.5.3', 443))],
|
||||
[(AF_INET, SOCK_STREAM, 6, '', ('224.0.0.1', 443))],
|
||||
[(AF_INET6, SOCK_STREAM, 6, '', ('::1', 443, 0, 0))],
|
||||
[(AF_INET6, SOCK_STREAM, 6, '', ('fe80::1', 443, 0, 0))],
|
||||
[(AF_INET6, SOCK_STREAM, 6, '', ('ff00::1', 443, 0, 0))],
|
||||
[(AF_INET6, SOCK_STREAM, 6, '', ('fc00::1', 443, 0, 0))],
|
||||
])
|
||||
def test_dns_resolving_to_local_blocked(res):
|
||||
with mock.patch('socket.getaddrinfo') as mock_addr:
|
||||
mock_addr.return_value = res
|
||||
with pytest.raises(HTTPError, match="Request to (multicast|private|local) address.*"):
|
||||
requests.get("https://example.org", timeout=0.1)
|
||||
with pytest.raises(HTTPError, match="Request to (multicast|private|local) address.*"):
|
||||
requests.get("http://example.org", timeout=0.1)
|
||||
|
||||
|
||||
def test_dns_remote_allowed():
|
||||
class SocketOk(Exception):
|
||||
pass
|
||||
|
||||
def side_effect(*args, **kwargs):
|
||||
raise SocketOk
|
||||
|
||||
with mock.patch('socket.getaddrinfo') as mock_addr, mock.patch('socket.socket') as mock_socket:
|
||||
mock_addr.return_value = [(AF_INET, SOCK_STREAM, 6, '', ('8.8.8.8', 443))]
|
||||
mock_socket.side_effect = side_effect
|
||||
with pytest.raises(SocketOk):
|
||||
requests.get("https://example.org", timeout=0.1)
|
||||
|
||||
|
||||
@override_settings(ALLOW_HTTP_TO_PRIVATE_NETWORKS=True)
|
||||
def test_local_is_allowed():
|
||||
class SocketOk(Exception):
|
||||
pass
|
||||
|
||||
def side_effect(*args, **kwargs):
|
||||
raise SocketOk
|
||||
|
||||
with mock.patch('socket.getaddrinfo') as mock_addr, mock.patch('socket.socket') as mock_socket:
|
||||
mock_addr.return_value = [(AF_INET, SOCK_STREAM, 6, '', ('10.0.0.1', 443))]
|
||||
mock_socket.side_effect = side_effect
|
||||
with pytest.raises(SocketOk):
|
||||
requests.get("https://example.org", timeout=0.1)
|
||||
@@ -1162,6 +1162,65 @@ class WaitingListTest(EventTestMixin, SoupTest):
|
||||
assert wle.voucher is None
|
||||
assert wle.locale == 'en'
|
||||
|
||||
def test_initial_selection(self):
|
||||
with scopes_disabled():
|
||||
cat = ItemCategory.objects.create(event=self.event, name='Tickets')
|
||||
self.item.category = cat
|
||||
self.item.save()
|
||||
|
||||
item2 = Item.objects.create(
|
||||
event=self.event, name='VIP ticket',
|
||||
default_price=Decimal('25.00'),
|
||||
active=True, category=cat,
|
||||
)
|
||||
self.q.items.add(item2)
|
||||
|
||||
response = self.client.get(
|
||||
'/%s/%s/waitinglist/?item=%d' % (
|
||||
self.orga.slug, self.event.slug, item2.pk
|
||||
)
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
doc = BeautifulSoup(response.render().content, "lxml")
|
||||
|
||||
select = doc.find('select', {'name': 'itemvar'})
|
||||
optgroup = select.find('optgroup')
|
||||
self.assertIsNotNone(optgroup, 'Choices should be grouped by category')
|
||||
self.assertEqual(optgroup['label'], 'Tickets')
|
||||
|
||||
selected = select.find_all('option', selected=True)
|
||||
self.assertEqual(len(selected), 1, 'Exactly one option should be pre-selected')
|
||||
self.assertEqual(selected[0]['value'], str(item2.pk))
|
||||
|
||||
def test_initial_selection_with_variation(self):
|
||||
with scopes_disabled():
|
||||
cat = ItemCategory.objects.create(event=self.event, name='Tickets')
|
||||
self.item.category = cat
|
||||
self.item.has_variations = True
|
||||
self.item.save()
|
||||
|
||||
var1 = ItemVariation.objects.create(item=self.item, value='Standard')
|
||||
var2 = ItemVariation.objects.create(item=self.item, value='Premium')
|
||||
self.q.variations.add(var1, var2)
|
||||
|
||||
response = self.client.get(
|
||||
'/%s/%s/waitinglist/?item=%d&var=%d' % (
|
||||
self.orga.slug, self.event.slug,
|
||||
self.item.pk, var2.pk,
|
||||
)
|
||||
)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
doc = BeautifulSoup(response.render().content, "lxml")
|
||||
|
||||
select = doc.find('select', {'name': 'itemvar'})
|
||||
optgroup = select.find('optgroup')
|
||||
self.assertIsNotNone(optgroup, 'Choices should be grouped by category')
|
||||
self.assertEqual(optgroup['label'], 'Tickets')
|
||||
|
||||
selected = select.find_all('option', selected=True)
|
||||
self.assertEqual(len(selected), 1, 'Exactly one option should be pre-selected')
|
||||
self.assertEqual(selected[0]['value'], '%d-%d' % (self.item.pk, var2.pk))
|
||||
|
||||
def test_subevent_valid(self):
|
||||
with scopes_disabled():
|
||||
self.event.has_subevents = True
|
||||
|
||||
Reference in New Issue
Block a user