Compare commits

..
Author SHA1 Message Date
Kara Engelhardt 547de7aa26 pdf tests: Save temporary pdfs for failed test debugging 2026-09-18 16:49:29 +02:00
Kara Engelhardt 03bca84e00 Add pdf tests 2026-09-18 12:16:14 +02:00
16 changed files with 951 additions and 196 deletions
+1
View File
@@ -44,6 +44,7 @@ recursive-include src *.py
recursive-include src *.svg
recursive-include src *.txt
recursive-include src Makefile
recursive-include src *.pdf
recursive-exclude doc *
recursive-exclude deployment *
+1
View File
@@ -129,6 +129,7 @@ dev = [
"pytest==9.1.*",
"playwright",
"responses",
"pypdfium2"
]
[project.entry-points."distutils.commands"]
+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,
)
-1
View File
@@ -774,7 +774,6 @@ class CoreUserImpersonatedLogEntryType(UserImpersonatedLogEntryType):
'pretix.user.settings.2fa.disabled': _('Two-factor authentication has been disabled.'),
'pretix.user.settings.2fa.regenemergency': _('Your two-factor emergency codes have been regenerated.'),
'pretix.user.settings.2fa.emergency': _('A two-factor emergency code has been generated.'),
'pretix.user.settings.2fa.resetdrift': _('TOTP drift has been reset.'),
'pretix.user.settings.2fa.device.added': _('A new two-factor authentication device "{name}" has been added to '
'your account.'),
'pretix.user.settings.2fa.device.deleted': _('The two-factor authentication device "{name}" has been removed '
@@ -1,7 +1,6 @@
{% extends "pretixcontrol/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% load icon %}
{% block title %}{% trans "User" %}{% endblock %}
{% block content %}
<h1>{% trans "User" %} {{ user.email }}</h1>
@@ -17,10 +16,6 @@
{% csrf_token %}
<button class="btn btn-default">{% trans "Generate 2FA emergency token" %}</button>
</form>
<form action="{% url "control:users.resetdrift" id=user.pk %}" method="post" class="form-inline helper-display-inline">
{% csrf_token %}
<button class="btn btn-default">{% trans "Reset 2FA drift" %}</button>
</form>
{% endif %}
<form action="{% url "control:users.impersonate" id=user.pk %}" method="post" class="form-inline helper-display-inline">
{% csrf_token %}
@@ -64,72 +59,8 @@
{% bootstrap_field form.is_verified layout='control' %}
{% endif %}
{% bootstrap_field form.last_login layout='control' %}
{% bootstrap_field form.needs_password_change layout='control' %}
{% bootstrap_field form.require_2fa layout='control' %}
<div class="form-group">
<div class="col-md-9 col-md-offset-3">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">
{% trans "Available two-factor authentication methods" %}
</h3>
</div>
<table class="panel-body table table-hover">
{% for d in devices %}
<tr>
<td>
{% if d.devicetype == 'totp' %}
TOTP
{% elif d.devicetype == 'u2f' %}
U2F
{% elif d.devicetype == 'webauthn' %}
WebAuthn
{% elif d.devicetype == 'emergency' %}
{% trans "Emergency tokens" %}
{% endif %}
{% if d.confirmed %}
{% icon "check" %}
{% else %}
{% icon "warning" %}
{% endif %}
</td>
<td>
{{ d.name }}
</td>
<td>
{% if d.throttling_failure_timestamp %}
{% blocktrans trimmed with date=d.throttling_failure_timestamp|date:"SHORT_DATETIME_FORMAT" count cnt=d.throttling_failure_count %}
1 failed attempt since {{ date }}
{% plural %}
{{ cnt }} failed attempts since {{ date }}
{% endblocktrans %}
<br>
{% endif %}
{% if d.devicetype == 'totp' %}
<small>
<code>step = {{ d.step }},
t0 = {{ d.t0 }},
digits = {{ d.digits }},
tolerance = {{ d.tolerance }},
drift = {{ d.drift }},
last_t = {{ d.last_t }}</code>
</small>
{% elif d.devicetype == 'u2f' %}
<small>
<code>sign_count = {{ d.sign_count }}</code>
</small>
{% elif d.devicetype == 'emergency' %}
<small>
<code>token_count = {{ d.token_set.count }}</code>
</small>
{% endif %}
</td>
</tr>
{% endfor %}
</table>
</div>
</div>
</div>
{% bootstrap_field form.needs_password_change layout='control' %}
</fieldset>
<fieldset>
<legend>{% trans "Team memberships" %}</legend>
-1
View File
@@ -78,7 +78,6 @@ urlpatterns = [
re_path(r'^users/(?P<id>\d+)/impersonate$', users.UserImpersonateView.as_view(), name='users.impersonate'),
re_path(r'^users/(?P<id>\d+)/anonymize$', users.UserAnonymizeView.as_view(), name='users.anonymize'),
re_path(r'^users/(?P<id>\d+)/emergencytoken$', users.UserEmergencyTokenView.as_view(), name='users.emergencytoken'),
re_path(r'^users/(?P<id>\d+)/resetdrift$', users.Reset2FADriftView.as_view(), name='users.resetdrift'),
re_path(r'^pdf/editor/webfonts.css', pdf.FontsCSSView.as_view(), name='pdf.css'),
re_path(r'^settings/?$', user.UserSettings.as_view(), name='user.settings'),
re_path(r'^settings/history/$', user.UserHistoryView.as_view(), name='user.settings.history'),
+2 -38
View File
@@ -41,18 +41,15 @@ from django.utils.translation import gettext_lazy as _
from django.views import View
from django.views.generic import ListView, TemplateView
from django_otp.plugins.otp_static.models import StaticDevice
from django_otp.plugins.otp_totp.models import TOTPDevice
from hijack import signals
from pretix.base.auth import get_auth_backends
from pretix.base.models import U2FDevice, User, WebAuthnDevice
from pretix.base.models import User
from pretix.control.forms.filter import UserFilterForm
from pretix.control.forms.users import UserEditForm
from pretix.control.permissions import AdministratorPermissionRequiredMixin
from pretix.control.views import CreateView, UpdateView
from pretix.control.views.user import (
REAL_DEVICE_TYPES, RecentAuthenticationRequiredMixin,
)
from pretix.control.views.user import RecentAuthenticationRequiredMixin
def get_used_backend(request):
@@ -110,21 +107,6 @@ class UserEditView(AdministratorPermissionRequiredMixin, RecentAuthenticationReq
ctx['backend'] = (
b[self.object.auth_backend].verbose_name if self.object.auth_backend in b else self.object.auth_backend
)
ctx['devices'] = []
for dt in [*REAL_DEVICE_TYPES, StaticDevice]:
objs = list(dt.objects.filter(user=self.request.user, confirmed=True))
for obj in objs:
if dt == TOTPDevice:
obj.devicetype = 'totp'
elif dt == U2FDevice:
obj.devicetype = 'u2f'
elif dt == WebAuthnDevice:
obj.devicetype = 'webauthn'
elif dt == StaticDevice:
obj.devicetype = 'emergency'
ctx['devices'] += objs
return ctx
def get_success_url(self):
@@ -201,24 +183,6 @@ class UserEmergencyTokenView(AdministratorPermissionRequiredMixin, RecentAuthent
return reverse('control:users.edit', kwargs=self.kwargs)
class Reset2FADriftView(AdministratorPermissionRequiredMixin, RecentAuthenticationRequiredMixin, View):
def get(self, request, *args, **kwargs):
return redirect(reverse('control:users.edit', kwargs=self.kwargs))
def post(self, request, *args, **kwargs):
self.object = get_object_or_404(User, pk=self.kwargs.get("id"))
self.object.totpdevice_set.update(drift=0)
self.object.log_action('pretix.user.settings.2fa.resetdrift', user=self.request.user)
messages.success(request, _(
'The drift values for TOTP devices have been reset.'
))
return redirect(self.get_success_url())
def get_success_url(self):
return reverse('control:users.edit', kwargs=self.kwargs)
class UserAnonymizeView(AdministratorPermissionRequiredMixin, RecentAuthenticationRequiredMixin, TemplateView):
template_name = "pretixcontrol/users/anonymize.html"
+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
+23 -1
View File
@@ -21,6 +21,8 @@
#
import inspect
import os
import re
from pathlib import Path
import pytest
from django.core.cache import cache
@@ -28,7 +30,6 @@ from django.test import override_settings
from django.utils import translation
from django_scopes import scopes_disabled
from fakeredis import FakeRedisConnection
from hierarkey.proxy import dirty_cache_keys
from xdist.dsession import DSession
from pretix.testutils.mock import get_redis_connection
@@ -85,6 +86,7 @@ def reset_locale():
@pytest.fixture(autouse=True)
def reset_hierarkey_cache_state():
from hierarkey.proxy import dirty_cache_keys
dirty_cache_keys.set(set())
@@ -143,3 +145,23 @@ def set_lock_namespaces(request):
@pytest.fixture
def class_monkeypatch(request, monkeypatch):
request.cls.monkeypatch = monkeypatch
def pytest_addoption(parser):
parser.addoption(
'--pdf-dir', action='store', help="output dir for files generated by pdf tests", type=Path
)
parser.addoption(
'--pdf-tmp-keep', action='store_true', help="keep the temporary directory created for pdf tests"
)
@pytest.fixture
def pdf_dir(request, tmp_path_factory):
dirname = re.sub('[^a-zA-Z0-9]+', '_', request.node.name).strip("_")
if dir := request.config.getoption('--pdf-dir'):
pdf_dir: Path = (dir / dirname).absolute()
pdf_dir.mkdir(exist_ok=True, parents=True)
yield pdf_dir
else:
yield tmp_path_factory.mktemp(dirname)
@@ -0,0 +1,99 @@
%PDF-1.3
%âãÏÓ
1 0 obj
<<
/PageMode /UseNone
/Pages 2 0 R
/Type /Catalog
>>
endobj
2 0 obj
<<
/Count 1
/Kids [ 3 0 R ]
/Type /Pages
>>
endobj
3 0 obj
<<
/Contents 4 0 R
/MediaBox [ 0 0 595.2756 841.8898 ]
/Parent 2 0 R
/Resources <<
/Font 5 0 R
/ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>>
/Rotate 0
/Trans <<
>>
/Type /Page
/CropBox [ 10 10 307.84 220 ]
>>
endobj
4 0 obj
<<
/Length 117
>>
stream
q
1 0.0 0.0 1 10 10 cm
1 0 0 1 0 0 cm
BT
/F1 12 Tf
14.4 TL
ET
1 0 0 rg
n
56.69291 56.69291 141.7323 28.34646 re
B*
Q
endstream
endobj
5 0 obj
<<
/F1 6 0 R
>>
endobj
6 0 obj
<<
/BaseFont /Helvetica
/Encoding /WinAnsiEncoding
/Name /F1
/Subtype /Type1
/Type /Font
>>
endobj
7 0 obj
<<
/Author (anonymous)
/CreationDate (D\07220260917173038\05302\04700\047)
/Creator (anonymous)
/Keywords ()
/ModDate (D\07220260917173038\05302\04700\047)
/Producer (ReportLab PDF Library \055 \050opensource\051)
/Subject (unspecified)
/Title (untitled)
/Trapped /False
>>
endobj
xref
0 8
0000000000 65535 f
0000000015 00000 n
0000000083 00000 n
0000000142 00000 n
0000000371 00000 n
0000000539 00000 n
0000000570 00000 n
0000000677 00000 n
trailer
<<
/Size 8
/Root 1 0 R
/Info 7 0 R
/ID [ <a3fd776687419b9853cf896bf5d6dc96> <a3fd776687419b9853cf896bf5d6dc96> ]
>>
startxref
966
%%EOF
@@ -0,0 +1,213 @@
%PDF-1.3
%âãÏÓ
1 0 obj
<<
/Producer (pypdf)
>>
endobj
2 0 obj
<<
/Type /Pages
/Count 2
/Kids [ 4 0 R 11 0 R ]
>>
endobj
3 0 obj
<<
/Type /Catalog
/Pages 2 0 R
>>
endobj
4 0 obj
<<
/Contents 5 0 R
/MediaBox [ 0 0 297.84 210 ]
/Resources <<
/Font 6 0 R
/ProcSet [ /ImageB /ImageC /ImageI /PDF /Text ]
>>
/Rotate 0
/Trans <<
>>
/Type /Page
/Parent 2 0 R
>>
endobj
5 0 obj
[ 9 0 R 10 0 R ]
endobj
6 0 obj
<<
/F1 7 0 R
/F1-0 8 0 R
>>
endobj
7 0 obj
<<
/BaseFont /Helvetica
/Encoding /WinAnsiEncoding
/Name /F1
/Subtype /Type1
/Type /Font
>>
endobj
8 0 obj
<<
/BaseFont /Helvetica
/Encoding /WinAnsiEncoding
/Name /F1
/Subtype /Type1
/Type /Font
>>
endobj
9 0 obj
<<
/Length 170
>>
stream
q
1 0.0 0.0 1 -10 -10 cm
10 10 297.84 210 re
W
n
q
1 0.0 0.0 1 10 10 cm
1 0 0 1 0 0 cm
BT
/F1-0 12 Tf
14.4 TL
ET
1 0 0 rg
n
56.69291 56.69291 141.7323 28.34646 re
B*
Q
Q
endstream
endobj
10 0 obj
<<
/Length 106
>>
stream
q
1 0 0 1 0 0 cm BT /F1 12 Tf 14.4 TL ET
q
1 0 0 1 37.10551 140.9669 cm
q
1 0 0 1 0 5.669531 cm
Q
Q
Q
endstream
endobj
11 0 obj
<<
/Contents 12 0 R
/MediaBox [ 0 0 297.84 210 ]
/Resources <<
/Font 13 0 R
/ProcSet [ /ImageB /ImageC /ImageI /PDF /Text ]
>>
/Rotate 0
/Trans <<
>>
/Type /Page
/Parent 2 0 R
>>
endobj
12 0 obj
[ 15 0 R 16 0 R ]
endobj
13 0 obj
<<
/F1 14 0 R
/F1-0 8 0 R
>>
endobj
14 0 obj
<<
/BaseFont /Helvetica
/Encoding /WinAnsiEncoding
/Name /F1
/Subtype /Type1
/Type /Font
>>
endobj
15 0 obj
<<
/Length 170
>>
stream
q
1 0.0 0.0 1 -10 -10 cm
10 10 297.84 210 re
W
n
q
1 0.0 0.0 1 10 10 cm
1 0 0 1 0 0 cm
BT
/F1-0 12 Tf
14.4 TL
ET
1 0 0 rg
n
56.69291 56.69291 141.7323 28.34646 re
B*
Q
Q
endstream
endobj
16 0 obj
<<
/Length 106
>>
stream
q
1 0 0 1 0 0 cm BT /F1 12 Tf 14.4 TL ET
q
1 0 0 1 37.10551 140.9669 cm
q
1 0 0 1 0 5.669531 cm
Q
Q
Q
endstream
endobj
xref
0 17
0000000000 65535 f
0000000015 00000 n
0000000054 00000 n
0000000120 00000 n
0000000169 00000 n
0000000361 00000 n
0000000393 00000 n
0000000436 00000 n
0000000543 00000 n
0000000650 00000 n
0000000871 00000 n
0000001029 00000 n
0000001224 00000 n
0000001258 00000 n
0000001303 00000 n
0000001411 00000 n
0000001633 00000 n
trailer
<<
/Size 17
/Root 3 0 R
/Info 1 0 R
>>
startxref
1791
%%EOF
@@ -0,0 +1,98 @@
%PDF-1.3
%âãÏÓ
1 0 obj
<<
/PageMode /UseNone
/Pages 2 0 R
/Type /Catalog
>>
endobj
2 0 obj
<<
/Count 1
/Kids [ 3 0 R ]
/Type /Pages
>>
endobj
3 0 obj
<<
/Contents 4 0 R
/MediaBox [ 10 10 307.84 220 ]
/Parent 2 0 R
/Resources <<
/Font 5 0 R
/ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>>
/Rotate 0
/Trans <<
>>
/Type /Page
>>
endobj
4 0 obj
<<
/Length 117
>>
stream
q
1 0.0 0.0 1 10 10 cm
1 0 0 1 0 0 cm
BT
/F1 12 Tf
14.4 TL
ET
1 0 0 rg
n
56.69291 56.69291 141.7323 28.34646 re
B*
Q
endstream
endobj
5 0 obj
<<
/F1 6 0 R
>>
endobj
6 0 obj
<<
/BaseFont /Helvetica
/Encoding /WinAnsiEncoding
/Name /F1
/Subtype /Type1
/Type /Font
>>
endobj
7 0 obj
<<
/Author (anonymous)
/CreationDate (D\07220260917173038\05302\04700\047)
/Creator (anonymous)
/Keywords ()
/ModDate (D\07220260917173038\05302\04700\047)
/Producer (ReportLab PDF Library \055 \050opensource\051)
/Subject (unspecified)
/Title (untitled)
/Trapped /False
>>
endobj
xref
0 8
0000000000 65535 f
0000000015 00000 n
0000000083 00000 n
0000000142 00000 n
0000000336 00000 n
0000000504 00000 n
0000000535 00000 n
0000000642 00000 n
trailer
<<
/Size 8
/Root 1 0 R
/Info 7 0 R
/ID [ <a3fd776687419b9853cf896bf5d6dc96> <a3fd776687419b9853cf896bf5d6dc96> ]
>>
startxref
931
%%EOF
@@ -0,0 +1,213 @@
%PDF-1.3
%âãÏÓ
1 0 obj
<<
/Producer (pypdf)
>>
endobj
2 0 obj
<<
/Type /Pages
/Count 2
/Kids [ 4 0 R 11 0 R ]
>>
endobj
3 0 obj
<<
/Type /Catalog
/Pages 2 0 R
>>
endobj
4 0 obj
<<
/Contents 5 0 R
/MediaBox [ 0 0 297.84 210 ]
/Resources <<
/Font 6 0 R
/ProcSet [ /ImageB /ImageC /ImageI /PDF /Text ]
>>
/Rotate 0
/Trans <<
>>
/Type /Page
/Parent 2 0 R
>>
endobj
5 0 obj
[ 9 0 R 10 0 R ]
endobj
6 0 obj
<<
/F1 7 0 R
/F1-0 8 0 R
>>
endobj
7 0 obj
<<
/BaseFont /Helvetica
/Encoding /WinAnsiEncoding
/Name /F1
/Subtype /Type1
/Type /Font
>>
endobj
8 0 obj
<<
/BaseFont /Helvetica
/Encoding /WinAnsiEncoding
/Name /F1
/Subtype /Type1
/Type /Font
>>
endobj
9 0 obj
<<
/Length 170
>>
stream
q
1 0.0 0.0 1 -10 -10 cm
10 10 297.84 210 re
W
n
q
1 0.0 0.0 1 10 10 cm
1 0 0 1 0 0 cm
BT
/F1-0 12 Tf
14.4 TL
ET
1 0 0 rg
n
56.69291 56.69291 141.7323 28.34646 re
B*
Q
Q
endstream
endobj
10 0 obj
<<
/Length 106
>>
stream
q
1 0 0 1 0 0 cm BT /F1 12 Tf 14.4 TL ET
q
1 0 0 1 37.10551 140.9669 cm
q
1 0 0 1 0 5.669531 cm
Q
Q
Q
endstream
endobj
11 0 obj
<<
/Contents 12 0 R
/MediaBox [ 0 0 297.84 210 ]
/Resources <<
/Font 13 0 R
/ProcSet [ /ImageB /ImageC /ImageI /PDF /Text ]
>>
/Rotate 0
/Trans <<
>>
/Type /Page
/Parent 2 0 R
>>
endobj
12 0 obj
[ 15 0 R 16 0 R ]
endobj
13 0 obj
<<
/F1 14 0 R
/F1-0 8 0 R
>>
endobj
14 0 obj
<<
/BaseFont /Helvetica
/Encoding /WinAnsiEncoding
/Name /F1
/Subtype /Type1
/Type /Font
>>
endobj
15 0 obj
<<
/Length 170
>>
stream
q
1 0.0 0.0 1 -10 -10 cm
10 10 297.84 210 re
W
n
q
1 0.0 0.0 1 10 10 cm
1 0 0 1 0 0 cm
BT
/F1-0 12 Tf
14.4 TL
ET
1 0 0 rg
n
56.69291 56.69291 141.7323 28.34646 re
B*
Q
Q
endstream
endobj
16 0 obj
<<
/Length 106
>>
stream
q
1 0 0 1 0 0 cm BT /F1 12 Tf 14.4 TL ET
q
1 0 0 1 37.10551 140.9669 cm
q
1 0 0 1 0 5.669531 cm
Q
Q
Q
endstream
endobj
xref
0 17
0000000000 65535 f
0000000015 00000 n
0000000054 00000 n
0000000120 00000 n
0000000169 00000 n
0000000361 00000 n
0000000393 00000 n
0000000436 00000 n
0000000543 00000 n
0000000650 00000 n
0000000871 00000 n
0000001029 00000 n
0000001224 00000 n
0000001258 00000 n
0000001303 00000 n
0000001411 00000 n
0000001633 00000 n
trailer
<<
/Size 17
/Root 3 0 R
/Info 1 0 R
>>
startxref
1791
%%EOF
Binary file not shown.
@@ -0,0 +1,216 @@
%PDF-1.7
%âãÏÓ
1 0 obj
<<
/Producer (pypdf)
>>
endobj
2 0 obj
<<
/Type /Pages
/Count 2
/Kids [ 4 0 R 11 0 R ]
>>
endobj
3 0 obj
<<
/Type /Catalog
/Pages 2 0 R
>>
endobj
4 0 obj
<<
/Contents 5 0 R
/MediaBox [ 0 0 210 297.84 ]
/Resources <<
/Font 6 0 R
/ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
/ExtGState <<
/GS0 8 0 R
>>
>>
/Rotate 0
/Trans <<
>>
/Type /Page
/Parent 2 0 R
>>
endobj
5 0 obj
[ 9 0 R 10 0 R ]
endobj
6 0 obj
<<
/F1 7 0 R
>>
endobj
7 0 obj
<<
/BaseFont /Helvetica
/Encoding /WinAnsiEncoding
/Name /F1
/Subtype /Type1
/Type /Font
>>
endobj
8 0 obj
<<
/Type /ExtGState
/OP false
/op false
/OPM 1
>>
endobj
9 0 obj
<<
/Length 287
>>
stream
q
0.00000000000000006123234 -1 1 0.00000000000000006123234 0.0 297.84 cm
0.0 0.0 297.84 210 re
W
n
/GS0 gs
0.925811 0.881331 0.538643 rg
0 0.23622 297.638 209.764 re
f*
0.476631 0.756542 0.260899 rg
0 0.23622 297.638 209.764 re
292.638 205 m
5 205 l
5 5.23622 l
292.638 5.23622 l
h
f*
Q
endstream
endobj
10 0 obj
<<
/Length 106
>>
stream
q
1 0 0 1 0 0 cm BT /F1 12 Tf 14.4 TL ET
q
1 0 0 1 37.10551 140.9669 cm
q
1 0 0 1 0 5.669531 cm
Q
Q
Q
endstream
endobj
11 0 obj
<<
/Contents 12 0 R
/MediaBox [ 0 0 210 297.84 ]
/Resources <<
/Font 13 0 R
/ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
/ExtGState <<
/GS0 8 0 R
>>
>>
/Rotate 0
/Trans <<
>>
/Type /Page
/Parent 2 0 R
>>
endobj
12 0 obj
[ 15 0 R 16 0 R ]
endobj
13 0 obj
<<
/F1 14 0 R
>>
endobj
14 0 obj
<<
/BaseFont /Helvetica
/Encoding /WinAnsiEncoding
/Name /F1
/Subtype /Type1
/Type /Font
>>
endobj
15 0 obj
<<
/Length 287
>>
stream
q
0.00000000000000006123234 -1 1 0.00000000000000006123234 0.0 297.84 cm
0.0 0.0 297.84 210 re
W
n
/GS0 gs
0.925811 0.881331 0.538643 rg
0 0.23622 297.638 209.764 re
f*
0.476631 0.756542 0.260899 rg
0 0.23622 297.638 209.764 re
292.638 205 m
5 205 l
5 5.23622 l
292.638 5.23622 l
h
f*
Q
endstream
endobj
16 0 obj
<<
/Length 106
>>
stream
q
1 0 0 1 0 0 cm BT /F1 12 Tf 14.4 TL ET
q
1 0 0 1 37.10551 140.9669 cm
q
1 0 0 1 0 5.669531 cm
Q
Q
Q
endstream
endobj
xref
0 17
0000000000 65535 f
0000000015 00000 n
0000000054 00000 n
0000000120 00000 n
0000000169 00000 n
0000000389 00000 n
0000000421 00000 n
0000000452 00000 n
0000000559 00000 n
0000000624 00000 n
0000000962 00000 n
0000001120 00000 n
0000001343 00000 n
0000001377 00000 n
0000001410 00000 n
0000001518 00000 n
0000001857 00000 n
trailer
<<
/Size 17
/Root 3 0 R
/Info 1 0 R
>>
startxref
2015
%%EOF
+55
View File
@@ -32,13 +32,18 @@
# 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.
import logging
from datetime import timedelta
from decimal import Decimal
from io import BytesIO
from pathlib import Path
import pypdfium2
import pytest
from django.core.files import File
from django.utils.timezone import now
from django_scopes import scope
from PIL import ImageChops
from pypdf import PdfReader
from pretix.base.models import (
@@ -118,3 +123,53 @@ def test_generate_pdf_multi(env):
assert ftype == 'application/pdf'
pdf = PdfReader(BytesIO(buf))
assert len(pdf.pages) == 1
def asset_path(name):
return Path(__file__).parent / "assets" / name
def compare_pdfs(pdf_dir: Path, inp_a: Path | bytes, inp_b: Path | bytes):
logging.info(f"Comparing pdfs, writing files to {pdf_dir}")
pdf_a = pypdfium2.PdfDocument(inp_a)
pdf_b = pypdfium2.PdfDocument(inp_b)
pdf_a.save(pdf_dir / "a.pdf")
pdf_a.save(pdf_dir / "b.pdf")
assert len(pdf_a) == len(pdf_b)
for i, (expected_page, output_page) in enumerate(zip(pdf_a, pdf_b)):
expected_render = expected_page.render()
output_render = output_page.render()
assert expected_render.height == output_render.height
assert expected_render.width == output_render.width
diff = ImageChops.difference(expected_render.to_pil(), output_render.to_pil())
if diff.getbbox():
diff.save(pdf_dir / f"{i}.png")
assert not diff.getbbox(), f"Page {i} differs."
@pytest.mark.django_db
@pytest.mark.parametrize("case", [
"bg-rotated",
"bg-mediabox-offset",
"bg-cropbox"
])
def test_generate_pdf_weird_bgs(pdf_dir, env, case):
event, order, shirt = env
asset_folder = asset_path(case)
with open(asset_folder / "bg.pdf", 'rb') as fi:
event.badge_layouts.create(name="Default", default=True, background=File(fi, name="test.pdf"))
e = BadgeExporter(event, organizer=event.organizer)
fname, ftype, buf = e.render({
'items': [shirt.pk],
'rendering': 'one',
'include_pending': True
})
assert ftype == 'application/pdf'
assert buf
compare_pdfs(pdf_dir, buf, asset_folder / "expected.pdf")