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

View File

@@ -0,0 +1,354 @@
{% extends "pretixcontrol/event/base.html" %}
{% load i18n %}
{% load staticfiles %}
{% load compress %}
{% 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 %}
<h1>
{% trans "PDF Editor" %}
{% if title %}
<small>{{ title }}</small>
{% endif %}
</h1>
<script type="application/json" id="editor-data">
{{ layout|safe }}
</script>
<div class="row">
<div class="col-md-9">
<div class="panel panel-default">
<div class="panel-heading">
<div class="pull-right">
<div class="btn-group">
<button type="button" class="btn btn-default btn-xs" id="toolbox-source"
title="{% trans "Code" %}">
<span class="fa fa-code"></span>
</button>
<button type="button" class="btn btn-default btn-xs" id="toolbox-paste"
title="{% trans "Paste" %}">
<span class="fa fa-paste"></span>
</button>
<button type="button" class="btn btn-default btn-xs" id="toolbox-undo"
title="{% trans "Undo" %}">
<span class="fa fa-undo"></span>
</button>
<button type="button" class="btn btn-default btn-xs" id="toolbox-redo"
title="{% trans "Redo" %}">
<span class="fa fa-repeat"></span>
</button>
</div>
</div>
{% trans "Editor" %}
</div>
<div class="panel-body">
<div id="editor-canvas-area">
<canvas id="pdf-canvas"
data-pdf-url="{{ pdf }}"
data-worker-url="{% static "pretixplugins/ticketoutputpdf/pdf.worker.js" %}">
</canvas>
<div id="fabric-container">
<canvas id="fabric-canvas">
</canvas>
</div>
<div id="source-container">
<div class="alert alert-warning">
<strong>
{% blocktrans trimmed %}
This feature is only intended for advanced users. We recommend to only use it
to copy and share ticket designs, not to modify the design source code.
{% endblocktrans %}
</strong>
</div>
<p>
<textarea id="source-textarea" class="form-control"></textarea>
</p>
<p class="text-right">
<button class="btn btn-default" id="source-close">
{% trans "Cancel" %}
</button>
<button class="btn btn-default" id="source-save">
{% trans "Apply" %}
</button>
</p>
</div>
<div id="loading-container">
<div id="loading-upload">
<span class="fa fa-cog big-rotating-icon"></span>
<p>
{% trans "Uploading new PDF background…" %}
</p>
<div class="progress">
<div class="progress-bar" style="width: 0%;">
</div>
</div>
</div>
<div id="loading-initial">
<h2>{% trans "Welcome to the PDF ticket editor!" %}</h2>
<p>
{% blocktrans trimmed %}
This editor allows you to create a design for the PDF tickets of your event.
You can upload a background PDF and then use this tool to place texts and
a QR code on the ticket.
{% endblocktrans %}
</p>
<p>&nbsp;</p>
<p>
<span class="fa fa-eye fa-2x"></span>
</p>
<p>
{% blocktrans trimmed %}
Please note that the editor can only provide a rough preview. Some details,
for example in text rendering, might look slightly different in the final
tickets. You can use the "Preview" button on the right for a more precise
preview.
{% endblocktrans %}
</p>
<p>&nbsp;</p>
<p>
<span class="fa fa-chrome fa-2x"></span>
<span class="fa fa-firefox fa-2x"></span>
<span class="fa fa-opera fa-2x"></span>
</p>
<p>
{% blocktrans trimmed %}
The editor is tested with recent versions of Google Chrome, Mozilla Firefox
and Opera. Other browsers, especially Internet Explorer or Microsoft Edge, might
have problems displaying your background PDF or loading the correct fonts.
{% endblocktrans %}
</p>
<noscript>
<div class="alert alert-danger">
{% blocktrans trimmed %}
The editor requires JavaScript to work. Please enable JavaScript in your
browser to continue.
{% endblocktrans %}
</div>
</noscript>
<p>&nbsp;</p>
<p>
<em id="editor-loading">
<span class="fa fa-cog fa-spin"></span>
{% trans "Loading…" %}
</em>
<button id="editor-start" class="btn btn-primary sr-only">
{% trans "Start editing" %}
</button>
</p>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-3" id="editor-toolbox-area">
<div class="panel panel-default" id="toolbox">
<div class="panel-heading">
<div class="pull-right object-buttons">
<div class="btn-group">
<button type="button" class="btn btn-default btn-xs" id="toolbox-cut"
title="{% trans "Cut" %}">
<span class="fa fa-cut"></span>
</button>
<button type="button" class="btn btn-default btn-xs" id="toolbox-copy"
title="{% trans "Copy" %}">
<span class="fa fa-copy"></span>
</button>
<button type="button" class="btn btn-danger btn-xs" id="toolbox-delete"
title="{% trans "Delete" %}">
<span class="fa fa-trash"></span>
</button>
</div>
</div>
<span id="toolbox-heading">
{% trans "Loading…" %}
</span>
</div>
<div class="panel-body" id="toolbox-body">
<div class="row control-group pdf-info">
<div class="col-sm-6">
<label>{% trans "Width (mm)" %}</label><br>
<input type="number" id="pdf-info-width" class="input-block-level form-control" disabled>
</div>
<div class="col-sm-6">
<label>{% trans "Height (mm)" %}</label><br>
<input type="number" id="pdf-info-height" class="input-block-level form-control" disabled>
</div>
</div>
<div class="row control-group pdf-info">
<div class="col-sm-12">
<label>{% trans "Background PDF" %}</label><br>
<span class="btn btn-default fileinput-button">
<i class="fa fa-upload"></i>
<span>{% trans "Upload new background" %}</span>
<input id="fileupload" type="file" name="background">
</span>
</div>
</div>
<div class="row control-group position">
<div class="col-sm-6">
<label>{% trans "x (mm)" %}</label><br>
<input type="number" value="13" class="input-block-level form-control" step="0.01"
id="toolbox-position-x">
</div>
<div class="col-sm-6">
<label>{% trans "y (mm)" %}</label><br>
<input type="number" value="13" class="input-block-level form-control" step="0.01"
id="toolbox-position-y">
</div>
</div>
<div class="row control-group squaresize">
<div class="col-sm-12">
<label>{% trans "Size (mm)" %}</label><br>
<input type="number" value="13" class="input-block-level form-control" step="0.01"
id="toolbox-squaresize">
</div>
</div>
<div class="row control-group squaresize">
<div class="col-sm-12">
<p>
{% blocktrans trimmed %}
The final QR code will be slightly smaller because some whitespace is required
for proper scanning.
{% endblocktrans %}
</p>
</div>
</div>
<div class="row control-group text">
<div class="col-sm-6">
<label>{% trans "Font size (pt)" %}</label><br>
<input type="number" value="13" class="input-block-level form-control" step="0.1"
id="toolbox-fontsize">
</div>
<div class="col-sm-6">
<label>&nbsp;</label><br>
<div class="btn-group btn-group-justified" role="group">
<div class="btn-group" role="group">
<button type="button" class="btn btn-default toggling" data-action="bold">
<span class="fa fa-bold"></span>
</button>
</div>
<div class="btn-group" role="group">
<button type="button" class="btn btn-default toggling" data-action="italic">
<span class="fa fa-italic"></span>
</button>
</div>
</div>
</div>
</div>
<div class="row control-group text">
<div class="col-sm-6">
<label>{% trans "Text color" %}</label><br>
<input type="text" value="#000000" class="input-block-level form-control colorpickerfield"
id="toolbox-col">
</div>
<div class="col-sm-6">
<label>&nbsp;</label><br>
<div class="btn-group btn-group-justified" id="toolbox-align">
<div class="btn-group" role="group">
<button type="button" class="btn btn-default option toggling" data-action="left">
<span class="fa fa-align-left"></span>
</button>
</div>
<div class="btn-group" role="group">
<button type="button" class="btn btn-default option toggling" data-action="center">
<span class="fa fa-align-center"></span>
</button>
</div>
<div class="btn-group" role="group">
<button type="button" class="btn btn-default option toggling" data-action="right">
<span class="fa fa-align-right"></span>
</button>
</div>
</div>
</div>
</div>
<div class="row control-group text">
<div class="col-sm-12">
<label>{% trans "Font" %}</label><br>
<select class="input-block-level form-control" id="toolbox-fontfamily">
<option>Open Sans</option>
{% for family in fonts.keys %}
<option>{{ family }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="row control-group text">
<div class="col-sm-12">
<label>{% trans "Width (mm)" %}</label><br>
<input type="number" value="13" class="input-block-level form-control" step="0.01"
id="toolbox-textwidth">
</div>
</div>
<div class="row control-group text">
<div class="col-sm-12">
<label>{% trans "Text content" %}</label><br>
<select class="input-block-level form-control" id="toolbox-content">
{% for varname, var in variables.items %}
<option data-sample="{{ var.editor_sample }}" value="{{ varname }}">{{ var.label }}</option>
{% endfor %}
{% for p in request.organizer.meta_properties.all %}
<option value="meta:{{ p.name }}">
{% trans "Event attribute:" %} {{ p.name }}
</option>
{% endfor %}
<option value="other">{% trans "Other…" %}</option>
</select>
<textarea type="text" value="" class="input-block-level form-control"
id="toolbox-content-other"></textarea>
</div>
</div>
</div>
</div>
<div class="editor-toolbox-text panel panel-default">
<div class="panel-heading">
{% trans "Add a new object" %}
</div>
<div class="panel-body add-buttons">
<button class="btn btn-default btn-block" id="editor-add-text" disabled>
<span class="fa fa-font"></span>
{% trans "Text" %}
</button>
<button class="btn btn-default btn-block" id="editor-add-qrcode" disabled>
<span class="fa fa-qrcode"></span>
{% trans "QR code area" %}
</button>
</div>
</div>
<form method="post" action="" id="preview-form" target="_blank">
<div class="form-group submit-group">
{% csrf_token %}
<input type="hidden" value="" name="data">
<input type="hidden" value="" name="background">
<input type="hidden" value="true" name="preview">
<button type="submit" class="btn btn-default btn-lg" id="editor-preview">
{% trans "Preview" %}
</button>
<button type="submit" class="btn btn-primary btn-save" id="editor-save">
{% trans "Save" %}
</button>
</div>
</form>
</div>
</div>
<script type="text/javascript" src="{% static "pretixplugins/ticketoutputpdf/pdf.js" %}"></script>
<script type="text/javascript" src="{% static "pretixplugins/ticketoutputpdf/fabric.min.js" %}"></script>
<script type="text/javascript" src="{% static "pretixplugins/ticketoutputpdf/editor.js" %}"></script>
{% for family, styles in fonts.items %}
{% for style, formats in styles.items %}
<span class="preload-font" data-family="{{ family }}" data-style="{{ style }}">
giItT1WQy@!-/#
</span>
{% endfor %}
{% endfor %}
{% endblock %}

View File

@@ -0,0 +1,36 @@
{% load staticfiles %}
{% for family, styles in fonts.items %}
{% for style, formats in styles.items %}
@font-face {
font-family: '{{ family }}';
{% if style == "italic" or style == "bolditalic" %}
font-style: italic;
{% else %}
font-style: normal;
{% endif %}
{% if style == "bold" or style == "bolditalic" %}
font-weight: bold;
{% else %}
font-weight: normal;
{% endif %}
src: {% if "woff2" in formats %}url('{% static formats.woff2 %}') format('woff2'),{% endif %}
{% if "woff" in formats %}url('{% static formats.woff %}') format('woff'),{% endif %}
{% if "truetype" in formats %}url('{% static formats.truetype %}') format('truetype'){% endif %};
}
.preload-font[data-family="{{family}}"][data-style="{{style}}"] {
font-family: '{{ family }}';
{% if style == "italic" or style == "bolditalic" %}
font-style: italic;
{% else %}
font-style: normal;
{% endif %}
{% if style == "bold" or style == "bolditalic" %}
font-weight: bold;
{% else %}
font-weight: normal;
{% endif %}
}
{% endfor %}
{% endfor %}

View File

@@ -2,7 +2,7 @@ from django.conf.urls import include, url
from pretix.control.views import (
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,
)
@@ -97,6 +97,8 @@ urlpatterns = [
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/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/select2$', typeahead.subevent_select2, name='event.subevents.select2'),
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