mirror of
https://github.com/pretix/pretix.git
synced 2026-06-27 03:56:15 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9da8c1f7b2 | |||
| 6b340682b2 | |||
| 0dc436067f | |||
| db66c91108 | |||
| 3a1db55e8b | |||
| 57da5cbae2 |
@@ -24,7 +24,7 @@ jobs:
|
||||
name: Packaging
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.13"]
|
||||
python-version: ["3.11"]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
|
||||
@@ -24,10 +24,10 @@ jobs:
|
||||
name: Check gettext syntax
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python 3.13
|
||||
- name: Set up Python 3.11
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.13
|
||||
python-version: 3.11
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
@@ -49,10 +49,10 @@ jobs:
|
||||
name: Spellcheck
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python 3.13
|
||||
- name: Set up Python 3.11
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.13
|
||||
python-version: 3.11
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
|
||||
@@ -24,10 +24,10 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python 3.13
|
||||
- name: Set up Python 3.11
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.13
|
||||
python-version: 3.11
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
@@ -44,10 +44,10 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python 3.13
|
||||
- name: Set up Python 3.11
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.13
|
||||
python-version: 3.11
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
@@ -64,10 +64,10 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Python 3.13
|
||||
- name: Set up Python 3.11
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.13
|
||||
python-version: 3.11
|
||||
- name: Install Dependencies
|
||||
run: pip3 install licenseheaders
|
||||
- name: Run licenseheaders
|
||||
|
||||
@@ -23,15 +23,13 @@ jobs:
|
||||
name: Tests
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.11", "3.13", "3.14"]
|
||||
python-version: ["3.10", "3.11", "3.13"]
|
||||
database: [sqlite, postgres]
|
||||
exclude:
|
||||
- database: sqlite
|
||||
python-version: "3.10"
|
||||
- database: sqlite
|
||||
python-version: "3.11"
|
||||
- database: sqlite
|
||||
python-version: "3.12"
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:15
|
||||
@@ -83,4 +81,4 @@ jobs:
|
||||
file: src/coverage.xml
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
fail_ci_if_error: false
|
||||
if: matrix.database == 'postgres' && matrix.python-version == '3.13'
|
||||
if: matrix.database == 'postgres' && matrix.python-version == '3.11'
|
||||
|
||||
+7
-8
@@ -3,7 +3,7 @@ name = "pretix"
|
||||
dynamic = ["version"]
|
||||
description = "Reinventing presales, one ticket at a time"
|
||||
readme = "README.rst"
|
||||
requires-python = ">=3.11"
|
||||
requires-python = ">=3.10"
|
||||
license = {file = "LICENSE"}
|
||||
keywords = ["tickets", "web", "shop", "ecommerce"]
|
||||
authors = [
|
||||
@@ -19,11 +19,10 @@ classifiers = [
|
||||
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
|
||||
"Environment :: Web Environment",
|
||||
"License :: OSI Approved :: GNU Affero General Public License v3",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Programming Language :: Python :: 3.13",
|
||||
"Programming Language :: Python :: 3.14",
|
||||
"Framework :: Django :: 5.2",
|
||||
"Framework :: Django :: 4.2",
|
||||
]
|
||||
|
||||
dependencies = [
|
||||
@@ -37,7 +36,7 @@ dependencies = [
|
||||
"css-inline==0.20.*",
|
||||
"defusedcsv>=1.1.0",
|
||||
"dnspython==2.*",
|
||||
"Django[argon2]==5.2.*",
|
||||
"Django[argon2]==4.2.*,>=4.2.26",
|
||||
"django-bootstrap3==26.1",
|
||||
"django-compressor==4.6.0",
|
||||
"django-countries==8.2.*",
|
||||
@@ -60,7 +59,7 @@ dependencies = [
|
||||
"dnspython==2.8.*",
|
||||
"drf_ujson2==1.7.*",
|
||||
"geoip2==5.*",
|
||||
"importlib_metadata==9.*", # Polyfill, we can probably drop this once we require Python 3.10+
|
||||
"importlib_metadata==8.*", # Polyfill, we can probably drop this once we require Python 3.10+
|
||||
"isoweek",
|
||||
"jsonschema",
|
||||
"kombu==5.6.*",
|
||||
@@ -93,7 +92,7 @@ dependencies = [
|
||||
"redis==7.1.*",
|
||||
"reportlab==4.4.*",
|
||||
"requests==2.32.*",
|
||||
"sentry-sdk==2.56.*",
|
||||
"sentry-sdk==2.54.*",
|
||||
"sepaxml==2.7.*",
|
||||
"stripe==7.9.*",
|
||||
"text-unidecode==1.*",
|
||||
|
||||
@@ -19,4 +19,4 @@
|
||||
# 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/>.
|
||||
#
|
||||
__version__ = "2026.3.4"
|
||||
__version__ = "2026.3.0.dev0"
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
|
||||
@@ -69,9 +69,7 @@ class ReusableMediaExporter(OrganizerLevelExportMixin, ListExporter):
|
||||
date_format(medium.expires, 'SHORT_DATETIME_FORMAT') if medium.expires else '',
|
||||
medium.customer.identifier if medium.customer_id else '',
|
||||
f"{medium.linked_orderposition.order.code}-{medium.linked_orderposition.positionid}" if medium.linked_orderposition_id else '',
|
||||
# we cannot determine here whether user has permission organizer.giftcards:read
|
||||
# so default to not showing giftcard secret
|
||||
medium.linked_giftcard.secret[:3] + "…" if medium.linked_giftcard_id else '',
|
||||
medium.linked_giftcard.secret if medium.linked_giftcard_id else '',
|
||||
medium.notes,
|
||||
]
|
||||
yield row
|
||||
|
||||
@@ -196,7 +196,8 @@ class RegistrationForm(forms.Form):
|
||||
def clean_password(self):
|
||||
password1 = self.cleaned_data.get('password', '')
|
||||
user = User(email=self.cleaned_data.get('email'))
|
||||
validate_password(password1, user=user)
|
||||
if validate_password(password1, user=user) is not None:
|
||||
raise forms.ValidationError(_(password_validators_help_texts()), code='pw_invalid')
|
||||
return password1
|
||||
|
||||
def clean_email(self):
|
||||
|
||||
@@ -45,6 +45,7 @@ import pycountry
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.gis.geoip2 import GeoIP2
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.core.validators import (
|
||||
@@ -101,7 +102,6 @@ from pretix.helpers.countries import (
|
||||
from pretix.helpers.escapejson import escapejson_attr
|
||||
from pretix.helpers.http import get_client_ip
|
||||
from pretix.helpers.i18n import get_format_without_seconds
|
||||
from pretix.helpers.security import get_geoip
|
||||
from pretix.presale.signals import question_form_fields
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -393,7 +393,7 @@ class WrappedPhoneNumberPrefixWidget(PhoneNumberPrefixWidget):
|
||||
|
||||
def guess_country_from_request(request, event):
|
||||
if settings.HAS_GEOIP:
|
||||
g = get_geoip()
|
||||
g = GeoIP2()
|
||||
try:
|
||||
res = g.country(get_client_ip(request))
|
||||
if res['country_code'] and len(res['country_code']) == 2:
|
||||
|
||||
@@ -22,7 +22,9 @@
|
||||
import datetime
|
||||
import logging
|
||||
import math
|
||||
import re
|
||||
import textwrap
|
||||
import unicodedata
|
||||
from collections import defaultdict
|
||||
from decimal import Decimal
|
||||
from io import BytesIO
|
||||
@@ -56,8 +58,8 @@ from pretix.base.services.currencies import SOURCE_NAMES
|
||||
from pretix.base.signals import register_invoice_renderers
|
||||
from pretix.base.templatetags.money import money_filter
|
||||
from pretix.helpers.reportlab import (
|
||||
FontFallbackParagraph, PlainTextParagraph, ThumbnailingImageReader,
|
||||
normalize_text, register_ttf_font_if_new, reshaper,
|
||||
FontFallbackParagraph, ThumbnailingImageReader, register_ttf_font_if_new,
|
||||
reshaper,
|
||||
)
|
||||
from pretix.presale.style import get_fonts
|
||||
|
||||
@@ -257,8 +259,18 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
|
||||
register_ttf_font_if_new(family + ' B I', finders.find(styles['bolditalic']['truetype']))
|
||||
|
||||
def _normalize(self, text):
|
||||
# alias kept for plugin compatibility
|
||||
return normalize_text(text)
|
||||
# reportlab does not support unicode combination characters
|
||||
# It's important we do this before we use ArabicReshaper
|
||||
text = unicodedata.normalize("NFKC", text)
|
||||
|
||||
# reportlab does not support RTL, ligature-heavy scripts like Arabic. Therefore, we use ArabicReshaper
|
||||
# to resolve all ligatures and python-bidi to switch RTL texts.
|
||||
try:
|
||||
text = "<br />".join(get_display(reshaper.reshape(l)) for l in re.split("<br ?/>", text))
|
||||
except:
|
||||
logger.exception('Reshaping/Bidi fixes failed on string {}'.format(repr(text)))
|
||||
|
||||
return text
|
||||
|
||||
def _upper(self, val):
|
||||
# We uppercase labels, but not in every language
|
||||
@@ -339,15 +351,10 @@ class BaseReportlabInvoiceRenderer(BaseInvoiceRenderer):
|
||||
return 'invoice.pdf', 'application/pdf', buffer.read()
|
||||
|
||||
def _clean_text(self, text, tags=None):
|
||||
# For backwards compatibility with customer content, we need to support tags like <br> and <b> in a few text
|
||||
# fields. Therefore, we can't use PlainTextParagraph for these, but run bleach instead to limit the allowed
|
||||
# tags.
|
||||
return self._normalize(
|
||||
bleach.clean(
|
||||
text,
|
||||
tags=set(tags) if tags else set()
|
||||
).strip().replace('<br>', '<br />').replace('\n', '<br />\n')
|
||||
)
|
||||
return self._normalize(bleach.clean(
|
||||
text,
|
||||
tags=set(tags) if tags else set()
|
||||
).strip().replace('<br>', '<br />').replace('\n', '<br />\n'))
|
||||
|
||||
|
||||
class PaidMarker(Flowable):
|
||||
@@ -398,7 +405,8 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
invoice_to_top = 52 * mm
|
||||
|
||||
def _draw_invoice_to(self, canvas):
|
||||
p = PlainTextParagraph(self.invoice.address_invoice_to, style=self.stylesheet['Normal'])
|
||||
p = FontFallbackParagraph(self._clean_text(self.invoice.address_invoice_to),
|
||||
style=self.stylesheet['Normal'])
|
||||
p.wrapOn(canvas, self.invoice_to_width, self.invoice_to_height)
|
||||
p_size = p.wrap(self.invoice_to_width, self.invoice_to_height)
|
||||
p.drawOn(canvas, self.invoice_to_left, self.pagesize[1] - p_size[1] - self.invoice_to_top)
|
||||
@@ -409,8 +417,8 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
invoice_from_top = 17 * mm
|
||||
|
||||
def _draw_invoice_from(self, canvas):
|
||||
p = PlainTextParagraph(
|
||||
self.invoice.full_invoice_from,
|
||||
p = FontFallbackParagraph(
|
||||
self._clean_text(self.invoice.full_invoice_from),
|
||||
style=self.stylesheet['InvoiceFrom']
|
||||
)
|
||||
p.wrapOn(canvas, self.invoice_from_width, self.invoice_from_height)
|
||||
@@ -540,12 +548,13 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
def _draw_event(self, canvas):
|
||||
def shorten(txt):
|
||||
txt = str(txt)
|
||||
p = PlainTextParagraph(txt, style=self.stylesheet['Normal'])
|
||||
txt = bleach.clean(txt, tags=set()).strip()
|
||||
p = FontFallbackParagraph(self._normalize(txt.strip().replace('\n', '<br />\n')), style=self.stylesheet['Normal'])
|
||||
p_size = p.wrap(self.event_width, self.event_height)
|
||||
|
||||
while p_size[1] > 2 * self.stylesheet['Normal'].leading:
|
||||
txt = ' '.join(txt.replace('…', '').split()[:-1]) + '…'
|
||||
p = PlainTextParagraph(txt, style=self.stylesheet['Normal'])
|
||||
p = FontFallbackParagraph(self._normalize(txt.strip().replace('\n', '<br />\n')), style=self.stylesheet['Normal'])
|
||||
p_size = p.wrap(self.event_width, self.event_height)
|
||||
return txt
|
||||
|
||||
@@ -563,7 +572,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
else:
|
||||
p_str = shorten(self.invoice.event.name)
|
||||
|
||||
p = PlainTextParagraph(p_str, style=self.stylesheet['Normal'])
|
||||
p = FontFallbackParagraph(self._normalize(p_str.strip().replace('\n', '<br />\n')), style=self.stylesheet['Normal'])
|
||||
p.wrapOn(canvas, self.event_width, self.event_height)
|
||||
p_size = p.wrap(self.event_width, self.event_height)
|
||||
p.drawOn(canvas, self.event_left, self.pagesize[1] - self.event_top - p_size[1])
|
||||
@@ -636,37 +645,39 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
|
||||
type_info_text = self.invoice.transmission_type_instance.pdf_info_text()
|
||||
if type_info_text:
|
||||
story.append(PlainTextParagraph(
|
||||
story.append(FontFallbackParagraph(
|
||||
type_info_text,
|
||||
self.stylesheet['WarningBlock']
|
||||
))
|
||||
|
||||
if self.invoice.custom_field:
|
||||
story.append(PlainTextParagraph(
|
||||
story.append(FontFallbackParagraph(
|
||||
'{}: {}'.format(
|
||||
str(self.invoice.event.settings.invoice_address_custom_field),
|
||||
self.invoice.custom_field,
|
||||
self._clean_text(str(self.invoice.event.settings.invoice_address_custom_field)),
|
||||
self._clean_text(self.invoice.custom_field),
|
||||
),
|
||||
self.stylesheet['Normal']
|
||||
))
|
||||
|
||||
if self.invoice.internal_reference:
|
||||
story.append(PlainTextParagraph(
|
||||
pgettext('invoice', 'Customer reference: {reference}').format(
|
||||
reference=self.invoice.internal_reference,
|
||||
),
|
||||
story.append(FontFallbackParagraph(
|
||||
self._normalize(pgettext('invoice', 'Customer reference: {reference}').format(
|
||||
reference=self._clean_text(self.invoice.internal_reference),
|
||||
)),
|
||||
self.stylesheet['Normal']
|
||||
))
|
||||
|
||||
if self.invoice.invoice_to_vat_id:
|
||||
story.append(PlainTextParagraph(
|
||||
pgettext('invoice', 'Customer VAT ID') + ': ' + self.invoice.invoice_to_vat_id,
|
||||
story.append(FontFallbackParagraph(
|
||||
self._normalize(pgettext('invoice', 'Customer VAT ID')) + ': ' +
|
||||
self._clean_text(self.invoice.invoice_to_vat_id),
|
||||
self.stylesheet['Normal']
|
||||
))
|
||||
|
||||
if self.invoice.invoice_to_beneficiary:
|
||||
story.append(PlainTextParagraph(
|
||||
pgettext('invoice', 'Beneficiary') + ':\n' + self.invoice.invoice_to_beneficiary,
|
||||
story.append(FontFallbackParagraph(
|
||||
self._normalize(pgettext('invoice', 'Beneficiary')) + ':<br />' +
|
||||
self._clean_text(self.invoice.invoice_to_beneficiary),
|
||||
self.stylesheet['Normal']
|
||||
))
|
||||
|
||||
@@ -696,11 +707,11 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
|
||||
story = [
|
||||
NextPageTemplate('FirstPage'),
|
||||
PlainTextParagraph(
|
||||
(
|
||||
FontFallbackParagraph(
|
||||
self._normalize(
|
||||
pgettext('invoice', 'Tax Invoice') if str(self.invoice.invoice_from_country) == 'AU'
|
||||
else pgettext('invoice', 'Invoice')
|
||||
) if not self.invoice.is_cancellation else pgettext('invoice', 'Cancellation'),
|
||||
) if not self.invoice.is_cancellation else self._normalize(pgettext('invoice', 'Cancellation')),
|
||||
self.stylesheet['Heading1']
|
||||
),
|
||||
Spacer(1, 5 * mm),
|
||||
@@ -722,17 +733,17 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
]
|
||||
if has_taxes:
|
||||
tdata = [(
|
||||
PlainTextParagraph(pgettext('invoice', 'Description'), self.stylesheet['Bold']),
|
||||
PlainTextParagraph(pgettext('invoice', 'Qty'), self.stylesheet['BoldRightNoSplit']),
|
||||
PlainTextParagraph(pgettext('invoice', 'Tax rate'), self.stylesheet['BoldRightNoSplit']),
|
||||
PlainTextParagraph(pgettext('invoice', 'Net'), self.stylesheet['BoldRightNoSplit']),
|
||||
PlainTextParagraph(pgettext('invoice', 'Gross'), self.stylesheet['BoldRightNoSplit']),
|
||||
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Description')), self.stylesheet['Bold']),
|
||||
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Qty')), self.stylesheet['BoldRightNoSplit']),
|
||||
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Tax rate')), self.stylesheet['BoldRightNoSplit']),
|
||||
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Net')), self.stylesheet['BoldRightNoSplit']),
|
||||
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Gross')), self.stylesheet['BoldRightNoSplit']),
|
||||
)]
|
||||
else:
|
||||
tdata = [(
|
||||
PlainTextParagraph(pgettext('invoice', 'Description'), self.stylesheet['Bold']),
|
||||
PlainTextParagraph(pgettext('invoice', 'Qty'), self.stylesheet['BoldRightNoSplit']),
|
||||
PlainTextParagraph(pgettext('invoice', 'Amount'), self.stylesheet['BoldRightNoSplit']),
|
||||
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Description')), self.stylesheet['Bold']),
|
||||
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Qty')), self.stylesheet['BoldRightNoSplit']),
|
||||
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Amount')), self.stylesheet['BoldRightNoSplit']),
|
||||
)]
|
||||
|
||||
def _group_key(line):
|
||||
@@ -769,8 +780,8 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
max_height = self.stylesheet['Normal'].leading * 5
|
||||
p_style = self.stylesheet['Normal']
|
||||
for __ in range(1000):
|
||||
p = PlainTextParagraph(
|
||||
curr_description,
|
||||
p = FontFallbackParagraph(
|
||||
self._clean_text(curr_description, tags=['br']),
|
||||
p_style
|
||||
)
|
||||
h = p.wrap(max_width, doc.height)[1]
|
||||
@@ -851,7 +862,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
# Group together at the end of the invoice
|
||||
request_show_service_date = period_line
|
||||
elif period_line:
|
||||
description_p_list.append(PlainTextParagraph(
|
||||
description_p_list.append(FontFallbackParagraph(
|
||||
period_line,
|
||||
self.stylesheet['Fineprint']
|
||||
))
|
||||
@@ -863,7 +874,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
net_price=money_filter(net_value, self.invoice.event.currency),
|
||||
gross_price=money_filter(gross_value, self.invoice.event.currency),
|
||||
)
|
||||
description_p_list.append(PlainTextParagraph(
|
||||
description_p_list.append(FontFallbackParagraph(
|
||||
single_price_line,
|
||||
self.stylesheet['Fineprint']
|
||||
))
|
||||
@@ -872,11 +883,11 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
description_p_list.pop(0),
|
||||
str(len(lines)),
|
||||
localize(tax_rate) + " %",
|
||||
PlainTextParagraph(
|
||||
FontFallbackParagraph(
|
||||
money_filter(net_value * len(lines), self.invoice.event.currency).replace('\xa0', ' '),
|
||||
self.stylesheet['NormalRight']
|
||||
),
|
||||
PlainTextParagraph(
|
||||
FontFallbackParagraph(
|
||||
money_filter(gross_value * len(lines), self.invoice.event.currency).replace('\xa0', ' '),
|
||||
self.stylesheet['NormalRight']
|
||||
),
|
||||
@@ -893,14 +904,14 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
single_price_line = pgettext('invoice', 'Single price: {price}').format(
|
||||
price=money_filter(gross_value, self.invoice.event.currency),
|
||||
)
|
||||
description_p_list.append(PlainTextParagraph(
|
||||
description_p_list.append(FontFallbackParagraph(
|
||||
single_price_line,
|
||||
self.stylesheet['Fineprint']
|
||||
))
|
||||
tdata.append((
|
||||
description_p_list.pop(0),
|
||||
str(len(lines)),
|
||||
PlainTextParagraph(
|
||||
FontFallbackParagraph(
|
||||
money_filter(gross_value * len(lines), self.invoice.event.currency).replace('\xa0', ' '),
|
||||
self.stylesheet['NormalRight']
|
||||
),
|
||||
@@ -933,12 +944,12 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
|
||||
if has_taxes:
|
||||
tdata.append([
|
||||
PlainTextParagraph(pgettext('invoice', 'Invoice total'), self.stylesheet['Bold']), '', '', '',
|
||||
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Invoice total')), self.stylesheet['Bold']), '', '', '',
|
||||
money_filter(total, self.invoice.event.currency)
|
||||
])
|
||||
else:
|
||||
tdata.append([
|
||||
PlainTextParagraph(pgettext('invoice', 'Invoice total'), self.stylesheet['Bold']), '',
|
||||
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Invoice total')), self.stylesheet['Bold']), '',
|
||||
money_filter(total, self.invoice.event.currency)
|
||||
])
|
||||
|
||||
@@ -947,12 +958,12 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
pending_sum = self.invoice.order.pending_sum
|
||||
if pending_sum != total:
|
||||
tdata.append(
|
||||
[PlainTextParagraph(pgettext('invoice', 'Received payments'), self.stylesheet['Normal'])] +
|
||||
[FontFallbackParagraph(self._normalize(pgettext('invoice', 'Received payments')), self.stylesheet['Normal'])] +
|
||||
(['', '', ''] if has_taxes else ['']) +
|
||||
[money_filter(pending_sum - total, self.invoice.event.currency)]
|
||||
)
|
||||
tdata.append(
|
||||
[PlainTextParagraph(pgettext('invoice', 'Outstanding payments'), self.stylesheet['Bold'])] +
|
||||
[FontFallbackParagraph(self._normalize(pgettext('invoice', 'Outstanding payments')), self.stylesheet['Bold'])] +
|
||||
(['', '', ''] if has_taxes else ['']) +
|
||||
[money_filter(pending_sum, self.invoice.event.currency)]
|
||||
)
|
||||
@@ -969,12 +980,12 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
s=Sum('amount')
|
||||
)['s'] or Decimal('0.00')
|
||||
tdata.append(
|
||||
[PlainTextParagraph(pgettext('invoice', 'Paid by gift card'), self.stylesheet['Normal'])] +
|
||||
[FontFallbackParagraph(self._normalize(pgettext('invoice', 'Paid by gift card')), self.stylesheet['Normal'])] +
|
||||
(['', '', ''] if has_taxes else ['']) +
|
||||
[money_filter(giftcard_sum, self.invoice.event.currency)]
|
||||
)
|
||||
tdata.append(
|
||||
[PlainTextParagraph(pgettext('invoice', 'Remaining amount'), self.stylesheet['Bold'])] +
|
||||
[FontFallbackParagraph(self._normalize(pgettext('invoice', 'Remaining amount')), self.stylesheet['Bold'])] +
|
||||
(['', '', ''] if has_taxes else ['']) +
|
||||
[money_filter(total - giftcard_sum, self.invoice.event.currency)]
|
||||
)
|
||||
@@ -997,14 +1008,14 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
story.append(Spacer(1, 10 * mm))
|
||||
|
||||
if request_show_service_date:
|
||||
story.append(PlainTextParagraph(
|
||||
pgettext('invoice', 'Invoice period: {daterange}').format(daterange=request_show_service_date),
|
||||
story.append(FontFallbackParagraph(
|
||||
self._normalize(pgettext('invoice', 'Invoice period: {daterange}').format(daterange=request_show_service_date)),
|
||||
self.stylesheet['Normal']
|
||||
))
|
||||
|
||||
if self.invoice.payment_provider_text:
|
||||
story.append(FontFallbackParagraph(
|
||||
self._clean_text(self.invoice.payment_provider_text, tags=['br', 'b']),
|
||||
self._normalize(self.invoice.payment_provider_text),
|
||||
self.stylesheet['Normal']
|
||||
))
|
||||
|
||||
@@ -1028,10 +1039,10 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
('FONTNAME', (0, 0), (-1, -1), self.font_regular),
|
||||
]
|
||||
thead = [
|
||||
PlainTextParagraph(pgettext('invoice', 'Tax rate'), self.stylesheet['Fineprint']),
|
||||
PlainTextParagraph(pgettext('invoice', 'Net value'), self.stylesheet['FineprintRight']),
|
||||
PlainTextParagraph(pgettext('invoice', 'Gross value'), self.stylesheet['FineprintRight']),
|
||||
PlainTextParagraph(pgettext('invoice', 'Tax'), self.stylesheet['FineprintRight']),
|
||||
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Tax rate')), self.stylesheet['Fineprint']),
|
||||
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Net value')), self.stylesheet['FineprintRight']),
|
||||
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Gross value')), self.stylesheet['FineprintRight']),
|
||||
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Tax')), self.stylesheet['FineprintRight']),
|
||||
''
|
||||
]
|
||||
tdata = [thead]
|
||||
@@ -1042,7 +1053,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
continue
|
||||
tax = taxvalue_map[idx]
|
||||
tdata.append([
|
||||
PlainTextParagraph(localize(rate) + " % " + name, self.stylesheet['Fineprint']),
|
||||
FontFallbackParagraph(self._normalize(localize(rate) + " % " + name), self.stylesheet['Fineprint']),
|
||||
money_filter(gross - tax, self.invoice.event.currency),
|
||||
money_filter(gross, self.invoice.event.currency),
|
||||
money_filter(tax, self.invoice.event.currency),
|
||||
@@ -1061,7 +1072,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
table.setStyle(TableStyle(tstyledata))
|
||||
story.append(Spacer(5 * mm, 5 * mm))
|
||||
story.append(KeepTogether([
|
||||
PlainTextParagraph(pgettext('invoice', 'Included taxes'), self.stylesheet['FineprintHeading']),
|
||||
FontFallbackParagraph(self._normalize(pgettext('invoice', 'Included taxes')), self.stylesheet['FineprintHeading']),
|
||||
table
|
||||
]))
|
||||
|
||||
@@ -1078,7 +1089,7 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
net = gross - tax
|
||||
|
||||
tdata.append([
|
||||
PlainTextParagraph(localize(rate) + " % " + name, self.stylesheet['Fineprint']),
|
||||
FontFallbackParagraph(self._normalize(localize(rate) + " % " + name), self.stylesheet['Fineprint']),
|
||||
fmt(net), fmt(gross), fmt(tax), ''
|
||||
])
|
||||
|
||||
@@ -1087,13 +1098,13 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
|
||||
story.append(KeepTogether([
|
||||
Spacer(1, height=2 * mm),
|
||||
PlainTextParagraph(
|
||||
pgettext(
|
||||
FontFallbackParagraph(
|
||||
self._normalize(pgettext(
|
||||
'invoice', 'Using the conversion rate of 1:{rate} as published by the {authority} on '
|
||||
'{date}, this corresponds to:'
|
||||
).format(rate=localize(self.invoice.foreign_currency_rate),
|
||||
authority=SOURCE_NAMES.get(self.invoice.foreign_currency_source, "?"),
|
||||
date=date_format(self.invoice.foreign_currency_rate_date, "SHORT_DATE_FORMAT")),
|
||||
date=date_format(self.invoice.foreign_currency_rate_date, "SHORT_DATE_FORMAT"))),
|
||||
self.stylesheet['Fineprint']
|
||||
),
|
||||
Spacer(1, height=3 * mm),
|
||||
@@ -1102,14 +1113,14 @@ class ClassicInvoiceRenderer(BaseReportlabInvoiceRenderer):
|
||||
elif self.invoice.foreign_currency_display and self.invoice.foreign_currency_rate:
|
||||
foreign_total = round_decimal(total * self.invoice.foreign_currency_rate)
|
||||
story.append(Spacer(1, 5 * mm))
|
||||
story.append(PlainTextParagraph(
|
||||
story.append(FontFallbackParagraph(self._normalize(
|
||||
pgettext(
|
||||
'invoice', 'Using the conversion rate of 1:{rate} as published by the {authority} on '
|
||||
'{date}, the invoice total corresponds to {total}.'
|
||||
).format(rate=localize(self.invoice.foreign_currency_rate),
|
||||
date=date_format(self.invoice.foreign_currency_rate_date, "SHORT_DATE_FORMAT"),
|
||||
authority=SOURCE_NAMES.get(self.invoice.foreign_currency_source, "?"),
|
||||
total=fmt(foreign_total)),
|
||||
total=fmt(foreign_total))),
|
||||
self.stylesheet['Fineprint']
|
||||
))
|
||||
|
||||
@@ -1151,8 +1162,11 @@ class Modern1Renderer(ClassicInvoiceRenderer):
|
||||
def _draw_invoice_from(self, canvas):
|
||||
if not self.invoice.invoice_from:
|
||||
return
|
||||
c = self.invoice.address_invoice_from.strip().split('\n')
|
||||
p = PlainTextParagraph(' · '.join(c), style=self.stylesheet['Sender'])
|
||||
c = [
|
||||
self._clean_text(l)
|
||||
for l in self.invoice.address_invoice_from.strip().split('\n')
|
||||
]
|
||||
p = FontFallbackParagraph(self._normalize(' · '.join(c)), style=self.stylesheet['Sender'])
|
||||
p.wrapOn(canvas, self.invoice_to_width, 15.7 * mm)
|
||||
p.drawOn(canvas, self.invoice_to_left, self.pagesize[1] - self.invoice_to_top + 2 * mm)
|
||||
super()._draw_invoice_from(canvas)
|
||||
@@ -1211,8 +1225,8 @@ class Modern1Renderer(ClassicInvoiceRenderer):
|
||||
_draw(pgettext('invoice', 'Order code'), self.invoice.order.full_code, value_size, self.left_margin, 45 * mm, **kwargs)
|
||||
]
|
||||
|
||||
p = PlainTextParagraph(
|
||||
date_format(self.invoice.date, "DATE_FORMAT"),
|
||||
p = FontFallbackParagraph(
|
||||
self._normalize(date_format(self.invoice.date, "DATE_FORMAT")),
|
||||
style=ParagraphStyle(name=f'Normal{value_size}', fontName=self.font_regular, fontSize=value_size, leading=value_size * 1.2)
|
||||
)
|
||||
w = stringWidth(p.text, p.frags[0].fontName, p.frags[0].fontSize)
|
||||
@@ -1269,7 +1283,7 @@ class Modern1SimplifiedRenderer(Modern1Renderer):
|
||||
i = []
|
||||
|
||||
if not self.invoice.event.has_subevents and self.invoice.event.settings.show_dates_on_frontpage:
|
||||
i.append(PlainTextParagraph(
|
||||
i.append(FontFallbackParagraph(
|
||||
pgettext('invoice', 'Event date: {date_range}').format(
|
||||
date_range=self.invoice.event.get_date_range_display(),
|
||||
),
|
||||
|
||||
@@ -36,9 +36,8 @@ from django.core.management.commands.makemigrations import Command as Parent
|
||||
|
||||
from ._migrations import monkeypatch_migrations
|
||||
|
||||
monkeypatch_migrations()
|
||||
|
||||
|
||||
class Command(Parent):
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
monkeypatch_migrations()
|
||||
return super().handle(*args, **kwargs)
|
||||
pass
|
||||
|
||||
@@ -64,7 +64,7 @@ class Command(BaseCommand):
|
||||
if not periodic_task.receivers or periodic_task.sender_receivers_cache.get(self) is NO_RECEIVERS:
|
||||
return
|
||||
|
||||
for receiver in periodic_task._live_receivers(self)[0]:
|
||||
for receiver in periodic_task._live_receivers(self):
|
||||
name = f'{receiver.__module__}.{receiver.__name__}'
|
||||
if options['list_tasks']:
|
||||
print(name)
|
||||
|
||||
@@ -41,20 +41,16 @@ class Migration(migrations.Migration):
|
||||
name='datetime',
|
||||
field=models.DateTimeField(),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
'logentry',
|
||||
models.Index(fields=('datetime', 'id'), name="pretixbase__datetim_b1fe5a_idx"),
|
||||
migrations.AlterIndexTogether(
|
||||
name='logentry',
|
||||
index_together={('datetime', 'id')},
|
||||
),
|
||||
migrations.AddIndex(
|
||||
'order',
|
||||
models.Index(fields=["datetime", "id"], name="pretixbase__datetim_66aff0_idx"),
|
||||
migrations.AlterIndexTogether(
|
||||
name='order',
|
||||
index_together={('datetime', 'id'), ('last_modified', 'id')},
|
||||
),
|
||||
migrations.AddIndex(
|
||||
'order',
|
||||
models.Index(fields=["last_modified", "id"], name="pretixbase__last_mo_4ebf8b_idx"),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
'transaction',
|
||||
models.Index(fields=('datetime', 'id'), name="pretixbase__datetim_b20405_idx"),
|
||||
migrations.AlterIndexTogether(
|
||||
name='transaction',
|
||||
index_together={('datetime', 'id')},
|
||||
),
|
||||
]
|
||||
|
||||
@@ -61,10 +61,7 @@ class Migration(migrations.Migration):
|
||||
options={
|
||||
'ordering': ('identifier', 'type', 'organizer'),
|
||||
'unique_together': {('identifier', 'type', 'organizer')},
|
||||
'indexes': [
|
||||
models.Index(fields=('identifier', 'type', 'organizer'), name='reusable_medium_organizer_index'),
|
||||
models.Index(fields=('updated', 'id'), name="pretixbase__updated_093277_idx")
|
||||
],
|
||||
'index_together': {('identifier', 'type', 'organizer'), ('updated', 'id')},
|
||||
},
|
||||
bases=(models.Model, pretix.base.models.base.LoggingMixin),
|
||||
),
|
||||
|
||||
@@ -9,6 +9,31 @@ class Migration(migrations.Migration):
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameIndex(
|
||||
model_name="logentry",
|
||||
new_name="pretixbase__datetim_b1fe5a_idx",
|
||||
old_fields=("datetime", "id"),
|
||||
),
|
||||
migrations.RenameIndex(
|
||||
model_name="order",
|
||||
new_name="pretixbase__datetim_66aff0_idx",
|
||||
old_fields=("datetime", "id"),
|
||||
),
|
||||
migrations.RenameIndex(
|
||||
model_name="order",
|
||||
new_name="pretixbase__last_mo_4ebf8b_idx",
|
||||
old_fields=("last_modified", "id"),
|
||||
),
|
||||
migrations.RenameIndex(
|
||||
model_name="reusablemedium",
|
||||
new_name="pretixbase__updated_093277_idx",
|
||||
old_fields=("updated", "id"),
|
||||
),
|
||||
migrations.RenameIndex(
|
||||
model_name="transaction",
|
||||
new_name="pretixbase__datetim_b20405_idx",
|
||||
old_fields=("datetime", "id"),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="attendeeprofile",
|
||||
name="id",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Generated by Django 4.2.10 on 2024-04-02 15:16
|
||||
|
||||
from django.db import migrations, models
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
@@ -10,8 +10,8 @@ class Migration(migrations.Migration):
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveIndex(
|
||||
"reusablemedium",
|
||||
'reusable_medium_organizer_index',
|
||||
migrations.AlterIndexTogether(
|
||||
name="reusablemedium",
|
||||
index_together=set(),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -88,7 +88,7 @@ class LogEntry(models.Model):
|
||||
|
||||
class Meta:
|
||||
ordering = ('-datetime', '-id')
|
||||
indexes = [models.Index(fields=["datetime", "id"], name="pretixbase__datetim_b1fe5a_idx")]
|
||||
indexes = [models.Index(fields=["datetime", "id"])]
|
||||
|
||||
def display(self):
|
||||
from pretix.base.logentrytype_registry import log_entry_types
|
||||
|
||||
@@ -122,7 +122,7 @@ class ReusableMedium(LoggedModel):
|
||||
class Meta:
|
||||
unique_together = (("identifier", "type", "organizer"),)
|
||||
indexes = [
|
||||
models.Index(fields=("updated", "id"), name="pretixbase__updated_093277_idx"),
|
||||
models.Index(fields=("updated", "id")),
|
||||
]
|
||||
ordering = "identifier", "type", "organizer"
|
||||
|
||||
|
||||
@@ -336,8 +336,8 @@ class Order(LockModel, LoggedModel):
|
||||
verbose_name_plural = _("Orders")
|
||||
ordering = ("-datetime", "-pk")
|
||||
indexes = [
|
||||
models.Index(fields=["datetime", "id"], name="pretixbase__datetim_66aff0_idx"),
|
||||
models.Index(fields=["last_modified", "id"], name="pretixbase__last_mo_4ebf8b_idx"),
|
||||
models.Index(fields=["datetime", "id"]),
|
||||
models.Index(fields=["last_modified", "id"]),
|
||||
]
|
||||
constraints = [
|
||||
models.UniqueConstraint(fields=["organizer", "code"], name="order_organizer_code_uniq"),
|
||||
@@ -3080,7 +3080,7 @@ class Transaction(models.Model):
|
||||
class Meta:
|
||||
ordering = 'datetime', 'pk'
|
||||
indexes = [
|
||||
models.Index(fields=['datetime', 'id'], name="pretixbase__datetim_b20405_idx")
|
||||
models.Index(fields=['datetime', 'id'])
|
||||
]
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
@@ -1045,7 +1045,7 @@ class Renderer:
|
||||
except:
|
||||
logger.exception('Reshaping/Bidi fixes failed on string {}'.format(repr(text)))
|
||||
|
||||
p = Paragraph(text, style=style) # not using AutoEscapeParagraph is safe as we escape above
|
||||
p = Paragraph(text, style=style)
|
||||
return p, ad, lineheight
|
||||
|
||||
def _draw_textcontainer(self, canvas: Canvas, op: OrderPosition, order: Order, o: dict):
|
||||
|
||||
@@ -411,7 +411,7 @@ def mail_send_task(self, **kwargs) -> bool:
|
||||
try:
|
||||
outgoing_mail = OutgoingMail.objects.select_for_update(of=OF_SELF).get(pk=outgoing_mail)
|
||||
except OutgoingMail.DoesNotExist:
|
||||
logger.info(f"Ignoring job for non existing email {outgoing_mail}")
|
||||
logger.info(f"Ignoring job for non existing email {outgoing_mail.guid}")
|
||||
return False
|
||||
if outgoing_mail.status == OutgoingMail.STATUS_INFLIGHT:
|
||||
logger.info(f"Ignoring job for inflight email {outgoing_mail.guid}")
|
||||
|
||||
@@ -100,7 +100,7 @@ def primary_font_kwargs():
|
||||
|
||||
choices = [('Open Sans', 'Open Sans')]
|
||||
choices += sorted([
|
||||
(a, FontSelect.FontOption(title=a, data=v)) for a, v in get_fonts(pdf_support_required=False).items()
|
||||
(a, {"title": a, "data": v}) for a, v in get_fonts(pdf_support_required=False).items()
|
||||
], key=lambda a: a[0])
|
||||
return {
|
||||
'choices': choices,
|
||||
@@ -4148,14 +4148,6 @@ def validate_event_settings(event, settings_dict):
|
||||
)
|
||||
]}
|
||||
)
|
||||
if (
|
||||
settings_dict.get('invoice_address_from_vat_id') and
|
||||
settings_dict.get('invoice_address_from_country') and
|
||||
settings_dict.get('invoice_address_from_country') not in VAT_ID_COUNTRIES
|
||||
):
|
||||
raise ValidationError({
|
||||
'invoice_address_from_vat_id': _('VAT-ID is not supported for "{}".').format(settings_dict.get('invoice_address_from_country'))
|
||||
})
|
||||
|
||||
payment_term_last = settings_dict.get('payment_term_last')
|
||||
if payment_term_last and event.presale_end:
|
||||
|
||||
+19
-60
@@ -32,7 +32,6 @@
|
||||
# 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
|
||||
import warnings
|
||||
from typing import Any, Callable, Generic, List, Tuple, TypeVar
|
||||
|
||||
@@ -49,8 +48,6 @@ from .plugins import (
|
||||
PLUGIN_LEVEL_ORGANIZER,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
app_cache = {}
|
||||
T = TypeVar('T')
|
||||
|
||||
@@ -63,25 +60,23 @@ def _populate_app_cache():
|
||||
|
||||
def get_defining_app(o):
|
||||
# If sentry packed this in a wrapper, unpack that
|
||||
module = getattr(o, "__module__", None)
|
||||
if module and "sentry" in module:
|
||||
if "sentry" in o.__module__:
|
||||
o = o.__wrapped__
|
||||
|
||||
if hasattr(o, "__mocked_app"):
|
||||
return o.__mocked_app
|
||||
|
||||
# Find the Django application this belongs to
|
||||
searchpath = module or getattr(o.__class__, "__module__", None) or ""
|
||||
searchpath = o.__module__
|
||||
|
||||
# Core modules are always active
|
||||
if searchpath and any(searchpath.startswith(cm) for cm in settings.CORE_MODULES):
|
||||
if any(searchpath.startswith(cm) for cm in settings.CORE_MODULES):
|
||||
return 'CORE'
|
||||
|
||||
if not app_cache:
|
||||
_populate_app_cache()
|
||||
|
||||
app = None
|
||||
while searchpath:
|
||||
while True:
|
||||
app = app_cache.get(searchpath)
|
||||
if "." not in searchpath or app:
|
||||
break
|
||||
@@ -162,7 +157,7 @@ class PluginSignal(Generic[T], django.dispatch.Signal):
|
||||
if not app_cache:
|
||||
_populate_app_cache()
|
||||
|
||||
for receiver in self._live_receivers(sender)[0]:
|
||||
for receiver in self._sorted_receivers(sender):
|
||||
if self._is_receiver_active(sender, receiver):
|
||||
response = receiver(signal=self, sender=sender, **named)
|
||||
responses.append((receiver, response))
|
||||
@@ -184,7 +179,7 @@ class PluginSignal(Generic[T], django.dispatch.Signal):
|
||||
if not app_cache:
|
||||
_populate_app_cache()
|
||||
|
||||
for receiver in self._live_receivers(sender)[0]:
|
||||
for receiver in self._sorted_receivers(sender):
|
||||
if self._is_receiver_active(sender, receiver):
|
||||
named[chain_kwarg_name] = response
|
||||
response = receiver(signal=self, sender=sender, **named)
|
||||
@@ -209,7 +204,7 @@ class PluginSignal(Generic[T], django.dispatch.Signal):
|
||||
if not app_cache:
|
||||
_populate_app_cache()
|
||||
|
||||
for receiver in self._live_receivers(sender)[0]:
|
||||
for receiver in self._sorted_receivers(sender):
|
||||
if self._is_receiver_active(sender, receiver):
|
||||
try:
|
||||
response = receiver(signal=self, sender=sender, **named)
|
||||
@@ -219,35 +214,17 @@ class PluginSignal(Generic[T], django.dispatch.Signal):
|
||||
responses.append((receiver, response))
|
||||
return responses
|
||||
|
||||
def asend(self, sender: T, **named):
|
||||
raise NotImplementedError() # NOQA
|
||||
|
||||
def asend_robust(self, sender: T, **named):
|
||||
raise NotImplementedError() # NOQA
|
||||
|
||||
def _live_receivers(self, sender):
|
||||
orig_list, orig_async_list = super()._live_receivers(sender)
|
||||
|
||||
if orig_async_list:
|
||||
logger.error('Async receivers are not supported.')
|
||||
raise NotImplementedError
|
||||
|
||||
def _getattr_fallback_to_class(obj, key):
|
||||
return getattr(obj, key, getattr(obj.__class__, key))
|
||||
|
||||
def _is_core_module(receiver):
|
||||
m = _getattr_fallback_to_class(receiver, "__module__")
|
||||
return any(m.startswith(c) for c in settings.CORE_MODULES)
|
||||
|
||||
def _sorted_receivers(self, sender):
|
||||
orig_list = self._live_receivers(sender)
|
||||
sorted_list = sorted(
|
||||
orig_list,
|
||||
key=lambda receiver: (
|
||||
0 if _is_core_module(receiver) else 1,
|
||||
_getattr_fallback_to_class(receiver, "__module__"),
|
||||
_getattr_fallback_to_class(receiver, "__name__"),
|
||||
0 if any(receiver.__module__.startswith(m) for m in settings.CORE_MODULES) else 1,
|
||||
receiver.__module__,
|
||||
receiver.__name__,
|
||||
)
|
||||
)
|
||||
return sorted_list, []
|
||||
return sorted_list
|
||||
|
||||
|
||||
class EventPluginSignal(PluginSignal[Event]):
|
||||
@@ -323,41 +300,23 @@ class GlobalSignal(django.dispatch.Signal):
|
||||
if not self.receivers or self.sender_receivers_cache.get(sender) is NO_RECEIVERS:
|
||||
return response
|
||||
|
||||
for receiver in self._live_receivers(sender)[0]:
|
||||
for receiver in self._live_receivers(sender):
|
||||
named[chain_kwarg_name] = response
|
||||
response = receiver(signal=self, sender=sender, **named)
|
||||
return response
|
||||
|
||||
def asend(self, sender: T, **named):
|
||||
raise NotImplementedError() # NOQA
|
||||
|
||||
def asend_robust(self, sender: T, **named):
|
||||
raise NotImplementedError() # NOQA
|
||||
|
||||
def _live_receivers(self, sender):
|
||||
# Ensure consistent sorting of receivers
|
||||
orig_list, orig_async_list = super()._live_receivers(sender)
|
||||
|
||||
if orig_async_list:
|
||||
logger.error('Async receivers are not supported.')
|
||||
raise NotImplementedError
|
||||
|
||||
def _getattr_fallback_to_class(obj, key):
|
||||
return getattr(obj, key, getattr(obj.__class__, key))
|
||||
|
||||
def _is_core_module(receiver):
|
||||
m = _getattr_fallback_to_class(receiver, "__module__")
|
||||
return any(m.startswith(c) for c in settings.CORE_MODULES)
|
||||
|
||||
orig_list = super()._live_receivers(sender)
|
||||
sorted_list = sorted(
|
||||
orig_list,
|
||||
key=lambda receiver: (
|
||||
0 if _is_core_module(receiver) else 1,
|
||||
_getattr_fallback_to_class(receiver, "__module__"),
|
||||
_getattr_fallback_to_class(receiver, "__name__"),
|
||||
0 if any(receiver.__module__.startswith(m) for m in settings.CORE_MODULES) else 1,
|
||||
receiver.__module__,
|
||||
receiver.__name__,
|
||||
)
|
||||
)
|
||||
return sorted_list, []
|
||||
return sorted_list
|
||||
|
||||
|
||||
class DeprecatedSignal(GlobalSignal):
|
||||
|
||||
@@ -2,14 +2,13 @@
|
||||
{% load i18n %}
|
||||
{% load rich_text %}
|
||||
{% load static %}
|
||||
{% load wrap_in %}
|
||||
{% block title %}{% trans "Redirect" %}{% endblock %}
|
||||
{% block content %}
|
||||
<i class="fa fa-link fa-fw big-icon"></i>
|
||||
<div class="error-details">
|
||||
<h1>{% trans "Redirect" %}</h1>
|
||||
<h3>
|
||||
{% blocktrans trimmed with host=hostname|wrap_in:'strong' %}
|
||||
{% blocktrans trimmed with host="<strong>"|add:hostname|add:"</strong>"|safe %}
|
||||
The link you clicked on wants to redirect you to a destination on the website {{ host }}.
|
||||
{% endblocktrans %}
|
||||
{% blocktrans trimmed %}
|
||||
|
||||
@@ -34,7 +34,6 @@
|
||||
|
||||
import datetime
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
@@ -421,11 +420,6 @@ class SplitDateTimeField(forms.SplitDateTimeField):
|
||||
class FontSelect(forms.RadioSelect):
|
||||
option_template_name = 'pretixcontrol/font_option.html'
|
||||
|
||||
@dataclass
|
||||
class FontOption:
|
||||
title: str
|
||||
data: str
|
||||
|
||||
|
||||
class ItemMultipleChoiceField(SafeModelMultipleChoiceField):
|
||||
def label_from_instance(self, obj):
|
||||
|
||||
@@ -63,7 +63,7 @@ from pretix.base.forms import (
|
||||
from pretix.base.models import Event, Organizer, TaxRule, Team
|
||||
from pretix.base.models.event import EventFooterLink, EventMetaValue, SubEvent
|
||||
from pretix.base.models.organizer import TeamQuerySet
|
||||
from pretix.base.models.tax import TAX_CODE_LISTS, VAT_ID_COUNTRIES
|
||||
from pretix.base.models.tax import TAX_CODE_LISTS
|
||||
from pretix.base.reldate import RelativeDateField, RelativeDateTimeField
|
||||
from pretix.base.services.placeholders import FormPlaceholderMixin
|
||||
from pretix.base.settings import (
|
||||
@@ -73,8 +73,8 @@ from pretix.base.settings import (
|
||||
)
|
||||
from pretix.base.validators import multimail_validate
|
||||
from pretix.control.forms import (
|
||||
FontSelect, MultipleLanguagesWidget, SalesChannelCheckboxSelectMultiple,
|
||||
SlugWidget, SplitDateTimeField, SplitDateTimePickerWidget,
|
||||
MultipleLanguagesWidget, SalesChannelCheckboxSelectMultiple, SlugWidget,
|
||||
SplitDateTimeField, SplitDateTimePickerWidget,
|
||||
)
|
||||
from pretix.control.forms.widgets import Select2
|
||||
from pretix.helpers.countries import CachedCountries
|
||||
@@ -531,13 +531,6 @@ class EventUpdateForm(I18nModelForm):
|
||||
|
||||
class EventSettingsValidationMixin:
|
||||
|
||||
def clean_invoice_address_from_vat_id(self):
|
||||
value = self.cleaned_data.get('invoice_address_from_vat_id')
|
||||
country = self.cleaned_data.get('invoice_address_from_country')
|
||||
if value and country and country not in VAT_ID_COUNTRIES:
|
||||
return None
|
||||
return value
|
||||
|
||||
def clean(self):
|
||||
data = super().clean()
|
||||
settings_dict = self.obj.settings.freeze()
|
||||
@@ -729,7 +722,7 @@ class EventSettingsForm(EventSettingsValidationMixin, FormPlaceholderMixin, Sett
|
||||
del self.fields['event_list_filters']
|
||||
del self.fields['event_calendar_future_only']
|
||||
self.fields['primary_font'].choices = [('Open Sans', 'Open Sans')] + sorted([
|
||||
(a, FontSelect.FontOption(title=a, data=v)) for a, v in get_fonts(self.event, pdf_support_required=False).items()
|
||||
(a, {"title": a, "data": v}) for a, v in get_fonts(self.event, pdf_support_required=False).items()
|
||||
], key=lambda a: a[0])
|
||||
|
||||
# create "virtual" fields for better UX when editing <name>_asked and <name>_required fields
|
||||
|
||||
@@ -363,7 +363,7 @@ def get_global_navigation(request):
|
||||
'icon': 'dashboard',
|
||||
},
|
||||
]
|
||||
if request.user.is_in_any_teams or request.user.is_staff:
|
||||
if request.user.is_in_any_teams:
|
||||
nav += [
|
||||
{
|
||||
'label': _('Events'),
|
||||
|
||||
@@ -38,7 +38,6 @@ from pretix import __version__
|
||||
from pretix.base.models import Order, OrderPayment, Transaction
|
||||
from pretix.base.plugins import get_all_plugins
|
||||
from pretix.base.templatetags.money import money_filter
|
||||
from pretix.helpers.reportlab import PlainTextParagraph
|
||||
from pretix.plugins.reports.exporters import ReportlabExportMixin
|
||||
from pretix.settings import DATA_DIR
|
||||
|
||||
@@ -80,23 +79,23 @@ class SysReport(ReportlabExportMixin):
|
||||
style_small.fontSize = 6
|
||||
|
||||
story = [
|
||||
PlainTextParagraph("System report", headlinestyle),
|
||||
Paragraph("System report", headlinestyle),
|
||||
Spacer(1, 5 * mm),
|
||||
PlainTextParagraph("Usage", subheadlinestyle),
|
||||
Paragraph("Usage", subheadlinestyle),
|
||||
Spacer(1, 5 * mm),
|
||||
self._usage_table(),
|
||||
Spacer(1, 5 * mm),
|
||||
PlainTextParagraph("Installed versions", subheadlinestyle),
|
||||
Paragraph("Installed versions", subheadlinestyle),
|
||||
Spacer(1, 5 * mm),
|
||||
self._tech_table(),
|
||||
Spacer(1, 5 * mm),
|
||||
PlainTextParagraph("Plugins", subheadlinestyle),
|
||||
Paragraph("Plugins", subheadlinestyle),
|
||||
Spacer(1, 5 * mm),
|
||||
PlainTextParagraph(self._get_plugin_versions(), style_small),
|
||||
Paragraph(self._get_plugin_versions(), style_small),
|
||||
Spacer(1, 5 * mm),
|
||||
PlainTextParagraph("Custom templates", subheadlinestyle),
|
||||
Paragraph("Custom templates", subheadlinestyle),
|
||||
Spacer(1, 5 * mm),
|
||||
PlainTextParagraph(self._get_custom_templates(), style_small),
|
||||
Paragraph(self._get_custom_templates(), style_small),
|
||||
Spacer(1, 5 * mm),
|
||||
]
|
||||
|
||||
@@ -122,13 +121,13 @@ class SysReport(ReportlabExportMixin):
|
||||
("RIGHTPADDING", (-1, 0), (-1, -1), 0),
|
||||
]
|
||||
tdata = [
|
||||
[PlainTextParagraph("Site URL:", style), Paragraph(settings.SITE_URL, style)],
|
||||
[PlainTextParagraph("pretix version:", style), Paragraph(__version__, style)],
|
||||
[PlainTextParagraph("Python version:", style), Paragraph(sys.version, style)],
|
||||
[PlainTextParagraph("Platform:", style), Paragraph(platform.platform(), style)],
|
||||
[Paragraph("Site URL:", style), Paragraph(settings.SITE_URL, style)],
|
||||
[Paragraph("pretix version:", style), Paragraph(__version__, style)],
|
||||
[Paragraph("Python version:", style), Paragraph(sys.version, style)],
|
||||
[Paragraph("Platform:", style), Paragraph(platform.platform(), style)],
|
||||
[
|
||||
PlainTextParagraph("Database engine:", style),
|
||||
PlainTextParagraph(settings.DATABASES["default"]["ENGINE"], style),
|
||||
Paragraph("Database engine:", style),
|
||||
Paragraph(settings.DATABASES["default"]["ENGINE"], style),
|
||||
],
|
||||
]
|
||||
table = Table(tdata, colWidths=colwidths, repeatRows=0)
|
||||
@@ -207,7 +206,7 @@ class SysReport(ReportlabExportMixin):
|
||||
year_last = now().year
|
||||
tdata = [
|
||||
[
|
||||
PlainTextParagraph(l, style_small_head)
|
||||
Paragraph(l, style_small_head)
|
||||
for l in (
|
||||
"Time frame",
|
||||
"Currency",
|
||||
@@ -258,19 +257,19 @@ class SysReport(ReportlabExportMixin):
|
||||
|
||||
tdata.append(
|
||||
(
|
||||
PlainTextParagraph(
|
||||
Paragraph(
|
||||
date_format(first_day, "M Y")
|
||||
+ " – "
|
||||
+ date_format(after_day - timedelta(days=1), "M Y"),
|
||||
style_small,
|
||||
),
|
||||
PlainTextParagraph(c, style_small),
|
||||
PlainTextParagraph(str(orders_count), style_small) if i == 0 else "",
|
||||
PlainTextParagraph(money_filter(revenue_data.get("s_net") or 0, c), style_small),
|
||||
PlainTextParagraph(str(testmode_count), style_small) if i == 0 else "",
|
||||
PlainTextParagraph(str(unconfirmed_count), style_small) if i == 0 else "",
|
||||
PlainTextParagraph(str(revenue_data.get("c") or 0), style_small),
|
||||
PlainTextParagraph(money_filter(revenue_data.get("s_gross") or 0, c), style_small),
|
||||
Paragraph(c, style_small),
|
||||
Paragraph(str(orders_count), style_small) if i == 0 else "",
|
||||
Paragraph(money_filter(revenue_data.get("s_net") or 0, c), style_small),
|
||||
Paragraph(str(testmode_count), style_small) if i == 0 else "",
|
||||
Paragraph(str(unconfirmed_count), style_small) if i == 0 else "",
|
||||
Paragraph(str(revenue_data.get("c") or 0), style_small),
|
||||
Paragraph(money_filter(revenue_data.get("s_gross") or 0, c), style_small),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -19,7 +19,9 @@
|
||||
{% endif %}
|
||||
</h1>
|
||||
|
||||
{{ layout|json_script:"editor-data" }}
|
||||
<script type="application/json" id="editor-data">
|
||||
{{ layout|safe }}
|
||||
</script>
|
||||
<div class="row">
|
||||
<div class="col-md-9">
|
||||
<div class="panel panel-default panel-pdf-editor">
|
||||
|
||||
@@ -6,44 +6,35 @@
|
||||
<h1>{% trans "Add a two-factor authentication device" %}</h1>
|
||||
<form action="" method="post" class="form-horizontal">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="flow_token" value="{{ flow_token }}">
|
||||
{% bootstrap_form_errors form %}
|
||||
{% bootstrap_field form.name layout='horizontal' %}
|
||||
|
||||
<div class="form-group{% if form.devicetype.errors %} has-error{% endif %}">
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label">{% trans "Device type" %}</label>
|
||||
<div class="col-md-9">
|
||||
<div>
|
||||
<div class="big-radio radio">
|
||||
<label>
|
||||
<input type="radio" required value="totp" name="{{ form.devicetype.html_name }}" {% if form.devicetype.value == "totp" %}checked{% endif %}>
|
||||
<strong>{% trans "Smartphone with Authenticator app" %}</strong><br>
|
||||
<div class="help-block">
|
||||
{% blocktrans trimmed %}
|
||||
Use your smartphone with any Time-based One-Time-Password app like freeOTP, Google Authenticator or Proton Authenticator.
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div class="big-radio radio">
|
||||
<label>
|
||||
<input type="radio" required value="webauthn" name="{{ form.devicetype.html_name }}" {% if form.devicetype.value == "webauthn" %}checked{% endif %}>
|
||||
<strong>{% trans "WebAuthn-compatible hardware token" %}</strong><br>
|
||||
<div class="help-block">
|
||||
{% blocktrans trimmed %}
|
||||
Use a hardware token like the Yubikey, or other biometric authentication like fingerprint or face recognition.
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div class="big-radio radio">
|
||||
<label>
|
||||
<input type="radio" value="totp" name="{{ form.devicetype.html_name }}" {% if form.devicetype.value == "totp" %}checked{% endif %}>
|
||||
<strong>{% trans "Smartphone with the Authenticator application" %}</strong><br>
|
||||
<div class="help-block">
|
||||
{% blocktrans trimmed %}
|
||||
Use your smartphone with any Time-based One-Time-Password app like freeOTP, Google Authenticator or Proton Authenticator.
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div class="big-radio radio">
|
||||
<label>
|
||||
<input type="radio" value="webauthn" name="{{ form.devicetype.html_name }}" {% if form.devicetype.value == "webauthn" %}checked{% endif %}>
|
||||
<strong>{% trans "WebAuthn-compatible hardware token" %}</strong><br>
|
||||
<div class="help-block">
|
||||
{% blocktrans trimmed %}
|
||||
Use a hardware token like the Yubikey, or biometric authentication on iOS, macOS and Android.
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{% if form.devicetype.errors %}
|
||||
<div class="help-block">
|
||||
{% for error in form.devicetype.errors %}
|
||||
<p>{{ error|escape }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -69,14 +69,11 @@
|
||||
{% trans "Enter the displayed code here:" %}
|
||||
<form class="form form-inline" method="post" action="">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="flow_token" value="{{ flow_token }}">
|
||||
<input type="number" name="token" class="form-control" required="required">
|
||||
<button class="btn btn-primary" type="submit">
|
||||
{% trans "Continue" %}
|
||||
</button><br>
|
||||
<label>
|
||||
<input type="checkbox" name="activate" checked="checked" value="on">
|
||||
{% trans "Require second factor for future logins" %}
|
||||
</label>
|
||||
</form>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
@@ -12,13 +12,9 @@
|
||||
</p>
|
||||
<form class="form form-inline" method="post" action="" id="webauthn-form">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="flow_token" value="{{ flow_token }}">
|
||||
<input type="hidden" id="webauthn-response" name="token" class="form-control" required="required">
|
||||
<p>
|
||||
<label>
|
||||
<input type="checkbox" name="activate" checked="checked" value="on">
|
||||
{% trans "Require second factor for future logins" %}
|
||||
</label>
|
||||
</p>
|
||||
|
||||
<button class="btn btn-primary sr-only" type="submit"></button>
|
||||
</form>
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
<h1>{% trans "Delete a two-factor authentication device" %}</h1>
|
||||
<form action="" method="post" class="form-horizontal">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="flow_token" value="{{ flow_token }}">
|
||||
<p>{% blocktrans trimmed with device=device.name %}
|
||||
Are you sure you want to delete the authentication device "{{ device }}"?
|
||||
{% endblocktrans %}</p>
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
<h1>{% trans "Disable two-factor authentication" %}</h1>
|
||||
<form action="" method="post" class="form-horizontal">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="flow_token" value="{{ flow_token }}">
|
||||
<p>
|
||||
{% trans "Do you really want to disable two-factor authentication?" %}
|
||||
</p>
|
||||
|
||||
@@ -1,23 +1,58 @@
|
||||
{% extends "pretixcontrol/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% load icon %}
|
||||
{% block title %}{% trans "Enable two-factor authentication" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h1>{% trans "Enable two-factor authentication" %}</h1>
|
||||
<form action="" method="post" class="form-horizontal">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="flow_token" value="{{ flow_token }}">
|
||||
<p>
|
||||
{% trans "Do you really want to enable two-factor authentication?" %}
|
||||
</p>
|
||||
<p>
|
||||
{% trans "You will no longer be able to log in to pretix without one of your configured devices." %}
|
||||
{% trans "Please make sure to print out or copy the emergency tokens and store them in a safe place." %}
|
||||
</p>
|
||||
{% if new_emergency_tokens %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">{% trans "Your emergency codes" %}</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
If you lose access to your devices, you can use one of your emergency tokens to log in.
|
||||
We recommend to store them in a safe place, e.g. printed out or in a password manager.
|
||||
Every token can be used at most once.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<ul>
|
||||
{% for code in new_emergency_tokens %}
|
||||
<li>{{ code }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<p>
|
||||
<label>
|
||||
<input type="checkbox" required>
|
||||
{% trans "I stored my emergency tokens in a safe place." %}
|
||||
</label>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<p>
|
||||
{% icon "info-circle" %}
|
||||
{% blocktrans trimmed with generation_date_time=static_tokens_device.created_at %}
|
||||
You generated your emergency tokens on {{ generation_date_time }}.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
{% endif %}
|
||||
<div class="form-group submit-group">
|
||||
<a href="{% url "control:user.settings.2fa" %}" class="btn btn-default btn-cancel">
|
||||
{% trans "Cancel" %}
|
||||
</a>
|
||||
<button type="submit" class="btn btn-danger btn-save">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Enable" %}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
<h1>{% trans "Leave teams that require two-factor authentication" %}</h1>
|
||||
<form action="" method="post" class="form-horizontal">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="flow_token" value="{{ flow_token }}">
|
||||
<p>
|
||||
<strong>{% trans "Do you really want to leave the following teams?" %}</strong>
|
||||
</p>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{% extends "pretixcontrol/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load icon %}
|
||||
{% load bootstrap3 %}
|
||||
{% block title %}{% trans "Two-factor authentication" %}{% endblock %}
|
||||
{% block content %}
|
||||
@@ -120,7 +121,7 @@
|
||||
Delete
|
||||
</a>
|
||||
{% if d.devicetype == "totp" %}
|
||||
<span class="fa fa-mobile"></span>
|
||||
<span class="fa fa-mobile fa-lg"></span>
|
||||
{% elif d.devicetype == "webauthn" %}
|
||||
<span class="fa fa-usb"></span>
|
||||
{% elif d.devicetype == "u2f" %}
|
||||
@@ -152,19 +153,30 @@
|
||||
</p>
|
||||
{% if static_tokens_device %}
|
||||
<p>
|
||||
{% icon "info-circle" %}
|
||||
{% blocktrans trimmed with generation_date_time=static_tokens_device.created_at %}
|
||||
You generated your emergency tokens on {{ generation_date_time }}.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
{% else %}
|
||||
<a href="{% url "control:user.settings.2fa.regenemergency" %}" class="btn btn-default">
|
||||
<span class="fa fa-refresh"></span>
|
||||
{% trans "Generate new emergency tokens" %}
|
||||
</a>
|
||||
{% elif user.require_2fa %}
|
||||
<p>
|
||||
{% trans "You don't have any emergency tokens yet." %}
|
||||
{% icon "warning" %}
|
||||
<strong>{% trans "You don't have any emergency tokens yet." %}</strong>
|
||||
</p>
|
||||
<a href="{% url "control:user.settings.2fa.regenemergency" %}" class="btn btn-default">
|
||||
<span class="fa fa-refresh"></span>
|
||||
{% trans "Generate emergency tokens" %}
|
||||
</a>
|
||||
{% else %}
|
||||
<p class="help-block">
|
||||
{% icon "info-circle" %}
|
||||
{% trans "Emergency tokens will be generated when you enable two-factor authentication." %}
|
||||
</p>
|
||||
{% endif %}
|
||||
<a href="{% url "control:user.settings.2fa.regenemergency" %}" class="btn btn-default">
|
||||
<span class="fa fa-refresh"></span>
|
||||
{% trans "Generate new emergency tokens" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
<h1>{% trans "Regenerate emergency codes" %}</h1>
|
||||
<form action="" method="post" class="form-horizontal">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="flow_token" value="{{ flow_token }}">
|
||||
<p>
|
||||
{% trans "Do you really want to regenerate your emergency codes?" %}
|
||||
</p>
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
{% trans "Change login email address" %}
|
||||
</h1>
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="flow_token" value="{{ flow_token }}">
|
||||
{% bootstrap_form_errors form %}
|
||||
<p class="text-muted">
|
||||
{% trans "This changes the email address used to login to your account, as well as where we send email notifications." %}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
</h1>
|
||||
<br>
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="flow_token" value="{{ flow_token }}">
|
||||
{% bootstrap_form_errors form %}
|
||||
{% bootstrap_field form.email %}
|
||||
{% bootstrap_field form.old_pw %}
|
||||
|
||||
@@ -641,7 +641,7 @@ def user_index(request):
|
||||
|
||||
ctx = {
|
||||
'widgets': rearrange(widgets),
|
||||
'can_create_event': request.user.teams.with_organizer_permission("organizer.events:create").exists() or request.user.is_staff,
|
||||
'can_create_event': request.user.teams.with_organizer_permission("organizer.events:create").exists(),
|
||||
'upcoming': widgets_for_event_qs(
|
||||
request,
|
||||
annotated_event_query(request, lazy=True).filter(
|
||||
|
||||
@@ -289,7 +289,7 @@ class BaseEditorView(EventPermissionRequiredMixin, TemplateView):
|
||||
ctx['pdf'] = self.get_current_background()
|
||||
ctx['variables'] = self.get_variables()
|
||||
ctx['images'] = self.get_images()
|
||||
ctx['layout'] = self.get_current_layout()
|
||||
ctx['layout'] = json.dumps(self.get_current_layout())
|
||||
ctx['title'] = self.title
|
||||
ctx['locales'] = [p for p in settings.LANGUAGES if p[0] in self.request.event.settings.locales]
|
||||
ctx['maxfilesize'] = self.maxfilesize
|
||||
|
||||
@@ -89,13 +89,31 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
class RecentAuthenticationRequiredMixin:
|
||||
max_time = 900
|
||||
max_form_time = 900
|
||||
|
||||
@method_decorator(never_cache)
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
tdelta = time.time() - request.session.get('pretix_auth_login_time', 0)
|
||||
if tdelta > self.max_time:
|
||||
auth_is_recent = time.time() - request.session.get('pretix_auth_login_time', 0) < self.max_time
|
||||
allowed_by_token = (
|
||||
request.session.pop('pretix_reauthed_flow_token', None) == request.POST.get('flow_token', '')
|
||||
and request.session.pop('pretix_reauthed_flow_allowed_url', None) == request.get_full_path()
|
||||
and time.time() - request.session.pop('pretix_reauthed_flow_start_time', 0) < self.max_form_time
|
||||
)
|
||||
if auth_is_recent or allowed_by_token:
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
else:
|
||||
return redirect(reverse('control:user.reauth') + '?next=' + quote(request.get_full_path()))
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_flow_token(self):
|
||||
self.request.session['pretix_reauthed_flow_allowed_url'] = self.request.get_full_path()
|
||||
self.request.session['pretix_reauthed_flow_token'] = get_random_string(22)
|
||||
self.request.session['pretix_reauthed_flow_start_time'] = time.time()
|
||||
return self.request.session['pretix_reauthed_flow_token']
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data()
|
||||
ctx['flow_token'] = self.get_flow_token()
|
||||
return ctx
|
||||
|
||||
|
||||
class ReauthView(TemplateView):
|
||||
@@ -283,6 +301,7 @@ class UserHistoryView(ListView):
|
||||
|
||||
|
||||
class User2FAMainView(RecentAuthenticationRequiredMixin, TemplateView):
|
||||
max_time = 7200
|
||||
template_name = 'pretixcontrol/user/2fa_main.html'
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
@@ -465,25 +484,15 @@ class User2FADeviceConfirmWebAuthnView(RecentAuthenticationRequiredMixin, Templa
|
||||
notices = [
|
||||
_('A new two-factor authentication device has been added to your account.')
|
||||
]
|
||||
activate = request.POST.get('activate', '')
|
||||
if activate == 'on' and not self.request.user.require_2fa:
|
||||
self.request.user.require_2fa = True
|
||||
self.request.user.save()
|
||||
self.request.user.log_action('pretix.user.settings.2fa.enabled', user=self.request.user)
|
||||
notices.append(
|
||||
_('Two-factor authentication has been enabled.')
|
||||
)
|
||||
self.request.user.send_security_notice(notices)
|
||||
self.request.user.update_session_token()
|
||||
update_session_auth_hash(self.request, self.request.user)
|
||||
|
||||
note = ''
|
||||
if not self.request.user.require_2fa:
|
||||
note = ' ' + str(_('Please note that you still need to enable two-factor authentication for your '
|
||||
'account using the buttons below to make a second factor required for logging '
|
||||
'into your account.'))
|
||||
messages.success(request, str(_('The device has been verified and can now be used.')) + note)
|
||||
return redirect(reverse('control:user.settings.2fa'))
|
||||
messages.success(request, str(_('The device has been verified and can now be used.')))
|
||||
if self.request.user.require_2fa:
|
||||
return redirect(reverse('control:user.settings.2fa'))
|
||||
else:
|
||||
return redirect(reverse('control:user.settings.2fa.enable'))
|
||||
except Exception:
|
||||
messages.error(request, _('The registration could not be completed. Please try again.'))
|
||||
logger.exception('WebAuthn registration failed')
|
||||
@@ -494,6 +503,7 @@ class User2FADeviceConfirmWebAuthnView(RecentAuthenticationRequiredMixin, Templa
|
||||
|
||||
class User2FADeviceConfirmTOTPView(RecentAuthenticationRequiredMixin, TemplateView):
|
||||
template_name = 'pretixcontrol/user/2fa_confirm_totp.html'
|
||||
max_form_time = 7200 # this should have effectively no timeout, as the user might need to download the 2fa app first
|
||||
|
||||
@cached_property
|
||||
def device(self):
|
||||
@@ -514,7 +524,6 @@ class User2FADeviceConfirmTOTPView(RecentAuthenticationRequiredMixin, TemplateVi
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
token = request.POST.get('token', '')
|
||||
activate = request.POST.get('activate', '')
|
||||
if self.device.verify_token(token):
|
||||
self.device.confirmed = True
|
||||
self.device.save()
|
||||
@@ -526,24 +535,15 @@ class User2FADeviceConfirmTOTPView(RecentAuthenticationRequiredMixin, TemplateVi
|
||||
notices = [
|
||||
_('A new two-factor authentication device has been added to your account.')
|
||||
]
|
||||
if activate == 'on' and not self.request.user.require_2fa:
|
||||
self.request.user.require_2fa = True
|
||||
self.request.user.save()
|
||||
self.request.user.log_action('pretix.user.settings.2fa.enabled', user=self.request.user)
|
||||
notices.append(
|
||||
_('Two-factor authentication has been enabled.')
|
||||
)
|
||||
self.request.user.send_security_notice(notices)
|
||||
self.request.user.update_session_token()
|
||||
update_session_auth_hash(self.request, self.request.user)
|
||||
|
||||
note = ''
|
||||
if not self.request.user.require_2fa:
|
||||
note = ' ' + str(_('Please note that you still need to enable two-factor authentication for your '
|
||||
'account using the buttons below to make a second factor required for logging '
|
||||
'into your account.'))
|
||||
messages.success(request, str(_('The device has been verified and can now be used.')) + note)
|
||||
return redirect(reverse('control:user.settings.2fa'))
|
||||
messages.success(request, str(_('The device has been verified and can now be used.')))
|
||||
if self.request.user.require_2fa:
|
||||
return redirect(reverse('control:user.settings.2fa'))
|
||||
else:
|
||||
return redirect(reverse('control:user.settings.2fa.enable'))
|
||||
else:
|
||||
messages.error(request, _('The code you entered was not valid. If this problem persists, please check '
|
||||
'that the date and time of your phone are configured correctly.'))
|
||||
@@ -576,6 +576,7 @@ class User2FALeaveTeamsView(RecentAuthenticationRequiredMixin, TemplateView):
|
||||
|
||||
class User2FAEnableView(RecentAuthenticationRequiredMixin, TemplateView):
|
||||
template_name = 'pretixcontrol/user/2fa_enable.html'
|
||||
max_form_time = 7200 # this should have effectively no timeout, as the user might take some time to print out their emergency codes, and they would become invalid in case of a timeout
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if not any(dt.objects.filter(user=self.request.user, confirmed=True) for dt in REAL_DEVICE_TYPES):
|
||||
@@ -584,14 +585,39 @@ class User2FAEnableView(RecentAuthenticationRequiredMixin, TemplateView):
|
||||
return redirect(reverse('control:user.settings.2fa'))
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
new_tokens = None
|
||||
try:
|
||||
static_tokens_device = StaticDevice.objects.get(user=self.request.user, name='emergency')
|
||||
except StaticDevice.MultipleObjectsReturned:
|
||||
static_tokens_device = StaticDevice.objects.filter(
|
||||
user=self.request.user, name='emergency'
|
||||
).first()
|
||||
except StaticDevice.DoesNotExist:
|
||||
static_tokens_device = None
|
||||
|
||||
new_tokens = [get_random_string(length=12, allowed_chars='1234567890') for _ in range(10)]
|
||||
request.session['pretix_2fa_new_emergency_tokens'] = new_tokens
|
||||
return super().get(request, *args, new_emergency_tokens=new_tokens, static_tokens_device=static_tokens_device, **kwargs)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
notices = [
|
||||
_('Two-factor authentication has been enabled.')
|
||||
]
|
||||
if 'pretix_2fa_new_emergency_tokens' in request.session:
|
||||
d = StaticDevice.objects.create(user=self.request.user, name='emergency')
|
||||
for code in request.session['pretix_2fa_new_emergency_tokens']:
|
||||
d.token_set.create(token=code)
|
||||
self.request.user.log_action('pretix.user.settings.2fa.regenemergency', user=self.request.user)
|
||||
notices += [
|
||||
_('Your two-factor emergency codes have been regenerated.')
|
||||
]
|
||||
del request.session['pretix_2fa_new_emergency_tokens']
|
||||
self.request.user.require_2fa = True
|
||||
self.request.user.save()
|
||||
self.request.user.log_action('pretix.user.settings.2fa.enabled', user=self.request.user)
|
||||
messages.success(request, _('Two-factor authentication is now enabled for your account.'))
|
||||
self.request.user.send_security_notice([
|
||||
_('Two-factor authentication has been enabled.')
|
||||
])
|
||||
self.request.user.send_security_notice(notices)
|
||||
self.request.user.update_session_token()
|
||||
update_session_auth_hash(self.request, self.request.user)
|
||||
return redirect(reverse('control:user.settings.2fa'))
|
||||
|
||||
@@ -29,8 +29,3 @@ class PretixHelpersConfig(AppConfig):
|
||||
def ready(self):
|
||||
from .monkeypatching import monkeypatch_all_at_ready
|
||||
monkeypatch_all_at_ready()
|
||||
|
||||
# Ensure reportlab does not make any calls to the internet or the local disk
|
||||
from reportlab import rl_config
|
||||
rl_config.trustedHosts = []
|
||||
rl_config.trustedSchemes = ['data']
|
||||
|
||||
@@ -24,11 +24,8 @@ from datetime import datetime
|
||||
from http import cookies
|
||||
|
||||
from PIL import Image
|
||||
from django.core.exceptions import SuspiciousFileOperation
|
||||
from requests.adapters import HTTPAdapter
|
||||
|
||||
from pretix.helpers.reportlab import ThumbnailingImageReader
|
||||
|
||||
|
||||
def monkeypatch_vobject_performance():
|
||||
"""
|
||||
@@ -98,26 +95,8 @@ def monkeypatch_cookie_morsel():
|
||||
cookies.Morsel._reserved.setdefault("partitioned", "Partitioned")
|
||||
|
||||
|
||||
def monkeypatch_reportlab_imagereader():
|
||||
from reportlab.lib import utils
|
||||
old_init = utils.ImageReader.__init__
|
||||
|
||||
def new_init(self, fileName, ident=None): # noqa
|
||||
if not isinstance(fileName, Image.Image) and not hasattr(fileName, 'read') and not hasattr(fileName, 'str'):
|
||||
if not isinstance(self, ThumbnailingImageReader):
|
||||
# ThumbnailingImageReader is only used by us explicitly and not by using <img> in html, so it is safe
|
||||
raise SuspiciousFileOperation("reportlab should not be reading images from disk")
|
||||
|
||||
return types.MethodType(old_init, self)(
|
||||
fileName, ident
|
||||
)
|
||||
|
||||
utils.ImageReader.__init__ = new_init
|
||||
|
||||
|
||||
def monkeypatch_all_at_ready():
|
||||
monkeypatch_vobject_performance()
|
||||
monkeypatch_pillow_safer()
|
||||
monkeypatch_requests_timeout()
|
||||
monkeypatch_cookie_morsel()
|
||||
monkeypatch_reportlab_imagereader()
|
||||
|
||||
@@ -20,19 +20,14 @@
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import logging
|
||||
import re
|
||||
import unicodedata
|
||||
|
||||
from arabic_reshaper import ArabicReshaper
|
||||
from bidi import get_display
|
||||
from django.conf import settings
|
||||
from django.utils.functional import SimpleLazyObject
|
||||
from django.utils.html import escape
|
||||
from PIL import Image
|
||||
from reportlab.lib.styles import ParagraphStyle
|
||||
from reportlab.lib.utils import ImageReader
|
||||
from reportlab.pdfbase import pdfmetrics
|
||||
from reportlab.pdfbase.ttfonts import TTFont
|
||||
from reportlab.platypus import Paragraph
|
||||
|
||||
from pretix.presale.style import get_fonts
|
||||
@@ -75,20 +70,6 @@ reshaper = SimpleLazyObject(lambda: ArabicReshaper(configuration={
|
||||
}))
|
||||
|
||||
|
||||
def normalize_text(text: str) -> str:
|
||||
# reportlab does not support unicode combination characters
|
||||
# It's important we do this before we use ArabicReshaper
|
||||
text = unicodedata.normalize("NFKC", text)
|
||||
|
||||
# reportlab does not support RTL, ligature-heavy scripts like Arabic. Therefore, we use ArabicReshaper
|
||||
# to resolve all ligatures and python-bidi to switch RTL texts.
|
||||
try:
|
||||
text = "\n".join(get_display(reshaper.reshape(l)) for l in re.split("\n", text))
|
||||
except:
|
||||
logger.exception('Reshaping/Bidi fixes failed on string {}'.format(repr(text)))
|
||||
return text
|
||||
|
||||
|
||||
class FontFallbackParagraph(Paragraph):
|
||||
def __init__(self, text, style=None, *args, **kwargs):
|
||||
if style is None:
|
||||
@@ -106,8 +87,6 @@ class FontFallbackParagraph(Paragraph):
|
||||
if not text:
|
||||
return True
|
||||
font = pdfmetrics.getFont(font_name)
|
||||
if not isinstance(font, TTFont):
|
||||
return True
|
||||
return all(
|
||||
ord(c) in font.face.charToGlyph or not c.isprintable()
|
||||
for c in text
|
||||
@@ -123,24 +102,6 @@ class FontFallbackParagraph(Paragraph):
|
||||
return family
|
||||
|
||||
|
||||
class PlainTextParagraph(FontFallbackParagraph):
|
||||
def __init__(self, text, style=None, linebreaks=True, *args, **kwargs):
|
||||
if not isinstance(text, str):
|
||||
if hasattr(text, '__html__'):
|
||||
raise ValueError("It is contradictory to pass escaped content to PlainTextParagraph")
|
||||
text = str(text)
|
||||
|
||||
# Normalize unicode and apply reshaping
|
||||
text = normalize_text(text)
|
||||
|
||||
# Escape any HTML in the text
|
||||
text = escape(text)
|
||||
|
||||
if linebreaks:
|
||||
text = text.strip().replace("\n", "<br />\n")
|
||||
super().__init__(text, style, *args, **kwargs)
|
||||
|
||||
|
||||
def register_ttf_font_if_new(name, path):
|
||||
from reportlab.pdfbase import pdfmetrics
|
||||
from reportlab.pdfbase.ttfonts import TTFont
|
||||
|
||||
@@ -25,7 +25,7 @@ import time
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import login as auth_login
|
||||
from django.contrib.gis import geoip2
|
||||
from django.contrib.gis.geoip2 import GeoIP2
|
||||
from django.core.cache import cache
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
@@ -63,20 +63,14 @@ def get_user_agent_hash(request):
|
||||
_geoip = None
|
||||
|
||||
|
||||
def get_geoip() -> geoip2.GeoIP2:
|
||||
# See https://code.djangoproject.com/ticket/36988#ticket
|
||||
def _get_country(request):
|
||||
global _geoip
|
||||
|
||||
geoip2.SUPPORTED_DATABASE_TYPES.add("Geoacumen-Country")
|
||||
|
||||
if not _geoip:
|
||||
_geoip = geoip2.GeoIP2()
|
||||
return _geoip
|
||||
_geoip = GeoIP2()
|
||||
|
||||
|
||||
def _get_country(request):
|
||||
try:
|
||||
res = get_geoip().country(get_client_ip(request))
|
||||
res = _geoip.country(get_client_ip(request))
|
||||
except AddressNotFoundError:
|
||||
return None
|
||||
return res['country_code']
|
||||
|
||||
@@ -1,33 +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/>.
|
||||
#
|
||||
import logging
|
||||
|
||||
from django import template
|
||||
from django.utils.html import format_html
|
||||
|
||||
register = template.Library()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@register.filter
|
||||
def wrap_in(content, tag_name):
|
||||
return format_html(f'<{tag_name}>{{}}</{tag_name}>', content)
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -32,7 +32,6 @@ ausgecheckt
|
||||
ausgeklappt
|
||||
auswahl
|
||||
Authentication
|
||||
Authenticator
|
||||
Authenticator-App
|
||||
Autorisierungscode
|
||||
Autorisierungs-Endpunktes
|
||||
@@ -131,7 +130,6 @@ Eingangsscan
|
||||
Einlassbuchung
|
||||
Einlassdatum
|
||||
Einlasskontrolle
|
||||
Einmalpasswörter
|
||||
einzuchecken
|
||||
email
|
||||
E-Mail-Renderer
|
||||
@@ -165,7 +163,6 @@ Explorer
|
||||
FA
|
||||
Favicon
|
||||
F-Droid
|
||||
freeOTP
|
||||
Footer
|
||||
Footer-Link
|
||||
Footer-Text
|
||||
@@ -560,7 +557,6 @@ Zahlungs-ID
|
||||
Zahlungspflichtig
|
||||
Zehnerkarten
|
||||
Zeitbasiert
|
||||
zeitbasierte
|
||||
Zeitslotbuchung
|
||||
Zimpler
|
||||
ZIP-Datei
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -32,7 +32,6 @@ ausgecheckt
|
||||
ausgeklappt
|
||||
auswahl
|
||||
Authentication
|
||||
Authenticator
|
||||
Authenticator-App
|
||||
Autorisierungscode
|
||||
Autorisierungs-Endpunktes
|
||||
@@ -131,7 +130,6 @@ Eingangsscan
|
||||
Einlassbuchung
|
||||
Einlassdatum
|
||||
Einlasskontrolle
|
||||
Einmalpasswörter
|
||||
einzuchecken
|
||||
email
|
||||
E-Mail-Renderer
|
||||
@@ -165,7 +163,6 @@ Explorer
|
||||
FA
|
||||
Favicon
|
||||
F-Droid
|
||||
freeOTP
|
||||
Footer
|
||||
Footer-Link
|
||||
Footer-Text
|
||||
@@ -560,7 +557,6 @@ Zahlungs-ID
|
||||
Zahlungspflichtig
|
||||
Zehnerkarten
|
||||
Zeitbasiert
|
||||
zeitbasierte
|
||||
Zeitslotbuchung
|
||||
Zimpler
|
||||
ZIP-Datei
|
||||
|
||||
+288
-310
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-03-30 11:25+0000\n"
|
||||
"POT-Creation-Date: 2026-03-17 14:06+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-03-17 14:06+0000\n"
|
||||
"PO-Revision-Date: 2026-03-30 03:00+0000\n"
|
||||
"PO-Revision-Date: 2026-03-18 12:23+0000\n"
|
||||
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
|
||||
"Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/pretix-"
|
||||
"js/es/>\n"
|
||||
@@ -329,7 +329,7 @@ msgstr "Pedido no aprobado"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:68
|
||||
msgid "Checked-in Tickets"
|
||||
msgstr "Billetes registrados"
|
||||
msgstr "Registro de código QR"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:69
|
||||
msgid "Valid Tickets"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -8,8 +8,8 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-03-17 14:06+0000\n"
|
||||
"PO-Revision-Date: 2026-03-25 14:14+0000\n"
|
||||
"Last-Translator: Pietro Isotti <isottipietro@gmail.com>\n"
|
||||
"PO-Revision-Date: 2026-02-10 16:49+0000\n"
|
||||
"Last-Translator: Raffaele Doretto <ced@comune.portogruaro.ve.it>\n"
|
||||
"Language-Team: Italian <https://translate.pretix.eu/projects/pretix/pretix-"
|
||||
"js/it/>\n"
|
||||
"Language: it\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.16.2\n"
|
||||
"X-Generator: Weblate 5.15.2\n"
|
||||
|
||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
|
||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
|
||||
@@ -310,8 +310,9 @@ msgid "Ticket code revoked/changed"
|
||||
msgstr "Codice biglietto annullato/modificato"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:63
|
||||
#, fuzzy
|
||||
msgid "Ticket blocked"
|
||||
msgstr "Biglietto bloccato"
|
||||
msgstr "Biglietto non pagato"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:64
|
||||
msgid "Ticket not valid at this time"
|
||||
@@ -428,7 +429,7 @@ msgstr ""
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:276
|
||||
msgid "If this takes longer than a few minutes, please contact us."
|
||||
msgstr "Se questa operazione richiede alcuni minuti, si prega di contattarci."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/static/pretixbase/js/asynctask.js:331
|
||||
msgid "Close message"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-03-17 14:06+0000\n"
|
||||
"PO-Revision-Date: 2026-03-23 21:00+0000\n"
|
||||
"PO-Revision-Date: 2026-02-23 10:00+0000\n"
|
||||
"Last-Translator: Hijiri Umemoto <hijiri@umemoto.org>\n"
|
||||
"Language-Team: Japanese <https://translate.pretix.eu/projects/pretix/pretix-"
|
||||
"js/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.16.2\n"
|
||||
"X-Generator: Weblate 5.16\n"
|
||||
|
||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
|
||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
|
||||
@@ -60,7 +60,7 @@ msgstr "PayPal後払い"
|
||||
|
||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:41
|
||||
msgid "iDEAL | Wero"
|
||||
msgstr "iDEAL | Wero"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:42
|
||||
msgid "SEPA Direct Debit"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-03-17 14:06+0000\n"
|
||||
"PO-Revision-Date: 2026-03-25 08:00+0000\n"
|
||||
"PO-Revision-Date: 2026-01-26 22:00+0000\n"
|
||||
"Last-Translator: Renne Rocha <renne@rocha.dev.br>\n"
|
||||
"Language-Team: Portuguese (Brazil) <https://translate.pretix.eu/projects/"
|
||||
"pretix/pretix-js/pt_BR/>\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.16.2\n"
|
||||
"X-Generator: Weblate 5.15.2\n"
|
||||
|
||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
|
||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
|
||||
@@ -60,7 +60,7 @@ msgstr "PayPal Pay Later"
|
||||
|
||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:41
|
||||
msgid "iDEAL | Wero"
|
||||
msgstr "iDEAL | Wero"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js:42
|
||||
msgid "SEPA Direct Debit"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user