mirror of
https://github.com/pretix/pretix.git
synced 2026-09-19 17:14:41 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
547de7aa26 | ||
|
|
03bca84e00 |
@@ -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 *
|
||||
|
||||
@@ -129,6 +129,7 @@ dev = [
|
||||
"pytest==9.1.*",
|
||||
"playwright",
|
||||
"responses",
|
||||
"pypdfium2"
|
||||
]
|
||||
|
||||
[project.entry-points."distutils.commands"]
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user