mirror of
https://github.com/pretix/pretix.git
synced 2026-08-22 12:42:00 +00:00
Move PDF editor out of plugin and into core
This commit is contained in:
@@ -1,16 +1,13 @@
|
||||
from functools import partial
|
||||
|
||||
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 pretix.base.models import QuestionAnswer
|
||||
from pretix.base.signals import (
|
||||
EventPluginSignal, event_copy_data, register_data_exporters,
|
||||
from pretix.base.signals import ( # NOQA: legacy import
|
||||
event_copy_data, layout_text_variables, register_data_exporters,
|
||||
register_ticket_outputs,
|
||||
)
|
||||
from pretix.control.signals import html_head
|
||||
from pretix.presale.style import ( # NOQA: legacy import
|
||||
get_fonts, register_fonts,
|
||||
)
|
||||
@@ -28,37 +25,6 @@ def register_data(sender, **kwargs):
|
||||
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):
|
||||
try:
|
||||
a = op.answers.get(question_id=question_id)
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
#fabric-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
#editor-canvas-area {
|
||||
position: relative;
|
||||
min-height: 250px;
|
||||
}
|
||||
body {
|
||||
overflow-y: scroll;
|
||||
}
|
||||
#toolbox .control-group {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
#toolbox .position, #toolbox .squaresize, #toolbox[data-type] .pdf-info, #toolbox .text, #toolbox .object-buttons {
|
||||
display: none;
|
||||
}
|
||||
#toolbox[data-type] .position, #toolbox[data-type=barcodearea] .squaresize, #toolbox[data-type=text] .text, #toolbox[data-type=textarea] .text,
|
||||
#toolbox[data-type] .object-buttons {
|
||||
display: block;
|
||||
}
|
||||
#loading-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
background: white;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
#loading-container > div {
|
||||
max-width: 600px;
|
||||
margin: auto;
|
||||
}
|
||||
#loading-upload {
|
||||
display: none;
|
||||
}
|
||||
.preload-font {
|
||||
visibility: hidden;
|
||||
}
|
||||
#source-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
background: white;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
#source-container textarea {
|
||||
width: 100%;
|
||||
height: 250px;
|
||||
}
|
||||
@@ -1,727 +0,0 @@
|
||||
/*globals $, gettext, fabric, PDFJS*/
|
||||
fabric.Barcodearea = fabric.util.createClass(fabric.Rect, {
|
||||
type: 'barcodearea',
|
||||
|
||||
initialize: function (text, options) {
|
||||
options || (options = {});
|
||||
|
||||
this.callSuper('initialize', text, options);
|
||||
this.set('label', options.label || '');
|
||||
},
|
||||
|
||||
toObject: function () {
|
||||
return fabric.util.object.extend(this.callSuper('toObject'), {});
|
||||
},
|
||||
|
||||
_render: function (ctx) {
|
||||
this.callSuper('_render', ctx);
|
||||
|
||||
ctx.font = '16px Helvetica';
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.fillText(gettext('QR Code'), -this.width / 2, -this.height / 2 + 20);
|
||||
},
|
||||
});
|
||||
fabric.Barcodearea.fromObject = function (object, callback, forceAsync) {
|
||||
return fabric.Object._fromObject('Barcodearea', object, callback, forceAsync);
|
||||
};
|
||||
fabric.Textarea = fabric.util.createClass(fabric.Textbox, {
|
||||
type: 'textarea',
|
||||
|
||||
initialize: function (text, options) {
|
||||
options || (options = {});
|
||||
|
||||
this.callSuper('initialize', text, options);
|
||||
this.set('content', options.content || '');
|
||||
},
|
||||
|
||||
toObject: function(propertiesToInclude) {
|
||||
return this.callSuper('toObject', ['content'].concat(propertiesToInclude));
|
||||
}
|
||||
});
|
||||
fabric.Textarea.fromObject = function (object, callback, forceAsync) {
|
||||
return fabric.Object._fromObject('Textarea', object, callback, forceAsync, 'text');
|
||||
};
|
||||
|
||||
|
||||
var editor = {
|
||||
$pdfcv: null,
|
||||
$fcv: null,
|
||||
$cva: null,
|
||||
$fabric: null,
|
||||
objects: [],
|
||||
history: [],
|
||||
clipboard: [],
|
||||
pdf_page: null,
|
||||
pdf_scale: 1,
|
||||
pdf_viewport: null,
|
||||
_history_pos: 0,
|
||||
_history_modification_in_progress: false,
|
||||
dirty: false,
|
||||
pdf_url: null,
|
||||
uploaded_file_id: null,
|
||||
_window_loaded: false,
|
||||
_fabric_loaded: false,
|
||||
|
||||
_px2mm: function (v) {
|
||||
return v / editor.pdf_scale / 72 * editor.pdf_page.userUnit * 25.4;
|
||||
},
|
||||
|
||||
_mm2px: function (v) {
|
||||
return v * editor.pdf_scale * 72 / editor.pdf_page.userUnit / 25.4;
|
||||
},
|
||||
|
||||
_px2pt: function (v) {
|
||||
return v / editor.pdf_scale * editor.pdf_page.userUnit;
|
||||
},
|
||||
|
||||
_pt2px: function (v) {
|
||||
return v * editor.pdf_scale / editor.pdf_page.userUnit;
|
||||
},
|
||||
|
||||
dump: function (objs) {
|
||||
var d = [];
|
||||
objs = objs || editor.fabric.getObjects();
|
||||
|
||||
for (var i in objs) {
|
||||
var o = objs[i];
|
||||
var top = o.top;
|
||||
var left = o.left;
|
||||
if (o.group) {
|
||||
top += o.group.top + o.group.height / 2;
|
||||
left += o.group.left + o.group.width / 2;
|
||||
}
|
||||
if (o.type === "textarea") {
|
||||
var col = (new fabric.Color(o.getFill()))._source;
|
||||
d.push({
|
||||
type: "textarea",
|
||||
left: editor._px2mm(left).toFixed(2),
|
||||
bottom: editor._px2mm(editor.pdf_viewport.height - o.height - top).toFixed(2),
|
||||
fontsize: editor._px2pt(o.getFontSize()).toFixed(1),
|
||||
color: col,
|
||||
//lineheight: o.lineHeight,
|
||||
fontfamily: o.fontFamily,
|
||||
bold: o.fontWeight === 'bold',
|
||||
italic: o.fontStyle === 'italic',
|
||||
width: editor._px2mm(o.width).toFixed(2),
|
||||
content: o.content,
|
||||
text: o.text,
|
||||
align: o.textAlign,
|
||||
});
|
||||
} else if (o.type === "barcodearea") {
|
||||
d.push({
|
||||
type: "barcodearea",
|
||||
left: editor._px2mm(left).toFixed(2),
|
||||
bottom: editor._px2mm(editor.pdf_viewport.height - o.height * o.scaleY - top).toFixed(2),
|
||||
size: editor._px2mm(o.height * o.scaleY).toFixed(2)
|
||||
});
|
||||
}
|
||||
}
|
||||
return d;
|
||||
},
|
||||
|
||||
_add_from_data: function (d) {
|
||||
if (d.type === "barcodearea") {
|
||||
o = editor._add_qrcode();
|
||||
o.scaleToHeight(editor._mm2px(d.size));
|
||||
} else if (d.type === "textarea" || o.type === "text") {
|
||||
o = editor._add_text();
|
||||
o.setColor('rgb(' + d.color[0] + ',' + d.color[1] + ',' + d.color[2] + ')');
|
||||
o.setFontSize(editor._pt2px(d.fontsize));
|
||||
//o.setLineHeight(d.lineheight);
|
||||
o.setFontFamily(d.fontfamily);
|
||||
o.setFontWeight(d.bold ? 'bold' : 'normal');
|
||||
o.setFontStyle(d.italic ? 'italic' : 'normal');
|
||||
o.setWidth(editor._mm2px(d.width));
|
||||
o.content = d.content;
|
||||
o.setTextAlign(d.align);
|
||||
if (d.content === "other") {
|
||||
o.setText(d.text);
|
||||
} else {
|
||||
o.setText(editor._get_text_sample(d.content));
|
||||
}
|
||||
}
|
||||
|
||||
var new_top = editor.pdf_viewport.height - editor._mm2px(d.bottom) - (o.height * o.scaleY);
|
||||
o.set('left', editor._mm2px(d.left));
|
||||
o.set('top', new_top);
|
||||
o.setCoords();
|
||||
return o;
|
||||
},
|
||||
|
||||
load: function(data) {
|
||||
editor.fabric.clear();
|
||||
for (var i in data) {
|
||||
var d = data[i], o;
|
||||
editor._add_from_data(d);
|
||||
}
|
||||
editor.fabric.renderAll();
|
||||
editor._update_toolbox_values();
|
||||
},
|
||||
|
||||
_get_text_sample: function (key) {
|
||||
if (key.startsWith('meta:')) {
|
||||
return key.substr(5);
|
||||
}
|
||||
return $('#toolbox-content option[value='+key+']').attr('data-sample') || '';
|
||||
},
|
||||
|
||||
_load_pdf: function (dump) {
|
||||
// TODO: Loading indicators
|
||||
var url = editor.pdf_url;
|
||||
// TODO: Handle cross-origin issues if static files are on a different origin
|
||||
PDFJS.workerSrc = editor.$pdfcv.attr("data-worker-url");
|
||||
|
||||
// Asynchronous download of PDF
|
||||
var loadingTask = PDFJS.getDocument(url);
|
||||
loadingTask.promise.then(function (pdf) {
|
||||
console.log('PDF loaded');
|
||||
|
||||
// Fetch the first page
|
||||
var pageNumber = 1;
|
||||
pdf.getPage(pageNumber).then(function (page) {
|
||||
console.log('Page loaded');
|
||||
var canvas = document.getElementById('pdf-canvas');
|
||||
|
||||
var scale = editor.$cva.width() / page.getViewport(1.0).width;
|
||||
var viewport = page.getViewport(scale);
|
||||
|
||||
// Prepare canvas using PDF page dimensions
|
||||
var context = canvas.getContext('2d');
|
||||
context.clearRect(0, 0, canvas.width, canvas.height);
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
|
||||
editor.pdf_page = page;
|
||||
editor.pdf_scale = scale;
|
||||
editor.pdf_viewport = viewport;
|
||||
|
||||
// Render PDF page into canvas context
|
||||
var renderContext = {
|
||||
canvasContext: context,
|
||||
viewport: viewport
|
||||
};
|
||||
var renderTask = page.render(renderContext);
|
||||
renderTask.then(function () {
|
||||
console.log('Page rendered');
|
||||
editor._init_fabric(dump);
|
||||
});
|
||||
});
|
||||
}, function (reason) {
|
||||
var msg = gettext('The PDF background file could not be loaded for the following reason:');
|
||||
editor._error(msg + ' ' + reason);
|
||||
});
|
||||
},
|
||||
|
||||
_init_fabric: function (dump) {
|
||||
editor.$fcv.get(0).width = editor.$pdfcv.get(0).width;
|
||||
editor.$fcv.get(0).height = editor.$pdfcv.get(0).height;
|
||||
editor.fabric = new fabric.Canvas('fabric-canvas');
|
||||
|
||||
editor.fabric.on('object:modified', editor._create_savepoint);
|
||||
editor.fabric.on('object:added', editor._create_savepoint);
|
||||
editor.fabric.on('selection:cleared', editor._update_toolbox);
|
||||
editor.fabric.on('selection:created', editor._update_toolbox);
|
||||
editor.fabric.on('object:selected', editor._update_toolbox);
|
||||
editor.fabric.on('object:moving', editor._update_toolbox_values);
|
||||
editor.fabric.on('object:modified', editor._update_toolbox_values);
|
||||
editor.fabric.on('object:scaling', editor._update_toolbox_values);
|
||||
editor._update_toolbox();
|
||||
|
||||
$("#toolbox-content-other").hide();
|
||||
$(".add-buttons button").prop('disabled', false);
|
||||
|
||||
if (dump) {
|
||||
editor.load(dump);
|
||||
} else {
|
||||
var data = $.trim($("#editor-data").text());
|
||||
if (data) {
|
||||
editor.load(JSON.parse(data));
|
||||
}
|
||||
}
|
||||
editor.history = [];
|
||||
editor._create_savepoint();
|
||||
editor.dirty = !!dump;
|
||||
|
||||
if ($("#loading-upload").is(":visible")) {
|
||||
$("#loading-container, #loading-upload").hide();
|
||||
}
|
||||
|
||||
editor._fabric_loaded = true;
|
||||
console.log("Fabric loaded");
|
||||
if (editor._window_loaded) {
|
||||
editor._ready();
|
||||
}
|
||||
},
|
||||
|
||||
_window_load_event: function () {
|
||||
editor._window_loaded = true;
|
||||
console.log("Window loaded");
|
||||
if (editor._fabric_loaded) {
|
||||
editor._ready();
|
||||
}
|
||||
},
|
||||
|
||||
_ready: function () {
|
||||
$("#editor-loading").hide();
|
||||
$("#editor-start").removeClass("sr-only");
|
||||
$("#editor-start").click(function () {
|
||||
$("#loading-container").hide();
|
||||
$("#loading-initial").remove();
|
||||
});
|
||||
},
|
||||
|
||||
_update_toolbox_values: function () {
|
||||
var o = editor.fabric.getActiveObject();
|
||||
if (!o) {
|
||||
o = editor.fabric.getActiveGroup();
|
||||
if (!o) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
$("#toolbox-position-x").val(editor._px2mm(o.left).toFixed(2));
|
||||
$("#toolbox-position-y").val(editor._px2mm(editor.pdf_viewport.height - o.height * o.scaleY - o.top).toFixed(2));
|
||||
|
||||
if (o.type === "barcodearea") {
|
||||
$("#toolbox-squaresize").val(editor._px2mm(o.height * o.scaleY).toFixed(2));
|
||||
} else if (o.type === "text" || o.type === "textarea") {
|
||||
var col = (new fabric.Color(o.getFill()))._source;
|
||||
$("#toolbox-col").val("#" + ((1 << 24) + (col[0] << 16) + (col[1] << 8) + col[2]).toString(16).slice(1));
|
||||
$("#toolbox-fontsize").val(editor._px2pt(o.fontSize).toFixed(1));
|
||||
//$("#toolbox-lineheight").val(o.lineHeight);
|
||||
$("#toolbox-fontfamily").val(o.fontFamily);
|
||||
$("#toolbox").find("button[data-action=bold]").toggleClass('active', o.fontWeight === 'bold');
|
||||
$("#toolbox").find("button[data-action=italic]").toggleClass('active', o.fontStyle === 'italic');
|
||||
$("#toolbox").find("button[data-action=left]").toggleClass('active', o.textAlign === 'left');
|
||||
$("#toolbox").find("button[data-action=center]").toggleClass('active', o.textAlign === 'center');
|
||||
$("#toolbox").find("button[data-action=right]").toggleClass('active', o.textAlign === 'right');
|
||||
$("#toolbox-textwidth").val(editor._px2mm(o.width).toFixed(2));
|
||||
if (o.type === "textarea") {
|
||||
$("#toolbox-content").val(o.content);
|
||||
$("#toolbox-content-other").toggle($("#toolbox-content").val() === "other");
|
||||
if (o.content === "other") {
|
||||
$("#toolbox-content-other").val(o.text);
|
||||
} else {
|
||||
$("#toolbox-content-other").val("");
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_update_values_from_toolbox: function () {
|
||||
var o = editor.fabric.getActiveObject();
|
||||
if (!o) {
|
||||
o = editor.fabric.getActiveGroup();
|
||||
if (!o) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
var new_top = editor.pdf_viewport.height - editor._mm2px($("#toolbox-position-y").val()) - o.height * o.scaleY;
|
||||
o.set('left', editor._mm2px($("#toolbox-position-x").val()));
|
||||
o.set('top', new_top);
|
||||
|
||||
if (o.type === "barcodearea") {
|
||||
var new_h = editor._mm2px($("#toolbox-squaresize").val());
|
||||
new_top += o.height * o.scaleY - new_h;
|
||||
o.setHeight(new_h);
|
||||
o.setWidth(new_h);
|
||||
o.setScaleX(1);
|
||||
o.setScaleY(1);
|
||||
o.set('top', new_top)
|
||||
} else if (o.type === "textarea" || o.type === "text") {
|
||||
o.setColor($("#toolbox-col").val());
|
||||
o.setFontSize(editor._pt2px($("#toolbox-fontsize").val()));
|
||||
//o.setLineHeight($("#toolbox-lineheight").val());
|
||||
o.setFontFamily($("#toolbox-fontfamily").val());
|
||||
o.setFontWeight($("#toolbox").find("button[data-action=bold]").is('.active') ? 'bold' : 'normal');
|
||||
o.setFontStyle($("#toolbox").find("button[data-action=italic]").is('.active') ? 'italic' : 'normal');
|
||||
var align = $("#toolbox-align").find(".active").attr("data-action");
|
||||
if (align) {
|
||||
o.setTextAlign(align);
|
||||
}
|
||||
o.setWidth(editor._mm2px($("#toolbox-textwidth").val()));
|
||||
$("#toolbox-content-other").toggle($("#toolbox-content").val() === "other");
|
||||
o.content = $("#toolbox-content").val();
|
||||
if ($("#toolbox-content").val() === "other") {
|
||||
o.setText($("#toolbox-content-other").val());
|
||||
} else {
|
||||
o.setText(editor._get_text_sample($("#toolbox-content").val()));
|
||||
}
|
||||
}
|
||||
|
||||
o.setCoords();
|
||||
editor.fabric.renderAll();
|
||||
},
|
||||
|
||||
_update_toolbox: function () {
|
||||
if (editor.fabric.getActiveGroup()) {
|
||||
$("#toolbox").attr("data-type", "group");
|
||||
$("#toolbox-heading").text(gettext("Group of objects"));
|
||||
var g = editor.fabric.getActiveGroup();
|
||||
} else if (editor.fabric.getActiveObject()) {
|
||||
var o = editor.fabric.getActiveObject();
|
||||
$("#toolbox").attr("data-type", o.type);
|
||||
if (o.type === "textarea" || o.type === "text") {
|
||||
$("#toolbox-heading").text(gettext("Text object"));
|
||||
} else if (o.type === "barcodearea") {
|
||||
$("#toolbox-heading").text(gettext("Barcode area"));
|
||||
} else {
|
||||
$("#toolbox-heading").text(gettext("Object"));
|
||||
}
|
||||
} else {
|
||||
$("#toolbox").removeAttr("data-type");
|
||||
$("#toolbox-heading").text(gettext("Ticket design"));
|
||||
$("#pdf-info-width").val(editor._px2mm(editor.pdf_viewport.width).toFixed(2));
|
||||
$("#pdf-info-height").val(editor._px2mm(editor.pdf_viewport.height).toFixed(2));
|
||||
}
|
||||
editor._update_toolbox_values();
|
||||
},
|
||||
|
||||
_error: function (msg) {
|
||||
editor.$cva.before("<div class='alert alert-danger'>" + msg + "</div>");
|
||||
},
|
||||
|
||||
_add_text: function () {
|
||||
var text = new fabric.Textarea(editor._get_text_sample('event_name'), {
|
||||
left: 100,
|
||||
top: 100,
|
||||
width: editor._mm2px(50),
|
||||
lockRotation: true,
|
||||
fontFamily: 'Open Sans',
|
||||
lineHeight: 1,
|
||||
content: 'item',
|
||||
editable: false,
|
||||
fontSize: editor._pt2px(13)
|
||||
});
|
||||
text.setControlsVisibility({
|
||||
'tr': false,
|
||||
'tl': false,
|
||||
'mt': false,
|
||||
'br': false,
|
||||
'bl': false,
|
||||
'mb': false,
|
||||
'mr': true,
|
||||
'ml': true,
|
||||
'mtr': false
|
||||
});
|
||||
editor.fabric.add(text);
|
||||
editor._create_savepoint();
|
||||
return text;
|
||||
},
|
||||
|
||||
_add_qrcode: function () {
|
||||
var rect = new fabric.Barcodearea({
|
||||
left: 100,
|
||||
top: 100,
|
||||
width: 100,
|
||||
height: 100,
|
||||
lockRotation: true,
|
||||
lockUniScaling: true,
|
||||
fill: '#666',
|
||||
});
|
||||
rect.setControlsVisibility({'mtr': false});
|
||||
editor.fabric.add(rect);
|
||||
editor._create_savepoint();
|
||||
return rect;
|
||||
},
|
||||
|
||||
_cut: function () {
|
||||
editor._history_modification_in_progress = true;
|
||||
var thing = editor.fabric.getActiveObject() ? editor.fabric.getActiveObject() : editor.fabric.getActiveGroup();
|
||||
if (thing.type === "group") {
|
||||
editor.clipboard = editor.dump(thing._objects);
|
||||
thing.forEachObject(function (o) {
|
||||
o.remove();
|
||||
});
|
||||
thing.remove();
|
||||
} else {
|
||||
editor.clipboard = editor.dump([thing]);
|
||||
thing.remove();
|
||||
}
|
||||
editor.fabric.discardActiveGroup();
|
||||
editor.fabric.discardActiveObject();
|
||||
editor._history_modification_in_progress = false;
|
||||
editor._create_savepoint();
|
||||
},
|
||||
|
||||
_copy: function () {
|
||||
editor._history_modification_in_progress = true;
|
||||
var thing = editor.fabric.getActiveObject() ? editor.fabric.getActiveObject() : editor.fabric.getActiveGroup();
|
||||
if (thing.type === "group") {
|
||||
editor.clipboard = editor.dump(thing._objects);
|
||||
} else {
|
||||
editor.clipboard = editor.dump([thing]);
|
||||
}
|
||||
editor._history_modification_in_progress = false;
|
||||
editor._create_savepoint();
|
||||
},
|
||||
|
||||
_paste: function () {
|
||||
if (editor.clipboard.length < 1) {
|
||||
return;
|
||||
}
|
||||
editor._history_modification_in_progress = true;
|
||||
var objs = [];
|
||||
for (var i in editor.clipboard) {
|
||||
objs.push(editor._add_from_data(editor.clipboard[i]));
|
||||
}
|
||||
editor.fabric.discardActiveObject();
|
||||
editor.fabric.discardActiveGroup();
|
||||
if (editor.clipboard.length > 1) {
|
||||
var group = new fabric.Group(objs, {
|
||||
originX: 'left',
|
||||
originY: 'top',
|
||||
left: 100,
|
||||
top: 100,
|
||||
});
|
||||
group.setCoords();
|
||||
editor.fabric.setActiveGroup(group);
|
||||
} else {
|
||||
editor.fabric.setActiveObject(objs[0]);
|
||||
}
|
||||
editor._history_modification_in_progress = false;
|
||||
editor._create_savepoint();
|
||||
},
|
||||
|
||||
_delete: function () {
|
||||
var thing = editor.fabric.getActiveObject() ? editor.fabric.getActiveObject() : editor.fabric.getActiveGroup();
|
||||
if (thing.type === "group") {
|
||||
thing.forEachObject(function (o) {
|
||||
o.remove();
|
||||
});
|
||||
thing.remove();
|
||||
editor.fabric.discardActiveGroup();
|
||||
} else {
|
||||
thing.remove();
|
||||
editor.fabric.discardActiveObject();
|
||||
}
|
||||
editor._create_savepoint();
|
||||
},
|
||||
|
||||
_on_keydown: function (e) {
|
||||
var step = e.shiftKey ? editor._mm2px(10) : editor._mm2px(1);
|
||||
var thing = editor.fabric.getActiveObject() ? editor.fabric.getActiveObject() : editor.fabric.getActiveGroup();
|
||||
switch (e.keyCode) {
|
||||
case 38: /* Up arrow */
|
||||
thing.set('top', thing.get('top') - step);
|
||||
thing.setCoords();
|
||||
editor._create_savepoint();
|
||||
break;
|
||||
case 40: /* Down arrow */
|
||||
thing.set('top', thing.get('top') + step);
|
||||
thing.setCoords();
|
||||
editor._create_savepoint();
|
||||
break;
|
||||
case 37: /* Left arrow */
|
||||
thing.set('left', thing.get('left') - step);
|
||||
thing.setCoords();
|
||||
editor._create_savepoint();
|
||||
break;
|
||||
case 39: /* Right arrow */
|
||||
thing.set('left', thing.get('left') + step);
|
||||
thing.setCoords();
|
||||
editor._create_savepoint();
|
||||
break;
|
||||
case 46: /* Delete */
|
||||
editor._delete();
|
||||
break;
|
||||
case 89: /* Y */
|
||||
if (e.ctrlKey) {
|
||||
editor._redo();
|
||||
}
|
||||
break;
|
||||
case 90: /* Z */
|
||||
if (e.ctrlKey) {
|
||||
editor._undo();
|
||||
}
|
||||
break;
|
||||
case 88: /* X */
|
||||
if (e.ctrlKey) {
|
||||
editor._cut();
|
||||
}
|
||||
break;
|
||||
case 86: /* V */
|
||||
if (e.ctrlKey) {
|
||||
editor._paste();
|
||||
}
|
||||
break;
|
||||
case 67: /* C */
|
||||
if (e.ctrlKey) {
|
||||
editor._copy();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
editor.fabric.renderAll();
|
||||
editor._update_toolbox_values();
|
||||
},
|
||||
|
||||
_create_savepoint: function () {
|
||||
if (editor._history_modification_in_progress) {
|
||||
return;
|
||||
}
|
||||
var state = editor.dump();
|
||||
if (editor._history_pos > 0) {
|
||||
editor.history.splice(-1 * editor._history_pos, editor._history_pos);
|
||||
editor._history_pos = 0;
|
||||
}
|
||||
editor.history.push(state);
|
||||
editor.dirty = true;
|
||||
},
|
||||
|
||||
_undo: function undo() {
|
||||
if (editor._history_pos < editor.history.length - 1) {
|
||||
editor._history_modification_in_progress = true;
|
||||
editor._history_pos += 1;
|
||||
editor.fabric.clear().renderAll();
|
||||
editor.load(editor.history[editor.history.length - 1 - editor._history_pos]);
|
||||
editor._history_modification_in_progress = false;
|
||||
editor.dirty = true;
|
||||
}
|
||||
},
|
||||
|
||||
_redo: function redo() {
|
||||
if (editor._history_pos > 0) {
|
||||
editor._history_modification_in_progress = true;
|
||||
editor._history_pos -= 1;
|
||||
editor.load(editor.history[editor.history.length - 1 - editor._history_pos]);
|
||||
editor._history_modification_in_progress = false;
|
||||
editor.dirty = true;
|
||||
}
|
||||
},
|
||||
|
||||
_save: function () {
|
||||
$("#editor-save").prop('disabled', true).prepend('<span class="fa fa-cog fa-spin"></span>');
|
||||
var dump = editor.dump();
|
||||
$.post(window.location.href, {
|
||||
'data': JSON.stringify(dump),
|
||||
'csrfmiddlewaretoken': $("input[name=csrfmiddlewaretoken]").val(),
|
||||
'background': editor.uploaded_file_id,
|
||||
}, function (data) {
|
||||
if (data.status === 'ok') {
|
||||
$("#editor-save span").remove();
|
||||
$("#editor-save").prop('disabled', false);
|
||||
editor.dirty = false;
|
||||
editor.uploaded_file_id = null;
|
||||
} else {
|
||||
alert(gettext('Saving failed.'));
|
||||
}
|
||||
}, 'json');
|
||||
return false;
|
||||
},
|
||||
|
||||
_preview: function () {
|
||||
$("#preview-form input[name=data]").val(JSON.stringify(editor.dump()));
|
||||
$("#preview-form input[name=background]").val(editor.uploaded_file_id);
|
||||
$("#preview-form").get(0).submit();
|
||||
},
|
||||
|
||||
_replace_pdf_file: function (url) {
|
||||
editor.pdf_url = url;
|
||||
d = editor.dump();
|
||||
editor.fabric.dispose();
|
||||
editor._load_pdf(d);
|
||||
},
|
||||
|
||||
_source_show: function () {
|
||||
$("#source-textarea").text(JSON.stringify(editor.dump()));
|
||||
$("#source-container").show();
|
||||
},
|
||||
|
||||
_source_close: function () {
|
||||
$("#source-container").hide();
|
||||
},
|
||||
|
||||
_source_save: function () {
|
||||
editor.load(JSON.parse($("#source-textarea").val()));
|
||||
$("#source-container").hide();
|
||||
},
|
||||
|
||||
init: function () {
|
||||
editor.$pdfcv = $("#pdf-canvas");
|
||||
editor.pdf_url = editor.$pdfcv.attr("data-pdf-url");
|
||||
editor.$fcv = $("#fabric-canvas");
|
||||
editor.$cva = $("#editor-canvas-area");
|
||||
editor._load_pdf();
|
||||
$("#editor-add-qrcode").click(editor._add_qrcode);
|
||||
$("#editor-add-text").click(editor._add_text);
|
||||
editor.$cva.get(0).tabIndex = 1000;
|
||||
editor.$cva.on("keydown", editor._on_keydown);
|
||||
$("#editor-save").on("click", editor._save);
|
||||
$("#editor-preview").on("click", editor._preview);
|
||||
window.onbeforeunload = function () {
|
||||
if (editor.dirty) {
|
||||
return gettext("Do you really want to leave the editor without saving your changes?");
|
||||
}
|
||||
};
|
||||
$("#source-container").hide();
|
||||
|
||||
|
||||
$('#fileupload').fileupload({
|
||||
url: location.href,
|
||||
dataType: 'json',
|
||||
done: function (e, data) {
|
||||
if (data.result.status === "ok") {
|
||||
editor.uploaded_file_id = data.result.id;
|
||||
editor._replace_pdf_file(data.result.url);
|
||||
} else {
|
||||
alert(data.result.error || gettext("Error while uploading your PDF file, please try again."));
|
||||
$("#loading-container, #loading-upload").hide();
|
||||
}
|
||||
$("#fileupload").prop('disabled', false);
|
||||
$(".fileinput-button").removeClass("disabled");
|
||||
},
|
||||
add: function (e, data) {
|
||||
data.formData = {
|
||||
'csrfmiddlewaretoken': $("input[name=csrfmiddlewaretoken]").val()
|
||||
};
|
||||
$("#loading-container, #loading-upload").show();
|
||||
$("#loading-upload .progress").show();
|
||||
$('#loading-upload .progress-bar').css('width', 0);
|
||||
$("#fileupload").prop('disabled', true);
|
||||
$(".fileinput-button").addClass("disabled");
|
||||
data.process().done(function () {
|
||||
data.submit();
|
||||
});
|
||||
},
|
||||
progressall: function (e, data) {
|
||||
var progress = parseInt(data.loaded / data.total * 100, 10);
|
||||
$('#loading-upload .progress-bar').css('width', progress + '%');
|
||||
}
|
||||
}).prop('disabled', !$.support.fileInput).parent().addClass($.support.fileInput ? undefined : 'disabled');
|
||||
|
||||
$("#toolbox input[type=number], #toolbox textarea, #toolbox input[type=text]").bind('change keydown keyup' +
|
||||
' input', editor._update_values_from_toolbox);
|
||||
$("#toolbox input[type=number], #toolbox textarea, #toolbox input[type=text], #toolbox input[type=radio]").bind('change', editor._create_savepoint);
|
||||
$("#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 () {
|
||||
if ($(this).is(".option")) {
|
||||
$(this).addClass("active");
|
||||
$(this).parent().siblings().find("button").removeClass("active");
|
||||
} else {
|
||||
$(this).toggleClass("active");
|
||||
}
|
||||
editor._update_values_from_toolbox();
|
||||
editor._create_savepoint();
|
||||
});
|
||||
$("#toolbox .colorpickerfield").bind('changeColor', editor._update_values_from_toolbox);
|
||||
$("#toolbox-copy").bind('click', editor._copy);
|
||||
$("#toolbox-cut").bind('click', editor._cut);
|
||||
$("#toolbox-delete").bind('click', editor._delete);
|
||||
$("#toolbox-paste").bind('click', editor._paste);
|
||||
$("#toolbox-undo").bind('click', editor._undo);
|
||||
$("#toolbox-redo").bind('click', editor._redo);
|
||||
$("#toolbox-source").bind('click', editor._source_show);
|
||||
$("#source-close").bind('click', editor._source_close);
|
||||
$("#source-save").bind('click', editor._source_save);
|
||||
}
|
||||
};
|
||||
|
||||
$(function () {
|
||||
editor.init();
|
||||
});
|
||||
$(window).bind('load', editor._window_load_event);
|
||||
File diff suppressed because it is too large
Load Diff
-9
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
-50501
File diff suppressed because it is too large
Load Diff
-7
@@ -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 %}">
|
||||
-341
@@ -1,341 +0,0 @@
|
||||
{% extends "pretixcontrol/event/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load staticfiles %}
|
||||
{% block title %}{% trans "PDF Ticket Editor" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h1>{% trans "PDF Ticket Editor" %}</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> </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> </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> </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> </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> </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 %}
|
||||
|
||||
-36
@@ -1,36 +0,0 @@
|
||||
{% 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 %}
|
||||
@@ -2,7 +2,6 @@ import copy
|
||||
import logging
|
||||
import re
|
||||
import uuid
|
||||
from collections import OrderedDict
|
||||
from io import BytesIO
|
||||
|
||||
import bleach
|
||||
@@ -11,9 +10,7 @@ from django.core.files import File
|
||||
from django.core.files.storage import default_storage
|
||||
from django.http import HttpRequest
|
||||
from django.template.loader import get_template
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from pytz import timezone
|
||||
from reportlab.graphics import renderPDF
|
||||
from reportlab.graphics.barcode.qr import QrCodeWidget
|
||||
from reportlab.graphics.shapes import Drawing
|
||||
@@ -29,150 +26,13 @@ from reportlab.platypus import Paragraph
|
||||
|
||||
from pretix.base.i18n import language
|
||||
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.plugins.ticketoutputpdf.signals import (
|
||||
get_fonts, layout_text_variables,
|
||||
)
|
||||
from pretix.plugins.ticketoutputpdf.signals import get_fonts
|
||||
|
||||
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):
|
||||
identifier = 'pdf'
|
||||
verbose_name = _('PDF output')
|
||||
|
||||
@@ -5,9 +5,4 @@ from . import views
|
||||
urlpatterns = [
|
||||
url(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/pdfoutput/editor/$', views.EditorView.as_view(),
|
||||
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'),
|
||||
]
|
||||
|
||||
@@ -1,191 +1,52 @@
|
||||
import json
|
||||
import logging
|
||||
import mimetypes
|
||||
from datetime import timedelta
|
||||
|
||||
from django.contrib.staticfiles.templatetags.staticfiles import static
|
||||
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 django.templatetags.static import static
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import (
|
||||
CachedCombinedTicket, CachedFile, CachedTicket, InvoiceAddress,
|
||||
CachedCombinedTicket, CachedTicket, OrderPosition,
|
||||
)
|
||||
from pretix.control.permissions import EventPermissionRequiredMixin
|
||||
from pretix.helpers.database import rolledback_transaction
|
||||
from pretix.plugins.ticketoutputpdf.signals import get_fonts
|
||||
|
||||
from .ticketoutput import PdfTicketOutput, get_variables
|
||||
from pretix.control.views.pdf import BaseEditorView
|
||||
from pretix.plugins.ticketoutputpdf.ticketoutput import PdfTicketOutput
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EditorView(EventPermissionRequiredMixin, TemplateView):
|
||||
template_name = 'pretixplugins/ticketoutputpdf/index.html'
|
||||
permission = 'can_change_settings'
|
||||
accepted_formats = (
|
||||
'application/pdf',
|
||||
)
|
||||
maxfilesize = 1024 * 1024 * 10
|
||||
minfilesize = 10
|
||||
identifier = 'pdf'
|
||||
class EditorView(BaseEditorView):
|
||||
title = _('Default ticket layout')
|
||||
|
||||
def get_output(self, *args, **kwargs):
|
||||
return PdfTicketOutput(self.request.event, *args, **kwargs)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
resp = super().get(request, *args, **kwargs)
|
||||
resp._csp_ignore = True
|
||||
return resp
|
||||
def save_layout(self):
|
||||
super().save_layout()
|
||||
CachedTicket.objects.filter(
|
||||
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):
|
||||
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_layout_settings_key(self):
|
||||
return 'ticketoutput_pdf_layout'
|
||||
|
||||
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)
|
||||
def get_background_settings_key(self):
|
||||
return 'ticketoutput_pdf_background'
|
||||
|
||||
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)
|
||||
def get_default_background(self):
|
||||
return static('pretixpresale/pdf/ticket_default_a4.pdf')
|
||||
|
||||
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 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')
|
||||
def generate(self, p: OrderPosition, override_layout=None, override_background=None):
|
||||
prov = self.get_output(
|
||||
override_layout=override_layout,
|
||||
override_background=override_background
|
||||
)
|
||||
ctx['variables'] = get_variables(self.request.event)
|
||||
ctx['layout'] = json.dumps(
|
||||
self.request.event.settings.get('ticketoutput_{}_layout'.format(self.identifier), as_type=list)
|
||||
fname, mimet, data = prov.generate(p)
|
||||
return fname, mimet, data
|
||||
|
||||
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()
|
||||
)
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user