Move PDF editor out of plugin and into core

This commit is contained in:
Raphael Michel
2018-04-09 09:40:18 +02:00
parent f1d4a686b1
commit 87c54f07c6
19 changed files with 428 additions and 378 deletions

28
.gitattributes vendored
View File

@@ -1,17 +1,17 @@
src/static/fontawesome/* linguist-vendored src/pretix/static/fontawesome/* linguist-vendored
src/static/lightbox/* linguist-vendored src/pretix/static/lightbox/* linguist-vendored
src/static/typeahead/* linguist-vendored src/pretix/static/typeahead/* linguist-vendored
src/static/moment/* linguist-vendored src/pretix/static/moment/* linguist-vendored
src/static/datetimepicker/* linguist-vendored src/pretix/static/datetimepicker/* linguist-vendored
src/static/colorpicker/* linguist-vendored src/pretix/static/colorpicker/* linguist-vendored
src/static/fileupload/* linguist-vendored src/pretix/static/fileupload/* linguist-vendored
src/static/vuejs/* linguist-vendored src/pretix/static/vuejs/* linguist-vendored
src/static/select2/* linguist-vendored src/pretix/static/select2/* linguist-vendored
src/static/charts/* linguist-vendored src/pretix/static/charts/* linguist-vendored
src/static/rrule/* linguist-vendored src/pretix/static/rrule/* linguist-vendored
src/static/iframeresizer/* linguist-vendored src/pretix/static/iframeresizer/* linguist-vendored
src/pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/fabric.* linguist-vendored src/pretix/static/pdfjs/* linguist-vendored
src/pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/pdf.* linguist-vendored src/pretix/static/fabric/* linguist-vendored
# Denote all files that are truly binary and should not be modified. # Denote all files that are truly binary and should not be modified.
*.eot binary *.eot binary

View File

@@ -68,5 +68,5 @@ Dashboards
Ticket designs Ticket designs
"""""""""""""" """"""""""""""
.. automodule:: pretix.plugins.ticketoutputpdf.signals .. automodule:: pretix.base.signals
:members: layout_text_variables :members: layout_text_variables

143
src/pretix/base/pdf.py Normal file
View File

@@ -0,0 +1,143 @@
import copy
from collections import OrderedDict
from django.utils.formats import date_format
from django.utils.translation import ugettext_lazy as _
from pytz import timezone
from pretix.base.signals import layout_text_variables
from pretix.base.templatetags.money import money_filter
DEFAULT_VARIABLES = OrderedDict((
("secret", {
"label": _("Ticket code (barcode content)"),
"editor_sample": "tdmruoekvkpbv1o2mv8xccvqcikvr58u",
"evaluate": lambda orderposition, order, event: orderposition.secret
}),
("order", {
"label": _("Order code"),
"editor_sample": "A1B2C",
"evaluate": lambda orderposition, order, event: orderposition.order.code
}),
("item", {
"label": _("Product name"),
"editor_sample": _("Sample product"),
"evaluate": lambda orderposition, order, event: str(orderposition.item)
}),
("variation", {
"label": _("Variation name"),
"editor_sample": _("Sample variation"),
"evaluate": lambda op, order, event: str(op.variation) if op.variation else ''
}),
("item_description", {
"label": _("Product description"),
"editor_sample": _("Sample product description"),
"evaluate": lambda orderposition, order, event: str(orderposition.item.description)
}),
("itemvar", {
"label": _("Product name and variation"),
"editor_sample": _("Sample product sample variation"),
"evaluate": lambda orderposition, order, event: (
'{} - {}'.format(orderposition.item, orderposition.variation)
if orderposition.variation else str(orderposition.item)
)
}),
("item_category", {
"label": _("Product category"),
"editor_sample": _("Ticket category"),
"evaluate": lambda orderposition, order, event: (
str(orderposition.item.category.name) if orderposition.item.category else ""
)
}),
("price", {
"label": _("Price"),
"editor_sample": _("123.45 EUR"),
"evaluate": lambda op, order, event: money_filter(op.price, event.currency)
}),
("attendee_name", {
"label": _("Attendee name"),
"editor_sample": _("John Doe"),
"evaluate": lambda op, order, ev: op.attendee_name or (op.addon_to.attendee_name if op.addon_to else '')
}),
("event_name", {
"label": _("Event name"),
"editor_sample": _("Sample event name"),
"evaluate": lambda op, order, ev: str(ev.name)
}),
("event_date", {
"label": _("Event date"),
"editor_sample": _("May 31st, 2017"),
"evaluate": lambda op, order, ev: ev.get_date_from_display(show_times=False)
}),
("event_date_range", {
"label": _("Event date range"),
"editor_sample": _("May 31st June 4th, 2017"),
"evaluate": lambda op, order, ev: ev.get_date_range_display()
}),
("event_begin", {
"label": _("Event begin date and time"),
"editor_sample": _("2017-05-31 20:00"),
"evaluate": lambda op, order, ev: ev.get_date_from_display(show_times=True)
}),
("event_begin_time", {
"label": _("Event begin time"),
"editor_sample": _("20:00"),
"evaluate": lambda op, order, ev: ev.get_time_from_display()
}),
("event_admission", {
"label": _("Event admission date and time"),
"editor_sample": _("2017-05-31 19:00"),
"evaluate": lambda op, order, ev: date_format(
ev.date_admission.astimezone(timezone(ev.settings.timezone)),
"SHORT_DATETIME_FORMAT"
) if ev.date_admission else ""
}),
("event_admission_time", {
"label": _("Event admission time"),
"editor_sample": _("19:00"),
"evaluate": lambda op, order, ev: date_format(
ev.date_admission.astimezone(timezone(ev.settings.timezone)),
"TIME_FORMAT"
) if ev.date_admission else ""
}),
("event_location", {
"label": _("Event location"),
"editor_sample": _("Random City"),
"evaluate": lambda op, order, ev: str(ev.location).replace("\n", "<br/>\n")
}),
("invoice_name", {
"label": _("Invoice address: name"),
"editor_sample": _("John Doe"),
"evaluate": lambda op, order, ev: order.invoice_address.name if getattr(order, 'invoice_address') else ''
}),
("invoice_company", {
"label": _("Invoice address: company"),
"editor_sample": _("Sample company"),
"evaluate": lambda op, order, ev: order.invoice_address.company if getattr(order, 'invoice_address') else ''
}),
("addons", {
"label": _("List of Add-Ons"),
"editor_sample": _("Addon 1\nAddon 2"),
"evaluate": lambda op, order, ev: "<br/>".join([
'{} - {}'.format(p.item, p.variation) if p.variation else str(p.item)
for p in op.addons.select_related('item', 'variation')
])
}),
("organizer", {
"label": _("Organizer name"),
"editor_sample": _("Event organizer company"),
"evaluate": lambda op, order, ev: str(order.event.organizer.name)
}),
("organizer_info_text", {
"label": _("Organizer info text"),
"editor_sample": _("Event organizer info text"),
"evaluate": lambda op, order, ev: str(order.event.settings.organizer_info_text)
}),
))
def get_variables(event):
v = copy.copy(DEFAULT_VARIABLES)
for recv, res in layout_text_variables.send(sender=event):
v.update(res)
return v

View File

@@ -338,3 +338,22 @@ The ``message`` argument will contain an ``EmailMultiAlternatives`` object.
If the email is associated with a specific order, the ``order`` argument will be passed as well, otherwise If the email is associated with a specific order, the ``order`` argument will be passed as well, otherwise
it will be ``None``. it will be ``None``.
""" """
layout_text_variables = EventPluginSignal()
"""
This signal is sent out to collect variables that can be used to display text in ticket-related PDF layouts.
Receivers are expected to return a dictionary with globally unique identifiers as keys and more
dictionaries as values that contain keys like in the following example::
return {
"product": {
"label": _("Product name"),
"editor_sample": _("Sample product"),
"evaluate": lambda orderposition, order, event: str(orderposition.item)
}
}
The evaluate member will be called with the order position, order and event as arguments. The event might
also be a subevent, if applicable.
"""

View File

@@ -1,9 +1,22 @@
{% extends "pretixcontrol/event/base.html" %} {% extends "pretixcontrol/event/base.html" %}
{% load i18n %} {% load i18n %}
{% load staticfiles %} {% load staticfiles %}
{% load compress %}
{% block title %}{% trans "PDF Ticket Editor" %}{% endblock %} {% block title %}{% trans "PDF Ticket Editor" %}{% endblock %}
{% block custom_header %}
{{ block.super }}
{% compress css %}
<link type="text/css" rel="stylesheet" href="{% static "pretixcontrol/scss/pdfeditor.css" %}">
{% endcompress %}
<link type="text/css" rel="stylesheet" href="{% url "control:pdf.css" organizer=request.organizer.slug event=request.event.slug %}">
{% endblock %}
{% block content %} {% block content %}
<h1>{% trans "PDF Ticket Editor" %}</h1> <h1>
{% trans "PDF Editor" %}
{% if title %}
<small>{{ title }}</small>
{% endif %}
</h1>
<script type="application/json" id="editor-data"> <script type="application/json" id="editor-data">
{{ layout|safe }} {{ layout|safe }}

View File

@@ -2,7 +2,7 @@ from django.conf.urls import include, url
from pretix.control.views import ( from pretix.control.views import (
auth, checkin, dashboards, event, global_settings, item, main, orders, auth, checkin, dashboards, event, global_settings, item, main, orders,
organizer, search, subevents, typeahead, user, users, vouchers, organizer, pdf, search, subevents, typeahead, user, users, vouchers,
waitinglist, waitinglist,
) )
@@ -97,6 +97,8 @@ urlpatterns = [
url(r'^settings/tax/add$', event.TaxCreate.as_view(), name='event.settings.tax.add'), url(r'^settings/tax/add$', event.TaxCreate.as_view(), name='event.settings.tax.add'),
url(r'^settings/tax/(?P<rule>\d+)/delete$', event.TaxDelete.as_view(), name='event.settings.tax.delete'), url(r'^settings/tax/(?P<rule>\d+)/delete$', event.TaxDelete.as_view(), name='event.settings.tax.delete'),
url(r'^settings/widget$', event.WidgetSettings.as_view(), name='event.settings.widget'), url(r'^settings/widget$', event.WidgetSettings.as_view(), name='event.settings.widget'),
url(r'^pdf/editor/webfonts.css', pdf.FontsCSSView.as_view(), name='pdf.css'),
url(r'^pdf/editor/(?P<filename>[^/]+).pdf$', pdf.PdfView.as_view(), name='pdf.background'),
url(r'^subevents/$', subevents.SubEventList.as_view(), name='event.subevents'), url(r'^subevents/$', subevents.SubEventList.as_view(), name='event.subevents'),
url(r'^subevents/select2$', typeahead.subevent_select2, name='event.subevents.select2'), url(r'^subevents/select2$', typeahead.subevent_select2, name='event.subevents.select2'),
url(r'^subevents/(?P<subevent>\d+)/$', subevents.SubEventUpdate.as_view(), name='event.subevent'), url(r'^subevents/(?P<subevent>\d+)/$', subevents.SubEventUpdate.as_view(), name='event.subevent'),

View File

@@ -0,0 +1,198 @@
import json
import logging
import mimetypes
from datetime import timedelta
from django.core.files import File
from django.core.files.storage import default_storage
from django.http import (
FileResponse, HttpResponse, HttpResponseBadRequest, JsonResponse,
)
from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.utils.crypto import get_random_string
from django.utils.timezone import now
from django.utils.translation import ugettext as _
from django.views.generic import TemplateView
from pretix.base.i18n import language
from pretix.base.models import CachedFile, InvoiceAddress, OrderPosition
from pretix.base.pdf import get_variables
from pretix.control.permissions import EventPermissionRequiredMixin
from pretix.helpers.database import rolledback_transaction
from pretix.presale.style import get_fonts
logger = logging.getLogger(__name__)
class BaseEditorView(EventPermissionRequiredMixin, TemplateView):
template_name = 'pretixcontrol/pdf/index.html'
permission = 'can_change_settings'
accepted_formats = (
'application/pdf',
)
maxfilesize = 1024 * 1024 * 10
minfilesize = 10
title = None
def get(self, request, *args, **kwargs):
resp = super().get(request, *args, **kwargs)
resp._csp_ignore = True
return resp
def process_upload(self):
f = self.request.FILES.get('background')
error = False
if f.size > self.maxfilesize:
error = _('The uploaded PDF file is to large.')
if f.size < self.minfilesize:
error = _('The uploaded PDF file is to small.')
if mimetypes.guess_type(f.name)[0] not in self.accepted_formats:
error = _('Please only upload PDF files.')
# if there was an error, add error message to response_data and return
if error:
return error, None
return None, f
def _get_preview_position(self):
item = self.request.event.items.create(name=_("Sample product"), default_price=42.23,
description=_("Sample product description"))
item2 = self.request.event.items.create(name=_("Sample workshop"), default_price=23.40)
from pretix.base.models import Order
order = self.request.event.orders.create(status=Order.STATUS_PENDING, datetime=now(),
email='sample@pretix.eu',
locale=self.request.event.settings.locale,
expires=now(), code="PREVIEW1234", total=119)
p = order.positions.create(item=item, attendee_name=_("John Doe"), price=item.default_price)
order.positions.create(item=item2, attendee_name=_("John Doe"), price=item.default_price, addon_to=p)
order.positions.create(item=item2, attendee_name=_("John Doe"), price=item.default_price, addon_to=p)
InvoiceAddress.objects.create(order=order, name=_("John Doe"), company=_("Sample company"))
return p
def generate(self, p: OrderPosition, override_layout=None, override_background=None):
raise NotImplemented
def get_layout_settings_key(self):
raise NotImplemented
def get_background_settings_key(self):
raise NotImplemented
def get_default_background(self):
raise NotImplemented
def get_current_background(self):
return (
self.request.event.settings.get(self.get_background_settings_key()).url
if self.request.event.settings.get(self.get_background_settings_key())
else self.get_default_background()
)
def get_current_layout(self):
return self.request.event.settings.get(self.get_layout_settings_key(), as_type=list)
def save_layout(self):
self.request.event.settings.set(self.get_layout_settings_key(), self.request.POST.get("data"))
def save_background(self, f: CachedFile):
fexisting = self.request.event.settings.get(self.get_background_settings_key(), as_type=File)
if fexisting:
try:
default_storage.delete(fexisting.name)
except OSError: # pragma: no cover
logger.error('Deleting file %s failed.' % fexisting.name)
# Create new file
nonce = get_random_string(length=8)
fname = 'pub/%s-%s/%s/%s.%s.%s' % (
'event', 'settings', self.request.event.pk, self.get_layout_settings_key(), nonce, 'pdf'
)
newname = default_storage.save(fname, f.file)
self.request.event.settings.set(self.get_background_settings_key(), 'file://' + newname)
def post(self, request, *args, **kwargs):
if "background" in request.FILES:
error, fileobj = self.process_upload()
if error:
return JsonResponse({
"status": "error",
"error": error
})
c = CachedFile()
c.expires = now() + timedelta(days=7)
c.date = now()
c.filename = 'background_preview.pdf'
c.type = 'application/pdf'
c.file = fileobj
c.save()
c.refresh_from_db()
return JsonResponse({
"status": "ok",
"id": c.id,
"url": reverse('control:pdf.background', kwargs={
'event': request.event.slug,
'organizer': request.organizer.slug,
'filename': str(c.id)
})
})
cf = None
if request.POST.get("background", "").strip():
try:
cf = CachedFile.objects.get(id=request.POST.get("background"))
except CachedFile.DoesNotExist:
pass
if "preview" in request.POST:
with rolledback_transaction(), language(request.event.settings.locale):
p = self._get_preview_position()
fname, mimet, data = self.generate(
p,
override_layout=(json.loads(self.request.POST.get("data"))
if self.request.POST.get("data") else None),
override_background=cf.file if cf else None
)
resp = HttpResponse(data, content_type=mimet)
ftype = fname.split(".")[-1]
resp['Content-Disposition'] = 'attachment; filename="ticket-preview.{}"'.format(ftype)
return resp
elif "data" in request.POST:
if cf:
self.save_background(cf)
self.save_layout()
return JsonResponse({'status': 'ok'})
return HttpResponseBadRequest()
def get_variables(self):
return get_variables(self.request.event)
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['fonts'] = get_fonts()
ctx['pdf'] = self.get_current_background()
ctx['variables'] = self.get_variables()
ctx['layout'] = json.dumps(self.get_current_layout())
ctx['title'] = self.title
return ctx
class FontsCSSView(TemplateView):
content_type = 'text/css'
template_name = 'pretixcontrol/pdf/webfonts.css'
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['fonts'] = get_fonts()
return ctx
class PdfView(TemplateView):
def get(self, request, *args, **kwargs):
cf = get_object_or_404(CachedFile, id=kwargs.get("filename"), filename="background_preview.pdf")
resp = FileResponse(cf.file, content_type='application/pdf')
resp['Content-Disposition'] = 'attachment; filename="{}"'.format(cf.filename)
return resp

View File

@@ -1,16 +1,13 @@
from functools import partial from functools import partial
from django.dispatch import receiver from django.dispatch import receiver
from django.template.loader import get_template
from django.urls import resolve
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from pretix.base.models import QuestionAnswer from pretix.base.models import QuestionAnswer
from pretix.base.signals import ( from pretix.base.signals import ( # NOQA: legacy import
EventPluginSignal, event_copy_data, register_data_exporters, event_copy_data, layout_text_variables, register_data_exporters,
register_ticket_outputs, register_ticket_outputs,
) )
from pretix.control.signals import html_head
from pretix.presale.style import ( # NOQA: legacy import from pretix.presale.style import ( # NOQA: legacy import
get_fonts, register_fonts, get_fonts, register_fonts,
) )
@@ -28,37 +25,6 @@ def register_data(sender, **kwargs):
return AllTicketsPDF return AllTicketsPDF
@receiver(html_head, dispatch_uid="ticketoutputpdf_html_head")
def html_head_presale(sender, request=None, **kwargs):
url = resolve(request.path_info)
if url.namespace == 'plugins:ticketoutputpdf' and getattr(request, 'organizer', None):
template = get_template('pretixplugins/ticketoutputpdf/control_head.html')
return template.render({
'request': request
})
else:
return ""
layout_text_variables = EventPluginSignal()
"""
This signal is sent out to collect variables that can be used to display text in PDF ticket layouts.
Receivers are expected to return a dictionary with globally unique identifiers as keys and more
dictionaries as values that contain keys like in the following example::
return {
"product": {
"label": _("Product name"),
"editor_sample": _("Sample product"),
"evaluate": lambda orderposition, order, event: str(orderposition.item)
}
}
The evaluate member will be called with the order position, order and event as arguments. The event might
also be a subevent, if applicable.
"""
def get_answer(op, order, event, question_id): def get_answer(op, order, event, question_id):
try: try:
a = op.answers.get(question_id=question_id) a = op.answers.get(question_id=question_id)

View File

@@ -1,7 +0,0 @@
{% load staticfiles %}
{% load compress %}
{% compress css %}
<link type="text/css" rel="stylesheet" href="{% static "pretixplugins/ticketoutputpdf/editor.css" %}">
{% endcompress %}
<link type="text/css" rel="stylesheet" href="{% url "plugins:ticketoutputpdf:css" organizer=request.organizer.slug event=request.event.slug %}">

View File

@@ -2,7 +2,6 @@ import copy
import logging import logging
import re import re
import uuid import uuid
from collections import OrderedDict
from io import BytesIO from io import BytesIO
import bleach import bleach
@@ -11,9 +10,7 @@ from django.core.files import File
from django.core.files.storage import default_storage from django.core.files.storage import default_storage
from django.http import HttpRequest from django.http import HttpRequest
from django.template.loader import get_template from django.template.loader import get_template
from django.utils.formats import date_format
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from pytz import timezone
from reportlab.graphics import renderPDF from reportlab.graphics import renderPDF
from reportlab.graphics.barcode.qr import QrCodeWidget from reportlab.graphics.barcode.qr import QrCodeWidget
from reportlab.graphics.shapes import Drawing from reportlab.graphics.shapes import Drawing
@@ -29,150 +26,13 @@ from reportlab.platypus import Paragraph
from pretix.base.i18n import language from pretix.base.i18n import language
from pretix.base.models import Order, OrderPosition from pretix.base.models import Order, OrderPosition
from pretix.base.templatetags.money import money_filter from pretix.base.pdf import get_variables
from pretix.base.ticketoutput import BaseTicketOutput from pretix.base.ticketoutput import BaseTicketOutput
from pretix.plugins.ticketoutputpdf.signals import ( from pretix.plugins.ticketoutputpdf.signals import get_fonts
get_fonts, layout_text_variables,
)
logger = logging.getLogger('pretix.plugins.ticketoutputpdf') logger = logging.getLogger('pretix.plugins.ticketoutputpdf')
DEFAULT_VARIABLES = OrderedDict((
("secret", {
"label": _("Ticket code (barcode content)"),
"editor_sample": "tdmruoekvkpbv1o2mv8xccvqcikvr58u",
"evaluate": lambda orderposition, order, event: orderposition.secret
}),
("order", {
"label": _("Order code"),
"editor_sample": "A1B2C",
"evaluate": lambda orderposition, order, event: orderposition.order.code
}),
("item", {
"label": _("Product name"),
"editor_sample": _("Sample product"),
"evaluate": lambda orderposition, order, event: str(orderposition.item)
}),
("variation", {
"label": _("Variation name"),
"editor_sample": _("Sample variation"),
"evaluate": lambda op, order, event: str(op.variation) if op.variation else ''
}),
("item_description", {
"label": _("Product description"),
"editor_sample": _("Sample product description"),
"evaluate": lambda orderposition, order, event: str(orderposition.item.description)
}),
("itemvar", {
"label": _("Product name and variation"),
"editor_sample": _("Sample product sample variation"),
"evaluate": lambda orderposition, order, event: (
'{} - {}'.format(orderposition.item, orderposition.variation)
if orderposition.variation else str(orderposition.item)
)
}),
("item_category", {
"label": _("Product category"),
"editor_sample": _("Ticket category"),
"evaluate": lambda orderposition, order, event: (
str(orderposition.item.category.name) if orderposition.item.category else ""
)
}),
("price", {
"label": _("Price"),
"editor_sample": _("123.45 EUR"),
"evaluate": lambda op, order, event: money_filter(op.price, event.currency)
}),
("attendee_name", {
"label": _("Attendee name"),
"editor_sample": _("John Doe"),
"evaluate": lambda op, order, ev: op.attendee_name or (op.addon_to.attendee_name if op.addon_to else '')
}),
("event_name", {
"label": _("Event name"),
"editor_sample": _("Sample event name"),
"evaluate": lambda op, order, ev: str(ev.name)
}),
("event_date", {
"label": _("Event date"),
"editor_sample": _("May 31st, 2017"),
"evaluate": lambda op, order, ev: ev.get_date_from_display(show_times=False)
}),
("event_date_range", {
"label": _("Event date range"),
"editor_sample": _("May 31st June 4th, 2017"),
"evaluate": lambda op, order, ev: ev.get_date_range_display()
}),
("event_begin", {
"label": _("Event begin date and time"),
"editor_sample": _("2017-05-31 20:00"),
"evaluate": lambda op, order, ev: ev.get_date_from_display(show_times=True)
}),
("event_begin_time", {
"label": _("Event begin time"),
"editor_sample": _("20:00"),
"evaluate": lambda op, order, ev: ev.get_time_from_display()
}),
("event_admission", {
"label": _("Event admission date and time"),
"editor_sample": _("2017-05-31 19:00"),
"evaluate": lambda op, order, ev: date_format(
ev.date_admission.astimezone(timezone(ev.settings.timezone)),
"SHORT_DATETIME_FORMAT"
) if ev.date_admission else ""
}),
("event_admission_time", {
"label": _("Event admission time"),
"editor_sample": _("19:00"),
"evaluate": lambda op, order, ev: date_format(
ev.date_admission.astimezone(timezone(ev.settings.timezone)),
"TIME_FORMAT"
) if ev.date_admission else ""
}),
("event_location", {
"label": _("Event location"),
"editor_sample": _("Random City"),
"evaluate": lambda op, order, ev: str(ev.location).replace("\n", "<br/>\n")
}),
("invoice_name", {
"label": _("Invoice address: name"),
"editor_sample": _("John Doe"),
"evaluate": lambda op, order, ev: order.invoice_address.name if getattr(order, 'invoice_address') else ''
}),
("invoice_company", {
"label": _("Invoice address: company"),
"editor_sample": _("Sample company"),
"evaluate": lambda op, order, ev: order.invoice_address.company if getattr(order, 'invoice_address') else ''
}),
("addons", {
"label": _("List of Add-Ons"),
"editor_sample": _("Addon 1\nAddon 2"),
"evaluate": lambda op, order, ev: "<br/>".join([
'{} - {}'.format(p.item, p.variation) if p.variation else str(p.item)
for p in op.addons.select_related('item', 'variation')
])
}),
("organizer", {
"label": _("Organizer name"),
"editor_sample": _("Event organizer company"),
"evaluate": lambda op, order, ev: str(order.event.organizer.name)
}),
("organizer_info_text", {
"label": _("Organizer info text"),
"editor_sample": _("Event organizer info text"),
"evaluate": lambda op, order, ev: str(order.event.settings.organizer_info_text)
}),
))
def get_variables(event):
v = copy.copy(DEFAULT_VARIABLES)
for recv, res in layout_text_variables.send(sender=event):
v.update(res)
return v
class PdfTicketOutput(BaseTicketOutput): class PdfTicketOutput(BaseTicketOutput):
identifier = 'pdf' identifier = 'pdf'
verbose_name = _('PDF output') verbose_name = _('PDF output')

View File

@@ -5,9 +5,4 @@ from . import views
urlpatterns = [ urlpatterns = [
url(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/pdfoutput/editor/$', views.EditorView.as_view(), url(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/pdfoutput/editor/$', views.EditorView.as_view(),
name='editor'), name='editor'),
url(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/pdfoutput/editor/webfonts.css',
views.FontsCSSView.as_view(),
name='css'),
url(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/pdfoutput/editor/(?P<filename>[^/]+).pdf$',
views.PdfView.as_view(), name='pdf'),
] ]

View File

@@ -1,191 +1,52 @@
import json
import logging import logging
import mimetypes
from datetime import timedelta
from django.contrib.staticfiles.templatetags.staticfiles import static from django.templatetags.static import static
from django.core.files import File from django.utils.translation import ugettext_lazy as _
from django.core.files.storage import default_storage
from django.http import (
FileResponse, HttpResponse, HttpResponseBadRequest, JsonResponse,
)
from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.utils.crypto import get_random_string
from django.utils.timezone import now
from django.utils.translation import ugettext as _
from django.views.generic import TemplateView
from pretix.base.i18n import language
from pretix.base.models import ( from pretix.base.models import (
CachedCombinedTicket, CachedFile, CachedTicket, InvoiceAddress, CachedCombinedTicket, CachedTicket, OrderPosition,
) )
from pretix.control.permissions import EventPermissionRequiredMixin from pretix.control.views.pdf import BaseEditorView
from pretix.helpers.database import rolledback_transaction from pretix.plugins.ticketoutputpdf.ticketoutput import PdfTicketOutput
from pretix.plugins.ticketoutputpdf.signals import get_fonts
from .ticketoutput import PdfTicketOutput, get_variables
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class EditorView(EventPermissionRequiredMixin, TemplateView): class EditorView(BaseEditorView):
template_name = 'pretixplugins/ticketoutputpdf/index.html' title = _('Default ticket layout')
permission = 'can_change_settings'
accepted_formats = (
'application/pdf',
)
maxfilesize = 1024 * 1024 * 10
minfilesize = 10
identifier = 'pdf'
def get_output(self, *args, **kwargs): def get_output(self, *args, **kwargs):
return PdfTicketOutput(self.request.event, *args, **kwargs) return PdfTicketOutput(self.request.event, *args, **kwargs)
def get(self, request, *args, **kwargs): def save_layout(self):
resp = super().get(request, *args, **kwargs) super().save_layout()
resp._csp_ignore = True CachedTicket.objects.filter(
return resp order_position__order__event=self.request.event, provider='pdf'
).delete()
CachedCombinedTicket.objects.filter(
order__event=self.request.event, provider='pdf'
).delete()
def process_upload(self): def get_layout_settings_key(self):
f = self.request.FILES.get('background') return 'ticketoutput_pdf_layout'
error = False
if f.size > self.maxfilesize:
error = _('The uploaded PDF file is to large.')
if f.size < self.minfilesize:
error = _('The uploaded PDF file is to small.')
if mimetypes.guess_type(f.name)[0] not in self.accepted_formats:
error = _('Please only upload PDF files.')
# if there was an error, add error message to response_data and return
if error:
return error, None
return None, f
def _get_preview_position(self): def get_background_settings_key(self):
item = self.request.event.items.create(name=_("Sample product"), default_price=42.23, return 'ticketoutput_pdf_background'
description=_("Sample product description"))
item2 = self.request.event.items.create(name=_("Sample workshop"), default_price=23.40)
from pretix.base.models import Order def get_default_background(self):
order = self.request.event.orders.create(status=Order.STATUS_PENDING, datetime=now(), return static('pretixpresale/pdf/ticket_default_a4.pdf')
email='sample@pretix.eu',
locale=self.request.event.settings.locale,
expires=now(), code="PREVIEW1234", total=119)
p = order.positions.create(item=item, attendee_name=_("John Doe"), price=item.default_price) def generate(self, p: OrderPosition, override_layout=None, override_background=None):
order.positions.create(item=item2, attendee_name=_("John Doe"), price=item.default_price, addon_to=p) prov = self.get_output(
order.positions.create(item=item2, attendee_name=_("John Doe"), price=item.default_price, addon_to=p) override_layout=override_layout,
override_background=override_background
InvoiceAddress.objects.create(order=order, name=_("John Doe"), company=_("Sample company"))
return p
def post(self, request, *args, **kwargs):
if "background" in request.FILES:
error, fileobj = self.process_upload()
if error:
return JsonResponse({
"status": "error",
"error": error
})
c = CachedFile()
c.expires = now() + timedelta(days=7)
c.date = now()
c.filename = 'background_preview.pdf'
c.type = 'application/pdf'
c.file = fileobj
c.save()
c.refresh_from_db()
return JsonResponse({
"status": "ok",
"id": c.id,
"url": reverse('plugins:ticketoutputpdf:pdf', kwargs={
'event': request.event.slug,
'organizer': request.organizer.slug,
'filename': str(c.id)
})
})
cf = None
if request.POST.get("background", "").strip():
try:
cf = CachedFile.objects.get(id=request.POST.get("background"))
except CachedFile.DoesNotExist:
pass
if "preview" in request.POST:
with rolledback_transaction(), language(request.event.settings.locale):
p = self._get_preview_position()
prov = self.get_output(
override_layout=(json.loads(request.POST.get("data"))
if request.POST.get("data") else None),
override_background=cf.file if cf else None
)
fname, mimet, data = prov.generate(p)
resp = HttpResponse(data, content_type=mimet)
ftype = fname.split(".")[-1]
resp['Content-Disposition'] = 'attachment; filename="ticket-preview.{}"'.format(ftype)
return resp
elif "data" in request.POST:
if cf:
fexisting = request.event.settings.get('ticketoutput_{}_layout'.format(self.identifier), as_type=File)
if fexisting:
try:
default_storage.delete(fexisting.name)
except OSError: # pragma: no cover
logger.error('Deleting file %s failed.' % fexisting.name)
# Create new file
nonce = get_random_string(length=8)
fname = 'pub/%s-%s/%s/%s.%s.%s' % (
'event', 'settings', self.request.event.pk, 'ticketoutput_{}_layout'.format(self.identifier), nonce, 'pdf'
)
newname = default_storage.save(fname, cf.file)
request.event.settings.set('ticketoutput_{}_background'.format(self.identifier), 'file://' + newname)
request.event.settings.set('ticketoutput_{}_layout'.format(self.identifier), request.POST.get("data"))
CachedTicket.objects.filter(
order_position__order__event=self.request.event, provider=self.identifier
).delete()
CachedCombinedTicket.objects.filter(
order__event=self.request.event, provider=self.identifier
).delete()
return JsonResponse({'status': 'ok'})
return HttpResponseBadRequest()
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
prov = self.get_output()
ctx['fonts'] = get_fonts()
ctx['pdf'] = (
self.request.event.settings.get('ticketoutput_{}_background'.format(self.identifier)).url
if self.request.event.settings.get('ticketoutput_{}_background'.format(self.identifier))
else static('pretixpresale/pdf/ticket_default_a4.pdf')
) )
ctx['variables'] = get_variables(self.request.event) fname, mimet, data = prov.generate(p)
ctx['layout'] = json.dumps( return fname, mimet, data
self.request.event.settings.get('ticketoutput_{}_layout'.format(self.identifier), as_type=list)
def get_current_layout(self):
prov = self.get_output()
return (
self.request.event.settings.get(self.get_layout_settings_key(), as_type=list)
or prov._default_layout() or prov._default_layout()
) )
return ctx
class FontsCSSView(TemplateView):
content_type = 'text/css'
template_name = 'pretixplugins/ticketoutputpdf/webfonts.css'
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['fonts'] = get_fonts()
return ctx
class PdfView(TemplateView):
def get(self, request, *args, **kwargs):
cf = get_object_or_404(CachedFile, id=kwargs.get("filename"), filename="background_preview.pdf")
resp = FileResponse(cf.file, content_type='application/pdf')
resp['Content-Disposition'] = 'attachment; filename="{}"'.format(cf.filename)
return resp