forked from CGM_Public/pretix_original
Compare commits
27 Commits
release/4.
...
placeholde
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d9ce40d0ac | ||
|
|
d62c7553c2 | ||
|
|
4ce51b81ed | ||
|
|
f63fbaca4d | ||
|
|
7bab5b16a2 | ||
|
|
bdb0a176da | ||
|
|
345a05e4ae | ||
|
|
3bb590c1ae | ||
|
|
9a5e07e078 | ||
|
|
a563881659 | ||
|
|
50ebda332a | ||
|
|
9ae25caf5c | ||
|
|
a27a5c65aa | ||
|
|
395dbedbed | ||
|
|
441f32349f | ||
|
|
fd3311ed60 | ||
|
|
6cd8fb5b58 | ||
|
|
62f1e8d64b | ||
|
|
4eca90e287 | ||
|
|
e6c45e40a9 | ||
|
|
0bb41cc44e | ||
|
|
6c7f5f4888 | ||
|
|
c13d873f1f | ||
|
|
bddeb35520 | ||
|
|
9ce519bb0b | ||
|
|
1f9b0b842f | ||
|
|
69b482849a |
@@ -2,6 +2,7 @@
|
||||
file=/tmp/supervisor.sock
|
||||
|
||||
[supervisord]
|
||||
environment = AUTOMIGRATE="skip"
|
||||
logfile=/dev/stdout
|
||||
logfile_maxbytes=0
|
||||
loglevel=info
|
||||
|
||||
@@ -240,6 +240,9 @@ The following snippet is an example on how to configure a nginx proxy for pretix
|
||||
listen 80 default_server;
|
||||
listen [::]:80 ipv6only=on default_server;
|
||||
server_name pretix.mydomain.com;
|
||||
location / {
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
}
|
||||
server {
|
||||
listen 443 default_server;
|
||||
|
||||
@@ -225,6 +225,9 @@ The following snippet is an example on how to configure a nginx proxy for pretix
|
||||
listen 80 default_server;
|
||||
listen [::]:80 ipv6only=on default_server;
|
||||
server_name pretix.mydomain.com;
|
||||
location / {
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
}
|
||||
server {
|
||||
listen 443 default_server;
|
||||
|
||||
@@ -428,7 +428,7 @@ List of all orders
|
||||
``last_modified``, and ``status``. Default: ``datetime``
|
||||
:query string code: Only return orders that match the given order code
|
||||
:query string status: Only return orders in the given order status (see above)
|
||||
:query string search: Only return orders matching a given search query
|
||||
:query string search: Only return orders matching a given search query (matching for names, email addresses, and company names)
|
||||
:query integer item: Only return orders with a position that contains this item ID. *Warning:* Result will also include orders if they contain mixed items, and it will even return orders where the item is only contained in a canceled position.
|
||||
:query integer variation: Only return orders with a position that contains this variation ID. *Warning:* Result will also include orders if they contain mixed items and variations, and it will even return orders where the variation is only contained in a canceled position.
|
||||
:query boolean testmode: Only return orders with ``testmode`` set to ``true`` or ``false``
|
||||
|
||||
@@ -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__ = "4.12.0"
|
||||
__version__ = "4.13.0.dev0"
|
||||
|
||||
@@ -43,6 +43,7 @@ from pretix.base.i18n import (
|
||||
LazyCurrencyNumber, LazyDate, LazyExpiresDate, LazyNumber,
|
||||
)
|
||||
from pretix.base.models import Event
|
||||
from pretix.base.reldate import RelativeDateWrapper
|
||||
from pretix.base.settings import PERSON_NAME_SCHEMES
|
||||
from pretix.base.signals import (
|
||||
register_html_mail_renderers, register_mail_placeholders,
|
||||
@@ -469,6 +470,19 @@ def base_placeholders(sender, **kwargs):
|
||||
}
|
||||
),
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'order_modification_deadline_date_and_time', ['order', 'event'],
|
||||
lambda order, event:
|
||||
date_format(order.modify_deadline.astimezone(event.timezone), 'SHORT_DATETIME_FORMAT')
|
||||
if order.modify_deadline
|
||||
else '',
|
||||
lambda event: date_format(
|
||||
event.settings.get(
|
||||
'last_order_modification_date', as_type=RelativeDateWrapper
|
||||
).datetime(event).astimezone(event.timezone),
|
||||
'SHORT_DATETIME_FORMAT'
|
||||
) if event.settings.get('last_order_modification_date') else '',
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'event_location', ['event_or_subevent'], lambda event_or_subevent: str(event_or_subevent.location or ''),
|
||||
lambda event: str(event.location or ''),
|
||||
|
||||
@@ -610,7 +610,10 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
for k, label, w in name_scheme['fields']:
|
||||
headers.append(_('Invoice address name') + ': ' + str(label))
|
||||
headers += [
|
||||
_('Address'), _('ZIP code'), _('City'), _('Country'), pgettext('address', 'State'), _('VAT ID'),
|
||||
_('Invoice address street'), _('Invoice address ZIP code'), _('Invoice address city'),
|
||||
_('Invoice address country'),
|
||||
pgettext('address', 'Invoice address state'),
|
||||
_('VAT ID'),
|
||||
]
|
||||
headers += [
|
||||
_('Sales channel'), _('Order locale'),
|
||||
|
||||
@@ -746,6 +746,19 @@ class Order(LockModel, LoggedModel):
|
||||
length += 1
|
||||
iteration = 0
|
||||
|
||||
@property
|
||||
def modify_deadline(self):
|
||||
modify_deadline = self.event.settings.get('last_order_modification_date', as_type=RelativeDateWrapper)
|
||||
if self.event.has_subevents and modify_deadline:
|
||||
dates = [
|
||||
modify_deadline.datetime(se)
|
||||
for se in self.event.subevents.filter(id__in=self.positions.values_list('subevent', flat=True))
|
||||
]
|
||||
return min(dates) if dates else None
|
||||
elif modify_deadline:
|
||||
return modify_deadline.datetime(self.event)
|
||||
return None
|
||||
|
||||
@property
|
||||
def can_modify_answers(self) -> bool:
|
||||
"""
|
||||
@@ -758,16 +771,7 @@ class Order(LockModel, LoggedModel):
|
||||
if self.status not in (Order.STATUS_PENDING, Order.STATUS_PAID, Order.STATUS_EXPIRED):
|
||||
return False
|
||||
|
||||
modify_deadline = self.event.settings.get('last_order_modification_date', as_type=RelativeDateWrapper)
|
||||
if self.event.has_subevents and modify_deadline:
|
||||
dates = [
|
||||
modify_deadline.datetime(se)
|
||||
for se in self.event.subevents.filter(id__in=self.positions.values_list('subevent', flat=True))
|
||||
]
|
||||
modify_deadline = min(dates) if dates else None
|
||||
elif modify_deadline:
|
||||
modify_deadline = modify_deadline.datetime(self.event)
|
||||
|
||||
modify_deadline = self.modify_deadline
|
||||
if modify_deadline is not None and now() > modify_deadline:
|
||||
return False
|
||||
|
||||
|
||||
@@ -392,7 +392,7 @@ class InvoiceAddressCountry(ImportColumn):
|
||||
return list(countries)
|
||||
|
||||
def clean(self, value, previous_values):
|
||||
if value and not Country(value).numeric:
|
||||
if value and not (Country(value).numeric or value in settings.COUNTRIES_OVERRIDE):
|
||||
raise ValidationError(_("Please enter a valid country code."))
|
||||
return value
|
||||
|
||||
@@ -538,7 +538,7 @@ class AttendeeCountry(ImportColumn):
|
||||
return list(countries)
|
||||
|
||||
def clean(self, value, previous_values):
|
||||
if value and not Country(value).numeric:
|
||||
if value and not (Country(value).numeric or value in settings.COUNTRIES_OVERRIDE):
|
||||
raise ValidationError(_("Please enter a valid country code."))
|
||||
return value
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ from django.utils.html import conditional_escape
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _, pgettext
|
||||
from i18nfield.strings import LazyI18nString
|
||||
from PyPDF2 import PdfFileReader
|
||||
from PyPDF2 import PdfReader
|
||||
from pytz import timezone
|
||||
from reportlab.graphics import renderPDF
|
||||
from reportlab.graphics.barcode.qr import QrCodeWidget
|
||||
@@ -646,7 +646,7 @@ class Renderer:
|
||||
self.event = event
|
||||
if self.background_file:
|
||||
self.bg_bytes = self.background_file.read()
|
||||
self.bg_pdf = PdfFileReader(BytesIO(self.bg_bytes), strict=False)
|
||||
self.bg_pdf = PdfReader(BytesIO(self.bg_bytes), strict=False)
|
||||
else:
|
||||
self.bg_bytes = None
|
||||
self.bg_pdf = None
|
||||
@@ -861,7 +861,7 @@ class Renderer:
|
||||
canvas.restoreState()
|
||||
|
||||
def draw_page(self, canvas: Canvas, order: Order, op: OrderPosition, show_page=True, only_page=None):
|
||||
page_count = self.bg_pdf.getNumPages()
|
||||
page_count = len(self.bg_pdf.pages)
|
||||
|
||||
if not only_page and not show_page:
|
||||
raise ValueError("only_page=None and show_page=False cannot be combined")
|
||||
@@ -881,7 +881,7 @@ class Renderer:
|
||||
elif o['type'] == "poweredby":
|
||||
self._draw_poweredby(canvas, op, o)
|
||||
if self.bg_pdf:
|
||||
canvas.setPageSize((self.bg_pdf.getPage(page).mediaBox[2], self.bg_pdf.getPage(page).mediaBox[3]))
|
||||
canvas.setPageSize((self.bg_pdf.pages[0].mediabox[2], self.bg_pdf.pages[0].mediabox[3]))
|
||||
if show_page:
|
||||
canvas.showPage()
|
||||
|
||||
@@ -905,17 +905,17 @@ class Renderer:
|
||||
with open(os.path.join(d, 'out.pdf'), 'rb') as f:
|
||||
return BytesIO(f.read())
|
||||
else:
|
||||
from PyPDF2 import PdfFileReader, PdfFileWriter
|
||||
from PyPDF2 import PdfReader, PdfWriter
|
||||
buffer.seek(0)
|
||||
new_pdf = PdfFileReader(buffer)
|
||||
output = PdfFileWriter()
|
||||
new_pdf = PdfReader(buffer)
|
||||
output = PdfWriter()
|
||||
|
||||
for i, page in enumerate(new_pdf.pages):
|
||||
bg_page = copy.copy(self.bg_pdf.getPage(i))
|
||||
bg_page.mergePage(page)
|
||||
output.addPage(bg_page)
|
||||
bg_page = copy.copy(self.bg_pdf.pages[i])
|
||||
bg_page.merge_page(page)
|
||||
output.add_page(bg_page)
|
||||
|
||||
output.addMetadata({
|
||||
output.add_metadata({
|
||||
'/Title': str(title),
|
||||
'/Creator': 'pretix',
|
||||
})
|
||||
|
||||
@@ -57,6 +57,7 @@ from django_countries.fields import Country
|
||||
from hierarkey.models import GlobalSettingsBase, Hierarkey
|
||||
from i18nfield.forms import I18nFormField, I18nTextarea, I18nTextInput
|
||||
from i18nfield.strings import LazyI18nString
|
||||
from phonenumbers import PhoneNumber, parse
|
||||
from rest_framework import serializers
|
||||
|
||||
from pretix.api.serializers.fields import (
|
||||
@@ -3030,6 +3031,7 @@ settings_hierarkey.add_type(LazyI18nStringList,
|
||||
settings_hierarkey.add_type(RelativeDateWrapper,
|
||||
serialize=lambda rdw: rdw.to_string(),
|
||||
unserialize=lambda s: RelativeDateWrapper.from_string(s))
|
||||
settings_hierarkey.add_type(PhoneNumber, lambda pn: pn.as_international, lambda s: parse(s))
|
||||
|
||||
|
||||
@settings_hierarkey.set_global(cache_namespace='global')
|
||||
|
||||
52
src/pretix/base/templates/400_hostname.html
Normal file
52
src/pretix/base/templates/400_hostname.html
Normal file
@@ -0,0 +1,52 @@
|
||||
{% extends "error.html" %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% block title %}{% trans "Unknown host" %}{% endblock %}
|
||||
{% block content %}
|
||||
<i class="fa fa-question-circle-o fa-fw big-icon"></i>
|
||||
<div class="error-details">
|
||||
<h1>{% trans "Unknown host" %}</h1>
|
||||
<p>
|
||||
{% blocktrans trimmed with host=header_host %}
|
||||
Your browser told us that you want to access "{{ header_host }}". Unfortunately, we don't have
|
||||
any content for this domain.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
{% if is_fresh_install %}
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
It looks like this is a fresh installation of pretix. This error message is probably caused due to
|
||||
the fact that either your configuration includes the wrong site URL or your reverse proxy is sending
|
||||
the wrong header.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<dl>
|
||||
<dt>{% trans "Expected host according to configuration" %}</dt>
|
||||
<dd><code>{{ site_host }}</code></dd>
|
||||
<dt>{% trans "Received headers" %}</dt>
|
||||
<dd>
|
||||
<code>Host: {{ request.headers.Host }}</code>
|
||||
{% if xfh %}
|
||||
<br>
|
||||
<code>X-Forwarded-For: {{ xfh }}</code>
|
||||
{% if not settings.USE_X_FORWARDED_HOST %}({% trans "ignored" %}){% endif %}
|
||||
{% endif %}
|
||||
</dd>
|
||||
<dt>{% trans "Derived host from headers" %}</dt>
|
||||
<dd><code>{{ header_host }}</code></dd>
|
||||
</dl>
|
||||
{% else %}
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
If you just configured this as a domain for your ticket shop, you now need to set this up as a "custom domain"
|
||||
in your organizer account.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
{% endif %}
|
||||
<p class="links">
|
||||
<a id='goback' href='#'>{% trans "Take a step back" %}</a>
|
||||
· <a id='reload' href='#'>{% trans "Try again" %}</a>
|
||||
</p>
|
||||
<img src="{% static "pretixbase/img/pretix-logo.svg" %}" class="logo"/>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -71,7 +71,7 @@ def _default_context(request):
|
||||
except Resolver404:
|
||||
return {}
|
||||
|
||||
if not request.path.startswith(get_script_prefix() + 'control'):
|
||||
if not request.path.startswith(get_script_prefix() + 'control') or not hasattr(request, 'user'):
|
||||
return {}
|
||||
ctx = {
|
||||
'url_name': url.url_name,
|
||||
|
||||
@@ -39,7 +39,7 @@ from urllib.parse import urlencode, urlparse
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import validate_email
|
||||
from django.core.validators import MaxValueValidator, validate_email
|
||||
from django.db.models import Prefetch, Q, prefetch_related_objects
|
||||
from django.forms import (
|
||||
CheckboxSelectMultiple, formset_factory, inlineformset_factory,
|
||||
@@ -848,6 +848,7 @@ class InvoiceSettingsForm(SettingsForm):
|
||||
self.fields['invoice_generate_sales_channels'].choices = (
|
||||
(c.identifier, c.verbose_name) for c in get_all_sales_channels().values()
|
||||
)
|
||||
self.fields['invoice_numbers_counter_length'].validators.append(MaxValueValidator(15))
|
||||
|
||||
def clean(self):
|
||||
data = super().clean()
|
||||
|
||||
@@ -1344,22 +1344,36 @@ class ItemUpdateGeneral(ItemDetailMixin, EventPermissionRequiredMixin, MetaDataE
|
||||
def form_valid(self, form):
|
||||
self.save_meta()
|
||||
messages.success(self.request, _('Your changes have been saved.'))
|
||||
if form.has_changed() or any(f.has_changed() for f in self.plugin_forms):
|
||||
data = {
|
||||
k: form.cleaned_data.get(k)
|
||||
for k in form.changed_data
|
||||
}
|
||||
for f in self.plugin_forms:
|
||||
data.update({
|
||||
k: (f.cleaned_data.get(k).name
|
||||
if isinstance(f.cleaned_data.get(k), File)
|
||||
else f.cleaned_data.get(k))
|
||||
for k in f.changed_data
|
||||
})
|
||||
|
||||
change_data = {
|
||||
k: form.cleaned_data.get(k)
|
||||
for k in form.changed_data
|
||||
}
|
||||
for f in self.plugin_forms:
|
||||
change_data.update({
|
||||
k: (f.cleaned_data.get(k).name
|
||||
if isinstance(f.cleaned_data.get(k), File)
|
||||
else f.cleaned_data.get(k))
|
||||
for k in f.changed_data
|
||||
})
|
||||
|
||||
meta_changed = {}
|
||||
for f in self.meta_forms:
|
||||
meta_changed.update({
|
||||
k: (f.cleaned_data.get(k).name
|
||||
if isinstance(f.cleaned_data.get(k), File)
|
||||
else f.cleaned_data.get(k))
|
||||
for k in f.changed_data
|
||||
})
|
||||
if meta_changed:
|
||||
change_data['meta_data'] = meta_changed
|
||||
|
||||
if change_data:
|
||||
self.object.log_action(
|
||||
'pretix.event.item.changed', user=self.request.user, data=data
|
||||
'pretix.event.item.changed', user=self.request.user, data=change_data
|
||||
)
|
||||
invalidate_cache.apply_async(kwargs={'event': self.request.event.pk, 'item': self.object.pk})
|
||||
|
||||
for f in self.plugin_forms:
|
||||
f.save()
|
||||
|
||||
|
||||
@@ -1186,13 +1186,17 @@ class OrderTransition(OrderView):
|
||||
if ps == Decimal('0.00') and self.order.pending_sum <= Decimal('0.00'):
|
||||
p = self.order.payments.filter(state=OrderPayment.PAYMENT_STATE_CONFIRMED).last()
|
||||
if p:
|
||||
p._mark_order_paid(
|
||||
user=self.request.user,
|
||||
send_mail=self.mark_paid_form.cleaned_data['send_email'],
|
||||
force=self.mark_paid_form.cleaned_data.get('force', False),
|
||||
payment_refund_sum=self.order.payment_refund_sum,
|
||||
)
|
||||
messages.success(self.request, _('The order has been marked as paid.'))
|
||||
try:
|
||||
p._mark_order_paid(
|
||||
user=self.request.user,
|
||||
send_mail=self.mark_paid_form.cleaned_data['send_email'],
|
||||
force=self.mark_paid_form.cleaned_data.get('force', False),
|
||||
payment_refund_sum=self.order.payment_refund_sum,
|
||||
)
|
||||
except Quota.QuotaExceededException as e:
|
||||
messages.error(self.request, str(e))
|
||||
else:
|
||||
messages.success(self.request, _('The order has been marked as paid.'))
|
||||
return redirect(self.get_order_url())
|
||||
|
||||
try:
|
||||
|
||||
@@ -39,8 +39,8 @@ from django.utils.crypto import get_random_string
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views.generic import TemplateView
|
||||
from PyPDF2 import PdfFileReader, PdfFileWriter
|
||||
from PyPDF2.utils import PdfReadError
|
||||
from PyPDF2 import PdfReader, PdfWriter
|
||||
from PyPDF2.errors import PdfReadError
|
||||
from reportlab.lib.units import mm
|
||||
|
||||
from pretix.base.i18n import language
|
||||
@@ -153,9 +153,9 @@ class BaseEditorView(EventPermissionRequiredMixin, TemplateView):
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
if "emptybackground" in request.POST:
|
||||
p = PdfFileWriter()
|
||||
p = PdfWriter()
|
||||
try:
|
||||
p.addBlankPage(
|
||||
p.add_blank_page(
|
||||
width=float(request.POST.get('width')) * mm,
|
||||
height=float(request.POST.get('height')) * mm,
|
||||
)
|
||||
@@ -203,7 +203,7 @@ class BaseEditorView(EventPermissionRequiredMixin, TemplateView):
|
||||
|
||||
try:
|
||||
bg_bytes = c.file.read()
|
||||
PdfFileReader(BytesIO(bg_bytes), strict=False)
|
||||
PdfReader(BytesIO(bg_bytes), strict=False)
|
||||
except PdfReadError as e:
|
||||
return JsonResponse({
|
||||
"status": "error",
|
||||
|
||||
@@ -228,9 +228,9 @@ def nav_context_list(request):
|
||||
if query:
|
||||
qs_orga = qs_orga.filter(Q(name__icontains=query) | Q(slug__icontains=query))
|
||||
|
||||
if query:
|
||||
if query and len(query) >= 3:
|
||||
qs_orders = Order.objects.filter(
|
||||
code__icontains=query
|
||||
code__istartswith=query
|
||||
).select_related('event', 'event__organizer').only('event', 'code', 'pk').order_by()
|
||||
if not request.user.has_active_staff_session(request.session.session_key):
|
||||
qs_orders = qs_orders.filter(
|
||||
@@ -241,7 +241,7 @@ def nav_context_list(request):
|
||||
)
|
||||
|
||||
qs_vouchers = Voucher.objects.filter(
|
||||
code__icontains=query
|
||||
code__istartswith=query
|
||||
).select_related('event', 'event__organizer').only('event', 'code', 'pk').order_by()
|
||||
if not request.user.has_active_staff_session(request.session.session_key):
|
||||
qs_vouchers = qs_vouchers.filter(
|
||||
|
||||
@@ -43,6 +43,7 @@ from django.core.cache import cache
|
||||
from django.core.exceptions import DisallowedHost
|
||||
from django.http.request import split_domain_port
|
||||
from django.middleware.csrf import CsrfViewMiddleware as BaseCsrfMiddleware
|
||||
from django.shortcuts import render
|
||||
from django.urls import set_urlconf
|
||||
from django.utils.cache import patch_vary_headers
|
||||
from django.utils.deprecation import MiddlewareMixin
|
||||
@@ -112,7 +113,15 @@ class MultiDomainMiddleware(MiddlewareMixin):
|
||||
elif settings.DEBUG or domain in LOCAL_HOST_NAMES:
|
||||
request.urlconf = "pretix.multidomain.maindomain_urlconf"
|
||||
else:
|
||||
raise DisallowedHost("Unknown host: %r" % host)
|
||||
with scopes_disabled():
|
||||
is_fresh_install = not Event.objects.exists()
|
||||
return render(request, '400_hostname.html', {
|
||||
'header_host': domain,
|
||||
'site_host': default_domain,
|
||||
'settings': settings,
|
||||
'xfh': request.headers.get('X-Forwarded-Host'),
|
||||
'is_fresh_install': is_fresh_install,
|
||||
}, status=400)
|
||||
else:
|
||||
raise DisallowedHost("Invalid HTTP_HOST header: %r." % host)
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@ from django.db.models import Exists, OuterRef, Q
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.utils.timezone import make_aware
|
||||
from django.utils.translation import gettext as _, gettext_lazy
|
||||
from PyPDF2 import Transformation
|
||||
from reportlab.lib import pagesizes
|
||||
from reportlab.lib.units import mm
|
||||
from reportlab.pdfgen import canvas
|
||||
@@ -153,11 +154,19 @@ OPTIONS = OrderedDict([
|
||||
'offsets': [95 * mm, 55 * mm],
|
||||
'pagesize': pagesizes.A4,
|
||||
}),
|
||||
('herma_40x40', {
|
||||
'name': 'HERMA 40 x 40 mm (9642)',
|
||||
'cols': 4,
|
||||
'rows': 6,
|
||||
'margins': [13.5 * mm, 15 * mm, 13.5 * mm, 15 * mm],
|
||||
'offsets': [46 * mm, 46 * mm],
|
||||
'pagesize': pagesizes.A4,
|
||||
}),
|
||||
])
|
||||
|
||||
|
||||
def render_pdf(event, positions, opt):
|
||||
from PyPDF2 import PdfFileReader, PdfFileWriter
|
||||
from PyPDF2 import PdfReader, PdfWriter
|
||||
Renderer._register_fonts()
|
||||
|
||||
renderermap = {
|
||||
@@ -168,7 +177,7 @@ def render_pdf(event, positions, opt):
|
||||
default_renderer = _renderer(event, event.badge_layouts.get(default=True))
|
||||
except BadgeLayout.DoesNotExist:
|
||||
default_renderer = None
|
||||
output_pdf_writer = PdfFileWriter()
|
||||
output_pdf_writer = PdfWriter()
|
||||
|
||||
any = False
|
||||
npp = opt['cols'] * opt['rows']
|
||||
@@ -189,22 +198,19 @@ def render_pdf(event, positions, opt):
|
||||
p.showPage()
|
||||
p.save()
|
||||
buffer.seek(0)
|
||||
canvas_pdf_reader = PdfFileReader(buffer)
|
||||
empty_pdf_page = output_pdf_writer.addBlankPage(
|
||||
width=opt['pagesize'][0] if opt['pagesize'] else positions[0][1].bg_pdf.getPage(0).mediaBox[2],
|
||||
height=opt['pagesize'][1] if opt['pagesize'] else positions[0][1].bg_pdf.getPage(0).mediaBox[3],
|
||||
canvas_pdf_reader = PdfReader(buffer)
|
||||
empty_pdf_page = output_pdf_writer.add_blank_page(
|
||||
width=opt['pagesize'][0] if opt['pagesize'] else positions[0][1].bg_pdf.pages[0].mediabox[2],
|
||||
height=opt['pagesize'][1] if opt['pagesize'] else positions[0][1].bg_pdf.pages[0].mediabox[3],
|
||||
)
|
||||
for i, (op, r) in enumerate(positions):
|
||||
bg_page = copy.copy(r.bg_pdf.getPage(0))
|
||||
bg_page.trimBox = bg_page.mediaBox
|
||||
bg_page = copy.copy(r.bg_pdf.pages[0])
|
||||
bg_page.trimbox = bg_page.mediabox
|
||||
offsetx = opt['margins'][3] + (i % opt['cols']) * opt['offsets'][0]
|
||||
offsety = opt['margins'][2] + (opt['rows'] - 1 - i // opt['cols']) * opt['offsets'][1]
|
||||
empty_pdf_page.mergeTranslatedPage(
|
||||
bg_page,
|
||||
tx=offsetx,
|
||||
ty=offsety
|
||||
)
|
||||
empty_pdf_page.mergePage(canvas_pdf_reader.getPage(0))
|
||||
bg_page.add_transformation(Transformation().translate(offsetx, offsety))
|
||||
empty_pdf_page.merge_page(bg_page)
|
||||
empty_pdf_page.merge_page(canvas_pdf_reader.pages[0])
|
||||
|
||||
pagebuffer = []
|
||||
outbuffer = BytesIO()
|
||||
@@ -221,7 +227,7 @@ def render_pdf(event, positions, opt):
|
||||
if pagebuffer:
|
||||
render_page(pagebuffer)
|
||||
|
||||
output_pdf_writer.addMetadata({
|
||||
output_pdf_writer.add_metadata({
|
||||
'/Title': 'Badges',
|
||||
'/Creator': 'pretix',
|
||||
})
|
||||
|
||||
@@ -42,6 +42,7 @@ from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
from django.urls import resolve, reverse
|
||||
from django.utils import timezone
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_scopes import scope, scopes_disabled
|
||||
|
||||
@@ -134,6 +135,7 @@ def sendmail_run_rules(sender, **kwargs):
|
||||
with scopes_disabled():
|
||||
mails = ScheduledMail.objects.all()
|
||||
|
||||
unchanged = []
|
||||
for m in mails.filter(Q(last_computed__isnull=True)
|
||||
| Q(subevent__last_modified__gt=F('last_computed'))
|
||||
| Q(event__last_modified__gt=F('last_computed'))):
|
||||
@@ -141,6 +143,17 @@ def sendmail_run_rules(sender, **kwargs):
|
||||
m.recompute()
|
||||
if m.computed_datetime != previous:
|
||||
m.save(update_fields=['last_computed', 'computed_datetime'])
|
||||
else:
|
||||
unchanged.append(m.pk)
|
||||
|
||||
if unchanged:
|
||||
# Theoretically, we don't need to write back the unchanged ones to the database… but that will cause us to
|
||||
# recompute them on every run until eternity. So we want to set their last_computed date to something more
|
||||
# recent... but not for all of them at once, in case it's millions, so we don't stress the database without
|
||||
# cause
|
||||
batch_size = max(connection.ops.bulk_batch_size(['id'], unchanged) - 2, 100)
|
||||
for i in range(max(1, 5000 // batch_size)):
|
||||
ScheduledMail.objects.filter(pk__in=unchanged[i * batch_size:batch_size]).update(last_computed=now())
|
||||
|
||||
mails.filter(
|
||||
state=ScheduledMail.STATE_SCHEDULED,
|
||||
|
||||
@@ -43,7 +43,7 @@ from django.db.models import Q
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.utils.timezone import make_aware
|
||||
from django.utils.translation import gettext as _, gettext_lazy
|
||||
from PyPDF2.merger import PdfFileMerger
|
||||
from PyPDF2 import PdfMerger
|
||||
|
||||
from pretix.base.exporter import BaseExporter
|
||||
from pretix.base.i18n import language
|
||||
@@ -105,7 +105,7 @@ class AllTicketsPDF(BaseExporter):
|
||||
return d
|
||||
|
||||
def render(self, form_data):
|
||||
merger = PdfFileMerger()
|
||||
merger = PdfMerger()
|
||||
qs = OrderPosition.objects.filter(
|
||||
order__event__in=self.events
|
||||
).prefetch_related(
|
||||
|
||||
@@ -44,7 +44,7 @@ from django.http import HttpRequest
|
||||
from django.template.loader import get_template
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from PyPDF2 import PdfFileMerger
|
||||
from PyPDF2 import PdfMerger
|
||||
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import Order, OrderPosition
|
||||
@@ -112,7 +112,7 @@ class PdfTicketOutput(BaseTicketOutput):
|
||||
return renderer.render_background(buffer, _('Ticket'))
|
||||
|
||||
def generate_order(self, order: Order):
|
||||
merger = PdfFileMerger()
|
||||
merger = PdfMerger()
|
||||
with language(order.locale, self.event.settings.region):
|
||||
for op in order.positions_with_tickets:
|
||||
layout = override_layout.send_chained(
|
||||
|
||||
@@ -93,6 +93,10 @@ event_patterns = [
|
||||
name='event.payment.unlock'),
|
||||
re_path(r'resend/$', pretix.presale.views.user.ResendLinkView.as_view(), name='event.resend_link'),
|
||||
|
||||
re_path(r'^favicon.ico/?$',
|
||||
pretix.presale.views.organizer.OrganizerFavicon.as_view(),
|
||||
name='event.favicon'),
|
||||
|
||||
re_path(r'^order/(?P<order>[^/]+)/(?P<secret>[A-Za-z0-9]+)/open/(?P<hash>[a-z0-9]+)/$', pretix.presale.views.order.OrderOpen.as_view(),
|
||||
name='event.order.open'),
|
||||
re_path(r'^order/(?P<order>[^/]+)/(?P<secret>[A-Za-z0-9]+)/$', pretix.presale.views.order.OrderDetails.as_view(),
|
||||
@@ -164,6 +168,9 @@ event_patterns = [
|
||||
|
||||
organizer_patterns = [
|
||||
re_path(r'^$', pretix.presale.views.organizer.OrganizerIndex.as_view(), name='organizer.index'),
|
||||
re_path(r'^favicon.ico/?$',
|
||||
pretix.presale.views.organizer.OrganizerFavicon.as_view(),
|
||||
name='organizer.favicon'),
|
||||
re_path(r'^events/ical/$',
|
||||
pretix.presale.views.organizer.OrganizerIcalDownload.as_view(),
|
||||
name='organizer.ical'),
|
||||
|
||||
@@ -48,6 +48,7 @@ from django.db.models import Exists, Max, Min, OuterRef, Prefetch, Q
|
||||
from django.db.models.functions import Coalesce, Greatest
|
||||
from django.http import Http404, HttpResponse
|
||||
from django.shortcuts import redirect
|
||||
from django.templatetags.static import static
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.formats import date_format, get_format
|
||||
from django.utils.timezone import get_current_timezone, now
|
||||
@@ -66,6 +67,7 @@ from pretix.helpers.daterange import daterange
|
||||
from pretix.helpers.formats.en.formats import (
|
||||
SHORT_MONTH_DAY_FORMAT, WEEK_FORMAT,
|
||||
)
|
||||
from pretix.helpers.thumb import get_thumbnail
|
||||
from pretix.multidomain.urlreverse import eventreverse
|
||||
from pretix.presale.ical import get_public_ical
|
||||
from pretix.presale.views import OrganizerViewMixin
|
||||
@@ -1170,3 +1172,11 @@ class OrganizerIcalDownload(OrganizerViewMixin, View):
|
||||
if request.organizer.settings.meta_noindex:
|
||||
resp['X-Robots-Tag'] = 'noindex'
|
||||
return resp
|
||||
|
||||
|
||||
class OrganizerFavicon(View):
|
||||
def get(self, *args, **kwargs):
|
||||
if self.request.organizer.settings.favicon:
|
||||
return redirect(get_thumbnail(self.request.organizer.settings.favicon, '32x32^').thumb.url)
|
||||
else:
|
||||
return redirect(static("pretixbase/img/favicon.ico"))
|
||||
|
||||
@@ -170,7 +170,7 @@ PRETIX_SESSION_TIMEOUT_RELATIVE = 3600 * 3
|
||||
PRETIX_SESSION_TIMEOUT_ABSOLUTE = 3600 * 12
|
||||
PRETIX_PRIMARY_COLOR = '#8E44B3'
|
||||
|
||||
SITE_URL = config.get('pretix', 'url', fallback='http://localhost')
|
||||
SITE_URL = config.get('pretix', 'url', fallback='http://localhost:8000')
|
||||
if SITE_URL.endswith('/'):
|
||||
SITE_URL = SITE_URL[:-1]
|
||||
|
||||
|
||||
1398
src/pretix/static/npm_dir/package-lock.json
generated
1398
src/pretix/static/npm_dir/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -4,13 +4,13 @@
|
||||
"private": true,
|
||||
"scripts": {},
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.18.6",
|
||||
"@babel/preset-env": "^7.18.6",
|
||||
"@babel/core": "^7.18.10",
|
||||
"@babel/preset-env": "^7.18.9",
|
||||
"@rollup/plugin-babel": "^5.3.1",
|
||||
"@rollup/plugin-node-resolve": "^13.3.0",
|
||||
"vue": "^2.7.0",
|
||||
"rollup": "^2.75.7",
|
||||
"vue": "^2.7.8",
|
||||
"rollup": "^2.77.2",
|
||||
"rollup-plugin-vue": "^5.0.1",
|
||||
"vue-template-compiler": "^2.7.0"
|
||||
"vue-template-compiler": "^2.7.8"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -300,7 +300,6 @@ var editor = {
|
||||
|
||||
// Fetch the required page
|
||||
editor.pdf.getPage(page_number).then(function (page) {
|
||||
console.log('Page loaded');
|
||||
var canvas = document.getElementById('pdf-canvas');
|
||||
|
||||
var scale = editor.$cva.width() / page.getViewport(1.0).width;
|
||||
@@ -326,7 +325,6 @@ var editor = {
|
||||
editor.pdf_page_number = page_number
|
||||
editor._init_page_nav();
|
||||
|
||||
console.log('Page rendered');
|
||||
if (dump || !editor._fabric_loaded) {
|
||||
editor._init_fabric(dump);
|
||||
} else {
|
||||
@@ -349,7 +347,6 @@ var editor = {
|
||||
}
|
||||
$("#page_nav").append($li)
|
||||
$a.on("click", function (event) {
|
||||
console.log("switch to page", $(this).attr("data-page"));
|
||||
editor.fabric.deactivateAll();
|
||||
editor._load_page(parseInt($(this).attr("data-page")));
|
||||
event.preventDefault();
|
||||
@@ -369,7 +366,6 @@ var editor = {
|
||||
// Asynchronous download of PDF
|
||||
var loadingTask = PDFJS.getDocument(url);
|
||||
loadingTask.promise.then(function (pdf) {
|
||||
console.log('PDF loaded');
|
||||
|
||||
editor.pdf = pdf;
|
||||
editor.pdf_page_count = pdf.numPages;
|
||||
@@ -422,7 +418,6 @@ var editor = {
|
||||
}
|
||||
|
||||
editor._fabric_loaded = true;
|
||||
console.log("Fabric loaded");
|
||||
if (editor._window_loaded) {
|
||||
editor._ready();
|
||||
}
|
||||
@@ -430,7 +425,6 @@ var editor = {
|
||||
|
||||
_window_load_event: function () {
|
||||
editor._window_loaded = true;
|
||||
console.log("Window loaded");
|
||||
if (editor._fabric_loaded) {
|
||||
editor._ready();
|
||||
}
|
||||
@@ -520,7 +514,7 @@ var editor = {
|
||||
}
|
||||
},
|
||||
|
||||
_update_values_from_toolbox: function () {
|
||||
_update_values_from_toolbox: function (e) {
|
||||
var o = editor.fabric.getActiveObject();
|
||||
if (!o) {
|
||||
o = editor.fabric.getActiveGroup();
|
||||
@@ -553,8 +547,16 @@ var editor = {
|
||||
$("#toolbox-content-other-help").toggle($("#toolbox-content").val() === "other" || $("#toolbox-content").val() === "other_i18n");
|
||||
o.content = $("#toolbox-content").val();
|
||||
if ($("#toolbox-content").val() === "other") {
|
||||
if (e.target.id === "toolbox-content") {
|
||||
// user used dropdown to switch content-type, update value with value from i18n textarea
|
||||
$("#toolbox-content-other").val($("#toolbox-content-other-i18n textarea").val());
|
||||
}
|
||||
o.text = $("#toolbox-content-other").val();
|
||||
} else if ($("#toolbox-content").val() === "other_i18n") {
|
||||
if (e.target.id === "toolbox-content") {
|
||||
// user used dropdown to switch content-type, update value with value from "other" textarea
|
||||
$("#toolbox-content-other-i18n textarea").val($("#toolbox-content-other").val());
|
||||
}
|
||||
o.text_i18n = {}
|
||||
$("#toolbox-content-other-i18n textarea").each(function () {
|
||||
o.text_i18n[$(this).attr("lang")] = $(this).val();
|
||||
@@ -608,8 +610,16 @@ var editor = {
|
||||
$("#toolbox-content-other-help").toggle($("#toolbox-content").val() === "other" || $("#toolbox-content").val() === "other_i18n");
|
||||
o.content = $("#toolbox-content").val();
|
||||
if ($("#toolbox-content").val() === "other") {
|
||||
if (e.target.id === "toolbox-content") {
|
||||
// user used dropdown to switch content-type, update value with value from i18n textarea
|
||||
$("#toolbox-content-other").val($("#toolbox-content-other-i18n textarea").val());
|
||||
}
|
||||
o.setText($("#toolbox-content-other").val());
|
||||
} else if ($("#toolbox-content").val() === "other_i18n") {
|
||||
if (e.target.id === "toolbox-content") {
|
||||
// user used dropdown to switch content-type, update value with value from "other" textarea
|
||||
$("#toolbox-content-other-i18n textarea").val($("#toolbox-content-other").val());
|
||||
}
|
||||
o.text_i18n = {}
|
||||
$("#toolbox-content-other-i18n textarea").each(function () {
|
||||
o.text_i18n[$(this).attr("lang")] = $(this).val();
|
||||
@@ -620,6 +630,14 @@ var editor = {
|
||||
}
|
||||
}
|
||||
|
||||
// empty text-inputs if not in use
|
||||
if ($("#toolbox-content").val() !== "other") {
|
||||
$("#toolbox-content-other").val("");
|
||||
}
|
||||
if ($("#toolbox-content").val() !== "other_i18n") {
|
||||
$("#toolbox-content-other-i18n textarea").val("");
|
||||
}
|
||||
|
||||
o.setCoords();
|
||||
editor.fabric.renderAll();
|
||||
},
|
||||
@@ -1055,14 +1073,14 @@ var editor = {
|
||||
$("#toolbox label.btn").bind('click change', editor._update_values_from_toolbox);
|
||||
$("#toolbox select").bind('change', editor._update_values_from_toolbox);
|
||||
$("#toolbox select").bind('change', editor._create_savepoint);
|
||||
$("#toolbox button.toggling").bind('click change', function () {
|
||||
$("#toolbox button.toggling").bind('click change', function (e) {
|
||||
if ($(this).is(".option")) {
|
||||
$(this).addClass("active");
|
||||
$(this).parent().siblings().find("button").removeClass("active");
|
||||
} else {
|
||||
$(this).toggleClass("active");
|
||||
}
|
||||
editor._update_values_from_toolbox();
|
||||
editor._update_values_from_toolbox(e);
|
||||
editor._create_savepoint();
|
||||
});
|
||||
$("#toolbox .colorpickerfield").bind('changeColor', editor._update_values_from_toolbox);
|
||||
|
||||
@@ -12,13 +12,8 @@ $(function () {
|
||||
var $query = $(this).find('[data-typeahead-query]').length ? $(this).find('[data-typeahead-query]') : $($(this).attr("data-typeahead-field"));
|
||||
$container.find("li:not(.query-holder)").remove();
|
||||
var lastQuery = "";
|
||||
|
||||
$query.on("change", function () {
|
||||
if ($container.attr("data-typeahead-field") && $query.val() === "") {
|
||||
$container.removeClass('focused');
|
||||
$container.find("li:not(.query-holder)").remove();
|
||||
return;
|
||||
}
|
||||
var runQueryTimeout = null;
|
||||
function runQuery() {
|
||||
lastQuery = $query.val();
|
||||
var thisQuery = $query.val();
|
||||
$.getJSON(
|
||||
@@ -119,6 +114,17 @@ $(function () {
|
||||
$container.toggleClass('focused', $query.is(":focus") && $container.children().length > 0);
|
||||
}
|
||||
);
|
||||
}
|
||||
$query.on("change", function () {
|
||||
if ($container.attr("data-typeahead-field") && $query.val() === "") {
|
||||
$container.removeClass('focused');
|
||||
$container.find("li:not(.query-holder)").remove();
|
||||
return;
|
||||
}
|
||||
if (runQueryTimeout != null) {
|
||||
window.clearTimeout(runQueryTimeout)
|
||||
}
|
||||
runQueryTimeout = window.setTimeout(runQuery, 250)
|
||||
});
|
||||
$query.on("keydown", function (event) {
|
||||
var $selected = $container.find(".active");
|
||||
|
||||
@@ -211,7 +211,7 @@ setup(
|
||||
'psycopg2-binary',
|
||||
'pycountry',
|
||||
'pycparser==2.21',
|
||||
'PyPDF2==1.27.9',
|
||||
'PyPDF2==2.9.*',
|
||||
'python-bidi==0.4.*', # Support for Arabic in reportlab
|
||||
'python-dateutil==2.8.*',
|
||||
'python-u2flib-server==4.*',
|
||||
|
||||
@@ -93,7 +93,7 @@ def inputfile_factory():
|
||||
'D': 'Test',
|
||||
'E': 'Baz',
|
||||
'F': '0.00',
|
||||
'G': 'AU',
|
||||
'G': 'XK',
|
||||
'H': '',
|
||||
'I': 'Foo,Bar',
|
||||
'J': '2021-06-28 11:00:00',
|
||||
|
||||
@@ -39,7 +39,7 @@ from io import BytesIO
|
||||
import pytest
|
||||
from django.utils.timezone import now
|
||||
from django_scopes import scope
|
||||
from PyPDF2 import PdfFileReader
|
||||
from PyPDF2 import PdfReader
|
||||
|
||||
from pretix.base.models import (
|
||||
Event, Item, ItemVariation, Order, OrderPosition, Organizer,
|
||||
@@ -100,8 +100,8 @@ def test_generate_pdf(env):
|
||||
'include_pending': True
|
||||
})
|
||||
assert ftype == 'application/pdf'
|
||||
pdf = PdfFileReader(BytesIO(buf))
|
||||
assert pdf.numPages == 2
|
||||
pdf = PdfReader(BytesIO(buf))
|
||||
assert len(pdf.pages) == 2
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@@ -115,5 +115,5 @@ def test_generate_pdf_multi(env):
|
||||
'include_pending': True
|
||||
})
|
||||
assert ftype == 'application/pdf'
|
||||
pdf = PdfFileReader(BytesIO(buf))
|
||||
assert pdf.numPages == 1
|
||||
pdf = PdfReader(BytesIO(buf))
|
||||
assert len(pdf.pages) == 1
|
||||
|
||||
@@ -26,7 +26,7 @@ from io import BytesIO
|
||||
import pytest
|
||||
from django.utils.timezone import now
|
||||
from django_scopes import scope
|
||||
from PyPDF2 import PdfFileReader
|
||||
from PyPDF2 import PdfReader
|
||||
|
||||
from pretix.base.models import (
|
||||
Event, Item, ItemVariation, Order, OrderPosition, Organizer,
|
||||
@@ -70,5 +70,5 @@ def test_generate_pdf(env0):
|
||||
o = PdfTicketOutput(event)
|
||||
fname, ftype, buf = o.generate(order.positions.first())
|
||||
assert ftype == 'application/pdf'
|
||||
pdf = PdfFileReader(BytesIO(buf))
|
||||
assert pdf.numPages == 1
|
||||
pdf = PdfReader(BytesIO(buf))
|
||||
assert len(pdf.pages) == 1
|
||||
|
||||
Reference in New Issue
Block a user