Compare commits

..

2 Commits

Author SHA1 Message Date
Raphael Michel bc52dc81f3 New implementation 2026-04-01 10:38:48 +02:00
Raphael Michel f3dbbce39c Order change form: Allow to add multiple identical positions (Z#23227479) 2026-03-30 17:36:36 +02:00
82 changed files with 1154 additions and 2366 deletions
+5 -10
View File
@@ -1,16 +1,11 @@
Contributing to pretix
======================
Welcome to pretix, we are happy that you would like to contribute.
Before you do so, please make sure to read the following documents:
Hey there and welcome to pretix!
- [Contribution workflow](https://docs.pretix.eu/dev/development/contribution/general.html)
- [AI-assisted contribution policy](https://docs.pretix.eu/dev/development/contribution/ai.html)
- [Coding style and quality](https://docs.pretix.eu/dev/development/contribution/style.html)
- [Development setup](https://docs.pretix.eu/dev/development/setup.html)
- [Code of Conduct](https://docs.pretix.eu/dev/development/contribution/codeofconduct.html)
* We've got a contributors guide in [our documentation](https://docs.pretix.eu/dev/development/contribution/) together with notes on the [development setup](https://docs.pretix.eu/dev/development/setup.html).
Before we can accept your first PR we'll need you to sign [our **Contributor License Agreement** (CLA)](https://pretix.eu/about/en/cla).
You can find more information about the how and why in our [License FAQ](https://docs.pretix.eu/trust/licensing/faq/) and in our [license change blog post](https://pretix.eu/about/en/blog/20210412-license/).
* Please note that we have a [Code of Conduct](https://docs.pretix.eu/dev/development/contribution/codeofconduct.html) in place that applies to all project contributions, including issues, pull requests, etc.
* Before we can accept a PR from you we'll need you to sign [our CLA](https://pretix.eu/about/en/cla). You can find more information about the how and why in our [License FAQ](https://docs.pretix.eu/trust/licensing/faq/) and in our [license change blog post](https://pretix.eu/about/en/blog/20210412-license/).
**Before contributing new functionality, always open a discussion first.**
+1 -1
View File
@@ -1,4 +1,4 @@
FROM python:3.13-trixie
FROM python:3.11-bookworm
RUN apt-get update && \
apt-get install -y --no-install-recommends \
-24
View File
@@ -1,24 +0,0 @@
.. _`aipolicy`:
AI-assisted contribution policy
===============================
pretix is maintained by humans.
Every discussion, issue, and pull request is read and reviewed by humans (and sometimes machines, too).
We ask you to respect the time and effort put in by these humans by not sending low-effort, unqualified work, since it puts the burden of validation on the maintainer.
Therefore, the pretix project has strict rules for AI usage:
- **All AI usage in any form must be disclosed.** You must state the tool you used (e.g. Claude Code, Cursor, Amp) along with the extent that the work was AI-assisted.
- **The human-in-the-loop must fully understand all code.** If you can't explain what your changes do and how they interact with the greater system without the aid of AI tools, do not contribute to this project.
- **Issues and discussions can use AI assistance but must have a full human-in-the-loop.** This means that any content generated with AI must have been reviewed and edited by a human before submission. AI is very good at being overly verbose and including noise that distracts from the main point. Humans must do their research and trim this down.
- **No AI-generated media is allowed (art, images, videos, audio, etc.).** Text and code are the only acceptable AI-generated content, per the other rules in this policy.
- **Bad AI drivers will be excluded from the project.** People who produce bad contributions that are clearly AI (slop) will be blocked from our organization without warning.
This policy was inspired by the `ghostty project`_.
.. _ghostty project: https://github.com/ghostty-org/ghostty/blob/main/AI_POLICY.md
+11 -30
View File
@@ -1,39 +1,23 @@
Contribution workflow
=====================
General remarks
===============
You are interested in contributing to pretix? That is awesome!
If youre new to contributing to open source software, dont be afraid. Well happily review your code and give you
constructive and friendly feedback on your changes. Every contribution should go through the following steps.
constructive and friendly feedback on your changes.
Discussion & Design
-------------------
pretix is a large and mature project with more of a decade of history and hopefully many more decades to come.
Keeping pretix in good shape over long timeframes is first and foremost a fight against complexity.
With every additional feature, complexity grows, and both features and complexity are hard to remove.
Even if you are doing the initial work of the contribution, accepting the contribution is not free for us.
Not only will we need to maintain the feature, but every feature adds cost to the maintenance of every other feature it interacts with, and every feature adds effort for users to understand how pretix works.
Therefore, we must carefully select what features we add, based on how well they fit the system in general and of how much use they will be to our larger user base.
We strongly ask you to **create a discussion on GitHub for every new feature idea** outlining the use case and the proposed implementation design.
Pull requests without prior discussion will likely just be closed.
For bug fixes and very minor changes, you can skip this step and open a PR right away.
Development
-----------
To develop your contribution, you'll need pretix running locally on your machine. Head over to :ref:`devsetup` to learn how to do this.
First of all, you'll need pretix running locally on your machine. Head over to :ref:`devsetup` to learn how to do this.
If you run into any problems on your way, please do not hesitate to ask us anytime!
While developing, please have a look at our :ref:`aipolicy` and our guidelines on :ref:`codestyle`.
Please note that we bound ourselves to a :ref:`coc` that applies to all communication around the project. You can be
assured that we will not tolerate any form of harassment.
Sending a patch
---------------
Once you have a first draft of your changes, please `create a pull request`_ on our `GitHub repository`_.
If you improved pretix in any way, we'd be very happy if you contribute it
back to the main code base! The easiest way to do so is to `create a pull request`_
on our `GitHub repository`_.
We recommend that you create a feature branch for every issue you work on so the changes can
be reviewed individually.
@@ -41,17 +25,14 @@ Please use the test suite to check whether your changes break any existing featu
the code style checks to confirm you are consistent with pretix's coding style. You'll
find instructions on this in the :ref:`checksandtests` section of the development setup guide.
We automatically run the tests and the code style check on every pull request through GitHub Actions and we wont
We automatically run the tests and the code style check on every pull request on Travis CI and we wont
accept any pull requests without all tests passing. However, if you don't find out *why* they are not passing,
just send the pull request and tell us we'll be glad to help.
If you add a new feature, please include appropriate documentation into your patch. If you fix a bug,
please include a regression test, i.e. a test that fails without your changes and passes after applying your changes.
Again: If you get stuck, do not hesitate to contact us through GitHub discussions.
Please note that we bound ourselves to a :ref:`coc` that applies to all communication around the project. You can be
assured that we will not tolerate any form of harassment.
Again: If you get stuck, do not hesitate to contact any of us, or Raphael personally at mail@raphaelmichel.de.
.. _create a pull request: https://help.github.com/articles/creating-a-pull-request/
.. _GitHub repository: https://github.com/pretix/pretix
-1
View File
@@ -6,5 +6,4 @@ Contributing to pretix
general
style
ai
codeofconduct
+2 -2
View File
@@ -1,7 +1,5 @@
.. spelling:word-list:: Rebase rebasing
.. _`codestyle`:
Coding style and quality
========================
@@ -30,6 +28,8 @@ Code
Commits and Pull Requests
-------------------------
Most commits should start as pull requests, therefore this applies to the titles of pull requests as well since
the pull request title will become the commit message on merge. We prefer merging with GitHub's "Squash and merge"
feature if the PR contains multiple commits that do not carry value to keep. If there is value in keeping the
+7 -7
View File
@@ -33,9 +33,9 @@ dependencies = [
"bleach==6.3.*",
"celery==5.6.*",
"chardet==5.2.*",
"cryptography>=46.0.7",
"cryptography>=44.0.0",
"css-inline==0.20.*",
"defusedcsv>=3.0.0",
"defusedcsv>=1.1.0",
"dnspython==2.*",
"Django[argon2]==5.2.*",
"django-bootstrap3==26.1",
@@ -76,7 +76,7 @@ dependencies = [
"paypal-checkout-serversdk==1.0.*",
"PyJWT==2.12.*",
"phonenumberslite==9.0.*",
"Pillow==12.2.*",
"Pillow==12.1.*",
"pretix-plugin-build",
"protobuf==7.34.*",
"psycopg2-binary",
@@ -90,14 +90,14 @@ dependencies = [
"pytz-deprecation-shim==0.1.*",
"pyuca",
"qrcode==8.2",
"redis==7.4.*",
"redis==7.1.*",
"reportlab==4.4.*",
"requests==2.32.*",
"sentry-sdk==2.58.*",
"sentry-sdk==2.56.*",
"sepaxml==2.7.*",
"stripe==7.9.*",
"text-unidecode==1.*",
"tlds>=2026041800",
"tlds>=2020041600",
"tqdm==4.*",
"ua-parser==1.0.*",
"vobject==0.9.*",
@@ -117,7 +117,7 @@ dev = [
"isort==8.0.*",
"pep8-naming==0.15.*",
"potypo",
"pytest-asyncio>=1.3.0",
"pytest-asyncio>=0.24",
"pytest-cache",
"pytest-cov",
"pytest-django==4.*",
+3 -25
View File
@@ -31,9 +31,7 @@ from pretix.api.serializers.order import OrderPositionSerializer
from pretix.api.serializers.organizer import (
CustomerSerializer, GiftCardSerializer,
)
from pretix.base.models import (
Device, Order, OrderPosition, ReusableMedium, TeamAPIToken,
)
from pretix.base.models import Order, OrderPosition, ReusableMedium
logger = logging.getLogger(__name__)
@@ -82,7 +80,8 @@ class ReusableMediaSerializer(I18nAwareModelSerializer):
)
if 'linked_orderposition' in self.context['request'].query_params.getlist('expand'):
# Permission Check performed in to_representation
# No additional permission check performed, documented limitation of the permission system
# Would get to complex/unusable otherwise since the permission depends on the event
self.fields['linked_orderposition'] = NestedOrderPositionSerializer(read_only=True)
else:
self.fields['linked_orderposition'] = serializers.PrimaryKeyRelatedField(
@@ -118,27 +117,6 @@ 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 = (
+1 -5
View File
@@ -769,11 +769,7 @@ class PaymentDetailsField(serializers.Field):
pp = value.payment_provider
if not pp:
return {}
try:
return pp.api_payment_details(value)
except Exception:
logger.exception("Failed to retrieve payment_details")
return {}
return pp.api_payment_details(value)
class OrderPaymentSerializer(I18nAwareModelSerializer):
-13
View File
@@ -286,19 +286,6 @@ 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',
+1 -1
View File
@@ -1122,7 +1122,7 @@ class CheckinViewSet(viewsets.ReadOnlyModelViewSet):
permission = 'event.orders:read'
def get_queryset(self):
qs = Checkin.all.filter(list__event=self.request.event).select_related(
qs = Checkin.all.filter().select_related(
"position",
"device",
)
+29 -46
View File
@@ -381,15 +381,12 @@ class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
resp = HttpResponse(ct.file.file.read(), content_type='text/uri-list')
return resp
else:
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
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 resp
@action(detail=True, methods=['POST'])
def mark_paid(self, request, **kwargs):
@@ -1306,17 +1303,14 @@ class EventOrderPositionViewSet(OrderPositionViewSetMixin, viewsets.ModelViewSet
raise NotFound()
ftype, ignored = mimetypes.guess_type(answer.file.name)
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'
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 resp
@action(detail=True, url_name="printlog", url_path="printlog", methods=["POST"])
def printlog(self, request, **kwargs):
@@ -1371,18 +1365,15 @@ class EventOrderPositionViewSet(OrderPositionViewSetMixin, viewsets.ModelViewSet
if hasattr(image_file, 'seek'):
image_file.seek(0)
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'
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 resp
@action(detail=True, url_name='download', url_path='download/(?P<output>[^/]+)')
def download(self, request, output, **kwargs):
@@ -1408,15 +1399,12 @@ class EventOrderPositionViewSet(OrderPositionViewSetMixin, viewsets.ModelViewSet
resp = HttpResponse(ct.file.file.read(), content_type='text/uri-list')
return resp
else:
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
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 resp
@action(detail=True, methods=['POST'])
def regenerate_secrets(self, request, **kwargs):
@@ -1658,7 +1646,6 @@ class PaymentViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
count_waitinglist=False,
force=request.data.get('force', False),
send_mail=send_mail,
ignore_date=True,
)
except Quota.QuotaExceededException:
pass
@@ -1694,8 +1681,7 @@ class PaymentViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
auth=self.request.auth,
count_waitinglist=False,
send_mail=send_mail,
force=force,
ignore_date=True)
force=force)
except Quota.QuotaExceededException as e:
return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
except PaymentException as e:
@@ -2000,12 +1986,9 @@ class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
if not invoice.file:
raise RetryException()
return FileResponse(
invoice.file.file,
filename='{}.pdf'.format(invoice.number),
as_attachment=True,
content_type='application/pdf'
)
resp = FileResponse(invoice.file.file, content_type='application/pdf')
resp['Content-Disposition'] = 'attachment; filename="{}.pdf"'.format(invoice.number)
return resp
@action(detail=True, methods=['POST'])
def transmit(self, request, **kwargs):
-80
View File
@@ -19,10 +19,7 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
import ipaddress
import logging
import smtplib
import socket
from itertools import groupby
from smtplib import SMTPResponseException
from typing import TypeVar
@@ -240,80 +237,3 @@ 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
+3 -13
View File
@@ -47,7 +47,6 @@ from django.utils.formats import localize
from django.utils.translation import gettext, gettext_lazy as _
from pretix.base.models import Event
from pretix.base.models.auth import PermissionHolder
from pretix.helpers.safe_openpyxl import ( # NOQA: backwards compatibility for plugins using excel_safe
SafeWorkbook, remove_invalid_excel_chars as excel_safe,
)
@@ -60,20 +59,11 @@ class BaseExporter:
This is the base class for all data exporters
"""
def __init__(self, event, organizer, permission_holder: PermissionHolder=None, progress_callback=lambda v: None):
"""
:param event: Event context, can also be a queryset of events for multi-event exports
:param organizer: Organizer context
:param user: The user who triggered the export (or None).
:param token: The API token that triggered the export (or None).
:param device: The device that triggered the export (or None)
:param progress_callback: Callback function with progress
"""
def __init__(self, event, organizer, progress_callback=lambda v: None):
self.event = event
self.organizer = organizer
self.progress_callback = progress_callback
self.is_multievent = isinstance(event, QuerySet)
self.permission_holder = permission_holder
if isinstance(event, QuerySet):
self.events = event
self.event = None
@@ -190,7 +180,7 @@ class BaseExporter:
return True
@classmethod
def get_required_event_permission(cls) -> Optional[str]:
def get_required_event_permission(cls) -> str:
"""
The permission level required to use this exporter for events. For multi-event-exports, this will be used
to limit the selection of events. Will be ignored if the ``OrganizerLevelExportMixin`` mixin is used.
@@ -205,7 +195,7 @@ class OrganizerLevelExportMixin:
raise TypeError("required_event_permission may not be called on OrganizerLevelExportMixin")
@classmethod
def get_required_organizer_permission(cls) -> Optional[str]:
def get_required_organizer_permission(cls) -> str:
"""
The permission level required to use this exporter. Must be set for organizer-level exports. Set to `None` to
allow everyone with any access to the organizer.
-14
View File
@@ -1103,25 +1103,13 @@ class PaymentListExporter(ListExporter):
def iterate_list(self, form_data):
provider_names = dict(get_all_payment_providers())
i_numbers = Invoice.objects.filter(
order=OuterRef('order_id'),
).values('order').annotate(
m=GroupConcat('full_invoice_no', delimiter=', ')
).values(
'm'
).order_by()
payments = OrderPayment.objects.filter(
order__event__in=self.events,
state__in=form_data.get('payment_states', [])
).annotate(
order_invoice_numbers=Subquery(i_numbers, output_field=CharField()),
).select_related('order').prefetch_related('order__event').order_by('created')
refunds = OrderRefund.objects.filter(
order__event__in=self.events,
state__in=form_data.get('refund_states', [])
).annotate(
order_invoice_numbers=Subquery(i_numbers, output_field=CharField()),
).select_related('order').prefetch_related('order__event').order_by('created')
if form_data.get('end_date_range'):
@@ -1147,7 +1135,6 @@ class PaymentListExporter(ListExporter):
headers = [
_('Event slug'), _('Order'), _('Payment ID'), _('Creation date'), _('Completion date'), _('Status'),
_('Status code'), _('Amount'), _('Payment method'), _('Comment'), _('Matching ID'), _('Payment details'),
_('Invoice numbers'),
]
yield headers
@@ -1185,7 +1172,6 @@ class PaymentListExporter(ListExporter):
obj.comment if isinstance(obj, OrderRefund) else "",
matching_id,
payment_details,
obj.order_invoice_numbers,
]
yield row
+2 -8
View File
@@ -90,7 +90,7 @@ from pretix.base.settings import (
COUNTRIES_WITH_STATE_IN_ADDRESS, COUNTRY_STATE_LABEL,
PERSON_NAME_SALUTATIONS, PERSON_NAME_SCHEMES, PERSON_NAME_TITLE_GROUPS,
)
from pretix.base.templatetags.rich_text import URL_RE, rich_text
from pretix.base.templatetags.rich_text import rich_text
from pretix.base.timemachine import time_machine_now
from pretix.control.forms import (
ExtFileField, ExtValidationMixin, SizeValidationMixin, SplitDateTimeField,
@@ -227,15 +227,9 @@ class NamePartsFormField(forms.MultiValueField):
# bots.
r'^[^$€/%§{}<>~]*$',
message=_('Please do not use special characters in names.')
),
RegexValidator(
URL_RE,
inverse_match=True,
message=_('Please do not use special characters in names.')
)
]
}
self.max_length = defaults['max_length']
self.scheme_name = kwargs.pop('scheme')
self.titles = kwargs.pop('titles')
self.scheme = PERSON_NAME_SCHEMES.get(self.scheme_name)
@@ -293,7 +287,7 @@ class NamePartsFormField(forms.MultiValueField):
if self.require_all_fields and not all(v for v in value):
raise forms.ValidationError(self.error_messages['incomplete'], code='required')
if sum(len(v) for v in value.values() if v) > (self.max_length or 250):
if sum(len(v) for v in value.values() if v) > 250:
raise forms.ValidationError(_('Please enter a shorter name.'), code='max_length')
if value.get("salutation") == "empty":
-4
View File
@@ -70,10 +70,6 @@ def parse_csv(file, length=None, mode="strict", charset=None):
except ImportError:
charset = file.charset
data = data.decode(charset or "utf-8", mode)
# remove stray linebreaks from the end of the file
data = data.rstrip("\n")
# If the file was modified on a Mac, it only contains \r as line breaks
if '\r' in data and '\n' not in data:
data = data.replace('\r', '\n')
+3 -9
View File
@@ -29,9 +29,7 @@ import inspect
import logging
import os
import threading
from pathlib import Path
import django
from django.conf import settings
from django.db import transaction
@@ -76,14 +74,10 @@ def _transactions_mark_order_dirty(order_id, using=None):
if "PYTEST_CURRENT_TEST" in os.environ:
# We don't care about Order.objects.create() calls in test code so let's try to figure out if this is test code
# or not.
for frame in inspect.stack()[1:]:
if (
'pretix/base/models/orders' in frame.filename
or Path(frame.filename).is_relative_to(Path(django.__file__).parent)
):
# Ignore model- and django-internal code
for frame in inspect.stack():
if 'pretix/base/models/orders' in frame.filename:
continue
elif 'test_' in frame.filename or 'conftest.py' in frame.filename:
elif 'test_' in frame.filename or 'conftest.py in frame.filename':
return
elif 'pretix/' in frame.filename or 'pretix_' in frame.filename:
# This went through non-test code, let's consider it non-test
-21
View File
@@ -38,7 +38,6 @@ import operator
import secrets
from datetime import timedelta
from functools import reduce
from typing import Protocol
from django.conf import settings
from django.contrib.auth.models import (
@@ -68,14 +67,6 @@ class EmailAddressTakenError(IntegrityError):
pass
class PermissionHolder(Protocol):
def has_event_permission(self, organizer, event, perm_name=None, request=None, session_key=None) -> bool:
...
def has_organizer_permission(self, organizer, perm_name=None, request=None):
...
class UserManager(BaseUserManager):
"""
This is the user manager for our custom user model. See the User
@@ -705,18 +696,6 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
return self.teams.exists()
class UserWithStaffSession:
# Wrapper around a User object with a staff session, implementing the PermissionHolder Protocol
def __init__(self, user):
self.user = user
def has_event_permission(self, organizer, event, perm_name=None, request=None, session_key=None) -> bool:
return True
def has_organizer_permission(self, organizer, perm_name=None, request=None):
return True
class UserKnownLoginSource(models.Model):
user = models.ForeignKey('User', on_delete=models.CASCADE, related_name="known_login_sources")
agent_type = models.CharField(max_length=255, null=True, blank=True)
+1 -2
View File
@@ -229,7 +229,7 @@ class Device(LoggedModel):
"""
return self._organizer_permission_set() if self.organizer == organizer else set()
def has_event_permission(self, organizer, event, perm_name=None, request=None, session_key=None) -> bool:
def has_event_permission(self, organizer, event, perm_name=None, request=None) -> bool:
"""
Checks if this token is part of a team that grants access of type ``perm_name``
to the event ``event``.
@@ -238,7 +238,6 @@ class Device(LoggedModel):
:param event: The event to check
:param perm_name: The permission, e.g. ``event.orders:read``
:param request: This parameter is ignored and only defined for compatibility reasons.
:param session_key: This parameter is ignored and only defined for compatibility reasons.
:return: bool
"""
has_event_access = (self.all_events and organizer == self.organizer) or (
-6
View File
@@ -715,12 +715,6 @@ class Event(EventMixin, LoggedModel):
self.settings.name_scheme = 'given_family'
self.settings.payment_banktransfer_invoice_immediately = True
self.settings.low_availability_percentage = 10
self.settings.mail_send_order_free_attendee = True
self.settings.mail_send_order_placed_attendee = True
self.settings.mail_send_order_paid_attendee = True
self.settings.mail_send_order_approved_attendee = True
self.settings.mail_send_order_approved_free_attendee = True
self.settings.mail_text_download_reminder_attendee = True
@property
def social_image(self):
+2 -2
View File
@@ -590,7 +590,7 @@ class Order(LockModel, LoggedModel):
not kwargs.get('force_save_with_deferred_fields', None) and
(not update_fields or ('require_approval' not in update_fields and 'status' not in update_fields))
):
_fail("It is unsafe to call save() on an Order with deferred fields since we can't check if you missed "
_fail("It is unsafe to call save() on an OrderFee with deferred fields since we can't check if you missed "
"creating a transaction. Call save(force_save_with_deferred_fields=True) if you really want to do "
"this.")
@@ -2841,7 +2841,7 @@ class OrderPosition(AbstractPosition):
if Transaction.key(self) != self.__initial_transaction_key or self.canceled != self.__initial_canceled or not self.pk:
_transactions_mark_order_dirty(self.order_id, using=kwargs.get('using', None))
elif not kwargs.get('force_save_with_deferred_fields', None):
_fail("It is unsafe to call save() on an OrderPosition with deferred fields since we can't check if you missed "
_fail("It is unsafe to call save() on an OrderFee with deferred fields since we can't check if you missed "
"creating a transaction. Call save(force_save_with_deferred_fields=True) if you really want to do "
"this.")
+1 -8
View File
@@ -319,9 +319,6 @@ class TeamQuerySet(models.QuerySet):
def event_permission_q(cls, perm_name):
from ..permissions import assert_valid_event_permission
if perm_name is None:
return Q()
if perm_name.startswith('can_') and perm_name in OLD_TO_NEW_EVENT_COMPAT: # legacy
return reduce(operator.and_, [cls.event_permission_q(p) for p in OLD_TO_NEW_EVENT_COMPAT[perm_name]])
assert_valid_event_permission(perm_name, allow_legacy=False)
@@ -334,9 +331,6 @@ class TeamQuerySet(models.QuerySet):
def organizer_permission_q(cls, perm_name):
from ..permissions import assert_valid_organizer_permission
if perm_name is None:
return Q()
if perm_name.startswith('can_') and perm_name in OLD_TO_NEW_ORGANIZER_COMPAT: # legacy
return reduce(operator.and_, [cls.organizer_permission_q(p) for p in OLD_TO_NEW_ORGANIZER_COMPAT[perm_name]])
assert_valid_organizer_permission(perm_name, allow_legacy=False)
@@ -556,7 +550,7 @@ class TeamAPIToken(models.Model):
"""
return self.team.organizer_permission_set() if self.team.organizer == organizer else set()
def has_event_permission(self, organizer, event, perm_name=None, request=None, session_key=None) -> bool:
def has_event_permission(self, organizer, event, perm_name=None, request=None) -> bool:
"""
Checks if this token is part of a team that grants access of type ``perm_name``
to the event ``event``.
@@ -565,7 +559,6 @@ class TeamAPIToken(models.Model):
:param event: The event to check
:param perm_name: The permission, e.g. ``event.orders:read``
:param request: This parameter is ignored and only defined for compatibility reasons.
:param session_key: This parameter is ignored and only defined for compatibility reasons.
:return: bool
"""
has_event_access = (self.team.all_events and organizer == self.team.organizer) or (
+5 -16
View File
@@ -54,7 +54,7 @@ from bidi import get_display
from django.conf import settings
from django.contrib.staticfiles import finders
from django.core.exceptions import ValidationError
from django.db.models import Exists, Max, Min, OuterRef
from django.db.models import Max, Min
from django.db.models.fields.files import FieldFile
from django.dispatch import receiver
from django.utils.deconstruct import deconstructible
@@ -76,7 +76,7 @@ from reportlab.pdfgen.canvas import Canvas
from reportlab.platypus import Paragraph
from pretix.base.i18n import language
from pretix.base.models import Checkin, Event, Order, OrderPosition, Question
from pretix.base.models import Event, Order, OrderPosition, Question
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.signals import layout_image_variables, layout_text_variables
from pretix.base.templatetags.money import money_filter
@@ -379,13 +379,6 @@ DEFAULT_VARIABLES = OrderedDict((
str(p) for p in generate_compressed_addon_list(op, order, ev)
])
}),
("checked_in_addons", {
"label": _("List of Checked-In Add-Ons"),
"editor_sample": _("Add-on 1\n2x Add-on 2"),
"evaluate": lambda op, order, ev: "\n".join([
str(p) for p in generate_compressed_addon_list(op, order, ev, only_checked_in=True)
])
}),
("organizer", {
"label": _("Organizer name"),
"editor_sample": _("Event organizer company"),
@@ -757,16 +750,12 @@ def get_program_times(op: OrderPosition, ev: Event):
])
def generate_compressed_addon_list(op, order, event, only_checked_in=False):
def generate_compressed_addon_list(op, order, event):
itemcount = defaultdict(int)
addon_qs = (
addons = [p for p in (
op.addons.all() if 'addons' in getattr(op, '_prefetched_objects_cache', {})
else op.addons.select_related('item', 'variation')
)
if only_checked_in:
addon_qs = addon_qs.filter(Exists(Checkin.objects.filter(position=OuterRef('pk'))), canceled=False)
addons = [p for p in addon_qs if not p.canceled]
) if not p.canceled]
for pos in addons:
itemcount[pos.item, pos.variation] += 1
-28
View File
@@ -38,7 +38,6 @@ SOURCE_NAMES = {
None: _('European Central Bank'), # backwards-compatibility
'eu:ecb:eurofxref-daily': _('European Central Bank'),
'cz:cnb:rate-fixing-daily': _('Czech National Bank'),
'pl:nbp:table-a': _('National Bank of Poland'),
}
@@ -50,7 +49,6 @@ def fetch_rates(sender, **kwargs):
source_tasks = {
'eu:ecb:eurofxref-daily': fetch_ecb_rates,
'cz:cnb:rate-fixing-daily': fetch_cnb_cz_rates,
'pl:nbp:table-a': fetch_nbp_pl_rates,
}
for source_name, task in source_tasks.items():
@@ -146,29 +144,3 @@ def fetch_cnb_cz_rates():
rate=rate,
)
)
@app.task()
def fetch_nbp_pl_rates():
"""
Fetches currency rates from the Polish National Bank.
"""
r = requests.get("https://api.nbp.pl/api/exchangerates/tables/A/", headers={
"Accept": "application/json",
})
r.raise_for_status()
data = r.json()[0]
source_date = datetime.strptime(data["effectiveDate"], "%Y-%m-%d").date()
for r in data["rates"]:
rate = Decimal(r["mid"]).quantize(Decimal('0.000001'))
ExchangeRate.objects.update_or_create(
source='pl:nbp:table-a',
source_currency=r["code"],
other_currency='PLN',
defaults=dict(
source_date=source_date,
rate=rate,
)
)
+3 -19
View File
@@ -40,7 +40,6 @@ from pretix.base.models import (
CachedFile, Device, Event, Organizer, ScheduledEventExport, TeamAPIToken,
User, cachedfile_name,
)
from pretix.base.models.auth import UserWithStaffSession
from pretix.base.models.exports import ScheduledOrganizerExport
from pretix.base.services.mail import mail
from pretix.base.services.tasks import (
@@ -212,12 +211,7 @@ def init_event_exporters(event, user=None, token=None, device=None, request=None
if not perm_holder.has_event_permission(event.organizer, event, permission_name, request) and not staff_session:
continue
exporter: BaseExporter = response(
event=event,
organizer=event.organizer,
permission_holder=token or device or (UserWithStaffSession(user) if staff_session else user),
**kwargs
)
exporter: BaseExporter = response(event=event, organizer=event.organizer, **kwargs)
if not exporter.available_for_user(user if user and user.is_authenticated else None):
continue
@@ -249,12 +243,7 @@ def init_organizer_exporters(
continue
if issubclass(response, OrganizerLevelExportMixin):
exporter: BaseExporter = response(
event=Event.objects.none(),
organizer=organizer,
permission_holder=token or device or (UserWithStaffSession(user) if staff_session else user),
**kwargs,
)
exporter: BaseExporter = response(event=Event.objects.none(), organizer=organizer, **kwargs)
try:
if not perm_holder.has_organizer_permission(organizer, response.get_required_organizer_permission(), request) and not staff_session:
@@ -306,12 +295,7 @@ def init_organizer_exporters(
if not _has_permission_on_any_team_cache[permission_name] and not staff_session:
continue
exporter: BaseExporter = response(
event=_event_list_cache[permission_name],
organizer=organizer,
permission_holder=token or device or (UserWithStaffSession(user) if staff_session else user),
**kwargs,
)
exporter: BaseExporter = response(event=_event_list_cache[permission_name], organizer=organizer, **kwargs)
if not exporter.available_for_user(user if user and user.is_authenticated else None):
continue
+1 -15
View File
@@ -58,7 +58,6 @@ 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,
@@ -103,7 +102,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 and lp.state not in (OrderPayment.PAYMENT_STATE_FAILED, OrderPayment.PAYMENT_STATE_CANCELED):
if lp and lp.payment_provider:
if 'payment' in inspect.signature(lp.payment_provider.render_invoice_text).parameters:
payment = str(lp.payment_provider.render_invoice_text(invoice.order, lp))
else:
@@ -205,19 +204,6 @@ def build_invoice(invoice: Invoice) -> Invoice:
invoice.foreign_currency_rate = rate.rate.quantize(Decimal('0.0001'), ROUND_HALF_UP)
invoice.foreign_currency_rate_date = rate.source_date
invoice.foreign_currency_source = 'cz:cnb:rate-fixing-daily'
elif invoice.event.settings.invoice_eu_currencies == 'PLN' and invoice.event.currency != 'PLN':
invoice.foreign_currency_display = 'PLN'
if settings.FETCH_ECB_RATES:
rate = ExchangeRate.objects.filter(
source='pl:nbp:table-a',
source_currency=invoice.event.currency,
other_currency=invoice.foreign_currency_display,
source_date__gt=now().date() - timedelta(days=7)
).first()
if rate:
invoice.foreign_currency_rate = rate.rate.quantize(Decimal('0.0001'), ROUND_HALF_UP)
invoice.foreign_currency_rate_date = rate.source_date
invoice.foreign_currency_source = 'pl:nbp:table-a'
except InvoiceAddress.DoesNotExist:
ia = None
-2
View File
@@ -574,7 +574,6 @@ DEFAULTS = {
('True', _('Based on European Central Bank daily rates, whenever the invoice recipient is in an EU '
'country that uses a different currency.')),
('CZK', _('Based on Czech National Bank daily rates, whenever the invoice amount is not in CZK.')),
('PLN', _('Based on National Bank of Poland daily rates, whenever the invoice amount is not in PLN.')),
),
),
'serializer_kwargs': dict(
@@ -583,7 +582,6 @@ DEFAULTS = {
('True', _('Based on European Central Bank daily rates, whenever the invoice recipient is in an EU '
'country that uses a different currency.')),
('CZK', _('Based on Czech National Bank daily rates, whenever the invoice amount is not in CZK.')),
('PLN', _('Based on National Bank of Poland daily rates, whenever the invoice amount is not in PLN.')),
),
),
},
+1 -1
View File
@@ -436,7 +436,7 @@ class OrderPositionAddForm(forms.Form):
d['used_membership'] = [m for m in self.memberships if str(m.pk) == d['used_membership']][0]
else:
d['used_membership'] = None
if d.get("count", 1) > 1 and d.get("seat"):
if d.get("count", 1) and d.get("seat"):
raise ValidationError({
"seat": _("You can not choose a seat when adding multiple products at once.")
})
+1 -11
View File
@@ -28,7 +28,7 @@ from django.forms import formset_factory
from django.forms.utils import ErrorDict
from django.urls import reverse
from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _, pgettext_lazy
from django.utils.translation import gettext_lazy as _
from i18nfield.forms import I18nInlineFormSet
from pretix.base.forms import I18nModelForm
@@ -102,16 +102,6 @@ class SubEventBulkForm(SubEventForm):
required=False,
limit_choices=('date_from', 'date_to'),
)
skip_if_overlap = forms.BooleanField(
label=pgettext_lazy('subevent', 'Skip dates that overlap with any existing date'),
help_text=pgettext_lazy(
'subevent',
'This can be useful if all your dates happen in the same location and no repeated dates should '
'be created in conflict with existing special events. This respects even inactive dates and works best if '
'all dates have both a start and end time.'
),
required=False,
)
def __init__(self, *args, **kwargs):
self.event = kwargs['event']
@@ -379,8 +379,6 @@
<i class="fa fa-calendar"></i> {% trans "Add many time slots" %}</button>
</p>
</div>
<hr />
{% bootstrap_field form.skip_if_overlap layout="control" horizontal_label_class='sr-only' horizontal_field_class='col-md-12' %}
</fieldset>
<fieldset>
<legend>{% trans "General information" %}</legend>
+6 -1
View File
@@ -763,7 +763,12 @@ 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)
resp['Content-Disposition'] = 'inline; filename="{}"'.format(fname)
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)
return resp
@@ -300,4 +300,5 @@ class SysReportView(AdministratorPermissionRequiredMixin, TemplateView):
resp = HttpResponse(data)
resp['Content-Type'] = mime
resp['Content-Disposition'] = 'inline; filename="{}"'.format(name)
resp._csp_ignore = True
return resp
+14 -18
View File
@@ -710,26 +710,22 @@ class OrderDownload(AsyncAction, OrderView):
resp = HttpResponseRedirect(value.file.file.read())
return resp
else:
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
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 resp
elif isinstance(value, CachedCombinedTicket):
if value.type == 'text/uri-list':
resp = HttpResponseRedirect(value.file.file.read())
return resp
else:
return FileResponse(
value.file.file,
filename='{}-{}-{}{}'.format(
self.request.event.slug.upper(), self.order.code, self.output.identifier, value.extension
),
content_type=value.type
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 resp
else:
return redirect(self.get_self_url())
@@ -1835,15 +1831,15 @@ class InvoiceDownload(EventPermissionRequiredMixin, View):
return redirect(self.get_order_url())
try:
return FileResponse(
self.invoice.file.file,
filename='{}.pdf'.format(re.sub("[^a-zA-Z0-9-_.]+", "_", self.invoice.number)),
content_type='application/pdf'
)
resp = FileResponse(self.invoice.file.file, 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'
+1 -1
View File
@@ -1322,7 +1322,7 @@ class DeviceUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixi
def form_valid(self, form):
if form.has_changed():
self.object.log_action('pretix.device.changed', user=self.request.user, data={
k: form.cleaned_data[k] if k != 'limit_events' else [e.id for e in form.cleaned_data[k]]
k: getattr(self.object, k) if k != 'limit_events' else [e.id for e in getattr(self.object, k).all()]
for k in form.changed_data
})
+8 -2
View File
@@ -263,7 +263,12 @@ class BaseEditorView(EventPermissionRequiredMixin, TemplateView):
resp = HttpResponse(data, content_type=mimet)
ftype = fname.split(".")[-1]
resp['Content-Disposition'] = 'inline; filename="ticket-preview.{}"'.format(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="ticket-preview.{}"'.format(ftype)
resp._csp_ignore = True
else:
resp['Content-Disposition'] = 'attachment; filename="ticket-preview.{}"'.format(ftype)
return resp
elif "data" in request.POST:
if cf:
@@ -304,5 +309,6 @@ 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, filename=cf.filename, content_type='application/pdf')
resp = FileResponse(cf.file, content_type='application/pdf')
resp['Content-Disposition'] = 'attachment; filename="{}"'.format(cf.filename)
return resp
-29
View File
@@ -917,35 +917,6 @@ class SubEventBulkCreate(SubEventEditorMixin, EventPermissionRequiredMixin, Asyn
if len(subevents) > 100_000:
raise ValidationError(_('Please do not create more than 100.000 dates at once.'))
if form.cleaned_data.get("skip_if_overlap") and subevents:
def overlaps(a_from, a_to, b_from, b_to):
if a_from == b_from:
return True
if a_from > b_from:
# a starts after b
# check if it starts before b ends
return b_to and a_from < b_to
# a starts before b
# check if it ends before b starts
return a_to and a_to > b_from
date_min = min(se.date_from for se in subevents)
date_max = max(se.date_to or se.date_from for se in subevents)
dates_existing = list(self.request.event.subevents.annotate(
date_fromto=Coalesce('date_to', 'date_from'),
).filter(
date_from__lte=date_max,
date_fromto__gte=date_min,
).values('date_from', 'date_to'))
subevents = [
se for se in subevents if not any(
overlaps(se.date_from, se.date_to, other['date_from'], other['date_to'])
for other in dates_existing
)
]
if not subevents:
raise ValidationError(_('All dates would be skipped because they conflict with existing dates.'))
for i, se in enumerate(subevents):
se.save(clear_cache=False)
if i % 100 == 0:
+3 -3
View File
@@ -316,7 +316,7 @@ def nav_context_list(request):
page = 1
qs_events = request.user.get_events_with_any_permission(request).filter(
Q(name__icontains=i18ncomp(query)) | Q(slug__icontains=query) | Q(domain__domainname__iexact=query)
Q(name__icontains=i18ncomp(query)) | Q(slug__icontains=query)
).annotate(
min_from=Min('subevents__date_from'),
max_from=Max('subevents__date_from'),
@@ -331,7 +331,7 @@ def nav_context_list(request):
else:
qs_orga = Organizer.objects.filter(pk__in=request.user.teams.values_list('organizer', flat=True))
if query:
qs_orga = qs_orga.filter(Q(name__icontains=query) | Q(slug__icontains=query) | Q(domains__domainname__iexact=query))
qs_orga = qs_orga.filter(Q(name__icontains=query) | Q(slug__icontains=query))
qs_orga = qs_orga.annotate(
n_events=Count("events")
).order_by("-n_events")
@@ -619,7 +619,7 @@ def checkinlist_select2(request, **kwargs):
qs = request.event.checkin_lists.select_related('subevent').filter(
qf
).order_by('subevent__date_from', 'name', 'pk')
).order_by('name')
total = qs.count()
pagesize = 20
-132
View File
@@ -19,26 +19,12 @@
# 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():
@@ -103,123 +89,6 @@ 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")
@@ -230,5 +99,4 @@ 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
+34 -36
View File
@@ -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-04-17 03:00+0000\n"
"Last-Translator: Tim <plicnetwork@gmail.com>\n"
"PO-Revision-Date: 2026-03-18 12:23+0000\n"
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\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.17\n"
"X-Generator: Weblate 5.16.2\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 "Variante de producto"
msgstr "Variación del 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 "Número máximo de usos"
msgstr "Usos máximos"
#: 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 omitir la cuota"
msgstr "Permitir que se anule 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 en el precio"
msgstr "Efecto sobre los precios"
#: 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"
msgstr "Valor del vale de compra"
#: 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 "Muestra los productos ocultos vinculados a este vale"
msgstr "Mostrar los ocultados productos válidos con este vale de compra"
#: 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, el poseedor de este código de vale podrá comprar entradas "
"incluso si no quedan existencias."
"Si se activa, un titular de este vale de compra puede comprar entradas, "
"incluso si no queda ninguna."
#: 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 "Se aplica a la variante del producto seleccionado arriba."
msgstr "Esta variación del producto seleccionado arriba está siendo utilizada."
#: pretix/base/models/vouchers.py:277
msgid ""
"If enabled, the voucher is valid for any product affected by this quota."
msgstr ""
"Si se activa, el vale será válido para cualquier producto incluido en esta "
"cuota."
"Si está habilitado, el vale de compra es válido para cualquier producto "
"afectado por 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 ""
"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."
"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."
#: 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"
msgstr "Vales de compra"
#: pretix/base/models/vouchers.py:342
msgid "You cannot select a quota that belongs to a different event."
@@ -13331,7 +13331,7 @@ msgstr ""
#: pretix/base/settings.py:4157
#, python-brace-format
msgid "VAT-ID is not supported for \"{}\"."
msgstr "El NIF no es compatible con «{}»."
msgstr ""
#: pretix/base/settings.py:4164
msgid "The last payment date cannot be before the end of presale."
@@ -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"
msgstr "Correos electrónicos"
#: 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 en la cuota \"{quota}\""
msgstr "Cualquier producto del contingente \"{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 "Se ha seleccionado un producto no válido."
msgstr "Producto no válido seleccionado."
#: 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 solo coincide con productos ocultos, pero no has seleccionado que "
"deba mostrarlos."
"El vale de compra solo coincide con productos ocultos pero no has "
"seleccionado que los muestre."
#: 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 "Se han guardado los cambios."
msgstr "Los cambios se han guardado."
#: pretix/control/templates/pretixcontrol/event/plugins.html:34
#: pretix/control/templates/pretixcontrol/organizers/plugins.html:34
@@ -27567,30 +27567,28 @@ msgid "Add a two-factor authentication device"
msgstr "Añadir un dispositivo de autenticación de dos factores"
#: pretix/control/templates/pretixcontrol/user/2fa_add.html:19
#, fuzzy
#| msgid "Smartphone with the Authenticator application"
msgid "Smartphone with Authenticator app"
msgstr "Smartphone con la aplicación Authenticator"
msgstr "Celular con aplicación de autenticación"
#: pretix/control/templates/pretixcontrol/user/2fa_add.html:21
msgid ""
"Use your smartphone with any Time-based One-Time-Password app like freeOTP, "
"Google Authenticator or Proton Authenticator."
msgstr ""
"Use su smartphone con cualquier aplicación de contraseñas de un solo uso "
"basadas en el tiempo, como freeOTP, Google Authenticator o 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 "Token físico compatible con WebAuthn"
msgstr "Hardware compatible con token WebAuthn (p. ej. Yubikey)"
#: 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 ""
"Utiliza un dispositivo de seguridad físico, como el Yubikey, u otro método "
"de autenticación biométrica, como el reconocimiento de huellas dactilares o "
"facial."
#: pretix/control/templates/pretixcontrol/user/2fa_confirm_totp.html:8
msgid "To set up this device, please follow the following steps:"
@@ -28698,7 +28696,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 se pudieron guardar los cambios. Consulta los detalles a continuación."
"No hemos podido guardar los cambios. Consulte 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 +30862,7 @@ msgstr ""
#: pretix/plugins/badges/forms.py:33
msgid "Template"
msgstr "Template"
msgstr "Plantilla"
#: pretix/plugins/badges/forms.py:34
msgid ""
+10 -12
View File
@@ -4,10 +4,10 @@ msgstr ""
"Project-Id-Version: 1\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"
"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/"
"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"
@@ -13454,7 +13454,7 @@ msgstr ""
#: pretix/base/settings.py:4157
#, python-brace-format
msgid "VAT-ID is not supported for \"{}\"."
msgstr "Le numéro de TVA n'est pas pris en charge pour « {} »."
msgstr ""
#: pretix/base/settings.py:4164
msgid "The last payment date cannot be before the end of presale."
@@ -27774,30 +27774,28 @@ msgid "Add a two-factor authentication device"
msgstr "Ajouter un dispositif d'authentification à deux facteurs"
#: pretix/control/templates/pretixcontrol/user/2fa_add.html:19
#, fuzzy
#| msgid "Smartphone with the Authenticator application"
msgid "Smartphone with Authenticator app"
msgstr "Smartphone équipé de l'application Authenticator"
msgstr "Smartphone avec l'application Authenticator"
#: pretix/control/templates/pretixcontrol/user/2fa_add.html:21
msgid ""
"Use your smartphone with any Time-based One-Time-Password app like freeOTP, "
"Google Authenticator or Proton Authenticator."
msgstr ""
"Utilisez votre smartphone avec n'importe quelle application de mots de passe "
"à usage unique générés en temps réel, comme freeOTP, Google Authenticator ou "
"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 "Token matériel compatible WebAuthn"
msgstr "Token matériel compatible WebAuthn (par ex. Yubikey)"
#: 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 ""
"Utilisez une clé matérielle telle que la Yubikey, ou un autre moyen "
"d'authentification biométrique, comme la reconnaissance d'empreintes "
"digitales ou faciale."
#: pretix/control/templates/pretixcontrol/user/2fa_confirm_totp.html:8
msgid "To set up this device, please follow the following steps:"
+21 -21
View File
@@ -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-04-20 08:07+0000\n"
"Last-Translator: Yasunobu YesNo Kawaguchi <kawaguti@gmail.com>\n"
"PO-Revision-Date: 2026-03-23 21:00+0000\n"
"Last-Translator: Hijiri Umemoto <hijiri@umemoto.org>\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.17\n"
"X-Generator: Weblate 5.16.2\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 "名"
msgstr "名(First Name)"
#: 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 "VAT-IDは「{}」に対してサポートされていません。"
msgstr ""
#: 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,6 +26796,8 @@ 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アプリを搭載したスマートフォン"
@@ -26804,20 +26806,18 @@ 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対応のハードウェアトークン"
msgstr "WebAuthn対応のハードウェアトークン(例:Yubikey"
#: 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"
+10 -10
View File
@@ -7,10 +7,10 @@ msgstr ""
"Project-Id-Version: 1\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"
"PO-Revision-Date: 2026-03-18 12:23+0000\n"
"Last-Translator: Ruud Hendrickx <ruud@leckxicon.eu>\n"
"Language-Team: Dutch <https://translate.pretix.eu/projects/pretix/pretix/nl/>"
"\n"
"Language-Team: Dutch <https://translate.pretix.eu/projects/pretix/pretix/nl/"
">\n"
"Language: nl\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
@@ -13283,7 +13283,7 @@ msgstr ""
#: pretix/base/settings.py:4157
#, python-brace-format
msgid "VAT-ID is not supported for \"{}\"."
msgstr "Btw-nummer wordt niet ondersteund voor \"{}\"."
msgstr ""
#: pretix/base/settings.py:4164
msgid "The last payment date cannot be before the end of presale."
@@ -27461,28 +27461,28 @@ msgid "Add a two-factor authentication device"
msgstr "Twee-factor-authenticatieapparaat toevoegen"
#: pretix/control/templates/pretixcontrol/user/2fa_add.html:19
#, fuzzy
#| msgid "Smartphone with the Authenticator application"
msgid "Smartphone with Authenticator app"
msgstr "Smartphone met Authenticator-app"
msgstr "Smartphone met de Authenticator-applicatie"
#: pretix/control/templates/pretixcontrol/user/2fa_add.html:21
msgid ""
"Use your smartphone with any Time-based One-Time-Password app like freeOTP, "
"Google Authenticator or Proton Authenticator."
msgstr ""
"Gebruik uw smartphone met een willekeurige app voor tijdgebonden eenmalige "
"wachtwoorden, zoals freeOTP, Google Authenticator of 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-compatibel hardwaretoken"
msgstr "WebAuthn-compatibel hardware-token (bijvoorbeeld Yubikey)"
#: 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 ""
"Gebruik een hardwaretoken zoals de Yubikey, of een andere vorm van "
"biometrische authenticatie, zoals vingerafdruk- of gezichtsherkenning."
#: pretix/control/templates/pretixcontrol/user/2fa_confirm_totp.html:8
msgid "To set up this device, please follow the following steps:"
File diff suppressed because it is too large Load Diff
@@ -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-03-31 17:00+0000\n"
"PO-Revision-Date: 2026-03-18 14:50+0000\n"
"Last-Translator: Ruud Hendrickx <ruud@leckxicon.eu>\n"
"Language-Team: Dutch (informal) <https://translate.pretix.eu/projects/pretix/"
"pretix/nl_Informal/>\n"
@@ -13314,7 +13314,7 @@ msgstr ""
#: pretix/base/settings.py:4157
#, python-brace-format
msgid "VAT-ID is not supported for \"{}\"."
msgstr "Btw-nummer wordt niet ondersteund voor \"{}\"."
msgstr ""
#: pretix/base/settings.py:4164
msgid "The last payment date cannot be before the end of presale."
@@ -27518,28 +27518,28 @@ msgid "Add a two-factor authentication device"
msgstr "Twee-factor-authenticatieapparaat toevoegen"
#: pretix/control/templates/pretixcontrol/user/2fa_add.html:19
#, fuzzy
#| msgid "Smartphone with the Authenticator application"
msgid "Smartphone with Authenticator app"
msgstr "Smartphone met Authenticator-app"
msgstr "Smartphone met de Authenticator-applicatie"
#: pretix/control/templates/pretixcontrol/user/2fa_add.html:21
msgid ""
"Use your smartphone with any Time-based One-Time-Password app like freeOTP, "
"Google Authenticator or Proton Authenticator."
msgstr ""
"Gebruik je smartphone met een willekeurige app voor tijdgebonden eenmalige "
"wachtwoorden, zoals freeOTP, Google Authenticator of 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-compatibel hardwaretoken"
msgstr "WebAuthn-compatibel hardware-token (bijvoorbeeld Yubikey)"
#: 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 ""
"Gebruik een hardwaretoken zoals de Yubikey, of een andere vorm van "
"biometrische authenticatie, zoals vingerafdruk- of gezichtsherkenning."
#: pretix/control/templates/pretixcontrol/user/2fa_confirm_totp.html:8
msgid "To set up this device, please follow the following steps:"
@@ -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-03-30 21:00+0000\n"
"PO-Revision-Date: 2026-03-25 08:00+0000\n"
"Last-Translator: Renne Rocha <renne@rocha.dev.br>\n"
"Language-Team: Portuguese (Brazil) <https://translate.pretix.eu/projects/"
"pretix/pretix/pt_BR/>\n"
@@ -19613,7 +19613,7 @@ msgstr "Excluir"
#: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/import_form.html:91
#: pretix/presale/templates/pretixpresale/fragment_event_list_filter.html:22
msgid "Filter"
msgstr "Filtrar"
msgstr "Filtro"
#: pretix/control/templates/pretixcontrol/checkin/checkins.html:50
msgid "Your search did not match any check-ins."
@@ -28003,9 +28003,6 @@ msgid ""
"According to your event settings, sold out products are hidden from "
"customers. This way, customers will not be able to discover the waiting list."
msgstr ""
"De acordo com as configurações do seu evento, os produtos esgotados ficam "
"ocultos para os clientes. Dessa forma, os clientes não poderão descobrir a "
"lista de espera."
#: pretix/control/templates/pretixcontrol/waitinglist/index.html:38
msgid "Send vouchers"
@@ -28052,9 +28049,6 @@ msgid ""
"waiting list in, you could sell tickets worth an additional "
"<strong>%(amount)s</strong>."
msgstr ""
"Se você conseguir criar espaço suficiente em seu evento para acomodar todas "
"as pessoas na lista de espera, poderá vender ingressos no valor de um "
"adicional de <strong>%(amount)s</strong>."
#: pretix/control/templates/pretixcontrol/waitinglist/index.html:115
msgid "Successfully redeemed"
+2 -2
View File
@@ -216,7 +216,7 @@ class PayView(PaypalOrderView, TemplateView):
@scopes_disabled()
@event_permission_required('event.settings.payment:write')
@event_permission_required('event.settings.general: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.payment:write')
@event_permission_required('event.settings.general:write')
@require_POST
def isu_disconnect(request, **kwargs):
del request.event.settings.payment_paypal_connect_refresh_token
+43
View File
@@ -32,8 +32,11 @@
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under the License.
from itertools import chain
from django import forms
from django.core.exceptions import ValidationError
from django.utils.encoding import force_str
from django.utils.formats import date_format
from django.utils.html import escape
from django.utils.safestring import mark_safe
@@ -165,6 +168,46 @@ class QuestionsForm(BaseQuestionsForm):
)
class AddOnRadioSelect(forms.RadioSelect):
option_template_name = 'pretixpresale/forms/addon_choice_option.html'
def optgroups(self, name, value, attrs=None):
attrs = attrs or {}
groups = []
has_selected = False
for index, (option_value, option_label, option_desc) in enumerate(chain(self.choices)):
if option_value is None:
option_value = ''
if isinstance(option_label, (list, tuple)):
raise TypeError('Choice groups are not supported here')
group_name = None
subgroup = []
groups.append((group_name, subgroup, index))
selected = (
force_str(option_value) in value and
(has_selected is False or self.allow_multiple_selected)
)
if selected is True and has_selected is False:
has_selected = True
attrs['description'] = option_desc
subgroup.append(self.create_option(
name, option_value, option_label, selected, index,
subindex=None, attrs=attrs,
))
return groups
class AddOnVariationField(forms.ChoiceField):
def valid_value(self, value):
text_value = force_str(value)
for k, v, d in self.choices:
if value == k or text_value == force_str(k):
return True
return False
class MembershipForm(forms.Form):
required_css_class = 'required'
+3 -4
View File
@@ -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 target='_blank' href='{}'>{}</a>".format(
self.fields['password'].help_text = "<a href='{}'>{}</a>".format(
build_absolute_uri(False, 'presale:organizer.customer.resetpw', kwargs={
'organizer': request.organizer.slug,
}),
@@ -172,7 +172,7 @@ class RegistrationForm(forms.Form):
)
self.fields['name_parts'] = NamePartsFormField(
max_length=70,
max_length=255,
required=True,
scheme=request.organizer.settings.name_scheme,
titles=request.organizer.settings.name_scheme_titles,
@@ -296,8 +296,7 @@ class SetPasswordForm(forms.Form):
}
email = forms.EmailField(
label=_('Email'),
widget=forms.EmailInput(attrs={'autocomplete': 'username', 'readonly': 'readonly'}),
required=False,
disabled=True
)
password = forms.CharField(
label=_('Password'),
@@ -0,0 +1,3 @@
{% load rich_text %}
<label{% if widget.attrs.id %} for="{{ widget.attrs.id }}"{% endif %}>{% include "django/forms/widgets/input.html" %} {{ widget.label }}</label> {% if widget.attrs.description %}<span class="fa fa-info-circle toggle-variation-description" aria-hidden="true"></span>
<div class="variation-description addon-variation-description">{{ widget.attrs.description|rich_text }}</div>{% endif %}
-3
View File
@@ -91,9 +91,6 @@ 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'),
+13 -35
View File
@@ -70,21 +70,18 @@ def cached_invoice_address(request):
# do not create a session, if we don't have a session we also don't have an invoice address ;)
request._checkout_flow_invoice_address = InvoiceAddress()
return request._checkout_flow_invoice_address
cs = cart_session(request, create=False)
if cs is None:
cs = cart_session(request)
iapk = cs.get('invoice_address')
if not iapk:
request._checkout_flow_invoice_address = InvoiceAddress()
else:
iapk = cs.get('invoice_address')
if not iapk:
try:
with scopes_disabled():
request._checkout_flow_invoice_address = InvoiceAddress.objects.get(
pk=iapk, order__isnull=True
)
except InvoiceAddress.DoesNotExist:
request._checkout_flow_invoice_address = InvoiceAddress()
else:
try:
with scopes_disabled():
request._checkout_flow_invoice_address = InvoiceAddress.objects.get(
pk=iapk, order__isnull=True
)
except InvoiceAddress.DoesNotExist:
request._checkout_flow_invoice_address = InvoiceAddress()
return request._checkout_flow_invoice_address
@@ -114,14 +111,6 @@ class CartMixin:
return cached_invoice_address(self.request)
def get_cart(self, answers=False, queryset=None, order=None, downloads=False, payments=None):
from pretix.presale.views.cart import get_or_create_cart_id
if not get_or_create_cart_id(self.request, create=False) and not order:
# The user has no cart, so we can save a lot of work
return {
'positions': [],
# Other keys are not used on non-checkout pages
}
if queryset is not None:
prefetch = []
if answers:
@@ -177,8 +166,7 @@ class CartMixin:
else:
fees = []
if not order and lcp:
# Do not re-round for empty cart (useless) or confirmed order (incorrect)
if not order:
apply_rounding(self.request.event.settings.tax_rounding, self.invoice_address, self.request.event.currency, [*lcp, *fees])
total = sum([c.price for c in lcp]) + sum([f.value for f in fees])
@@ -289,12 +277,6 @@ class CartMixin:
}
def current_selected_payments(self, positions, fees, invoice_address, *, warn=False):
from pretix.presale.views.cart import get_or_create_cart_id
if not get_or_create_cart_id(self.request, create=False):
# No active cart ID, no payments there
return []
raw_payments = copy.deepcopy(self.cart_session.get('payments', []))
fees = [f for f in fees if f.fee_type != OrderFee.FEE_TYPE_PAYMENT] # we re-compute these here
@@ -379,13 +361,9 @@ def cart_exists(request):
from pretix.presale.views.cart import get_or_create_cart_id
if not hasattr(request, '_cart_cache'):
cid = get_or_create_cart_id(request, create=False)
if cid:
return CartPosition.objects.filter(
cart_id=cid, event=request.event
).exists()
else:
return False
return CartPosition.objects.filter(
cart_id=get_or_create_cart_id(request), event=request.event
).exists()
return bool(request._cart_cache)
+5 -23
View File
@@ -417,7 +417,7 @@ def get_or_create_cart_id(request, create=True):
return new_id
def cart_session(request, create=True):
def cart_session(request):
"""
Before pretix 1.8.0, all checkout-related information (like the entered email address) was stored
in the user's regular session dictionary. This led to data interference and leaks for example if a
@@ -428,9 +428,7 @@ def cart_session(request, create=True):
active cart session sub-dictionary for read and write access.
"""
request.session.modified = True
cart_id = get_or_create_cart_id(request, create=create)
if not cart_id and not create:
return None
cart_id = get_or_create_cart_id(request)
return request.session['carts'][cart_id]
@@ -555,18 +553,6 @@ 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
@@ -855,13 +841,9 @@ class AnswerDownload(EventViewMixin, View):
return Http404()
ftype, _ = mimetypes.guess_type(answer.file.name)
filename = '{}-cart-{}'.format(
resp = FileResponse(answer.file, content_type=ftype or 'application/binary')
resp['Content-Disposition'] = 'attachment; filename="{}-cart-{}"'.format(
self.request.event.slug.upper(),
os.path.basename(answer.file.name).split('.', 1)[1]
)
resp = FileResponse(
answer.file,
filename=filename,
content_type=ftype or 'application/binary'
)
).encode("ascii", "ignore")
return resp
+2
View File
@@ -681,6 +681,8 @@ 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":
+22 -19
View File
@@ -1220,26 +1220,30 @@ class OrderDownloadMixin:
resp = HttpResponseRedirect(value.file.file.read())
return resp
else:
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)
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
elif isinstance(value, CachedCombinedTicket):
if value.type == 'text/uri-list':
resp = HttpResponseRedirect(value.file.file.read())
return resp
else:
return FileResponse(
value.file.file,
filename="{}-{}-{}{}".format(
self.request.event.slug.upper(), self.order.code, self.output.identifier, value.extension),
content_type=value.type
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 resp
else:
return redirect(self.get_self_url())
@@ -1379,14 +1383,13 @@ class InvoiceDownload(EventViewMixin, OrderDetailMixin, View):
return redirect(self.get_order_url())
try:
return FileResponse(
invoice.file.file,
filename='{}.pdf'.format(re.sub("[^a-zA-Z0-9-_.]+", "_", invoice.number)),
content_type='application/pdf'
)
resp = FileResponse(invoice.file.file, 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:
+4 -9
View File
@@ -66,27 +66,22 @@ class WaitingView(EventViewMixin, FormView):
if customer else None
),
)
groups = {}
choices = []
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
group.append((f'{i.pk}-{v.pk}', f'{i.name} {v.value}'))
choices.append((f'{i.pk}-{v.pk}', f'{i.name} {v.value}'))
else:
if i.cached_availability[0] == Quota.AVAILABILITY_OK:
continue
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]
choices.append((f'{i.pk}', f'{i.name}'))
return choices
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
+3 -1
View File
@@ -530,10 +530,12 @@ 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 date as this can be done in the database
# only allow list-view of more than 50 subevents if ordering is by data 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'):
+2 -19
View File
@@ -157,7 +157,7 @@ DATABASES = {
'HOST': config.get('database', 'host', fallback=''),
'PORT': config.get('database', 'port', fallback=''),
'CONN_MAX_AGE': 0 if db_backend == 'sqlite3' else 120,
'CONN_HEALTH_CHECKS': db_backend != 'sqlite3',
'CONN_HEALTH_CHECKS': db_backend != 'sqlite3', # Will only be used from Django 4.1 onwards
'DISABLE_SERVER_SIDE_CURSORS': db_disable_server_side_cursors,
'OPTIONS': db_options,
'TEST': {}
@@ -179,21 +179,6 @@ if config.has_section('replica'):
}
DATABASE_ROUTERS = ['pretix.helpers.database.ReplicaRouter']
if config.has_section('dbreadonly'):
DATABASES['readonly'] = {
'ENGINE': 'django.db.backends.' + db_backend,
'NAME': config.get('dbreadonly', 'name', fallback=DATABASES['default']['NAME']),
'USER': config.get('dbreadonly', 'user', fallback=DATABASES['default']['USER']),
'PASSWORD': config.get('dbreadonly', 'password', fallback=DATABASES['default']['PASSWORD']),
'HOST': config.get('dbreadonly', 'host', fallback=DATABASES['default']['HOST']),
'PORT': config.get('dbreadonly', 'port', fallback=DATABASES['default']['PORT']),
'CONN_MAX_AGE': 0, # do not spam primary with open connections as long as readonly is only used occasionally
'CONN_HEALTH_CHECKS': db_backend != 'sqlite3',
'DISABLE_SERVER_SIDE_CURSORS': db_disable_server_side_cursors,
'OPTIONS': db_options,
'TEST': {}
}
STATIC_URL = config.get('urls', 'static', fallback='/static/')
MEDIA_URL = config.get('urls', 'media', fallback='/media/')
@@ -223,7 +208,6 @@ 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)
@@ -264,8 +248,7 @@ 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 = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_CUSTOM_SMTP_BACKEND = 'pretixbase.email.CheckPrivateNetworkSmtpBackend'
EMAIL_BACKEND = EMAIL_CUSTOM_SMTP_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_TIMEOUT = 60
ADMINS = [('Admin', n) for n in config.get('mail', 'admins', fallback='').split(",") if n]
+13 -12
View File
@@ -1835,9 +1835,10 @@
"integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="
},
"node_modules/brace-expansion": {
"version": "1.1.13",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
"integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
"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",
"optional": true,
"dependencies": {
"balanced-match": "^1.0.0",
@@ -2878,9 +2879,9 @@
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
},
"node_modules/picomatch": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"engines": {
"node": ">=8.6"
},
@@ -4935,9 +4936,9 @@
"integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="
},
"brace-expansion": {
"version": "1.1.13",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
"integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"optional": true,
"requires": {
"balanced-match": "^1.0.0",
@@ -5714,9 +5715,9 @@
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
},
"picomatch": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="
},
"pify": {
"version": "4.0.1",
@@ -71,7 +71,6 @@ $(document).ajaxError(function (event, jqXHR, settings, thrownError) {
});
var form_handlers = function (el) {
el.trigger("rescan.areYouSure");
el.find("[data-formset]").formset(
{
animateForms: true,
@@ -110,10 +110,6 @@ 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) {
@@ -730,16 +726,17 @@ var shared_methods = {
buy_callback: function (data) {
if (data.redirect) {
if (data.cart_id) {
this.$root.set_cart_id(data.cart_id);
this.$root.cart_id = data.cart_id;
setCookie(this.$root.cookieName, data.cart_id, 30);
}
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=' + encodeURIComponent(this.$root.get_cart_id());
url = url + '&iframe=1&locale=' + lang + '&take_cart_id=' + this.$root.cart_id;
} else {
url = url + '?iframe=1&locale=' + lang + '&take_cart_id=' + encodeURIComponent(this.$root.get_cart_id());
url = url + '?iframe=1&locale=' + lang + '&take_cart_id=' + this.$root.cart_id;
}
url += this.$root.consent_parameter;
if (this.$root.additionalURLParams) {
@@ -782,24 +779,15 @@ 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.is_button && this.$root.items.length === 0) {
if (this.$root.subevent && !this.$root.cart_id) {
// button with subevent but no items
redirect_url += this.$root.subevent + '/';
}
redirect_url += '?iframe=1&locale=' + lang;
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.cart_id) {
redirect_url += '&take_cart_id=' + this.$root.cart_id;
}
if (this.$root.widget_data) {
redirect_url += '&widget_data=' + encodeURIComponent(this.$root.widget_data_json);
@@ -1876,11 +1864,12 @@ 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 (this.$root.get_cart_id()) {
url += "&cart_id=" + encodeURIComponent(this.$root.get_cart_id());
if (cart_id) {
url += "&cart_id=" + encodeURIComponent(cart_id);
}
if (this.$root.date !== null) {
url += "&date=" + this.$root.date.substr(0, 7);
@@ -1950,6 +1939,7 @@ 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;
@@ -2014,8 +2004,8 @@ var shared_root_methods = {
if (this.$root.voucher_code) {
redirect_url += '&voucher=' + encodeURIComponent(this.$root.voucher_code);
}
if (this.$root.get_cart_id()) {
redirect_url += '&take_cart_id=' + encodeURIComponent(this.$root.get_cart_id());
if (this.$root.cart_id) {
redirect_url += '&take_cart_id=' + this.$root.cart_id;
}
if (this.$root.widget_data) {
redirect_url += '&widget_data=' + encodeURIComponent(this.$root.widget_data_json);
@@ -2037,28 +2027,7 @@ 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 = {
@@ -2080,8 +2049,9 @@ var shared_root_computed = {
},
voucherFormTarget: function () {
var form_target = this.target_url + 'w/' + widget_id + '/redeem?iframe=1&locale=' + lang;
if (this.get_cart_id()) {
form_target += "&take_cart_id=" + encodeURIComponent(this.get_cart_id());
var cookie = getCookie(this.cookieName);
if (cookie) {
form_target += "&take_cart_id=" + cookie;
}
if (this.subevent) {
form_target += "&subevent=" + this.subevent;
@@ -2121,8 +2091,9 @@ 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);
if (this.get_cart_id()) {
form_target += "&take_cart_id=" + encodeURIComponent(this.get_cart_id());
var cookie = getCookie(this.cookieName);
if (cookie) {
form_target += "&take_cart_id=" + cookie;
}
form_target += this.$root.consent_parameter
return form_target
@@ -2358,7 +2329,6 @@ 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 () {
@@ -2396,7 +2366,6 @@ 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++) {
@@ -2448,8 +2417,7 @@ 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,
keep_cart: keep_cart || items.length > 0,
button_text: button_text
}
},
created: function () {
@@ -2458,7 +2426,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;
@@ -2524,14 +2492,13 @@ 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: "",
keep_cart: true
button_text: ""
}
},
created: function () {
},
computed: shared_root_computed,
methods: shared_root_methods,
methods: shared_root_methods
});
create_overlay(app);
app.$nextTick(function () {
@@ -966,7 +966,6 @@ $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;
-3
View File
@@ -94,9 +94,6 @@ 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()
-29
View File
@@ -171,35 +171,6 @@ 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",
+2 -30
View File
@@ -730,34 +730,6 @@ def test_payment_create_confirmed(token_client, organizer, event, order):
assert len(djmail.outbox) == 0
@pytest.mark.django_db
def test_payment_create_confirmed_after_expiry(token_client, organizer, event, order):
djmail.outbox = []
order.expires = now() - datetime.timedelta(days=2)
order.save()
event.settings.payment_term_last = (now() - datetime.timedelta(days=2)).strftime('%Y-%m-%d')
resp = token_client.post('/api/v1/organizers/{}/events/{}/orders/{}/payments/'.format(
organizer.slug, event.slug, order.code
), format='json', data={
'provider': 'banktransfer',
'state': 'confirmed',
'amount': order.total,
'send_email': False,
'info': {
'foo': 'bar'
}
})
with scopes_disabled():
p = order.payments.last()
assert resp.status_code == 201
assert p.state == OrderPayment.PAYMENT_STATE_CONFIRMED
assert p.info_data == {'foo': 'bar'}
order.refresh_from_db()
assert order.status == Order.STATUS_PAID
assert len(djmail.outbox) == 0
@pytest.mark.django_db
def test_payment_create_pending(token_client, organizer, event, order):
resp = token_client.post('/api/v1/organizers/{}/events/{}/orders/{}/payments/'.format(
@@ -2081,7 +2053,7 @@ def test_pdf_data(token_client, organizer, event, order, django_assert_max_num_q
assert not resp.data['positions'][0].get('pdf_data')
# order list
with django_assert_max_num_queries(34):
with django_assert_max_num_queries(33):
resp = token_client.get('/api/v1/organizers/{}/events/{}/orders/?pdf_data=true'.format(
organizer.slug, event.slug
))
@@ -2096,7 +2068,7 @@ def test_pdf_data(token_client, organizer, event, order, django_assert_max_num_q
assert not resp.data['results'][0]['positions'][0].get('pdf_data')
# position list
with django_assert_max_num_queries(36):
with django_assert_max_num_queries(35):
resp = token_client.get('/api/v1/organizers/{}/events/{}/orderpositions/?pdf_data=true'.format(
organizer.slug, event.slug
))
-70
View File
@@ -252,76 +252,6 @@ 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",
-19
View File
@@ -123,8 +123,6 @@ def env():
ExchangeRate.objects.create(source_date=date.today(), source='eu:ecb:eurofxref-daily', source_currency='EUR', other_currency=currency, rate=rate)
ExchangeRate.objects.create(source_date=date.today(), source='cz:cnb:rate-fixing-daily', source_currency='EUR',
other_currency='CZK', rate=Decimal('25.0000'))
ExchangeRate.objects.create(source_date=date.today(), source='pl:nbp:table-a', source_currency='EUR',
other_currency='PLN', rate=Decimal('4.2355'))
yield event, o
@@ -349,23 +347,6 @@ def test_invoice_indirect_currency_conversion(env):
assert inv.foreign_currency_source == 'eu:ecb:eurofxref-daily'
@pytest.mark.django_db
def test_invoice_pln_currency_conversion(env):
event, order = env
event.settings.invoice_eu_currencies = 'PLN'
event.settings.set('invoice_language', 'en')
InvoiceAddress.objects.create(company='Acme Company', street='221B Baker Street', zipcode='12345', city='Warsaw',
country=Country('PL'), vat_id='PL123456780', vat_id_validated=True, order=order,
is_business=True)
inv = generate_invoice(order)
assert inv.foreign_currency_display == "PLN"
assert inv.foreign_currency_rate == Decimal("4.2355")
assert inv.foreign_currency_rate_date == date.today()
assert inv.foreign_currency_source == 'pl:nbp:table-a'
@pytest.mark.django_db
def test_invoice_czk_currency_conversion(env):
event, order = env
-117
View File
@@ -35,11 +35,8 @@
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
@@ -594,117 +591,3 @@ 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
-27
View File
@@ -991,30 +991,3 @@ def test_import_mixed_order_size_consistency(user, event, item):
).get()
assert ('Inconsistent data in row 2: Column Email address contains value "a2@example.com", but for this order, '
'the value has already been set to "a1@example.com".') in str(excinfo.value)
@pytest.mark.django_db
@scopes_disabled()
def test_import_line_endings_mix(event, item, user):
# Ensures import works with mixed file endings.
# See Ticket#23230806 where a file to import ends with \r\n
settings = dict(DEFAULT_SETTINGS)
settings['item'] = 'static:{}'.format(item.pk)
cf = inputfile_factory()
file = cf.file
file.seek(0)
data = file.read()
data = data.replace(b'\n', b'\r')
data = data.rstrip(b'\r\r')
data = data + b'\r\n'
print(data)
cf.file.save("input.csv", ContentFile(data))
cf.save()
import_orders.apply(
args=(event.pk, cf.id, settings, 'en', user.pk)
)
assert event.orders.count() == 3
assert OrderPosition.objects.count() == 3
+18 -12
View File
@@ -24,6 +24,7 @@ from decimal import Decimal
import pytest
from django.core import mail as djmail
from django.db import transaction
from django.utils.timezone import now
from django_scopes import scope
@@ -74,42 +75,47 @@ def user(team):
return user
@pytest.fixture
def monkeypatch_on_commit(monkeypatch):
monkeypatch.setattr("django.db.transaction.on_commit", lambda t: t())
@pytest.mark.django_db
def test_notification_trigger_event_specific(event, order, user, django_capture_on_commit_callbacks):
def test_notification_trigger_event_specific(event, order, user, monkeypatch_on_commit):
djmail.outbox = []
user.notification_settings.create(
method='mail', event=event, action_type='pretix.event.order.paid', enabled=True
)
with django_capture_on_commit_callbacks(execute=True):
with transaction.atomic():
order.log_action('pretix.event.order.paid', {})
assert len(djmail.outbox) == 1
assert djmail.outbox[0].subject.endswith("DUMMY: Order FOO has been marked as paid.")
@pytest.mark.django_db
def test_notification_trigger_global(event, order, user, django_capture_on_commit_callbacks):
def test_notification_trigger_global(event, order, user, monkeypatch_on_commit):
djmail.outbox = []
user.notification_settings.create(
method='mail', event=None, action_type='pretix.event.order.paid', enabled=True
)
with django_capture_on_commit_callbacks(execute=True):
with transaction.atomic():
order.log_action('pretix.event.order.paid', {})
assert len(djmail.outbox) == 1
@pytest.mark.django_db
def test_notification_trigger_global_wildcard(event, order, user, django_capture_on_commit_callbacks):
def test_notification_trigger_global_wildcard(event, order, user, monkeypatch_on_commit):
djmail.outbox = []
user.notification_settings.create(
method='mail', event=None, action_type='pretix.event.order.changed.*', enabled=True
)
with django_capture_on_commit_callbacks(execute=True):
with transaction.atomic():
order.log_action('pretix.event.order.changed.item', {})
assert len(djmail.outbox) == 1
@pytest.mark.django_db
def test_notification_enabled_global_ignored_specific(event, order, user, django_capture_on_commit_callbacks):
def test_notification_enabled_global_ignored_specific(event, order, user, monkeypatch_on_commit):
djmail.outbox = []
user.notification_settings.create(
method='mail', event=None, action_type='pretix.event.order.paid', enabled=True
@@ -117,24 +123,24 @@ def test_notification_enabled_global_ignored_specific(event, order, user, django
user.notification_settings.create(
method='mail', event=event, action_type='pretix.event.order.paid', enabled=False
)
with django_capture_on_commit_callbacks(execute=True):
with transaction.atomic():
order.log_action('pretix.event.order.paid', {})
assert len(djmail.outbox) == 0
@pytest.mark.django_db
def test_notification_ignore_same_user(event, order, user, django_capture_on_commit_callbacks):
def test_notification_ignore_same_user(event, order, user, monkeypatch_on_commit):
djmail.outbox = []
user.notification_settings.create(
method='mail', event=event, action_type='pretix.event.order.paid', enabled=True
)
with django_capture_on_commit_callbacks(execute=True):
with transaction.atomic():
order.log_action('pretix.event.order.paid', {}, user=user)
assert len(djmail.outbox) == 0
@pytest.mark.django_db
def test_notification_ignore_insufficient_permissions(event, order, user, team, django_capture_on_commit_callbacks):
def test_notification_ignore_insufficient_permissions(event, order, user, team, monkeypatch_on_commit):
djmail.outbox = []
team.all_event_permissions = False
team.limit_event_permissions = {"event.vouchers:read": True}
@@ -142,7 +148,7 @@ def test_notification_ignore_insufficient_permissions(event, order, user, team,
user.notification_settings.create(
method='mail', event=event, action_type='pretix.event.order.paid', enabled=True
)
with django_capture_on_commit_callbacks(execute=True):
with transaction.atomic():
order.log_action('pretix.event.order.paid', {})
assert len(djmail.outbox) == 0
+26 -23
View File
@@ -28,9 +28,8 @@ from zoneinfo import ZoneInfo
import pytest
from django.conf import settings
from django.core import mail as djmail
from django.db import transaction
from django.db.models import F, Sum
from django.test import TestCase, TransactionTestCase, override_settings
from django.test import TestCase, override_settings
from django.utils.timezone import make_aware, now
from django_countries.fields import Country
from django_scopes import scope
@@ -1226,6 +1225,12 @@ class DownloadReminderTests(TestCase):
assert len(djmail.outbox) == 0
@pytest.fixture
def class_monkeypatch(request, monkeypatch):
request.cls.monkeypatch = monkeypatch
@pytest.mark.usefixtures("class_monkeypatch")
class OrderCancelTests(TestCase):
def setUp(self):
super().setUp()
@@ -1253,6 +1258,7 @@ class OrderCancelTests(TestCase):
self.order.create_transactions()
generate_invoice(self.order)
djmail.outbox = []
self.monkeypatch.setattr("django.db.transaction.on_commit", lambda t: t())
@classscope(attr='o')
def test_cancel_canceled(self):
@@ -1345,14 +1351,14 @@ class OrderCancelTests(TestCase):
self.order.status = Order.STATUS_PAID
self.order.save()
djmail.outbox = []
with self.captureOnCommitCallbacks(execute=True):
cancel_order(self.order.pk, send_mail=True)
cancel_order(self.order.pk, send_mail=True)
print([s.subject for s in djmail.outbox])
print([s.to for s in djmail.outbox])
assert len(djmail.outbox) == 2
assert ["dummy@dummy.test"] == djmail.outbox[0].to
assert not any(["Invoice_" in a[0] for a in djmail.outbox[0].attachments])
assert ["invoice@example.org"] == djmail.outbox[1].to
assert any(["Invoice_" in a[0] for a in djmail.outbox[1].attachments])
assert ["invoice@example.org"] == djmail.outbox[0].to
assert any(["Invoice_" in a[0] for a in djmail.outbox[0].attachments])
assert ["dummy@dummy.test"] == djmail.outbox[1].to
assert not any(["Invoice_" in a[0] for a in djmail.outbox[1].attachments])
@classscope(attr='o')
def test_cancel_paid_with_too_high_fee(self):
@@ -1482,7 +1488,8 @@ class OrderCancelTests(TestCase):
assert self.order.all_logentries().filter(action_type='pretix.event.order.refund.requested').exists()
class BaseOrderChangeManagerTestCase:
@pytest.mark.usefixtures("class_monkeypatch")
class OrderChangeManagerTests(TestCase):
def setUp(self):
super().setUp()
self.o = Organizer.objects.create(name='Dummy', slug='dummy', plugins='pretix.plugins.banktransfer')
@@ -1545,6 +1552,7 @@ class BaseOrderChangeManagerTestCase:
self.seat_a1 = self.event.seats.create(seat_number="A1", product=self.stalls, seat_guid="A1")
self.seat_a2 = self.event.seats.create(seat_number="A2", product=self.stalls, seat_guid="A2")
self.seat_a3 = self.event.seats.create(seat_number="A3", product=self.stalls, seat_guid="A3")
self.monkeypatch.setattr("django.db.transaction.on_commit", lambda t: t())
def _enable_reverse_charge(self):
self.tr7.eu_reverse_charge = True
@@ -1558,8 +1566,6 @@ class BaseOrderChangeManagerTestCase:
country=Country('AT')
)
class OrderChangeManagerTests(BaseOrderChangeManagerTestCase, TestCase):
@classscope(attr='o')
def test_multiple_commits_forbidden(self):
self.ocm.change_price(self.op1, Decimal('10.00'))
@@ -3898,16 +3904,15 @@ class OrderChangeManagerTests(BaseOrderChangeManagerTestCase, TestCase):
@classscope(attr='o')
def test_set_valid_until(self):
with transaction.atomic():
self.event.settings.ticket_secret_generator = "pretix_sig1"
assign_ticket_secret(self.event, self.op1, force_invalidate=True, save=True)
old_secret = self.op1.secret
self.event.settings.ticket_secret_generator = "pretix_sig1"
assign_ticket_secret(self.event, self.op1, force_invalidate=True, save=True)
old_secret = self.op1.secret
dt = make_aware(datetime(2022, 9, 20, 15, 0, 0, 0))
self.ocm.change_valid_until(self.op1, dt)
self.ocm.commit()
self.op1.refresh_from_db()
assert self.op1.secret != old_secret
dt = make_aware(datetime(2022, 9, 20, 15, 0, 0, 0))
self.ocm.change_valid_until(self.op1, dt)
self.ocm.commit()
self.op1.refresh_from_db()
assert self.op1.secret != old_secret
@classscope(attr='o')
def test_unset_valid_from_until(self):
@@ -3932,8 +3937,6 @@ class OrderChangeManagerTests(BaseOrderChangeManagerTestCase, TestCase):
assert len(djmail.outbox) == 1
assert len(["Invoice_" in a[0] for a in djmail.outbox[0].attachments]) == 2
class OrderChangeManagerTransactionalTests(BaseOrderChangeManagerTestCase, TransactionTestCase):
@classscope(attr='o')
def test_new_invoice_send_somewhere_else(self):
generate_invoice(self.order)
+22 -16
View File
@@ -25,6 +25,7 @@ from decimal import Decimal
import pytest
import responses
from django.db import transaction
from django.utils.timezone import now
from django_scopes import scopes_disabled
@@ -81,9 +82,14 @@ def force_str(v):
return v.decode() if isinstance(v, bytes) else str(v)
@pytest.fixture
def monkeypatch_on_commit(monkeypatch):
monkeypatch.setattr("django.db.transaction.on_commit", lambda t: t())
@pytest.mark.django_db
@responses.activate
def test_webhook_trigger_event_specific(event, order, webhook, django_capture_on_commit_callbacks):
def test_webhook_trigger_event_specific(event, order, webhook, monkeypatch_on_commit):
responses.add_callback(
responses.POST, 'https://google.com',
callback=lambda r: (200, {}, 'ok'),
@@ -91,7 +97,7 @@ def test_webhook_trigger_event_specific(event, order, webhook, django_capture_on
match_querystring=None, # https://github.com/getsentry/responses/issues/464
)
with django_capture_on_commit_callbacks(execute=True):
with transaction.atomic():
le = order.log_action('pretix.event.order.paid', {})
assert len(responses.calls) == 1
assert json.loads(force_str(responses.calls[0].request.body)) == {
@@ -113,12 +119,12 @@ def test_webhook_trigger_event_specific(event, order, webhook, django_capture_on
@pytest.mark.django_db
@responses.activate
def test_webhook_trigger_global(event, order, webhook, django_capture_on_commit_callbacks):
def test_webhook_trigger_global(event, order, webhook, monkeypatch_on_commit):
webhook.limit_events.clear()
webhook.all_events = True
webhook.save()
responses.add(responses.POST, 'https://google.com', status=200)
with django_capture_on_commit_callbacks(execute=True):
with transaction.atomic():
le = order.log_action('pretix.event.order.paid', {})
assert len(responses.calls) == 1
assert json.loads(force_str(responses.calls[0].request.body)) == {
@@ -132,13 +138,13 @@ def test_webhook_trigger_global(event, order, webhook, django_capture_on_commit_
@pytest.mark.django_db
@responses.activate
def test_webhook_trigger_global_wildcard(event, order, webhook, django_capture_on_commit_callbacks):
def test_webhook_trigger_global_wildcard(event, order, webhook, monkeypatch_on_commit):
webhook.listeners.create(action_type="pretix.event.order.changed.*")
webhook.limit_events.clear()
webhook.all_events = True
webhook.save()
responses.add(responses.POST, 'https://google.com', status=200)
with django_capture_on_commit_callbacks(execute=True):
with transaction.atomic():
le = order.log_action('pretix.event.order.changed.item', {})
assert len(responses.calls) == 1
assert json.loads(force_str(responses.calls[0].request.body)) == {
@@ -152,30 +158,30 @@ def test_webhook_trigger_global_wildcard(event, order, webhook, django_capture_o
@pytest.mark.django_db
@responses.activate
def test_webhook_ignore_wrong_action_type(event, order, webhook, django_capture_on_commit_callbacks):
def test_webhook_ignore_wrong_action_type(event, order, webhook, monkeypatch_on_commit):
responses.add(responses.POST, 'https://google.com', status=200)
with django_capture_on_commit_callbacks(execute=True):
with transaction.atomic():
order.log_action('pretix.event.order.changed.item', {})
assert len(responses.calls) == 0
@pytest.mark.django_db
@responses.activate
def test_webhook_ignore_disabled(event, order, webhook, django_capture_on_commit_callbacks):
def test_webhook_ignore_disabled(event, order, webhook, monkeypatch_on_commit):
webhook.enabled = False
webhook.save()
responses.add(responses.POST, 'https://google.com', status=200)
with django_capture_on_commit_callbacks(execute=True):
with transaction.atomic():
order.log_action('pretix.event.order.changed.item', {})
assert len(responses.calls) == 0
@pytest.mark.django_db
@responses.activate
def test_webhook_ignore_wrong_event(event, order, webhook, django_capture_on_commit_callbacks):
def test_webhook_ignore_wrong_event(event, order, webhook, monkeypatch_on_commit):
webhook.limit_events.clear()
responses.add(responses.POST, 'https://google.com', status=200)
with django_capture_on_commit_callbacks(execute=True):
with transaction.atomic():
order.log_action('pretix.event.order.changed.item', {})
assert len(responses.calls) == 0
@@ -183,10 +189,10 @@ def test_webhook_ignore_wrong_event(event, order, webhook, django_capture_on_com
@pytest.mark.django_db
@pytest.mark.xfail(reason="retries can't be tested with celery_always_eager")
@responses.activate
def test_webhook_retry(event, order, webhook, django_capture_on_commit_callbacks):
def test_webhook_retry(event, order, webhook, monkeypatch_on_commit):
responses.add(responses.POST, 'https://google.com', status=500)
responses.add(responses.POST, 'https://google.com', status=200)
with django_capture_on_commit_callbacks(execute=True):
with transaction.atomic():
order.log_action('pretix.event.order.paid', {})
assert len(responses.calls) == 2
with scopes_disabled():
@@ -210,9 +216,9 @@ def test_webhook_retry(event, order, webhook, django_capture_on_commit_callbacks
@pytest.mark.django_db
@responses.activate
def test_webhook_disable_gone(event, order, webhook, django_capture_on_commit_callbacks):
def test_webhook_disable_gone(event, order, webhook, monkeypatch_on_commit):
responses.add(responses.POST, 'https://google.com', status=410)
with django_capture_on_commit_callbacks(execute=True):
with transaction.atomic():
order.log_action('pretix.event.order.paid', {})
assert len(responses.calls) == 1
webhook.refresh_from_db()
-5
View File
@@ -131,8 +131,3 @@ def set_lock_namespaces(request):
yield
else:
yield
@pytest.fixture
def class_monkeypatch(request, monkeypatch):
request.cls.monkeypatch = monkeypatch
+5
View File
@@ -385,6 +385,11 @@ class RegistrationFormTest(TestCase):
self.assertEqual(response.status_code, 403)
@pytest.fixture
def class_monkeypatch(request, monkeypatch):
request.cls.monkeypatch = monkeypatch
@pytest.mark.usefixtures("class_monkeypatch")
class Login2FAFormTest(TestCase):
+5
View File
@@ -49,6 +49,11 @@ from tests.base import SoupTest, extract_form_fields
from pretix.base.models import Event, LogEntry, Order, Organizer, Team, User
@pytest.fixture
def class_monkeypatch(request, monkeypatch):
request.cls.monkeypatch = monkeypatch
@pytest.mark.usefixtures("class_monkeypatch")
class EventsTest(SoupTest):
@scopes_disabled()
+5
View File
@@ -33,6 +33,11 @@ from tests.base import SoupTest, extract_form_fields
from pretix.base.models import Event, Organizer, OutgoingMail, Team, User
@pytest.fixture
def class_monkeypatch(request, monkeypatch):
request.cls.monkeypatch = monkeypatch
@pytest.mark.usefixtures("class_monkeypatch")
class OrganizerTest(SoupTest):
@scopes_disabled()
-86
View File
@@ -747,92 +747,6 @@ class SubEventsTest(SoupTest):
assert ses[1].date_from.isoformat() == "2018-04-12T11:29:31+00:00"
assert ses[-1].date_from.isoformat() == "2019-03-28T12:29:31+00:00"
def test_create_bulk_skip_existing(self):
with scopes_disabled():
self.event1.subevents.all().delete()
# SubEvent ends at rrule start time
self.event1.subevents.create(
date_from=datetime.datetime(2018, 4, 4, 9, 0, tzinfo=datetime.timezone.utc),
date_to=datetime.datetime(2018, 4, 4, 10, 0, tzinfo=datetime.timezone.utc),
)
# SubEvent overlaps rrule start
self.event1.subevents.create(
date_from=datetime.datetime(2018, 4, 5, 9, 30, tzinfo=datetime.timezone.utc),
date_to=datetime.datetime(2018, 4, 5, 10, 30, tzinfo=datetime.timezone.utc),
)
# SubEvent times are same as rrule
self.event1.subevents.create(
date_from=datetime.datetime(2018, 4, 6, 10, 0, tzinfo=datetime.timezone.utc),
date_to=datetime.datetime(2018, 4, 6, 11, 0, tzinfo=datetime.timezone.utc),
)
# SubEvent starts at rrule end time
self.event1.subevents.create(
date_from=datetime.datetime(2018, 4, 7, 11, 0, tzinfo=datetime.timezone.utc),
date_to=datetime.datetime(2018, 4, 7, 12, 0, tzinfo=datetime.timezone.utc),
)
# SubEvent overlaps entire rrule time
self.event1.subevents.create(
date_from=datetime.datetime(2018, 4, 8, 9, 0, tzinfo=datetime.timezone.utc),
date_to=datetime.datetime(2018, 4, 8, 12, 0, tzinfo=datetime.timezone.utc),
)
# SubEvent has before rrule time and no end
self.event1.subevents.create(
date_from=datetime.datetime(2018, 4, 9, 9, 0, tzinfo=datetime.timezone.utc),
)
existing_events = list(self.event1.subevents.values_list('pk', flat=True))
self.event1.settings.timezone = 'Europe/Berlin'
doc = self.post_doc('/control/event/ccc/30c3/subevents/bulk_add', {
'rruleformset-TOTAL_FORMS': '1',
'rruleformset-INITIAL_FORMS': '0',
'rruleformset-MIN_NUM_FORMS': '0',
'rruleformset-MAX_NUM_FORMS': '1000',
'rruleformset-0-end': 'count',
'rruleformset-0-count': '10',
'rruleformset-0-interval': '1',
'rruleformset-0-freq': 'weekly',
'rruleformset-0-dtstart': '2018-04-03',
'rruleformset-0-weekly_byweekday': ['MO', 'TU', 'WE', 'TH', 'FR', 'SA', 'SU'],
'rruleformset-0-yearly_same': 'on',
'rruleformset-0-monthly_same': 'on',
'timeformset-TOTAL_FORMS': '1',
'timeformset-INITIAL_FORMS': '0',
'timeformset-MIN_NUM_FORMS': '1',
'timeformset-MAX_NUM_FORMS': '1000',
'timeformset-0-time_from': '12:00:00',
'timeformset-0-time_to': '13:00:00',
'rruleformset-0-until': '2019-04-03',
'skip_if_overlap': 'on',
'name_0': 'Foo',
'active': 'on',
'frontpage_text_0': '',
'quotas-TOTAL_FORMS': '1',
'quotas-INITIAL_FORMS': '0',
'quotas-MIN_NUM_FORMS': '0',
'quotas-MAX_NUM_FORMS': '1000',
'quotas-0-name': 'Q1',
'quotas-0-size': '50',
'quotas-0-itemvars': str(self.ticket.pk),
'checkinlist_set-TOTAL_FORMS': '0',
'checkinlist_set-INITIAL_FORMS': '0',
'checkinlist_set-MIN_NUM_FORMS': '0',
'checkinlist_set-MAX_NUM_FORMS': '1000',
})
assert doc.select(".alert-success")
with scopes_disabled():
ses = list(self.event1.subevents.exclude(pk__in=existing_events).order_by('date_from'))
assert len(ses) == 7
assert [s.date_from.date().isoformat() for s in ses] == [
'2018-04-03',
'2018-04-04',
'2018-04-07',
'2018-04-09',
'2018-04-10',
'2018-04-11',
'2018-04-12'
]
def test_delete_bulk(self):
self.subevent2.active = True
self.subevent2.save()
+5
View File
@@ -286,6 +286,11 @@ class UserPasswordChangeTest(SoupTest):
assert self.user.needs_password_change is False
@pytest.fixture
def class_monkeypatch(request, monkeypatch):
request.cls.monkeypatch = monkeypatch
@pytest.mark.usefixtures("class_monkeypatch")
class UserSettings2FATest(SoupTest):
def setUp(self):
-93
View File
@@ -1,93 +0,0 @@
#
# 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)
+8 -15
View File
@@ -33,7 +33,7 @@ from django.conf import settings
from django.core import mail as djmail
from django.core.files.uploadedfile import SimpleUploadedFile
from django.core.signing import dumps
from django.test import Client, TestCase, TransactionTestCase
from django.test import TestCase
from django.utils.crypto import get_random_string
from django.utils.timezone import now
from django_countries.fields import Country
@@ -60,6 +60,12 @@ from pretix.testutils.sessions import get_cart_session_key
from .test_timemachine import TimemachineTestMixin
@pytest.fixture
def class_monkeypatch(request, monkeypatch):
request.cls.monkeypatch = monkeypatch
@pytest.mark.usefixtures("class_monkeypatch")
class BaseCheckoutTestCase:
@scopes_disabled()
def setUp(self):
@@ -98,6 +104,7 @@ class BaseCheckoutTestCase:
self.workshopquota.items.add(self.workshop2)
self.workshopquota.variations.add(self.workshop2a)
self.workshopquota.variations.add(self.workshop2b)
self.monkeypatch.setattr("django.db.transaction.on_commit", lambda t: t())
def _set_session(self, key, value):
session = self.client.session
@@ -4413,20 +4420,6 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
assert len(djmail.outbox) == 1
assert any(["Invoice_" in a[0] for a in djmail.outbox[0].attachments])
def test_checkout_empty_session_valid_cart(self):
client = Client()
with scopes_disabled():
api_cid = "{}@api".format(get_random_string(48))
CartPosition.objects.create(
event=self.event, cart_id=api_cid, item=self.ticket,
price=23, expires=now() + timedelta(minutes=10)
)
response = client.get('/%s/%s/w/1234567890abcdef/checkout/questions/' % (self.orga.slug, self.event.slug), query_params={"take_cart_id": api_cid})
assert '€23.00' in response.content.decode()
class CheckoutTransactionTestCase(BaseCheckoutTestCase, TransactionTestCase):
def test_order_confirmation_mail_invoice_sent_somewhere_else(self):
self.event.settings.invoice_address_asked = True
self.event.settings.invoice_address_required = True
-88
View File
@@ -36,7 +36,6 @@
import datetime
import re
from decimal import Decimal
from importlib import import_module
from json import loads
from zoneinfo import ZoneInfo
@@ -81,34 +80,6 @@ class EventMiddlewareTest(EventTestMixin, SoupTest):
doc = self.get_doc('/%s/%s/' % (self.orga.slug, self.event.slug))
self.assertIn(str(self.event.name), doc.find("h1").text)
def test_no_session_cookie_set_on_event_index_view(self):
resp = self.client.get('/%s/%s/' % (self.orga.slug, self.event.slug))
self.assertEqual(resp.status_code, 200)
assert settings.SESSION_COOKIE_NAME not in self.client.cookies
def test_no_cart_session_added_on_event_index_view(self):
# Make sure a session is present by doing a cart op on another event
event2 = Event.objects.create(
organizer=self.orga, name='30C3b', slug='30c3b',
date_from=datetime.datetime(now().year + 1, 12, 26, 14, 0, tzinfo=datetime.timezone.utc),
live=True,
)
self.client.post('/%s/%s/cart/add' % (self.orga.slug, event2.slug), {
'item_%d' % 1337: '1', # item does not need to exist
'ajax': 1
})
assert settings.SESSION_COOKIE_NAME in self.client.cookies
# Visit shop, make sure no session is created
resp = self.client.get('/%s/%s/' % (self.orga.slug, self.event.slug))
self.assertEqual(resp.status_code, 200)
SessionStore = import_module(settings.SESSION_ENGINE).SessionStore
session = SessionStore(self.client.cookies[settings.SESSION_COOKIE_NAME].value).load()
assert set(session.keys()) == {
f"current_cart_event_{event2.pk}", "carts"
}
def test_not_found(self):
resp = self.client.get('/%s/%s/' % ('foo', 'bar'))
self.assertEqual(resp.status_code, 404)
@@ -1162,65 +1133,6 @@ 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