mirror of
https://github.com/pretix/pretix.git
synced 2026-08-05 09:57:49 +00:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0bb0f5152e | ||
|
|
d176380cef | ||
|
|
3da2cd8e39 | ||
|
|
419fe7cba3 | ||
|
|
0e4727493e |
+2
-3
@@ -10,10 +10,9 @@ tests:
|
||||
- cd src
|
||||
- python manage.py check
|
||||
- make all compress
|
||||
- playwright install
|
||||
- PRETIX_CONFIG_FILE=tests/ci_sqlite.cfg py.test -n 3 tests --maxfail=100
|
||||
except:
|
||||
- '/^v.*$/'
|
||||
- pypi
|
||||
pypi:
|
||||
stage: release
|
||||
image:
|
||||
@@ -36,7 +35,7 @@ pypi:
|
||||
- twine check dist/*
|
||||
- twine upload dist/*
|
||||
only:
|
||||
- '/^v.*$/'
|
||||
- pypi
|
||||
artifacts:
|
||||
paths:
|
||||
- src/dist/
|
||||
|
||||
@@ -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.5.post1"
|
||||
__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):
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
{% extends "error.html" %}
|
||||
{% load i18n %}
|
||||
{% load eventurl %}
|
||||
{% load urlreplace %}
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
<h1>{% trans "Please continue in a new tab" %}</h1>
|
||||
<p class="larger">
|
||||
{% blocktrans trimmed %}
|
||||
For security reasons, the following step is only possible in a new tab.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<p class="larger">
|
||||
{% blocktrans trimmed %}
|
||||
If the new tab did not open automatically, please click the following button:
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<div class="text-center">
|
||||
<a href="{{ url }}"
|
||||
class="btn btn-primary btn-lg" target="_blank">
|
||||
<span class="fa fa-external-link-square"></span>
|
||||
{% trans "Continue in new tab" %}
|
||||
</a>
|
||||
{{ url|json_script:"framebreak-url" }}
|
||||
<script type="text/javascript" src="{% static "pretixbase/js/framebreak.js" %}"></script>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -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 %}
|
||||
|
||||
@@ -42,6 +42,8 @@ from bleach import DEFAULT_CALLBACKS, html5lib_shim
|
||||
from bleach.linkifier import build_email_re
|
||||
from django import template
|
||||
from django.conf import settings
|
||||
from django.core import signing
|
||||
from django.urls import reverse
|
||||
from django.utils.functional import SimpleLazyObject
|
||||
from django.utils.html import escape
|
||||
from django.utils.http import url_has_allowed_host_and_scheme
|
||||
@@ -52,7 +54,6 @@ from markdown.postprocessors import Postprocessor
|
||||
from markdown.treeprocessors import UnescapeTreeprocessor
|
||||
from tlds import tld_set
|
||||
|
||||
from pretix.base.views.redirect import safelink
|
||||
from pretix.helpers.format import SafeFormatter, format_map
|
||||
|
||||
register = template.Library()
|
||||
@@ -157,7 +158,8 @@ def safelink_callback(attrs, new=False):
|
||||
"""
|
||||
url = html.unescape(attrs.get((None, 'href'), '/'))
|
||||
if not url_has_allowed_host_and_scheme(url, allowed_hosts=None) and not url.startswith('mailto:') and not url.startswith('tel:'):
|
||||
attrs[None, 'href'] = safelink(url)
|
||||
signer = signing.Signer(salt='safe-redirect')
|
||||
attrs[None, 'href'] = reverse('redirect') + '?url=' + urllib.parse.quote(signer.sign(url))
|
||||
attrs[None, 'target'] = '_blank'
|
||||
attrs[None, 'rel'] = 'noopener'
|
||||
return attrs
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
# 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
|
||||
import urllib.parse
|
||||
|
||||
from django.core import signing
|
||||
@@ -27,8 +26,6 @@ from django.http import HttpResponseBadRequest, HttpResponseRedirect
|
||||
from django.shortcuts import render
|
||||
from django.urls import reverse
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _is_samesite_referer(request):
|
||||
referer = request.headers.get('referer')
|
||||
@@ -45,16 +42,11 @@ def _is_samesite_referer(request):
|
||||
|
||||
|
||||
def redir_view(request):
|
||||
framebreak = "framebreak" in request.GET
|
||||
salt = 'framebreak-safelink-url' if framebreak else 'safelink-url'
|
||||
signer = signing.Signer(salt='safe-redirect')
|
||||
try:
|
||||
url = signing.Signer(salt=salt).unsign(request.GET.get('url', ''))
|
||||
url = signer.unsign(request.GET.get('url', ''))
|
||||
except signing.BadSignature:
|
||||
try:
|
||||
# Backwards-compatibility for a change in 2026-06, remove after a while
|
||||
url = signing.Signer(salt='safe-redirect').unsign(request.GET.get('url', ''))
|
||||
except signing.BadSignature:
|
||||
return HttpResponseBadRequest('Invalid parameter')
|
||||
return HttpResponseBadRequest('Invalid parameter')
|
||||
|
||||
if not _is_samesite_referer(request):
|
||||
u = urllib.parse.urlparse(url)
|
||||
@@ -63,26 +55,11 @@ def redir_view(request):
|
||||
'url': url,
|
||||
})
|
||||
|
||||
if framebreak:
|
||||
r = render(request, 'pretixbase/framebreak.html', {
|
||||
'url': url,
|
||||
})
|
||||
r.xframe_options_exempt = True
|
||||
return r
|
||||
|
||||
r = HttpResponseRedirect(url)
|
||||
r['X-Robots-Tag'] = 'noindex'
|
||||
return r
|
||||
|
||||
|
||||
def safelink(url, framebreak=False):
|
||||
url = str(url)
|
||||
if not (url.startswith('https://') or url.startswith('http://') or url.startswith("/")):
|
||||
logger.warning('Invalid URL passed to safelink: %r', url)
|
||||
return '#invalid-url'
|
||||
salt = 'framebreak-safelink-url' if framebreak else 'safelink-url'
|
||||
signer = signing.Signer(salt=salt)
|
||||
u = reverse('redirect') + '?url=' + urllib.parse.quote(signer.sign(url))
|
||||
if framebreak:
|
||||
u += "&framebreak=true"
|
||||
return u
|
||||
def safelink(url):
|
||||
signer = signing.Signer(salt='safe-redirect')
|
||||
return reverse('redirect') + '?url=' + urllib.parse.quote(signer.sign(url))
|
||||
|
||||
@@ -1528,6 +1528,133 @@ class SubEventFilterForm(FilterForm):
|
||||
return self.event.organizer.meta_properties.filter(filter_allowed=True)
|
||||
|
||||
|
||||
class QuotaFilterForm(FilterForm):
|
||||
orders = {
|
||||
'-date': ('-subevent__date_from', 'name', 'pk'),
|
||||
'date': ('subevent__date_from', '-name', '-pk'),
|
||||
'size': ('size', 'name', 'pk'),
|
||||
'-size': ('-size', '-name', '-pk'),
|
||||
'name': ('name', 'pk'),
|
||||
'-name': ('-name', '-pk'),
|
||||
}
|
||||
subevent = forms.ModelChoiceField(
|
||||
label=pgettext_lazy('subevent', 'Date'),
|
||||
queryset=SubEvent.objects.none(),
|
||||
required=False,
|
||||
empty_label=pgettext_lazy('subevent', 'All dates')
|
||||
)
|
||||
date_from = forms.DateField(
|
||||
label=_('Date from'),
|
||||
required=False,
|
||||
widget=DatePickerWidget({
|
||||
'placeholder': _('Date from'),
|
||||
}),
|
||||
)
|
||||
date_until = forms.DateField(
|
||||
label=_('Date until'),
|
||||
required=False,
|
||||
widget=DatePickerWidget({
|
||||
'placeholder': _('Date until'),
|
||||
}),
|
||||
)
|
||||
time_from = forms.TimeField(
|
||||
label=_('Start time from'),
|
||||
required=False,
|
||||
widget=TimePickerWidget({}),
|
||||
)
|
||||
time_until = forms.TimeField(
|
||||
label=_('Start time until'),
|
||||
required=False,
|
||||
widget=TimePickerWidget({}),
|
||||
)
|
||||
weekday = forms.MultipleChoiceField(
|
||||
label=_('Weekday'),
|
||||
choices=(
|
||||
('2', _('Monday')),
|
||||
('3', _('Tuesday')),
|
||||
('4', _('Wednesday')),
|
||||
('5', _('Thursday')),
|
||||
('6', _('Friday')),
|
||||
('7', _('Saturday')),
|
||||
('1', _('Sunday')),
|
||||
),
|
||||
widget=forms.CheckboxSelectMultiple,
|
||||
required=False
|
||||
)
|
||||
query = forms.CharField(
|
||||
label=_('Quota name'),
|
||||
widget=forms.TextInput(),
|
||||
required=False
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.event = kwargs.pop('event')
|
||||
super().__init__(*args, **kwargs)
|
||||
if self.event.has_subevents:
|
||||
self.fields['date_from'].widget = DatePickerWidget()
|
||||
self.fields['date_until'].widget = DatePickerWidget()
|
||||
self.fields['subevent'].queryset = self.event.subevents.all()
|
||||
self.fields['subevent'].widget = Select2(
|
||||
attrs={
|
||||
'data-model-select2': 'event',
|
||||
'data-select2-url': reverse('control:event.subevents.select2', kwargs={
|
||||
'event': self.event.slug,
|
||||
'organizer': self.event.organizer.slug,
|
||||
}),
|
||||
'data-placeholder': pgettext_lazy('subevent', 'All dates')
|
||||
}
|
||||
)
|
||||
self.fields['subevent'].widget.choices = self.fields['subevent'].choices
|
||||
else:
|
||||
del self.fields['subevent']
|
||||
del self.fields['date_from']
|
||||
del self.fields['date_until']
|
||||
del self.fields['time_from']
|
||||
del self.fields['time_until']
|
||||
del self.fields['weekday']
|
||||
|
||||
def filter_qs(self, qs):
|
||||
fdata = self.cleaned_data
|
||||
|
||||
if fdata.get('weekday'):
|
||||
qs = qs.annotate(wday=ExtractWeekDay('subevent__date_from')).filter(wday__in=fdata.get('weekday'))
|
||||
|
||||
if fdata.get('subevent'):
|
||||
qs = qs.filter(subevent=fdata["subevent"])
|
||||
|
||||
if fdata.get('query'):
|
||||
query = fdata.get('query')
|
||||
qs = qs.filter(name__icontains=query)
|
||||
|
||||
if fdata.get('date_until'):
|
||||
date_end = make_aware(datetime.combine(
|
||||
fdata.get('date_until') + timedelta(days=1),
|
||||
time(hour=0, minute=0, second=0, microsecond=0)
|
||||
), get_current_timezone())
|
||||
qs = qs.filter(
|
||||
Q(subevent__date_to__isnull=True, subevent__date_from__lt=date_end) |
|
||||
Q(subevent__date_to__isnull=False, subevent__date_to__lt=date_end)
|
||||
)
|
||||
if fdata.get('date_from'):
|
||||
date_start = make_aware(datetime.combine(
|
||||
fdata.get('date_from'),
|
||||
time(hour=0, minute=0, second=0, microsecond=0)
|
||||
), get_current_timezone())
|
||||
qs = qs.filter(subevent__date_from__gte=date_start)
|
||||
|
||||
if fdata.get('time_until'):
|
||||
qs = qs.filter(subevent__date_from__time__lte=fdata.get('time_until'))
|
||||
if fdata.get('time_from'):
|
||||
qs = qs.filter(subevent__date_from__time__gte=fdata.get('time_from'))
|
||||
|
||||
if fdata.get('ordering'):
|
||||
qs = qs.order_by(*get_deterministic_ordering(Quota, self.get_order_by()))
|
||||
else:
|
||||
qs = qs.order_by('-subevent__date_from', 'name', 'pk')
|
||||
|
||||
return qs
|
||||
|
||||
|
||||
class OrganizerFilterForm(FilterForm):
|
||||
orders = {
|
||||
'slug': 'slug',
|
||||
|
||||
@@ -43,6 +43,7 @@ from django.core.exceptions import ValidationError
|
||||
from django.db.models import Max, Q
|
||||
from django.forms import ChoiceField, RadioSelect
|
||||
from django.forms.formsets import DELETION_FIELD_NAME
|
||||
from django.forms.utils import ErrorDict
|
||||
from django.urls import reverse
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.html import escape, format_html
|
||||
@@ -375,6 +376,60 @@ class QuotaForm(I18nModelForm):
|
||||
return inst
|
||||
|
||||
|
||||
class QuotaBulkEditForm(QuotaForm):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.mixed_values = kwargs.pop('mixed_values')
|
||||
self.queryset = kwargs.pop('queryset')
|
||||
super().__init__(**kwargs)
|
||||
self.fields.pop("subevent", None) # Would add extra complexity and it's hard to imagine a use case for that
|
||||
self.fields["name"].required = False
|
||||
self.fields["itemvars"].required = False
|
||||
|
||||
def clean(self):
|
||||
d = super().clean()
|
||||
if self.prefix + "name" in self.data.getlist('_bulk') and not d.get("name"):
|
||||
raise ValidationError({"name": _("This field is required.")})
|
||||
if self.prefix + "itemvars" in self.data.getlist('_bulk') and not d.get("itemvars"):
|
||||
raise ValidationError({"itemvars": _("This field is required.")})
|
||||
return d
|
||||
|
||||
def save(self, commit=True):
|
||||
objs = list(self.queryset)
|
||||
fields = set()
|
||||
|
||||
for k in self.fields:
|
||||
cb_val = self.prefix + k
|
||||
if cb_val not in self.data.getlist('_bulk'):
|
||||
continue
|
||||
|
||||
fields.add(k)
|
||||
if k == 'itemvars':
|
||||
selected_items = set(list(self.event.items.filter(id__in=[
|
||||
i.split('-')[0] for i in self.cleaned_data['itemvars']
|
||||
])))
|
||||
selected_variations = list(ItemVariation.objects.filter(item__event=self.event, id__in=[
|
||||
i.split('-')[1] for i in self.cleaned_data['itemvars'] if '-' in i
|
||||
]))
|
||||
for obj in objs:
|
||||
obj.items.set(selected_items)
|
||||
obj.variations.set(selected_variations)
|
||||
else:
|
||||
for obj in objs:
|
||||
setattr(obj, k, self.cleaned_data[k])
|
||||
|
||||
fields = [f for f in fields if f != 'itemvars']
|
||||
if fields:
|
||||
Quota.objects.bulk_update(objs, fields, 200)
|
||||
|
||||
def full_clean(self):
|
||||
if len(self.data) == 0:
|
||||
# form wasn't submitted
|
||||
self._errors = ErrorDict()
|
||||
return
|
||||
super().full_clean()
|
||||
|
||||
|
||||
class ItemCreateForm(I18nModelForm):
|
||||
NONE = 'none'
|
||||
EXISTING = 'existing'
|
||||
|
||||
@@ -212,7 +212,7 @@ class AuditLogMiddleware:
|
||||
if request.path.startswith(get_script_prefix() + 'control') and request.user.is_authenticated:
|
||||
if getattr(request.user, "is_hijacked", False):
|
||||
hijack_history = request.session.get('hijack_history', False)
|
||||
hijacker = get_object_or_404(User, pk=hijack_history[0]["user"])
|
||||
hijacker = get_object_or_404(User, pk=hijack_history[0])
|
||||
ss = hijacker.get_active_staff_session(request.session.get('hijacker_session'))
|
||||
if ss:
|
||||
ss.logs.create(
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
{% extends "pretixcontrol/event/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block content %}
|
||||
<h1>
|
||||
{% trans "Change multiple quotas" %}
|
||||
<small>
|
||||
{% blocktrans trimmed with number=quotas.count %}
|
||||
{{ number }} selected
|
||||
{% endblocktrans %}
|
||||
</small>
|
||||
</h1>
|
||||
<form class="form-horizontal" action="" method="post">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form_errors form %}
|
||||
<div class="hidden">
|
||||
{% for d in quotas %}
|
||||
<input type="hidden" name="quota" value="{{ d.pk }}">
|
||||
{% endfor %}
|
||||
</div>
|
||||
<fieldset>
|
||||
<legend>{% trans "General information" %}</legend>
|
||||
{% bootstrap_field form.name layout="bulkedit" %}
|
||||
{% bootstrap_field form.size layout="bulkedit" %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Items" %}</legend>
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
Please select the products or product variations this quota should be applied to. If you apply two
|
||||
quotas to the same product, it will only be available if <strong>both</strong> quotas have capacity
|
||||
left.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
{% bootstrap_field form.itemvars layout="bulkedit" %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Advanced options" %}</legend>
|
||||
{% bootstrap_field form.close_when_sold_out layout="bulkedit" %}
|
||||
{% bootstrap_field form.release_after_exit layout="bulkedit" %}
|
||||
{% bootstrap_field form.ignore_for_event_availability layout="bulkedit" %}
|
||||
</fieldset>
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,34 @@
|
||||
{% extends "pretixcontrol/event/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block title %}{% trans "Delete quotas" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h1>{% trans "Delete quotas" %}</h1>
|
||||
<form action="" method="post" class="form-horizontal">
|
||||
{% csrf_token %}
|
||||
{% if allowed %}
|
||||
<p>{% blocktrans trimmed count num=allowed|length %}
|
||||
Are you sure you want to delete the following quota?
|
||||
{% plural %}
|
||||
Are you sure you want to delete the following {{ num }} quotas?
|
||||
{% endblocktrans %}</p>
|
||||
<ul>
|
||||
{% for q in allowed %}
|
||||
<li>
|
||||
{{ q }} {% if q.subevent %}({{ q.subevent }}){% endif %}
|
||||
<input type="hidden" name="quota" value="{{ q.pk }}">
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
<div class="form-group submit-group">
|
||||
<a href="{% url "control:event.items.quotas" organizer=request.event.organizer.slug event=request.event.slug %}"
|
||||
class="btn btn-default btn-cancel">
|
||||
{% trans "Cancel" %}
|
||||
</a>
|
||||
<button type="submit" class="btn btn-danger btn-save" value="delete_confirm" name="action">
|
||||
{% trans "Delete" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -1,6 +1,7 @@
|
||||
{% extends "pretixcontrol/items/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load urlreplace %}
|
||||
{% load bootstrap3 %}
|
||||
{% block title %}{% trans "Quotas" %}{% endblock %}
|
||||
{% block inside %}
|
||||
<h1>{% trans "Quotas" %}</h1>
|
||||
@@ -13,21 +14,12 @@
|
||||
number of a specific ticket type at the same time.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
{% if request.event.has_subevents %}
|
||||
<form class="form-inline helper-display-inline" action="" method="get">
|
||||
{% include "pretixcontrol/event/fragment_subevent_choice_simple.html" %}
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if quotas|length == 0 %}
|
||||
{% if quotas|length == 0 and not filter_form.filtered %}
|
||||
<div class="empty-collection">
|
||||
<p>
|
||||
{% if request.GET.subevent %}
|
||||
{% trans "Your search did not match any quotas." %}
|
||||
{% else %}
|
||||
{% blocktrans trimmed %}
|
||||
You haven't created any quotas yet.
|
||||
{% endblocktrans %}
|
||||
{% endif %}
|
||||
{% blocktrans trimmed %}
|
||||
You haven't created any quotas yet.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
|
||||
{% if 'event.items:write' in request.eventpermset %}
|
||||
@@ -36,79 +28,160 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
{% trans "Filter" %}
|
||||
</h3>
|
||||
</div>
|
||||
<form class="panel-body filter-form" action="" method="get">
|
||||
<div class="row">
|
||||
<div class="{% if not filter_form.subevent %}col-lg-6{% else %}col-lg-2{% endif %} col-md-6 col-sm-6 col-xs-12">
|
||||
{% bootstrap_field filter_form.query %}
|
||||
</div>
|
||||
{% if filter_form.subevent %}
|
||||
<div class="col-lg-2 col-md-6 col-md-2 col-sm-6 col-xs-12">
|
||||
{% bootstrap_field filter_form.subevent %}
|
||||
</div>
|
||||
<div class="col-lg-2 col-md-3 col-sm-6 col-xs-12">
|
||||
{% bootstrap_field filter_form.date_from %}
|
||||
</div>
|
||||
<div class="col-lg-2 col-md-3 col-sm-6 col-xs-12">
|
||||
{% bootstrap_field filter_form.date_until %}
|
||||
</div>
|
||||
<div class="col-lg-2 col-md-3 col-sm-6 col-xs-12">
|
||||
{% bootstrap_field filter_form.time_from %}
|
||||
</div>
|
||||
<div class="col-lg-2 col-md-3 col-sm-6 col-xs-12">
|
||||
{% bootstrap_field filter_form.time_until %}
|
||||
</div>
|
||||
<div class="col-xs-12 one-line-checkboxes">
|
||||
{% bootstrap_field filter_form.weekday %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="text-right flip">
|
||||
<button class="btn btn-primary btn-lg" type="submit">
|
||||
<span class="fa fa-filter"></span>
|
||||
{% trans "Filter" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% if 'event.items:write' in request.eventpermset %}
|
||||
<p>
|
||||
<a href="{% url "control:event.items.quotas.add" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create a new quota" %}
|
||||
</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-quotas">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Quota name" %}
|
||||
<a href="?{% url_replace request 'ordering' '-name' %}"><i class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'ordering' 'name' %}"><i class="fa fa-caret-up"></i></a>
|
||||
</th>
|
||||
<th>{% trans "Products" %}</th>
|
||||
{% if request.event.has_subevents %}
|
||||
<th>{% trans "Date" context "subevent" %}
|
||||
<a href="?{% url_replace request 'ordering' '-date' %}"><i class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'ordering' 'date' %}"><i class="fa fa-caret-up"></i></a>
|
||||
</th>
|
||||
{% endif %}
|
||||
<th>{% trans "Total capacity" %}
|
||||
<a href="?{% url_replace request 'ordering' '-size' %}"><i class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'ordering' 'size' %}"><i class="fa fa-caret-up"></i></a>
|
||||
</th>
|
||||
<th>{% trans "Capacity left" %}</th>
|
||||
<th class="action-col-2"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for q in quotas %}
|
||||
<form action="{% url "control:event.items.quotas.bulkaction" organizer=request.event.organizer.slug event=request.event.slug %}" method="post">
|
||||
{% csrf_token %}
|
||||
{% for field in filter_form %}
|
||||
{{ field.as_hidden }}
|
||||
{% endfor %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-quotas">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>
|
||||
<strong><a href="{% url "control:event.items.quotas.show" organizer=request.event.organizer.slug event=request.event.slug quota=q.id %}">{{ q.name }}</a></strong>
|
||||
{% if q.ignore_for_event_availability %}
|
||||
<span class="fa fa-eye-slash text-muted" data-toggle="tooltip" title="{% trans "Ignore this quota when determining event availability" %}"></span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<ul>
|
||||
{% for item in q.cached_items %}
|
||||
{% if not item.has_variations %}
|
||||
<li><a href="{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=item.id %}">{{ item }}</a></li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% for v in q.variations.all %}
|
||||
<li><a href="{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=v.item.id %}#tab-0-3-open">
|
||||
{{ v.item }} – {{ v }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</td>
|
||||
{% if request.event.has_subevents %}
|
||||
<td>
|
||||
{{ q.subevent.name }} – {{ q.subevent.get_date_range_display_with_times }}
|
||||
</td>
|
||||
{% if "event.items:write" in request.eventpermset %}
|
||||
<th>
|
||||
<label aria-label="{% trans "select all rows for batch-operation" %}" class="batch-select-label"><input type="checkbox" data-toggle-table/></label>
|
||||
</th>
|
||||
{% endif %}
|
||||
<td>{% if q.size == None %}Unlimited{% else %}{{ q.size }}{% endif %}</td>
|
||||
<td>{% include "pretixcontrol/items/fragment_quota_availability.html" with availability=q.cached_avail closed=q.closed %}</td>
|
||||
<td class="text-right flip">
|
||||
{% if 'event.items:write' in request.eventpermset %}
|
||||
<a href="{% url "control:event.items.quotas.edit" organizer=request.event.organizer.slug event=request.event.slug quota=q.id %}" class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
|
||||
<a href="{% url "control:event.items.quotas.add" organizer=request.event.organizer.slug event=request.event.slug %}?copy_from={{ q.id }}"
|
||||
class="btn btn-sm btn-default" title="{% trans "Clone" %}" data-toggle="tooltip">
|
||||
<span class="fa fa-copy"></span>
|
||||
</a>
|
||||
<a href="{% url "control:event.items.quotas.delete" organizer=request.event.organizer.slug event=request.event.slug quota=q.id %}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
|
||||
{% endif %}
|
||||
</td>
|
||||
<th>{% trans "Quota name" %}
|
||||
<a href="?{% url_replace request 'filter-ordering' '-name' %}"><i class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'filter-ordering' 'name' %}"><i class="fa fa-caret-up"></i></a>
|
||||
</th>
|
||||
<th>{% trans "Products" %}</th>
|
||||
{% if request.event.has_subevents %}
|
||||
<th>{% trans "Date" context "subevent" %}
|
||||
<a href="?{% url_replace request 'filter-ordering' '-date' %}"><i class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'filter-ordering' 'date' %}"><i class="fa fa-caret-up"></i></a>
|
||||
</th>
|
||||
{% endif %}
|
||||
<th>{% trans "Total capacity" %}
|
||||
<a href="?{% url_replace request 'filter-ordering' '-size' %}"><i class="fa fa-caret-down"></i></a>
|
||||
<a href="?{% url_replace request 'filter-ordering' 'size' %}"><i class="fa fa-caret-up"></i></a>
|
||||
</th>
|
||||
<th>{% trans "Capacity left" %}</th>
|
||||
<th class="action-col-2"></th>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% if "event.items:write" in request.eventpermset and page_obj.paginator.num_pages > 1 %}
|
||||
<tr class="table-select-all warning hidden">
|
||||
<td>
|
||||
<input type="checkbox" name="__ALL" id="__all" data-results-total="{{ page_obj.paginator.count }}">
|
||||
</td>
|
||||
<td colspan="6">
|
||||
<label for="__all">
|
||||
{% trans "Select all results on other pages as well" %}
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for q in quotas %}
|
||||
<tr>
|
||||
{% if "event.items:write" in request.eventpermset %}
|
||||
<td>
|
||||
<label aria-label="{% trans "select row for batch-operation" %}" class="batch-select-label"><input type="checkbox" name="quota" class="batch-select-checkbox" value="{{ q.pk }}"/></label>
|
||||
</td>
|
||||
{% endif %}
|
||||
<td>
|
||||
<strong><a href="{% url "control:event.items.quotas.show" organizer=request.event.organizer.slug event=request.event.slug quota=q.id %}">{{ q.name }}</a></strong>
|
||||
{% if q.ignore_for_event_availability %}
|
||||
<span class="fa fa-eye-slash text-muted" data-toggle="tooltip" title="{% trans "Ignore this quota when determining event availability" %}"></span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<ul>
|
||||
{% for item in q.cached_items %}
|
||||
{% if not item.has_variations %}
|
||||
<li><a href="{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=item.id %}">{{ item }}</a></li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% for v in q.variations.all %}
|
||||
<li><a href="{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=v.item.id %}#tab-0-3-open">
|
||||
{{ v.item }} – {{ v }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</td>
|
||||
{% if request.event.has_subevents %}
|
||||
<td>
|
||||
{{ q.subevent.name }} – {{ q.subevent.get_date_range_display_with_times }}
|
||||
</td>
|
||||
{% endif %}
|
||||
<td>{% if q.size == None %}Unlimited{% else %}{{ q.size }}{% endif %}</td>
|
||||
<td>{% include "pretixcontrol/items/fragment_quota_availability.html" with availability=q.cached_avail closed=q.closed %}</td>
|
||||
<td class="text-right flip">
|
||||
{% if 'event.items:write' in request.eventpermset %}
|
||||
<a href="{% url "control:event.items.quotas.edit" organizer=request.event.organizer.slug event=request.event.slug quota=q.id %}" class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
|
||||
<a href="{% url "control:event.items.quotas.add" organizer=request.event.organizer.slug event=request.event.slug %}?copy_from={{ q.id }}"
|
||||
class="btn btn-sm btn-default" title="{% trans "Clone" %}" data-toggle="tooltip">
|
||||
<span class="fa fa-copy"></span>
|
||||
</a>
|
||||
<a href="{% url "control:event.items.quotas.delete" organizer=request.event.organizer.slug event=request.event.slug quota=q.id %}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% if "event.items:write" in request.eventpermset %}
|
||||
<div class="batch-select-actions">
|
||||
<button type="submit" class="btn btn-danger btn-save" name="action" value="delete">
|
||||
<i class="fa fa-trash"></i>{% trans "Delete selected" %}
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary btn-save" name="action" value="edit"
|
||||
formaction="{% url "control:event.items.quotas.bulkedit" organizer=request.event.organizer.slug event=request.event.slug %}">
|
||||
<i class="fa fa-edit"></i>{% trans "Edit selected" %}
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</form>
|
||||
{% endif %}
|
||||
{% include "pretixcontrol/pagination.html" %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -349,6 +349,8 @@ urlpatterns = [
|
||||
name='event.items.questions.edit'),
|
||||
re_path(r'^questions/add$', item.QuestionCreate.as_view(), name='event.items.questions.add'),
|
||||
re_path(r'^quotas/$', item.QuotaList.as_view(), name='event.items.quotas'),
|
||||
re_path(r'^quotas/bulk_action$', item.QuotaBulkAction.as_view(), name='event.items.quotas.bulkaction'),
|
||||
re_path(r'^quotas/bulk_edit$', item.QuotaBulkUpdateView.as_view(), name='event.items.quotas.bulkedit'),
|
||||
re_path(r'^quotas/(?P<quota>\d+)/$', item.QuotaView.as_view(), name='event.items.quotas.show'),
|
||||
re_path(r'^quotas/select$', typeahead.quotas_select2, name='event.items.quotas.select2'),
|
||||
re_path(r'^quotas/(?P<quota>\d+)/change$', item.QuotaUpdate.as_view(), name='event.items.quotas.edit'),
|
||||
|
||||
@@ -41,21 +41,22 @@ from json.decoder import JSONDecodeError
|
||||
from django.contrib import messages
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.core.files import File
|
||||
from django.db import transaction
|
||||
from django.db import models, transaction
|
||||
from django.db.models import (
|
||||
Count, Exists, F, OuterRef, Prefetch, ProtectedError, Q,
|
||||
Count, Exists, F, OuterRef, Prefetch, ProtectedError, Q, Subquery, Value,
|
||||
)
|
||||
from django.db.models.functions import Cast, Concat
|
||||
from django.forms.models import inlineformset_factory
|
||||
from django.http import (
|
||||
Http404, HttpResponse, HttpResponseBadRequest, HttpResponseRedirect,
|
||||
)
|
||||
from django.shortcuts import redirect
|
||||
from django.shortcuts import redirect, render
|
||||
from django.urls import resolve, reverse
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext, gettext_lazy as _
|
||||
from django.views.decorators.http import require_http_methods
|
||||
from django.views.generic import ListView
|
||||
from django.views.generic import FormView, ListView, View
|
||||
from django.views.generic.detail import DetailView, SingleObjectMixin
|
||||
from django_countries.fields import Country
|
||||
|
||||
@@ -65,7 +66,7 @@ from pretix.api.serializers.item import (
|
||||
)
|
||||
from pretix.base.forms import I18nFormSet
|
||||
from pretix.base.models import (
|
||||
CartPosition, Item, ItemCategory, ItemProgramTime, ItemVariation,
|
||||
CartPosition, Item, ItemCategory, ItemProgramTime, ItemVariation, LogEntry,
|
||||
OrderPosition, Question, QuestionAnswer, QuestionOption, Quota,
|
||||
SeatCategoryMapping, Voucher,
|
||||
)
|
||||
@@ -74,12 +75,15 @@ from pretix.base.models.items import ItemAddOn, ItemBundle, ItemMetaValue
|
||||
from pretix.base.services.quotas import QuotaAvailability
|
||||
from pretix.base.services.tickets import invalidate_cache
|
||||
from pretix.base.signals import quota_availability
|
||||
from pretix.control.forms.filter import QuestionAnswerFilterForm
|
||||
from pretix.control.forms.filter import (
|
||||
QuestionAnswerFilterForm, QuotaFilterForm,
|
||||
)
|
||||
from pretix.control.forms.item import (
|
||||
CategoryForm, ItemAddOnForm, ItemAddOnsFormSet, ItemBundleForm,
|
||||
ItemBundleFormSet, ItemCreateForm, ItemMetaValueForm, ItemProgramTimeForm,
|
||||
ItemProgramTimeFormSet, ItemUpdateForm, ItemVariationForm,
|
||||
ItemVariationsFormSet, QuestionForm, QuestionOptionForm, QuotaForm,
|
||||
ItemVariationsFormSet, QuestionForm, QuestionOptionForm, QuotaBulkEditForm,
|
||||
QuotaForm,
|
||||
)
|
||||
from pretix.control.permissions import (
|
||||
EventPermissionRequiredMixin, event_permission_required,
|
||||
@@ -87,6 +91,7 @@ from pretix.control.permissions import (
|
||||
from pretix.control.signals import item_forms, item_formsets
|
||||
from pretix.helpers.models import modelcopy
|
||||
|
||||
from ...helpers import GroupConcat
|
||||
from ...helpers.compat import CompatDeleteView
|
||||
from . import ChartContainingView, CreateView, PaginationMixin, UpdateView
|
||||
|
||||
@@ -831,13 +836,38 @@ class QuestionCreate(EventPermissionRequiredMixin, QuestionMixin, CreateView):
|
||||
return ret
|
||||
|
||||
|
||||
class QuotaList(PaginationMixin, ListView):
|
||||
class QuotaQueryMixin:
|
||||
|
||||
@cached_property
|
||||
def request_data(self):
|
||||
if self.request.method == "POST":
|
||||
return self.request.POST
|
||||
return self.request.GET
|
||||
|
||||
def get_queryset(self):
|
||||
qs = self.request.event.quotas
|
||||
if self.filter_form.is_valid():
|
||||
qs = self.filter_form.filter_qs(qs)
|
||||
|
||||
if 'quota' in self.request_data and '__ALL' not in self.request_data:
|
||||
qs = qs.filter(
|
||||
id__in=self.request_data.getlist('quota')
|
||||
)
|
||||
|
||||
return qs
|
||||
|
||||
@cached_property
|
||||
def filter_form(self):
|
||||
return QuotaFilterForm(data=self.request_data, prefix='filter', event=self.request.event)
|
||||
|
||||
|
||||
class QuotaList(PaginationMixin, QuotaQueryMixin, ListView):
|
||||
model = Quota
|
||||
context_object_name = 'quotas'
|
||||
template_name = 'pretixcontrol/items/quotas.html'
|
||||
|
||||
def get_queryset(self):
|
||||
qs = self.request.event.quotas.prefetch_related(
|
||||
return super().get_queryset().prefetch_related(
|
||||
Prefetch(
|
||||
"items",
|
||||
queryset=Item.objects.annotate(
|
||||
@@ -852,28 +882,10 @@ class QuotaList(PaginationMixin, ListView):
|
||||
queryset=self.request.event.subevents.all()
|
||||
)
|
||||
)
|
||||
if self.request.GET.get("subevent", "") != "":
|
||||
s = self.request.GET.get("subevent", "")
|
||||
qs = qs.filter(subevent_id=s)
|
||||
|
||||
valid_orders = {
|
||||
'-date': ('-subevent__date_from', 'name', 'pk'),
|
||||
'date': ('subevent__date_from', '-name', '-pk'),
|
||||
'size': ('size', 'name', 'pk'),
|
||||
'-size': ('-size', '-name', '-pk'),
|
||||
'name': ('name', 'pk'),
|
||||
'-name': ('-name', '-pk'),
|
||||
}
|
||||
|
||||
if self.request.GET.get("ordering", "-date") in valid_orders:
|
||||
qs = qs.order_by(*valid_orders[self.request.GET.get("ordering", "-date")])
|
||||
else:
|
||||
qs = qs.order_by('name', 'subevent__date_from', 'pk')
|
||||
|
||||
return qs
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data()
|
||||
ctx['filter_form'] = self.filter_form
|
||||
|
||||
qa = QuotaAvailability()
|
||||
qa.queue(*ctx['quotas'])
|
||||
@@ -884,6 +896,165 @@ class QuotaList(PaginationMixin, ListView):
|
||||
return ctx
|
||||
|
||||
|
||||
class QuotaBulkAction(QuotaQueryMixin, EventPermissionRequiredMixin, View):
|
||||
permission = 'event.items:write'
|
||||
|
||||
@transaction.atomic
|
||||
def post(self, request, *args, **kwargs):
|
||||
if request.POST.get('action') == 'delete':
|
||||
return render(request, 'pretixcontrol/items/quota_delete_bulk.html', {
|
||||
'allowed': self.get_queryset().select_related("subevent"),
|
||||
})
|
||||
elif request.POST.get('action') == 'delete_confirm':
|
||||
log_entries = []
|
||||
to_delete = []
|
||||
for obj in self.get_queryset():
|
||||
log_entries.append(obj.log_action('pretix.event.quota.deleted', user=self.request.user, save=False))
|
||||
to_delete.append(obj.pk)
|
||||
|
||||
if to_delete:
|
||||
LogEntry.bulk_create_and_postprocess(log_entries)
|
||||
Quota.objects.filter(pk__in=to_delete).delete()
|
||||
messages.success(request, _('The selected quotas have been deleted or disabled.'))
|
||||
return redirect(self.get_success_url())
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
return reverse('control:event.items.quotas', kwargs={
|
||||
'organizer': self.request.event.organizer.slug,
|
||||
'event': self.request.event.slug,
|
||||
})
|
||||
|
||||
|
||||
class QuotaBulkUpdateView(QuotaQueryMixin, EventPermissionRequiredMixin, FormView):
|
||||
template_name = 'pretixcontrol/items/quota_bulk_edit.html'
|
||||
permission = 'event.items:write'
|
||||
context_object_name = 'quota'
|
||||
form_class = QuotaBulkEditForm
|
||||
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().prefetch_related(None).order_by()
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
return HttpResponse(status=405)
|
||||
|
||||
@cached_property
|
||||
def is_submitted(self):
|
||||
# Usually, django considers a form "bound" / "submitted" on every POST request. However, this view is always
|
||||
# called with POST method, even if just to pass the selection of objects to work on, so we want to modify
|
||||
# that behaviour
|
||||
return '_bulk' in self.request.POST
|
||||
|
||||
def get_form_kwargs(self):
|
||||
initial = {}
|
||||
mixed_values = set()
|
||||
qs = self.get_queryset().annotate(
|
||||
items_list=Subquery(
|
||||
Quota.items.through.objects.filter(
|
||||
quota_id=OuterRef('pk'),
|
||||
item__variations__isnull=True,
|
||||
).order_by().values('quota_id').annotate(
|
||||
g=GroupConcat('item_id', separator=',', ordered=True)
|
||||
).values('g')
|
||||
),
|
||||
vars_list=Subquery(
|
||||
Quota.variations.through.objects.filter(
|
||||
quota_id=OuterRef('pk')
|
||||
).order_by().values('quota_id').annotate(
|
||||
g=GroupConcat(
|
||||
Concat(
|
||||
Cast(F('itemvariation__item_id'), output_field=models.TextField()),
|
||||
Value('-', output_field=models.TextField()),
|
||||
Cast(F('itemvariation_id'), output_field=models.TextField()),
|
||||
),
|
||||
separator=',',
|
||||
ordered=True
|
||||
)
|
||||
).values('g')
|
||||
),
|
||||
)
|
||||
|
||||
fields = {
|
||||
'name': 'name',
|
||||
'size': 'size',
|
||||
'subevent': 'subevent',
|
||||
'close_when_sold_out': 'close_when_sold_out',
|
||||
'release_after_exit': 'release_after_exit',
|
||||
'ignore_for_event_availability': 'ignore_for_event_availability',
|
||||
}
|
||||
for k, f in fields.items():
|
||||
existing_values = list(qs.order_by(f).values(f).annotate(c=Count('*')))
|
||||
if len(existing_values) == 1:
|
||||
initial[k] = existing_values[0][f]
|
||||
elif len(existing_values) > 1:
|
||||
mixed_values.add(k)
|
||||
initial[k] = None
|
||||
|
||||
item_values = list(qs.order_by("items_list").values("items_list").annotate(c=Count('*')))
|
||||
var_values = list(qs.order_by("vars_list").values("vars_list").annotate(c=Count('*')))
|
||||
if len(item_values) > 1 or len(var_values) > 1:
|
||||
mixed_values.add("itemvars")
|
||||
else:
|
||||
initial["itemvars"] = [iv for iv in (item_values[0]["items_list"] or "").split(",") + (var_values[0]["vars_list"] or "").split(",") if iv]
|
||||
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs['event'] = self.request.event
|
||||
kwargs['prefix'] = 'bulkedit'
|
||||
kwargs['initial'] = initial
|
||||
kwargs['queryset'] = self.get_queryset()
|
||||
kwargs['mixed_values'] = mixed_values
|
||||
if not self.is_submitted:
|
||||
kwargs['data'] = None
|
||||
kwargs['files'] = None
|
||||
return kwargs
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('control:event.items.quotas', kwargs={
|
||||
'organizer': self.request.event.organizer.slug,
|
||||
'event': self.request.event.slug,
|
||||
})
|
||||
|
||||
@transaction.atomic()
|
||||
def form_valid(self, form):
|
||||
log_entries = []
|
||||
|
||||
# Main form
|
||||
form.save()
|
||||
data = {
|
||||
k: v
|
||||
for k, v in form.cleaned_data.items()
|
||||
if k in form.changed_data
|
||||
}
|
||||
data['_raw_bulk_data'] = self.request.POST.dict()
|
||||
for obj in self.get_queryset():
|
||||
log_entries.append(
|
||||
obj.log_action('pretix.event.quota.changed', data=data, user=self.request.user, save=False)
|
||||
)
|
||||
|
||||
LogEntry.bulk_create_and_postprocess(log_entries)
|
||||
|
||||
messages.success(self.request, _('Your changes have been saved.'))
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['quotas'] = self.get_queryset()
|
||||
ctx['bulk_selected'] = self.request.POST.getlist("_bulk")
|
||||
return ctx
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
form = self.get_form()
|
||||
is_valid = (
|
||||
self.is_submitted and
|
||||
form.is_valid()
|
||||
)
|
||||
if is_valid:
|
||||
return self.form_valid(form)
|
||||
else:
|
||||
if self.is_submitted:
|
||||
messages.error(self.request, _('We could not save your changes. See below for details.'))
|
||||
return self.form_invalid(form)
|
||||
|
||||
|
||||
class QuotaCreate(EventPermissionRequiredMixin, CreateView):
|
||||
model = Quota
|
||||
form_class = QuotaForm
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -19,22 +19,19 @@
|
||||
# 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 hmac
|
||||
import json
|
||||
from contextlib import contextmanager
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth import (
|
||||
BACKEND_SESSION_KEY, HASH_SESSION_KEY, get_user_model, load_backend, login,
|
||||
logout,
|
||||
BACKEND_SESSION_KEY, get_user_model, load_backend, login,
|
||||
)
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.contrib.auth.views import redirect_to_login
|
||||
from django.db import transaction
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.urls import reverse
|
||||
from django.utils.crypto import get_random_string, salted_hmac
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views import View
|
||||
@@ -233,15 +230,7 @@ class UserImpersonateView(AdministratorPermissionRequiredMixin, RecentAuthentica
|
||||
hijacked = self.object
|
||||
|
||||
hijack_history = request.session.get("hijack_history", [])
|
||||
hijack_history.append({
|
||||
"user": request.user.pk,
|
||||
# We include the auth_hash, because it is unguessable. So should an attacker gain an attack vector to
|
||||
# modify hijack_history, they can't just insert or change a user that shouldn't be there. We HMAC it
|
||||
# again, though, since we also do not want the auth_hash of the admin user to be in the session of an
|
||||
# unprivileged user to contain the risk if there is some leak of session data.
|
||||
"auth_hash": salted_hmac(key_salt=b"hijack-history-hash", value=request.session[HASH_SESSION_KEY],
|
||||
algorithm="sha256", secret=settings.SECRET_KEY).hexdigest(),
|
||||
})
|
||||
hijack_history.append(request.user._meta.pk.value_to_string(hijacker))
|
||||
|
||||
backend = get_used_backend(request)
|
||||
backend = f"{backend.__module__}.{backend.__class__.__name__}"
|
||||
@@ -270,21 +259,8 @@ class UserImpersonateStopView(LoginRequiredMixin, View):
|
||||
hijs = request.session['hijacker_session']
|
||||
hijack_history = request.session.get("hijack_history", [])
|
||||
hijacked = request.user
|
||||
prev_session = hijack_history.pop()
|
||||
hijacker = get_object_or_404(get_user_model(), pk=prev_session["user"])
|
||||
|
||||
expected_hash = salted_hmac(
|
||||
key_salt=b"hijack-history-hash",
|
||||
value=hijacker.get_session_auth_hash(),
|
||||
algorithm="sha256",
|
||||
secret=settings.SECRET_KEY
|
||||
).hexdigest()
|
||||
if not hmac.compare_digest(expected_hash, prev_session["auth_hash"]):
|
||||
# Could be an attacker-controlled hijack history, but could also be e.g. a password change of the admin user
|
||||
# that happened during the hijack session
|
||||
logout(request)
|
||||
return redirect_to_login(request.get_full_path())
|
||||
|
||||
user_pk = hijack_history.pop()
|
||||
hijacker = get_object_or_404(get_user_model(), pk=user_pk)
|
||||
backend = get_used_backend(request)
|
||||
backend = f"{backend.__module__}.{backend.__class__.__name__}"
|
||||
with signals.no_update_last_login(), keep_session_age(request.session):
|
||||
|
||||
@@ -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']
|
||||
|
||||
@@ -117,12 +117,17 @@ class GroupConcat(Aggregate):
|
||||
template = "%(function)s(%(distinct)s%(field)s::text, '%(separator)s' ORDER BY %(field)s::text ASC)"
|
||||
else:
|
||||
template = "%(function)s(%(distinct)s%(field)s::text, '%(separator)s')"
|
||||
return super().as_sql(
|
||||
|
||||
template, params = super().as_sql(
|
||||
compiler, connection,
|
||||
function='string_agg',
|
||||
template=template,
|
||||
**extra_context,
|
||||
)
|
||||
if self.ordered:
|
||||
# ordered statement requires field parameters twice
|
||||
params = params + params
|
||||
return template, params
|
||||
|
||||
|
||||
class ReplicaRouter:
|
||||
|
||||
@@ -23,12 +23,9 @@ import types
|
||||
from datetime import datetime
|
||||
from http import cookies
|
||||
|
||||
from django.core.exceptions import SuspiciousFileOperation
|
||||
from PIL import Image
|
||||
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
|
||||
|
||||
@@ -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
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
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
File diff suppressed because it is too large
Load Diff
@@ -12,7 +12,6 @@ anonymized
|
||||
Auth
|
||||
authentification
|
||||
authenticator
|
||||
Authenticator
|
||||
automatical
|
||||
availabilities
|
||||
backend
|
||||
@@ -23,7 +22,6 @@ barcodes
|
||||
Bcc
|
||||
BCC
|
||||
BezahlCode
|
||||
biometric
|
||||
BLIK
|
||||
blocklist
|
||||
BN
|
||||
@@ -58,7 +56,6 @@ EPS
|
||||
eps
|
||||
favicon
|
||||
filetype
|
||||
freeOTP
|
||||
frontend
|
||||
frontpage
|
||||
Galician
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -64,7 +64,7 @@ from pretix.base.timeframes import (
|
||||
from pretix.control.forms.widgets import Select2
|
||||
from pretix.helpers.filenames import safe_for_filename
|
||||
from pretix.helpers.iter import chunked_iterable
|
||||
from pretix.helpers.reportlab import PlainTextParagraph
|
||||
from pretix.helpers.reportlab import FontFallbackParagraph
|
||||
from pretix.helpers.templatetags.jsonfield import JSONExtract
|
||||
from pretix.plugins.reports.exporters import ReportlabExportMixin
|
||||
|
||||
@@ -344,7 +344,7 @@ class PDFCheckinList(ReportlabExportMixin, CheckInListMixin, BaseExporter):
|
||||
]
|
||||
|
||||
story = [
|
||||
PlainTextParagraph(
|
||||
FontFallbackParagraph(
|
||||
cl.name,
|
||||
headlinestyle
|
||||
),
|
||||
@@ -352,7 +352,7 @@ class PDFCheckinList(ReportlabExportMixin, CheckInListMixin, BaseExporter):
|
||||
if cl.subevent:
|
||||
story += [
|
||||
Spacer(1, 3 * mm),
|
||||
PlainTextParagraph(
|
||||
FontFallbackParagraph(
|
||||
'{} ({} {})'.format(
|
||||
cl.subevent.name,
|
||||
cl.subevent.get_date_range_display(),
|
||||
@@ -382,10 +382,10 @@ class PDFCheckinList(ReportlabExportMixin, CheckInListMixin, BaseExporter):
|
||||
headrowstyle.fontName = 'OpenSansBd'
|
||||
for q in questions:
|
||||
txt = str(q.question)
|
||||
p = PlainTextParagraph(txt, headrowstyle)
|
||||
p = FontFallbackParagraph(txt, headrowstyle)
|
||||
while p.wrap(colwidths[len(tdata[0])], 5000)[1] > 30 * mm:
|
||||
txt = txt[:len(txt) - 50] + "..."
|
||||
p = PlainTextParagraph(txt, headrowstyle)
|
||||
p = FontFallbackParagraph(txt, headrowstyle)
|
||||
tdata[0].append(p)
|
||||
|
||||
qs = self._get_queryset(cl, form_data)
|
||||
@@ -432,8 +432,8 @@ class PDFCheckinList(ReportlabExportMixin, CheckInListMixin, BaseExporter):
|
||||
CBFlowable(bool(op.last_checked_in)) if not op.blocked else '—',
|
||||
'✘' if op.order.status != Order.STATUS_PAID else '✔',
|
||||
op.order.code,
|
||||
PlainTextParagraph(name, self.get_style()),
|
||||
PlainTextParagraph(bleach.clean(str(item), tags={'br'}).strip().replace('<br>', '<br/>'), self.get_style()),
|
||||
FontFallbackParagraph(name, self.get_style()),
|
||||
FontFallbackParagraph(bleach.clean(str(item), tags={'br'}).strip().replace('<br>', '<br/>'), self.get_style()),
|
||||
]
|
||||
acache = {}
|
||||
if op.addon_to:
|
||||
@@ -444,10 +444,10 @@ class PDFCheckinList(ReportlabExportMixin, CheckInListMixin, BaseExporter):
|
||||
for q in questions:
|
||||
txt = acache.get(q.pk, '')
|
||||
txt = bleach.clean(txt, tags={'br'}).strip().replace('<br>', '<br/>')
|
||||
p = PlainTextParagraph(txt, self.get_style())
|
||||
p = FontFallbackParagraph(txt, self.get_style())
|
||||
while p.wrap(colwidths[len(row)], 5000)[1] > 50 * mm:
|
||||
txt = txt[:len(txt) - 50] + "..."
|
||||
p = PlainTextParagraph(txt, self.get_style())
|
||||
p = FontFallbackParagraph(txt, self.get_style())
|
||||
row.append(p)
|
||||
if op.order.status != Order.STATUS_PAID:
|
||||
tstyledata += [
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
|
||||
import json
|
||||
import logging
|
||||
import urllib.parse
|
||||
from collections import OrderedDict
|
||||
from decimal import Decimal
|
||||
|
||||
@@ -41,6 +42,7 @@ import paypalrestsdk
|
||||
import paypalrestsdk.exceptions
|
||||
from django import forms
|
||||
from django.contrib import messages
|
||||
from django.core import signing
|
||||
from django.http import HttpRequest
|
||||
from django.template.loader import get_template
|
||||
from django.urls import reverse
|
||||
@@ -56,7 +58,6 @@ from pretix.base.forms import SecretKeySettingsField
|
||||
from pretix.base.models import Event, Order, OrderPayment, OrderRefund, Quota
|
||||
from pretix.base.payment import BasePaymentProvider, PaymentException
|
||||
from pretix.base.settings import SettingsSandbox
|
||||
from pretix.base.views.redirect import safelink
|
||||
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||
from pretix.plugins.paypal.api import Api
|
||||
from pretix.plugins.paypal.models import ReferencedPayPalObject
|
||||
@@ -347,9 +348,14 @@ class Paypal(BasePaymentProvider):
|
||||
request.session['payment_paypal_id'] = payment.id
|
||||
for link in payment.links:
|
||||
if link.method == "REDIRECT" and link.rel == "approval_url":
|
||||
return safelink(link.href, framebreak=True)
|
||||
else:
|
||||
return str(link.href)
|
||||
if request.session.get('iframe_session', False):
|
||||
signer = signing.Signer(salt='safe-redirect')
|
||||
return (
|
||||
build_absolute_uri(request.event, 'plugins:paypal:redirect') + '?url=' +
|
||||
urllib.parse.quote(signer.sign(link.href))
|
||||
)
|
||||
else:
|
||||
return str(link.href)
|
||||
else:
|
||||
messages.error(request, _('We had trouble communicating with PayPal'))
|
||||
logger.error('Error on creating payment: ' + str(payment.error))
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
{% load compress %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{{ settings.PRETIX_INSTANCE_NAME }}</title>
|
||||
{% compress css %}
|
||||
<link rel="stylesheet" type="text/x-scss" href="{% static "pretixbase/scss/cachedfiles.scss" %}"/>
|
||||
{% endcompress %}
|
||||
{% compress js %}
|
||||
<script type="text/javascript" src="{% static "jquery/js/jquery-3.6.4.min.js" %}"></script>
|
||||
{% endcompress %}
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>{% trans "The payment process has started in a new window." %}</h1>
|
||||
|
||||
<p>
|
||||
{% trans "The window to enter your payment data was not opened or was closed?" %}
|
||||
</p>
|
||||
<p>
|
||||
<a href="{{ url }}" target="_blank" class="btn btn-default btn-lg">
|
||||
<span class="fa fa-external-link-square"></span>
|
||||
{% trans "Click here in order to open the window." %}
|
||||
</a>
|
||||
</p>
|
||||
<script>
|
||||
window.open('{{ url|escapejs }}');
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -21,12 +21,13 @@
|
||||
#
|
||||
from django.urls import include, re_path
|
||||
|
||||
from .views import abort, oauth_disconnect, success
|
||||
from .views import abort, oauth_disconnect, redirect_view, success
|
||||
|
||||
event_patterns = [
|
||||
re_path(r'^paypal/', include([
|
||||
re_path(r'^abort/$', abort, name='abort'),
|
||||
re_path(r'^return/$', success, name='return'),
|
||||
re_path(r'^redirect/$', redirect_view, name='redirect'),
|
||||
|
||||
re_path(r'w/(?P<cart_namespace>[a-zA-Z0-9]{16})/abort/', abort, name='abort'),
|
||||
re_path(r'w/(?P<cart_namespace>[a-zA-Z0-9]{16})/return/', success, name='return'),
|
||||
|
||||
@@ -39,10 +39,13 @@ from decimal import Decimal
|
||||
import paypalrestsdk
|
||||
import paypalrestsdk.exceptions
|
||||
from django.contrib import messages
|
||||
from django.core import signing
|
||||
from django.db.models import Sum
|
||||
from django.http import HttpResponse
|
||||
from django.http import HttpResponse, HttpResponseBadRequest
|
||||
from django.shortcuts import render
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.decorators.clickjacking import xframe_options_exempt
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.views.decorators.http import require_POST
|
||||
from django_scopes import scopes_disabled
|
||||
@@ -58,6 +61,21 @@ from pretix.plugins.paypal.payment import Paypal
|
||||
logger = logging.getLogger('pretix.plugins.paypal')
|
||||
|
||||
|
||||
@xframe_options_exempt
|
||||
def redirect_view(request, *args, **kwargs):
|
||||
signer = signing.Signer(salt='safe-redirect')
|
||||
try:
|
||||
url = signer.unsign(request.GET.get('url', ''))
|
||||
except signing.BadSignature:
|
||||
return HttpResponseBadRequest('Invalid parameter')
|
||||
|
||||
r = render(request, 'pretixplugins/paypal/redirect.html', {
|
||||
'url': url,
|
||||
})
|
||||
r._csp_ignore = True
|
||||
return r
|
||||
|
||||
|
||||
def success(request, *args, **kwargs):
|
||||
pid = request.GET.get('paymentId')
|
||||
token = request.GET.get('token')
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
{% load compress %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{{ settings.PRETIX_INSTANCE_NAME }}</title>
|
||||
{% compress css %}
|
||||
<link rel="stylesheet" type="text/x-scss" href="{% static "pretixbase/scss/cachedfiles.scss" %}"/>
|
||||
{% endcompress %}
|
||||
{% compress js %}
|
||||
<script type="text/javascript" src="{% static "jquery/js/jquery-3.6.4.min.js" %}"></script>
|
||||
{% endcompress %}
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>{% trans "The payment process has started in a new window." %}</h1>
|
||||
|
||||
<p>
|
||||
{% trans "The window to enter your payment data was not opened or was closed?" %}
|
||||
</p>
|
||||
<p>
|
||||
<a href="{{ url }}" target="_blank" class="btn btn-default btn-lg">
|
||||
<span class="fa fa-external-link-square"></span>
|
||||
{% trans "Click here in order to open the window." %}
|
||||
</a>
|
||||
</p>
|
||||
<script>
|
||||
window.open('{{ url|escapejs }}');
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -22,13 +22,15 @@
|
||||
from django.urls import include, re_path
|
||||
|
||||
from .views import (
|
||||
PayView, XHRView, abort, isu_disconnect, isu_return, success, webhook,
|
||||
PayView, XHRView, abort, isu_disconnect, isu_return, redirect_view,
|
||||
success, webhook,
|
||||
)
|
||||
|
||||
event_patterns = [
|
||||
re_path(r'^paypal2/', include([
|
||||
re_path(r'^abort/$', abort, name='abort'),
|
||||
re_path(r'^return/$', success, name='return'),
|
||||
re_path(r'^redirect/$', redirect_view, name='redirect'),
|
||||
re_path(r'^xhr/$', XHRView.as_view(), name='xhr'),
|
||||
re_path(r'^pay/(?P<order>[^/]+)/(?P<hash>[^/]+)/(?P<payment>[^/]+)/$', PayView.as_view(), name='pay'),
|
||||
re_path(r'^(?P<order>[^/][^w]+)/(?P<secret>[A-Za-z0-9]+)/xhr/$', XHRView.as_view(), name='xhr'),
|
||||
|
||||
@@ -36,10 +36,13 @@ import logging
|
||||
from decimal import Decimal
|
||||
|
||||
from django.contrib import messages
|
||||
from django.core import signing
|
||||
from django.core.cache import cache
|
||||
from django.db import transaction
|
||||
from django.db.models import Sum
|
||||
from django.http import Http404, HttpResponse, JsonResponse
|
||||
from django.http import (
|
||||
Http404, HttpResponse, HttpResponseBadRequest, JsonResponse,
|
||||
)
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
from django.utils.decorators import method_decorator
|
||||
@@ -101,6 +104,21 @@ class PaypalOrderView:
|
||||
}) + ('?paid=yes' if self.order.status == Order.STATUS_PAID else ''))
|
||||
|
||||
|
||||
@xframe_options_exempt
|
||||
def redirect_view(request, *args, **kwargs):
|
||||
signer = signing.Signer(salt='safe-redirect')
|
||||
try:
|
||||
url = signer.unsign(request.GET.get('url', ''))
|
||||
except signing.BadSignature:
|
||||
return HttpResponseBadRequest('Invalid parameter')
|
||||
|
||||
r = render(request, 'pretixplugins/paypal2/redirect.html', {
|
||||
'url': url,
|
||||
})
|
||||
r._csp_ignore = True
|
||||
return r
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name='dispatch')
|
||||
@method_decorator(xframe_options_exempt, 'dispatch')
|
||||
class XHRView(View):
|
||||
|
||||
@@ -36,7 +36,7 @@ from reportlab.lib import colors, pagesizes
|
||||
from reportlab.lib.enums import TA_CENTER, TA_RIGHT
|
||||
from reportlab.lib.units import mm
|
||||
from reportlab.platypus import (
|
||||
KeepTogether, PageTemplate, Spacer, Table, TableStyle,
|
||||
KeepTogether, PageTemplate, Paragraph, Spacer, Table, TableStyle,
|
||||
)
|
||||
|
||||
from pretix.base.exporter import BaseExporter
|
||||
@@ -49,7 +49,7 @@ from pretix.base.timeframes import (
|
||||
resolve_timeframe_to_datetime_start_inclusive_end_exclusive,
|
||||
)
|
||||
from pretix.control.forms.filter import get_all_payment_providers
|
||||
from pretix.helpers.reportlab import PlainTextParagraph
|
||||
from pretix.helpers.reportlab import FontFallbackParagraph
|
||||
from pretix.plugins.reports.exporters import ReportlabExportMixin
|
||||
|
||||
|
||||
@@ -311,13 +311,13 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
|
||||
|
||||
tdata = [
|
||||
[
|
||||
PlainTextParagraph(self._transaction_group_header_label(), tstyle_bold),
|
||||
PlainTextParagraph(_("Price"), tstyle_bold_right),
|
||||
PlainTextParagraph(_("Tax rate"), tstyle_bold_right),
|
||||
PlainTextParagraph("#", tstyle_bold_right),
|
||||
PlainTextParagraph(_("Net total"), tstyle_bold_right),
|
||||
PlainTextParagraph(_("Tax total"), tstyle_bold_right),
|
||||
PlainTextParagraph(_("Gross total"), tstyle_bold_right),
|
||||
FontFallbackParagraph(self._transaction_group_header_label(), tstyle_bold),
|
||||
FontFallbackParagraph(_("Price"), tstyle_bold_right),
|
||||
FontFallbackParagraph(_("Tax rate"), tstyle_bold_right),
|
||||
FontFallbackParagraph("#", tstyle_bold_right),
|
||||
FontFallbackParagraph(_("Net total"), tstyle_bold_right),
|
||||
FontFallbackParagraph(_("Tax total"), tstyle_bold_right),
|
||||
FontFallbackParagraph(_("Gross total"), tstyle_bold_right),
|
||||
]
|
||||
]
|
||||
|
||||
@@ -347,12 +347,12 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
|
||||
|
||||
if e != last_group:
|
||||
if last_group_head_idx > 0 and e is not None:
|
||||
tdata[last_group_head_idx][4] = PlainTextParagraph(money_filter(sum_price_by_group - sum_tax_by_group, currency), tstyle_bold_right),
|
||||
tdata[last_group_head_idx][5] = PlainTextParagraph(money_filter(sum_tax_by_group, currency), tstyle_bold_right),
|
||||
tdata[last_group_head_idx][6] = PlainTextParagraph(money_filter(sum_price_by_group, currency), tstyle_bold_right),
|
||||
tdata[last_group_head_idx][4] = Paragraph(money_filter(sum_price_by_group - sum_tax_by_group, currency), tstyle_bold_right),
|
||||
tdata[last_group_head_idx][5] = Paragraph(money_filter(sum_tax_by_group, currency), tstyle_bold_right),
|
||||
tdata[last_group_head_idx][6] = Paragraph(money_filter(sum_price_by_group, currency), tstyle_bold_right),
|
||||
tdata.append(
|
||||
[
|
||||
PlainTextParagraph(
|
||||
FontFallbackParagraph(
|
||||
e,
|
||||
tstyle_bold,
|
||||
),
|
||||
@@ -375,20 +375,20 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
|
||||
text = self._transaction_row_label(r)
|
||||
tdata.append(
|
||||
[
|
||||
PlainTextParagraph(text, tstyle),
|
||||
PlainTextParagraph(
|
||||
FontFallbackParagraph(text, tstyle),
|
||||
Paragraph(
|
||||
money_filter(r["price"], currency)
|
||||
if "price" in r and r["price"] is not None
|
||||
else "",
|
||||
tstyle_right,
|
||||
),
|
||||
PlainTextParagraph(localize(r["tax_rate"].normalize()) + " %", tstyle_right),
|
||||
PlainTextParagraph(str(r["sum_cont"]), tstyle_right),
|
||||
PlainTextParagraph(
|
||||
Paragraph(localize(r["tax_rate"].normalize()) + " %", tstyle_right),
|
||||
Paragraph(str(r["sum_cont"]), tstyle_right),
|
||||
Paragraph(
|
||||
money_filter(r["sum_price"] - r["sum_tax"], currency), tstyle_right
|
||||
),
|
||||
PlainTextParagraph(money_filter(r["sum_tax"], currency), tstyle_right),
|
||||
PlainTextParagraph(money_filter(r["sum_price"], currency), tstyle_right),
|
||||
Paragraph(money_filter(r["sum_tax"], currency), tstyle_right),
|
||||
Paragraph(money_filter(r["sum_price"], currency), tstyle_right),
|
||||
]
|
||||
)
|
||||
sum_cnt_by_tax_rate[r["tax_rate"]] += r["sum_cont"]
|
||||
@@ -398,19 +398,19 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
|
||||
sum_tax_by_group += r["sum_tax"]
|
||||
|
||||
if last_group_head_idx > 0 and last_group is not None:
|
||||
tdata[last_group_head_idx][4] = PlainTextParagraph(money_filter(sum_price_by_group - sum_tax_by_group, currency), tstyle_bold_right),
|
||||
tdata[last_group_head_idx][5] = PlainTextParagraph(money_filter(sum_tax_by_group, currency), tstyle_bold_right),
|
||||
tdata[last_group_head_idx][6] = PlainTextParagraph(money_filter(sum_price_by_group, currency), tstyle_bold_right),
|
||||
tdata[last_group_head_idx][4] = Paragraph(money_filter(sum_price_by_group - sum_tax_by_group, currency), tstyle_bold_right),
|
||||
tdata[last_group_head_idx][5] = Paragraph(money_filter(sum_tax_by_group, currency), tstyle_bold_right),
|
||||
tdata[last_group_head_idx][6] = Paragraph(money_filter(sum_price_by_group, currency), tstyle_bold_right),
|
||||
|
||||
if len(sum_tax_by_tax_rate) > 1:
|
||||
for tax_rate in sorted(sum_tax_by_tax_rate.keys(), reverse=True):
|
||||
tdata.append(
|
||||
[
|
||||
PlainTextParagraph(_("Sum"), tstyle),
|
||||
PlainTextParagraph("", tstyle_right),
|
||||
PlainTextParagraph(localize(tax_rate.normalize()) + " %", tstyle_right),
|
||||
PlainTextParagraph("", tstyle_right),
|
||||
PlainTextParagraph(
|
||||
FontFallbackParagraph(_("Sum"), tstyle),
|
||||
Paragraph("", tstyle_right),
|
||||
Paragraph(localize(tax_rate.normalize()) + " %", tstyle_right),
|
||||
Paragraph("", tstyle_right),
|
||||
Paragraph(
|
||||
money_filter(
|
||||
sum_price_by_tax_rate[tax_rate]
|
||||
- sum_tax_by_tax_rate[tax_rate],
|
||||
@@ -418,10 +418,10 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
|
||||
),
|
||||
tstyle_right,
|
||||
),
|
||||
PlainTextParagraph(
|
||||
Paragraph(
|
||||
money_filter(sum_tax_by_tax_rate[tax_rate], currency), tstyle_right
|
||||
),
|
||||
PlainTextParagraph(
|
||||
Paragraph(
|
||||
money_filter(sum_price_by_tax_rate[tax_rate], currency),
|
||||
tstyle_right,
|
||||
),
|
||||
@@ -439,11 +439,11 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
|
||||
|
||||
tdata.append(
|
||||
[
|
||||
PlainTextParagraph(_("Sum"), tstyle_bold),
|
||||
PlainTextParagraph("", tstyle_right),
|
||||
PlainTextParagraph("", tstyle_right),
|
||||
PlainTextParagraph("", tstyle_bold_right),
|
||||
PlainTextParagraph(
|
||||
FontFallbackParagraph(_("Sum"), tstyle_bold),
|
||||
Paragraph("", tstyle_right),
|
||||
Paragraph("", tstyle_right),
|
||||
Paragraph("", tstyle_bold_right),
|
||||
Paragraph(
|
||||
money_filter(
|
||||
sum(sum_price_by_tax_rate.values())
|
||||
- sum(sum_tax_by_tax_rate.values()),
|
||||
@@ -451,11 +451,11 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
|
||||
),
|
||||
tstyle_bold_right,
|
||||
),
|
||||
PlainTextParagraph(
|
||||
Paragraph(
|
||||
money_filter(sum(sum_tax_by_tax_rate.values()), currency),
|
||||
tstyle_bold_right,
|
||||
),
|
||||
PlainTextParagraph(
|
||||
Paragraph(
|
||||
money_filter(sum(sum_price_by_tax_rate.values()), currency),
|
||||
tstyle_bold_right,
|
||||
),
|
||||
@@ -493,10 +493,10 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
|
||||
|
||||
tdata = [
|
||||
[
|
||||
PlainTextParagraph(_("Payment method"), tstyle_bold),
|
||||
PlainTextParagraph(_("Payments"), tstyle_bold_right),
|
||||
PlainTextParagraph(_("Refunds"), tstyle_bold_right),
|
||||
PlainTextParagraph(_("Total"), tstyle_bold_right),
|
||||
FontFallbackParagraph(_("Payment method"), tstyle_bold),
|
||||
FontFallbackParagraph(_("Payments"), tstyle_bold_right),
|
||||
FontFallbackParagraph(_("Refunds"), tstyle_bold_right),
|
||||
FontFallbackParagraph(_("Total"), tstyle_bold_right),
|
||||
]
|
||||
]
|
||||
|
||||
@@ -537,20 +537,20 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
|
||||
for p in providers:
|
||||
tdata.append(
|
||||
[
|
||||
PlainTextParagraph(provider_names.get(p, p), tstyle),
|
||||
PlainTextParagraph(
|
||||
Paragraph(provider_names.get(p, p), tstyle),
|
||||
FontFallbackParagraph(
|
||||
money_filter(payments_by_provider[p], currency)
|
||||
if p in payments_by_provider
|
||||
else "",
|
||||
tstyle_right,
|
||||
),
|
||||
PlainTextParagraph(
|
||||
Paragraph(
|
||||
money_filter(refunds_by_provider[p], currency)
|
||||
if p in refunds_by_provider
|
||||
else "",
|
||||
tstyle_right,
|
||||
),
|
||||
PlainTextParagraph(
|
||||
Paragraph(
|
||||
money_filter(
|
||||
payments_by_provider.get(p, Decimal("0.00"))
|
||||
- refunds_by_provider.get(p, Decimal("0.00")),
|
||||
@@ -563,20 +563,20 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
|
||||
|
||||
tdata.append(
|
||||
[
|
||||
PlainTextParagraph(_("Sum"), tstyle_bold),
|
||||
PlainTextParagraph(
|
||||
FontFallbackParagraph(_("Sum"), tstyle_bold),
|
||||
Paragraph(
|
||||
money_filter(
|
||||
sum(payments_by_provider.values(), Decimal("0.00")), currency
|
||||
),
|
||||
tstyle_bold_right,
|
||||
),
|
||||
PlainTextParagraph(
|
||||
Paragraph(
|
||||
money_filter(
|
||||
sum(refunds_by_provider.values(), Decimal("0.00")), currency
|
||||
),
|
||||
tstyle_bold_right,
|
||||
),
|
||||
PlainTextParagraph(
|
||||
Paragraph(
|
||||
money_filter(
|
||||
sum(payments_by_provider.values(), Decimal("0.00"))
|
||||
- sum(refunds_by_provider.values(), Decimal("0.00")),
|
||||
@@ -641,7 +641,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
|
||||
open_before = tx_before - p_before + r_before
|
||||
tdata.append(
|
||||
[
|
||||
PlainTextParagraph(
|
||||
FontFallbackParagraph(
|
||||
_("Pending payments at {datetime}").format(
|
||||
datetime=date_format(
|
||||
(df_start - datetime.timedelta.resolution).astimezone(
|
||||
@@ -653,7 +653,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
|
||||
tstyle,
|
||||
),
|
||||
"",
|
||||
PlainTextParagraph(money_filter(open_before, currency), tstyle_right),
|
||||
Paragraph(money_filter(open_before, currency), tstyle_right),
|
||||
]
|
||||
)
|
||||
else:
|
||||
@@ -670,30 +670,30 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
|
||||
] or Decimal("0.00")
|
||||
tdata.append(
|
||||
[
|
||||
PlainTextParagraph(_("Orders"), tstyle),
|
||||
PlainTextParagraph("+", tstyle_center),
|
||||
PlainTextParagraph(money_filter(tx_during, currency), tstyle_right),
|
||||
FontFallbackParagraph(_("Orders"), tstyle),
|
||||
Paragraph("+", tstyle_center),
|
||||
Paragraph(money_filter(tx_during, currency), tstyle_right),
|
||||
]
|
||||
)
|
||||
tdata.append(
|
||||
[
|
||||
PlainTextParagraph(_("Payments"), tstyle),
|
||||
PlainTextParagraph("-", tstyle_center),
|
||||
PlainTextParagraph(money_filter(p_during, currency), tstyle_right),
|
||||
FontFallbackParagraph(_("Payments"), tstyle),
|
||||
Paragraph("-", tstyle_center),
|
||||
Paragraph(money_filter(p_during, currency), tstyle_right),
|
||||
]
|
||||
)
|
||||
tdata.append(
|
||||
[
|
||||
PlainTextParagraph(_("Refunds"), tstyle),
|
||||
PlainTextParagraph("+", tstyle_center),
|
||||
PlainTextParagraph(money_filter(r_during, currency), tstyle_right),
|
||||
FontFallbackParagraph(_("Refunds"), tstyle),
|
||||
Paragraph("+", tstyle_center),
|
||||
Paragraph(money_filter(r_during, currency), tstyle_right),
|
||||
]
|
||||
)
|
||||
|
||||
open_after = open_before + tx_during - p_during + r_during
|
||||
tdata.append(
|
||||
[
|
||||
PlainTextParagraph(
|
||||
Paragraph(
|
||||
_("Pending payments at {datetime}").format(
|
||||
datetime=date_format(
|
||||
((df_end or now()) - datetime.timedelta.resolution).astimezone(
|
||||
@@ -704,8 +704,8 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
|
||||
),
|
||||
tstyle_bold,
|
||||
),
|
||||
PlainTextParagraph("=", tstyle_center),
|
||||
PlainTextParagraph(money_filter(open_after, currency), tstyle_bold_right),
|
||||
Paragraph("=", tstyle_center),
|
||||
Paragraph(money_filter(open_after, currency), tstyle_bold_right),
|
||||
]
|
||||
)
|
||||
tstyledata += [
|
||||
@@ -752,7 +752,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
|
||||
)
|
||||
tdata.append(
|
||||
[
|
||||
PlainTextParagraph(
|
||||
Paragraph(
|
||||
_("Total gift card value at {datetime}").format(
|
||||
datetime=date_format(
|
||||
(df_start - datetime.timedelta.resolution).astimezone(
|
||||
@@ -763,7 +763,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
|
||||
),
|
||||
tstyle,
|
||||
),
|
||||
PlainTextParagraph(money_filter(tx_before, currency), tstyle_right),
|
||||
Paragraph(money_filter(tx_before, currency), tstyle_right),
|
||||
]
|
||||
)
|
||||
else:
|
||||
@@ -774,8 +774,8 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
|
||||
] or Decimal("0.00")
|
||||
tdata.append(
|
||||
[
|
||||
PlainTextParagraph(_("Gift card transactions (credit)"), tstyle),
|
||||
PlainTextParagraph(money_filter(tx_during_pos, currency), tstyle_right),
|
||||
FontFallbackParagraph(_("Gift card transactions (credit)"), tstyle),
|
||||
Paragraph(money_filter(tx_during_pos, currency), tstyle_right),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -784,15 +784,15 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
|
||||
] or Decimal("0.00")
|
||||
tdata.append(
|
||||
[
|
||||
PlainTextParagraph(_("Gift card transactions (debit)"), tstyle),
|
||||
PlainTextParagraph(money_filter(tx_during_neg, currency), tstyle_right),
|
||||
FontFallbackParagraph(_("Gift card transactions (debit)"), tstyle),
|
||||
Paragraph(money_filter(tx_during_neg, currency), tstyle_right),
|
||||
]
|
||||
)
|
||||
|
||||
open_after = tx_before + tx_during_pos + tx_during_neg
|
||||
tdata.append(
|
||||
[
|
||||
PlainTextParagraph(
|
||||
Paragraph(
|
||||
_("Total gift card value at {datetime}").format(
|
||||
datetime=date_format(
|
||||
((df_end or now()) - datetime.timedelta.resolution).astimezone(
|
||||
@@ -803,7 +803,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
|
||||
),
|
||||
tstyle_bold,
|
||||
),
|
||||
PlainTextParagraph(money_filter(open_after, currency), tstyle_bold_right),
|
||||
Paragraph(money_filter(open_after, currency), tstyle_bold_right),
|
||||
]
|
||||
)
|
||||
tstyledata += [
|
||||
@@ -854,10 +854,10 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
|
||||
style_small.leading = 10
|
||||
|
||||
story = [
|
||||
PlainTextParagraph(self.verbose_name, style_h1),
|
||||
FontFallbackParagraph(self.verbose_name, style_h1),
|
||||
Spacer(0, 3 * mm),
|
||||
PlainTextParagraph(
|
||||
"\n".join(escape(f) for f in self.describe_filters(form_data)),
|
||||
FontFallbackParagraph(
|
||||
"<br />".join(escape(f) for f in self.describe_filters(form_data)),
|
||||
style_small,
|
||||
),
|
||||
]
|
||||
@@ -870,7 +870,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
|
||||
if s:
|
||||
story += [
|
||||
Spacer(0, 3 * mm),
|
||||
PlainTextParagraph(_("Orders") + c_head, style_h2),
|
||||
FontFallbackParagraph(_("Orders") + c_head, style_h2),
|
||||
Spacer(0, 3 * mm),
|
||||
*s
|
||||
]
|
||||
@@ -881,7 +881,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
|
||||
if s:
|
||||
story += [
|
||||
Spacer(0, 8 * mm),
|
||||
PlainTextParagraph(_("Payments") + c_head, style_h2),
|
||||
FontFallbackParagraph(_("Payments") + c_head, style_h2),
|
||||
Spacer(0, 3 * mm),
|
||||
*s
|
||||
]
|
||||
@@ -894,7 +894,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
|
||||
Spacer(0, 8 * mm),
|
||||
KeepTogether(
|
||||
[
|
||||
PlainTextParagraph(_("Open items") + c_head, style_h2),
|
||||
FontFallbackParagraph(_("Open items") + c_head, style_h2),
|
||||
Spacer(0, 3 * mm),
|
||||
*s
|
||||
]
|
||||
@@ -912,7 +912,7 @@ class ReportExporter(ReportlabExportMixin, BaseExporter):
|
||||
Spacer(0, 8 * mm),
|
||||
KeepTogether(
|
||||
[
|
||||
PlainTextParagraph(_("Gift cards") + c_head, style_h2),
|
||||
FontFallbackParagraph(_("Gift cards") + c_head, style_h2),
|
||||
Spacer(0, 3 * mm),
|
||||
*s,
|
||||
]
|
||||
|
||||
@@ -70,7 +70,7 @@ from pretix.base.timeframes import (
|
||||
)
|
||||
from pretix.control.forms.filter import OverviewFilterForm
|
||||
from pretix.helpers.reportlab import (
|
||||
PlainTextParagraph, register_ttf_font_if_new,
|
||||
FontFallbackParagraph, register_ttf_font_if_new,
|
||||
)
|
||||
from pretix.presale.style import get_fonts
|
||||
|
||||
@@ -282,7 +282,7 @@ class OverviewReport(Report):
|
||||
headlinestyle.fontSize = 15
|
||||
headlinestyle.fontName = 'OpenSansBd'
|
||||
story = [
|
||||
PlainTextParagraph(_('Orders by product') + ' ' + (_('(excl. taxes)') if net else _('(incl. taxes)')), headlinestyle),
|
||||
FontFallbackParagraph(_('Orders by product') + ' ' + (_('(excl. taxes)') if net else _('(incl. taxes)')), headlinestyle),
|
||||
Spacer(1, 5 * mm)
|
||||
]
|
||||
return story
|
||||
@@ -292,7 +292,7 @@ class OverviewReport(Report):
|
||||
if form_data.get('date_axis') and form_data.get('date_range'):
|
||||
d_start, d_end = resolve_timeframe_to_dates_inclusive(now(), form_data['date_range'], self.timezone)
|
||||
story += [
|
||||
PlainTextParagraph(_('{axis} between {start} and {end}').format(
|
||||
FontFallbackParagraph(_('{axis} between {start} and {end}').format(
|
||||
axis=dict(OverviewFilterForm(event=self.event).fields['date_axis'].choices)[form_data.get('date_axis')],
|
||||
start=date_format(d_start, 'SHORT_DATE_FORMAT') if d_start else '–',
|
||||
end=date_format(d_end, 'SHORT_DATE_FORMAT') if d_end else '–',
|
||||
@@ -305,13 +305,13 @@ class OverviewReport(Report):
|
||||
subevent = self.event.subevents.get(pk=self.form_data.get('subevent'))
|
||||
except SubEvent.DoesNotExist:
|
||||
subevent = self.form_data.get('subevent')
|
||||
story.append(PlainTextParagraph(pgettext('subevent', 'Date: {}').format(subevent), self.get_style()))
|
||||
story.append(FontFallbackParagraph(pgettext('subevent', 'Date: {}').format(subevent), self.get_style()))
|
||||
story.append(Spacer(1, 5 * mm))
|
||||
|
||||
if form_data.get('subevent_date_range'):
|
||||
d_start, d_end = resolve_timeframe_to_datetime_start_inclusive_end_exclusive(now(), form_data['subevent_date_range'], self.timezone)
|
||||
story += [
|
||||
PlainTextParagraph(_('{axis} between {start} and {end}').format(
|
||||
FontFallbackParagraph(_('{axis} between {start} and {end}').format(
|
||||
axis=_('Event date'),
|
||||
start=date_format(d_start, 'SHORT_DATE_FORMAT') if d_start else '–',
|
||||
end=date_format(d_end - timedelta(hours=1), 'SHORT_DATE_FORMAT') if d_end else '–',
|
||||
@@ -384,13 +384,13 @@ class OverviewReport(Report):
|
||||
tdata = [
|
||||
[
|
||||
_('Product'),
|
||||
PlainTextParagraph(_('Canceled'), tstyle_th),
|
||||
FontFallbackParagraph(_('Canceled'), tstyle_th),
|
||||
'',
|
||||
PlainTextParagraph(_('Expired'), tstyle_th),
|
||||
FontFallbackParagraph(_('Expired'), tstyle_th),
|
||||
'',
|
||||
PlainTextParagraph(_('Approval pending'), tstyle_th),
|
||||
FontFallbackParagraph(_('Approval pending'), tstyle_th),
|
||||
'',
|
||||
PlainTextParagraph(_('Purchased'), tstyle_th),
|
||||
FontFallbackParagraph(_('Purchased'), tstyle_th),
|
||||
'', '', '', '', ''
|
||||
],
|
||||
[
|
||||
@@ -421,14 +421,14 @@ class OverviewReport(Report):
|
||||
for tup in items_by_category:
|
||||
if tup[0]:
|
||||
tdata.append([
|
||||
PlainTextParagraph(str(tup[0]), tstyle_bold)
|
||||
FontFallbackParagraph(str(tup[0]), tstyle_bold)
|
||||
])
|
||||
for l, s in states:
|
||||
tdata[-1].append(str(tup[0].num[l][0]))
|
||||
tdata[-1].append(floatformat(tup[0].num[l][2 if net else 1], places))
|
||||
for item in tup[1]:
|
||||
tdata.append([
|
||||
PlainTextParagraph(str(item), tstyle)
|
||||
FontFallbackParagraph(str(item), tstyle)
|
||||
])
|
||||
for l, s in states:
|
||||
tdata[-1].append(str(item.num[l][0]))
|
||||
@@ -436,7 +436,7 @@ class OverviewReport(Report):
|
||||
if item.has_variations:
|
||||
for var in item.all_variations:
|
||||
tdata.append([
|
||||
PlainTextParagraph(" " + str(var), tstyle)
|
||||
FontFallbackParagraph(" " + str(var), tstyle)
|
||||
])
|
||||
for l, s in states:
|
||||
tdata[-1].append(str(var.num[l][0]))
|
||||
@@ -568,7 +568,7 @@ class OrderTaxListReportPDF(Report):
|
||||
tstyledata.append(('SPAN', (5 + 2 * i, 0), (6 + 2 * i, 0)))
|
||||
|
||||
story = [
|
||||
PlainTextParagraph(_('Orders by tax rate ({currency})').format(currency=self.event.currency), headlinestyle),
|
||||
FontFallbackParagraph(_('Orders by tax rate ({currency})').format(currency=self.event.currency), headlinestyle),
|
||||
Spacer(1, 5 * mm)
|
||||
]
|
||||
tdata = [
|
||||
|
||||
@@ -46,6 +46,7 @@ import stripe
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.core import signing
|
||||
from django.db import transaction
|
||||
from django.http import HttpRequest
|
||||
from django.template.loader import get_template
|
||||
@@ -71,7 +72,6 @@ from pretix.base.payment import (
|
||||
)
|
||||
from pretix.base.plugins import get_all_plugins
|
||||
from pretix.base.settings import SettingsSandbox
|
||||
from pretix.base.views.redirect import safelink
|
||||
from pretix.helpers import OF_SELF
|
||||
from pretix.helpers.countries import CachedCountries
|
||||
from pretix.helpers.http import get_client_ip
|
||||
@@ -745,7 +745,15 @@ class StripeMethod(BasePaymentProvider):
|
||||
|
||||
def redirect(self, request, url):
|
||||
if request.session.get('iframe_session', False):
|
||||
return safelink(url, framebreak=True)
|
||||
return (
|
||||
build_absolute_uri(request.event, 'plugins:stripe:redirect') +
|
||||
'?data=' + signing.dumps({
|
||||
'url': url,
|
||||
'session': {
|
||||
'payment_stripe_order_secret': request.session['payment_stripe_order_secret'],
|
||||
},
|
||||
}, salt='safe-redirect')
|
||||
)
|
||||
else:
|
||||
return str(url)
|
||||
|
||||
@@ -1045,7 +1053,11 @@ class StripeMethod(BasePaymentProvider):
|
||||
'hash': payment.order.tagged_secret('plugins:stripe'),
|
||||
})
|
||||
if not self.redirect_in_widget_allowed and request.session.get('iframe_session', False):
|
||||
return safelink(url, framebreak=True)
|
||||
return build_absolute_uri(self.event, 'plugins:stripe:redirect') + '?data=' + signing.dumps({
|
||||
'url': url,
|
||||
'session': {},
|
||||
}, salt='safe-redirect')
|
||||
|
||||
return url
|
||||
|
||||
def _confirm_payment_intent(self, request, payment):
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
{% load compress %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{{ settings.PRETIX_INSTANCE_NAME }}</title>
|
||||
{% compress css %}
|
||||
<link rel="stylesheet" type="text/x-scss" href="{% static "pretixbase/scss/cachedfiles.scss" %}"/>
|
||||
{% endcompress %}
|
||||
{% compress js %}
|
||||
<script type="text/javascript" src="{% static "jquery/js/jquery-3.6.4.min.js" %}"></script>
|
||||
{% endcompress %}
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>{% trans "The payment process has started in a new window." %}</h1>
|
||||
|
||||
<p>
|
||||
{% trans "The window to enter your payment data was not opened or was closed?" %}
|
||||
</p>
|
||||
<p>
|
||||
<a href="{{ url }}" target="_blank" class="btn btn-default btn-lg">
|
||||
<span class="fa fa-external-link-square"></span>
|
||||
{% trans "Click here in order to open the window." %}
|
||||
</a>
|
||||
</p>
|
||||
<script>
|
||||
window.open('{{ url|escapejs }}');
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user