Compare commits

..

2 Commits

Author SHA1 Message Date
Lukas Bockstaller a7388aa0a0 formatting 2026-04-08 12:33:02 +02:00
Lukas Bockstaller 166aa33b1b handle mixed line endings in import 2026-04-08 12:30:34 +02:00
57 changed files with 789 additions and 1750 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.**
-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
+5 -5
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",
@@ -93,11 +93,11 @@ dependencies = [
"redis==7.4.*",
"reportlab==4.4.*",
"requests==2.32.*",
"sentry-sdk==2.58.*",
"sentry-sdk==2.57.*",
"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
-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":
-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):
-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,
)
)
+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 -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'
+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
+27 -27
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-31 17:00+0000\n"
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
"Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/pretix/"
"es/>\n"
"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."
@@ -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
@@ -28698,7 +28698,7 @@ msgstr "Se ha creado la nueva lista de asistentes."
#: pretix/plugins/ticketoutputpdf/views.py:132
msgid "We could not save your changes. See below for details."
msgstr ""
"No 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 +30864,7 @@ msgstr ""
#: pretix/plugins/badges/forms.py:33
msgid "Template"
msgstr "Template"
msgstr "Plantilla"
#: pretix/plugins/badges/forms.py:34
msgid ""
+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"
@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-03-30 11:22+0000\n"
"PO-Revision-Date: 2026-04-08 18:00+0000\n"
"PO-Revision-Date: 2026-04-01 17:00+0000\n"
"Last-Translator: Ruud Hendrickx <ruud@leckxicon.eu>\n"
"Language-Team: Dutch (Belgium) <https://translate.pretix.eu/projects/pretix/"
"pretix/nl_BE/>\n"
@@ -31346,7 +31346,7 @@ msgstr "We zullen u een e-mail sturen zodra we uw betaling ontvangen hebben."
#: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/refund_export.html:7
#: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/sepa_export.html:7
msgid "Export bank transfer refunds"
msgstr "Terugbetalingen per bankoverschrijving exporteren"
msgstr ""
#: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/refund_export.html:9
#, python-format
@@ -31354,8 +31354,6 @@ msgid ""
"<strong>%(num_new)s</strong> Bank transfer refunds have been placed and are "
"not yet part of an export."
msgstr ""
"<strong>%(num_new)s</strong> terugbetalingen per bankoverschrijving zijn "
"aangemaakt en nog niet geëxporteerd."
#: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/refund_export.html:15
msgid "In test mode, your exports will only contain test mode orders."
@@ -31368,8 +31366,6 @@ msgid ""
"If you want, you can now also create these exports for multiple events "
"combined."
msgstr ""
"Als u dat wilt, kunt u deze exportbestanden nu ook voor meerdere evenementen "
"tegelijk aanmaken."
#: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/refund_export.html:22
msgid "Go to organizer-level exports"
@@ -31381,7 +31377,7 @@ msgstr "Nieuw exportbestand aanmaken"
#: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/refund_export.html:38
msgid "Aggregate transactions to the same bank account"
msgstr "Overschrijvingen naar hetzelfde rekeningnummer samenvoegen"
msgstr ""
#: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/refund_export.html:43
msgid ""
+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'
+2 -2
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,
@@ -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'),
+5 -11
View File
@@ -114,10 +114,8 @@ 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
if not self.request.session.session_key and not order:
# The user has not even a session ID yet, so they can't have a cart and we can save a lot of work
return {
'positions': [],
# Other keys are not used on non-checkout pages
@@ -379,13 +377,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)
+3 -19
View File
@@ -555,18 +555,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 +843,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'):
+1 -3
View File
@@ -223,7 +223,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 +263,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",
-28
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(
-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
-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()
-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)
+1 -13
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, TransactionTestCase
from django.utils.crypto import get_random_string
from django.utils.timezone import now
from django_countries.fields import Country
@@ -4413,18 +4413,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):
-59
View File
@@ -1162,65 +1162,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