Compare commits

..
7 changed files with 61 additions and 180 deletions
+10 -24
View File
@@ -294,43 +294,29 @@
}
},
"node_modules/@humanfs/core": {
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
"integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==",
"version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
"integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@humanfs/types": "^0.15.0"
},
"engines": {
"node": ">=18.18.0"
}
},
"node_modules/@humanfs/node": {
"version": "0.16.8",
"resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz",
"integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==",
"version": "0.16.7",
"resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz",
"integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@humanfs/core": "^0.19.2",
"@humanfs/types": "^0.15.0",
"@humanfs/core": "^0.19.1",
"@humanwhocodes/retry": "^0.4.0"
},
"engines": {
"node": ">=18.18.0"
}
},
"node_modules/@humanfs/types": {
"version": "0.15.0",
"resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz",
"integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=18.18.0"
}
},
"node_modules/@humanwhocodes/module-importer": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
@@ -3394,9 +3380,9 @@
}
},
"node_modules/postcss-selector-parser": {
"version": "7.1.5",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.5.tgz",
"integrity": "sha512-KvvtD7SrlBP7dlgkBghEE3r84CABm5SmV2aNcG4oCA+qDnJ/tvKonFVvwWAyyWUEwxuNawdfEAZKP9zM3oZ2Uw==",
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz",
"integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==",
"dev": true,
"license": "MIT",
"dependencies": {
+2 -2
View File
@@ -33,7 +33,7 @@ dependencies = [
"bleach==6.4.*",
"celery==5.6.*",
"chardet==5.2.*",
"cryptography>=50.0.1",
"cryptography>=50.0.0",
"css-inline==0.21.*",
"defusedcsv>=3.0.0",
"dnspython==2.*",
@@ -112,7 +112,7 @@ dev = [
"aiohttp==3.14.*",
"coverage",
"coveralls",
"fakeredis==2.38.*",
"fakeredis==2.37.*",
"flake8==7.3.*",
"freezegun",
"isort==8.0.*",
+2 -2
View File
@@ -71,8 +71,8 @@ class EmailTransmissionType(TransmissionType):
def transmission_info_to_form_data(self, transmission_info: dict) -> dict:
return {
"transmission_email_other": bool(transmission_info.get("transmission_email_address")),
"transmission_email_address": transmission_info.get("transmission_email_address"),
"transmission_email_other": bool(transmission_info.get("transmission_email_address")) if transmission_info else False,
"transmission_email_address": transmission_info.get("transmission_email_address") if transmission_info else "",
}
def form_data_to_transmission_info(self, form_data: dict) -> dict:
+12 -27
View File
@@ -801,18 +801,6 @@ def generate_compressed_addon_list(op, order, event, only_checked_in=False):
return addonlist
def get_sizebox(page: pypdf.PageObject):
mediabox = page.mediabox
cropbox = page.cropbox
return pypdf.generic.RectangleObject((
max(mediabox[0], cropbox[0]),
max(mediabox[1], cropbox[1]),
min(mediabox[2], cropbox[2]),
min(mediabox[3], cropbox[3]),
))
class Renderer:
def __init__(self, event, layout, background_file):
@@ -1165,10 +1153,11 @@ class Renderer:
elif o['type'] == "poweredby":
self._draw_poweredby(canvas, op, o)
if self.bg_pdf:
first_page = self.bg_pdf.pages[0]
sizebox = get_sizebox(first_page)
page_size = (sizebox.width, sizebox.height)
if first_page.rotation in (90, 270):
page_size = (
self.bg_pdf.pages[0].mediabox[2] - self.bg_pdf.pages[0].mediabox[0],
self.bg_pdf.pages[0].mediabox[3] - self.bg_pdf.pages[0].mediabox[1]
)
if self.bg_pdf.pages[0].get('/Rotate') in (90, 270):
# swap dimensions due to pdf being rotated
page_size = page_size[::-1]
canvas.setPageSize(page_size)
@@ -1323,18 +1312,14 @@ def merge_background(fg_pdf: PdfWriter, bg_pdf: PdfWriter, out_file, compress):
def _merge_with_correct_page_media_box(output: pypdf.PdfWriter, fg_page: pypdf.PageObject, bg_page: pypdf.PageObject):
"""
Adds fg_page to output, merging bg_page behind it.
If bg_page has a non-zero mergebox/cropbox or is rotated via /Rotate, a transformation is applied to fix this."""
trsf = pypdf.Transformation()
if bg_page.rotation != 0:
trsf = trsf.rotate(-bg_page.rotation)
mb = get_sizebox(bg_page)
pt1 = trsf.apply_on(mb.lower_left)
pt2 = trsf.apply_on(mb.upper_right)
trsf = trsf.translate(-min(pt1[0], pt2[0]), -min(pt1[1], pt2[1]))
bg_page.transfer_rotation_to_content()
media_box = bg_page.mediabox
trsf = pypdf.Transformation()
if media_box.bottom != 0:
trsf = trsf.translate(0, -media_box.bottom)
if media_box.left != 0:
trsf = trsf.translate(-media_box.left, 0)
fg_page = output.add_page(fg_page)
fg_page.merge_transformed_page(bg_page, trsf, over=False, expand=False)
+23 -52
View File
@@ -20,7 +20,6 @@
# <https://www.gnu.org/licenses/>.
#
from decimal import ROUND_HALF_UP, Decimal
from typing import Optional
from babel import Locale, UnknownLocaleError
from babel.numbers import format_currency
@@ -36,32 +35,32 @@ register = template.Library()
@register.filter("money")
def money_filter(value: Optional[Decimal | float | int | str], arg='', hide_currency=False):
if isinstance(value, (float, int, str)):
if value == '':
return value
def money_filter(value: Decimal, arg='', hide_currency=False):
if isinstance(value, (float, int)):
value = Decimal(value)
if value is None:
value = Decimal('0.00')
if not isinstance(value, Decimal):
if value == '':
return value
raise TypeError("Invalid data type passed to money filter: %r" % type(value))
if not arg:
raise ValueError("No currency passed.")
arg = arg.upper()
if value.normalize().as_tuple().exponent < -9:
# Heuristic: It's unlikely we'll ever see values of less than 0.000000001 in any currency. Therefore, if we
# do see them, we very likely deal with a floating point error. This happens mostly in dev mode when computations
# are made in SQLite, which uses REAL precision, but it can also happen when we naively pass a float from Python
# land to this filter (even though it should not happen).
value = value.quantize(Decimal('1e-9'), ROUND_HALF_UP).normalize()
currency_places = settings.CURRENCY_PLACES.get(arg, 2)
required_places = -value.normalize().as_tuple().exponent
render_places = max(currency_places, required_places)
places = settings.CURRENCY_PLACES.get(arg, 2)
rounded = value.quantize(Decimal('1') / 10 ** places, ROUND_HALF_UP)
if places < 2 and rounded != value:
# We display decimal places even if we shouldn't for this currency if rounding
# would make the numbers incorrect. If this branch executes, it's likely a bug in
# pretix, but we won't show wrong numbers!
if hide_currency:
return floatformat(value, "2g")
else:
return '{} {}'.format(arg, floatformat(value, "2g"))
if hide_currency:
return floatformat(value, f"{render_places}g")
return floatformat(value, f"{places}g")
try:
locale = Locale(get_babel_locale())
@@ -69,29 +68,14 @@ def money_filter(value: Optional[Decimal | float | int | str], arg='', hide_curr
locale = "en"
try:
return format_currency(
value,
arg,
locale=locale,
# We only allow Babel to restrict the digits to the digits defined by the currency if this does not remove any
# precision in case we have sub-currency precision (which we shouldn't have in most places, but it's still
# better than showing wrong data). Note: Weird precision effects can occur after in-database arithmetic
# on SQLite, since SQLite does not have fixed-decimal computation.
currency_digits=currency_places >= required_places,
decimal_quantization=currency_places >= required_places,
)
return format_currency(value, arg, locale=locale)
except:
return '{} {}'.format(arg, floatformat(value, f"{render_places}g"))
@register.filter("money_without_currency")
def money_filter_without_currency(value: Optional[Decimal | float | int | str], arg=''):
return money_filter(value, arg, hide_currency=True)
return '{} {}'.format(arg, floatformat(value, f"{places}g"))
@register.filter("money_numberfield")
def money_numberfield_filter(value: Optional[Decimal | float | int | str], arg=''):
if isinstance(value, (float, int, str)):
def money_numberfield_filter(value: Decimal, arg=''):
if isinstance(value, (float, int)):
value = Decimal(value)
if not isinstance(value, Decimal):
raise TypeError("Invalid data type passed to money filter: %r" % type(value))
@@ -103,28 +87,15 @@ def money_numberfield_filter(value: Optional[Decimal | float | int | str], arg='
@register.filter(is_safe=True)
def tax_rate_format(number: Optional[Decimal | float | int | str]):
def tax_rate_format(number):
"""
Display a Decimal to its significant decimal places, used for tax rates.
"""
if isinstance(number, (float, int, str)):
if number == '':
return number
number = Decimal(number)
if number is None:
number = Decimal('0.00')
if not isinstance(number, Decimal):
raise TypeError("Invalid data type passed to tax rate format filter: %r" % type(number))
if number.normalize().as_tuple().exponent < -9:
# Heuristic: It's unlikely we'll ever see values of less than 0.000000001 in any currency. Therefore, if we
# do see them, we very likely deal with a floating point error. This happens mostly in dev mode when computations
# are made in SQLite, which uses REAL precision, but it can also happen when we naively pass a float from Python
# land to this filter (even though it should not happen).
number = number.quantize(Decimal('1e-9'), ROUND_HALF_UP).normalize()
assert isinstance(number, Decimal)
return mark_safe(
formats.number_format(
number,
-number.normalize().as_tuple().exponent,
number.normalize(),
-number.as_tuple().exponent,
use_l10n=True,
force_grouping=False,
)
+6 -40
View File
@@ -50,12 +50,11 @@ from django.shortcuts import redirect, render
from django.urls import reverse
from django.utils.functional import cached_property
from django.utils.http import url_has_allowed_host_and_scheme
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _, ngettext
from django.utils.translation import gettext_lazy as _
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods
from django.views.generic import TemplateView
from django_otp import devices_for_user
from django_otp import match_token
from django_otp.plugins.otp_static.models import StaticDevice
from webauthn.helpers import generate_challenge
@@ -464,7 +463,6 @@ class Login2FAView(TemplateView):
token = request.POST.get('token', '').strip().replace(' ', '')
valid = False
retry_after = None
if 'webauthn_challenge' in self.request.session and token.startswith('{'):
challenge = self.request.session['webauthn_challenge']
@@ -520,28 +518,12 @@ class Login2FAView(TemplateView):
valid = True
break
else:
with transaction.atomic():
for device in devices_for_user(self.user, for_verify=True):
if isinstance(device, StaticDevice) and len(token) < 12:
# If we enter a wrong TOTP token (which is 6 characters), do not even try if it is a valid
# emergency token, which will only "lock up" the StaticDevice due to the throttling plugin
# and just locks people out without security gain.
continue
if device.verify_token(token):
valid = True
break
elif hasattr(device, 'verify_is_allowed'):
verify_allowed, reason_dict = device.verify_is_allowed()
if not verify_allowed:
if not retry_after or reason_dict['locked_until'] > retry_after:
retry_after = reason_dict['locked_until']
else:
device = None
if isinstance(device, StaticDevice):
valid = match_token(self.user, token)
if isinstance(valid, StaticDevice):
self.user.send_security_notice([
_("A recovery code for two-factor authentification was used to log in.")
])
if valid:
logger.info(f"Backend login successful for user {self.user.pk} with 2FA.")
pretix_successful_logins.inc(1)
@@ -554,23 +536,7 @@ class Login2FAView(TemplateView):
return redirect('control:index')
else:
pretix_failed_logins.inc(1, reason="2fa")
msg = _('Invalid code, please try again.')
if retry_after:
seconds = int((retry_after - now()).total_seconds())
minutes = int(seconds // 60)
if minutes:
msg = ngettext(
'Invalid code. Please try again after waiting {value} minute.',
'Invalid code. Please try again after waiting {value} minutes.',
minutes,
).format(value=minutes)
elif seconds:
msg = ngettext(
'Invalid code. Please try again after waiting {value} second.',
'Invalid code. Please try again after waiting {value} seconds.',
seconds,
).format(value=seconds)
messages.error(request, msg)
messages.error(request, _('Invalid code, please try again.'))
return redirect('control:auth.login.2fa')
def get_context_data(self, **kwargs):
+6 -33
View File
@@ -26,7 +26,7 @@ from django.template import Context, Template
from django.test import RequestFactory
from django.utils import translation
from pretix.base.templatetags.money import money_filter, tax_rate_format
from pretix.base.templatetags.money import money_filter
TEMPLATE_REPLACE_PAGE = Template(
"{% load urlreplace %}{% url_replace request 'page' 3 %}"
@@ -60,9 +60,7 @@ def test_urlreplace_replace_parameter():
"locale,amount,currency,expected",
[
("en", None, "USD", "$0.00"),
("en", "", "USD", ""),
("en", 1000000, "USD", "$1,000,000.00"),
("en", 2.23, "USD", "$2.23"),
("en", Decimal("1000.00"), "USD", "$1,000.00"),
("de", Decimal("1.23"), "EUR", "1,23" + NBSP + ""),
("de", Decimal("1000.00"), "EUR", "1.000,00" + NBSP + ""),
@@ -72,14 +70,11 @@ def test_urlreplace_replace_parameter():
# unknown currency
("de", Decimal("1234.56"), "FOO", "1.234,56" + NBSP + "FOO"),
("de", Decimal("1234.567"), "FOO", "1.234,567" + NBSP + "FOO"),
("de", Decimal("1234.567"), "FOO", "1.234,57" + NBSP + "FOO"),
# deal with precision that is higher than the currency
("de", Decimal("1.234"), "EUR", "1,234" + NBSP + ""),
("de", 1.234, "EUR", "1,234" + NBSP + ""),
("de", Decimal("1.2340"), "EUR", "1,234" + NBSP + ""),
("de", Decimal("1.2300"), "EUR", "1,23" + NBSP + ""),
("de", Decimal("1023.1"), "JPY", "1.023,10" + NBSP + "¥"),
# rounding errors
("de", Decimal("1.234"), "EUR", "1,23" + NBSP + ""),
("de", Decimal("1023.1"), "JPY", "JPY 1.023,10"),
]
)
def test_money_filter(locale, amount, currency, expected):
@@ -103,31 +98,9 @@ def test_money_filter(locale, amount, currency, expected):
[
("de", Decimal("1000.00"), "EUR", "1.000,00"),
("en", Decimal("1000.00"), "EUR", "1,000.00"),
("de", Decimal("1023.1"), "JPY", "1.023,1"),
("de", Decimal("1023.1"), "JPY", "1.023,10"),
]
)
def test_money_filter_hidecurrency(locale, amount, currency, expected):
translation.activate(locale)
assert money_filter(amount, currency, hide_currency=True) == expected
@pytest.mark.parametrize(
"locale,rate,expected",
[
("de", Decimal("2.00"), "2"),
("de", Decimal("2.50"), "2,5"),
("de", Decimal("2.2340"), "2,234"),
("en", Decimal("2.00"), "2"),
("en", Decimal("2.50"), "2.5"),
("en", Decimal("4.3e7"), "43000000"),
("en", Decimal("2.2340"), "2.234"),
("en", "2.23", "2.23"),
("en", 2.23, "2.23"),
("en", 2, "2"),
("en", "", ""),
("en", None, "0"),
]
)
def test_tax_rate_format(locale, rate, expected):
translation.activate(locale)
assert tax_rate_format(rate) == expected