forked from CGM_Public/pretix_original
Fix #41 -- Drag-and-drop ticket editor
Undo/redo Useful toolbox Font selection Add text content Use hex for colors JS-side dump and load Save Load layout, proper undo/redo First steps to Python rendering More PDF rendering Copy and paste Buttons for keyboard actions Splash Screen Block unbeforeunload in dirty state Remove debugging output Preview Upload new PDFs via the editor Fix bugs during PDF reload, link in settings form New default ticket Add OpenSans BI Custom fonts, fix tests
This commit is contained in:
@@ -1,9 +1,56 @@
|
||||
from django.dispatch import receiver
|
||||
from django.dispatch import Signal, receiver
|
||||
from django.template.loader import get_template
|
||||
from django.urls import resolve
|
||||
|
||||
from pretix.base.signals import register_ticket_outputs
|
||||
from pretix.control.signals import html_head
|
||||
|
||||
|
||||
@receiver(register_ticket_outputs, dispatch_uid="output_pdf")
|
||||
def register_ticket_outputs(sender, **kwargs):
|
||||
from .ticketoutput import PdfTicketOutput
|
||||
return PdfTicketOutput
|
||||
|
||||
|
||||
@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':
|
||||
template = get_template('pretixplugins/ticketoutputpdf/control_head.html')
|
||||
return template.render({
|
||||
'request': request
|
||||
})
|
||||
else:
|
||||
return ""
|
||||
|
||||
|
||||
register_fonts = Signal()
|
||||
"""
|
||||
Return a dictionaries of the following structure. Paths should be relative to static root.
|
||||
|
||||
{
|
||||
"font name": {
|
||||
"regular": {
|
||||
"truetype": "….ttf",
|
||||
"woff": "…",
|
||||
"woff2": "…"
|
||||
},
|
||||
"bold": {
|
||||
...
|
||||
},
|
||||
"italic": {
|
||||
...
|
||||
},
|
||||
"bolditalic": {
|
||||
...
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
def get_fonts():
|
||||
f = {}
|
||||
for recv, value in register_fonts.send(0):
|
||||
f.update(value)
|
||||
return f
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
#fabric-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
#editor-canvas-area {
|
||||
position: relative;
|
||||
min-height: 500px;
|
||||
}
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,708 @@
|
||||
/*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];
|
||||
if (o.type === "textarea") {
|
||||
var col = (new fabric.Color(o.getFill()))._source;
|
||||
d.push({
|
||||
type: "textarea",
|
||||
left: editor._px2mm(o.left).toFixed(2),
|
||||
bottom: editor._px2mm(editor.pdf_viewport.height - o.height - o.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(o.left).toFixed(2),
|
||||
bottom: editor._px2mm(editor.pdf_viewport.height - o.height * o.scaleY - o.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.text_samples[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();
|
||||
},
|
||||
|
||||
text_samples: {
|
||||
"secret": "tdmruoekvkpbv1o2mv8xccvqcikvr58u",
|
||||
"order": "A1B2C",
|
||||
"item": gettext("Sample product"),
|
||||
"variation": gettext("Sample variation"),
|
||||
"itemvar": gettext("Sample product – sample variation"),
|
||||
"price": gettext("123.45 EUR"),
|
||||
"attendee_name": gettext("John Doe"),
|
||||
"event_name": gettext("Sample event name"),
|
||||
"event_date": gettext("May 31st, 2017"),
|
||||
"event_begin_time": gettext("20:00"),
|
||||
"event_location": gettext("Random City")
|
||||
},
|
||||
|
||||
_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.text_samples[$("#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.text_samples['item'], {
|
||||
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');
|
||||
},
|
||||
|
||||
_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);
|
||||
},
|
||||
|
||||
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?");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
$('#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);
|
||||
}
|
||||
};
|
||||
|
||||
$(function () {
|
||||
editor.init();
|
||||
});
|
||||
$(window).bind('load', editor._window_load_event);
|
||||
26834
src/pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/fabric.js
vendored
Normal file
26834
src/pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/fabric.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
9
src/pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/fabric.min.js
vendored
Normal file
9
src/pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/fabric.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
9052
src/pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/pdf.js
vendored
Normal file
9052
src/pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/pdf.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
50501
src/pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/pdf.worker.js
vendored
Normal file
50501
src/pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/pdf.worker.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,7 @@
|
||||
{% 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 %}">
|
||||
@@ -0,0 +1,22 @@
|
||||
{% load i18n %}
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label">
|
||||
{% trans "Ticket design" %}
|
||||
</label>
|
||||
<div class="col-md-9">
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
You can customize the ticket design with our PDF ticket editor. There, you can upload a PDF file used
|
||||
as a background for the tickets and then place various texts and QR codes on the background at the
|
||||
positions of your choice. The editor is easy to use thanks to its drag-and-drop user interface, but it
|
||||
requires a modern browser and a decent internet connection.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<p>
|
||||
<a class="btn btn-default" target="_blank"
|
||||
href="{% url "plugins:ticketoutputpdf:editor" organizer=request.organizer.slug event=request.event.slug %}">
|
||||
{% trans "Open the PDF editor in a new tab" %}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,323 @@
|
||||
{% 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-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"
|
||||
{% if request.event.settings.ticketoutput_pdf_background %}
|
||||
data-pdf-url="{{ request.event.settings.ticketoutput_pdf_background.url }}"
|
||||
{% else %}
|
||||
data-pdf-url="{% static "pretixpresale/pdf/ticket_default_a4.pdf" %}"
|
||||
{% endif %}
|
||||
data-worker-url="{% static "pretixplugins/ticketoutputpdf/pdf.worker.js" %}">
|
||||
|
||||
</canvas>
|
||||
<div id="fabric-container">
|
||||
<canvas id="fabric-canvas">
|
||||
</canvas>
|
||||
</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 Chome, 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">
|
||||
<option value="secret">{% trans "Ticket code (barcode content)" %}</option>
|
||||
<option value="order">{% trans "Order code" %}</option>
|
||||
<option value="item">{% trans "Product name" %}</option>
|
||||
<option value="variation">{% trans "Variation name" %}</option>
|
||||
<option value="itemvar">{% trans "Product name and variation" %}</option>
|
||||
<option value="price">{% trans "Price" %}</option>
|
||||
<option value="attendee_name">{% trans "Attendee name" %}</option>
|
||||
<option value="event_name">{% trans "Event name" %}</option>
|
||||
<option value="event_date">{% trans "Event date" %}</option>
|
||||
<option value="event_begin_time">{% trans "Event begin time" %}</option>
|
||||
<option value="event_location">{% trans "Event location" %}</option>
|
||||
<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 %}
|
||||
|
||||
@@ -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 %}
|
||||
@@ -1,17 +1,31 @@
|
||||
import copy
|
||||
import logging
|
||||
from collections import OrderedDict
|
||||
import uuid
|
||||
from io import BytesIO
|
||||
|
||||
from django import forms
|
||||
from django.contrib.staticfiles import finders
|
||||
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 localize
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from reportlab.graphics import renderPDF
|
||||
from reportlab.graphics.barcode.qr import QrCodeWidget
|
||||
from reportlab.graphics.shapes import Drawing
|
||||
from reportlab.lib.colors import Color
|
||||
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT
|
||||
from reportlab.lib.styles import ParagraphStyle
|
||||
from reportlab.lib.units import mm
|
||||
from reportlab.pdfbase import pdfmetrics
|
||||
from reportlab.pdfbase.pdfmetrics import getAscentDescent
|
||||
from reportlab.pdfbase.ttfonts import TTFont
|
||||
from reportlab.pdfgen.canvas import Canvas
|
||||
from reportlab.platypus import Paragraph
|
||||
|
||||
from pretix.base.models import Order
|
||||
from pretix.base.models import Order, OrderPosition
|
||||
from pretix.base.ticketoutput import BaseTicketOutput
|
||||
from pretix.control.forms import ExtFileField
|
||||
from pretix.plugins.ticketoutputpdf.signals import get_fonts
|
||||
|
||||
logger = logging.getLogger('pretix.plugins.ticketoutputpdf')
|
||||
|
||||
@@ -21,71 +35,97 @@ class PdfTicketOutput(BaseTicketOutput):
|
||||
verbose_name = _('PDF output')
|
||||
download_button_text = _('PDF')
|
||||
|
||||
def _draw_page(self, p, op, order):
|
||||
from reportlab.graphics.shapes import Drawing
|
||||
from reportlab.lib import units
|
||||
from reportlab.graphics.barcode.qr import QrCodeWidget
|
||||
from reportlab.graphics import renderPDF
|
||||
def __init__(self, event, override_layout=None, override_background=None):
|
||||
self.override_layout = override_layout
|
||||
self.override_background = override_background
|
||||
super().__init__(event)
|
||||
|
||||
event_s = self.settings.get('event_s', default=22, as_type=float)
|
||||
if event_s:
|
||||
p.setFont("Helvetica", event_s)
|
||||
event_x = self.settings.get('event_x', default=15, as_type=float)
|
||||
event_y = self.settings.get('event_y', default=235, as_type=float)
|
||||
p.drawString(event_x * units.mm, event_y * units.mm, str(self.event.name))
|
||||
def _register_fonts(self):
|
||||
pdfmetrics.registerFont(TTFont('Open Sans', finders.find('fonts/OpenSans-Regular.ttf')))
|
||||
pdfmetrics.registerFont(TTFont('Open Sans I', finders.find('fonts/OpenSans-Italic.ttf')))
|
||||
pdfmetrics.registerFont(TTFont('Open Sans B', finders.find('fonts/OpenSans-Bold.ttf')))
|
||||
pdfmetrics.registerFont(TTFont('Open Sans B I', finders.find('fonts/OpenSans-BoldItalic.ttf')))
|
||||
|
||||
order_s = self.settings.get('order_s', default=17, as_type=float)
|
||||
if order_s:
|
||||
p.setFont("Helvetica", order_s)
|
||||
order_x = self.settings.get('order_x', default=15, as_type=float)
|
||||
order_y = self.settings.get('order_y', default=220, as_type=float)
|
||||
p.drawString(order_x * units.mm, order_y * units.mm, _('Order code: {code}').format(code=order.code))
|
||||
for family, styles in get_fonts().items():
|
||||
print(family, finders.find(styles['regular']['truetype']))
|
||||
pdfmetrics.registerFont(TTFont(family, finders.find(styles['regular']['truetype'])))
|
||||
pdfmetrics.registerFont(TTFont(family + ' I', finders.find(styles['italic']['truetype'])))
|
||||
pdfmetrics.registerFont(TTFont(family + ' B', finders.find(styles['bold']['truetype'])))
|
||||
pdfmetrics.registerFont(TTFont(family + ' B I', finders.find(styles['bolditalic']['truetype'])))
|
||||
|
||||
name_s = self.settings.get('name_s', default=17, as_type=float)
|
||||
if name_s:
|
||||
p.setFont("Helvetica", name_s)
|
||||
name_x = self.settings.get('name_x', default=15, as_type=float)
|
||||
name_y = self.settings.get('name_y', default=210, as_type=float)
|
||||
item = str(op.item.name)
|
||||
if op.variation:
|
||||
item += " – " + str(op.variation)
|
||||
p.drawString(name_x * units.mm, name_y * units.mm, item)
|
||||
def _draw_barcodearea(self, canvas: Canvas, op: OrderPosition, o: dict):
|
||||
reqs = float(o['size']) * mm
|
||||
qrw = QrCodeWidget(op.secret, barLevel='H', barHeight=reqs, barWidth=reqs)
|
||||
d = Drawing(reqs, reqs)
|
||||
d.add(qrw)
|
||||
qr_x = float(o['left']) * mm
|
||||
qr_y = float(o['bottom']) * mm
|
||||
renderPDF.draw(d, canvas, qr_x, qr_y)
|
||||
|
||||
price_s = self.settings.get('price_s', default=17, as_type=float)
|
||||
if price_s:
|
||||
p.setFont("Helvetica", price_s)
|
||||
price_x = self.settings.get('price_x', default=15, as_type=float)
|
||||
price_y = self.settings.get('price_y', default=200, as_type=float)
|
||||
p.drawString(price_x * units.mm, price_y * units.mm, "%s %s" % (str(op.price), self.event.currency))
|
||||
def _get_text_content(self, op: OrderPosition, order: Order, o: dict):
|
||||
if o['content'] == 'other':
|
||||
return o['text'].replace("\n", "<br/>\n")
|
||||
elif o['content'] == 'order':
|
||||
return order.code
|
||||
elif o['content'] == 'item':
|
||||
return str(op.item)
|
||||
elif o['content'] == 'secret':
|
||||
return op.secret
|
||||
elif o['content'] == 'variation':
|
||||
return str(op.variation) if op.variation else ''
|
||||
elif o['content'] == 'itemvar':
|
||||
return '{} - {}'.format(op.item, op.variation) if op.variation else str(op.item)
|
||||
elif o['content'] == 'price':
|
||||
return '{} {}'.format(order.event.currency, localize(op.price))
|
||||
elif o['content'] == 'attendee_name':
|
||||
return op.attendee_name or (op.addon_to.attendee_name if op.addon_to else '')
|
||||
elif o['content'] == 'event_name':
|
||||
return str(order.event)
|
||||
elif o['content'] == 'event_location':
|
||||
return str(order.event.location)
|
||||
elif o['content'] == 'event_date':
|
||||
return order.event.get_date_from_display(show_times=False)
|
||||
elif o['content'] == 'event_begin_time':
|
||||
return order.event.get_date_from_display(show_times=False)
|
||||
return ''
|
||||
|
||||
qr_s = self.settings.get('qr_s', default=80, as_type=float)
|
||||
if qr_s:
|
||||
reqs = qr_s * units.mm
|
||||
qrw = QrCodeWidget(op.secret, barLevel='H')
|
||||
b = qrw.getBounds()
|
||||
w = b[2] - b[0]
|
||||
h = b[3] - b[1]
|
||||
d = Drawing(reqs, reqs, transform=[reqs / w, 0, 0, reqs / h, 0, 0])
|
||||
d.add(qrw)
|
||||
qr_x = self.settings.get('qr_x', default=10, as_type=float)
|
||||
qr_y = self.settings.get('qr_y', default=120, as_type=float)
|
||||
renderPDF.draw(d, p, qr_x * units.mm, qr_y * units.mm)
|
||||
def _draw_textarea(self, canvas: Canvas, op: OrderPosition, order: Order, o: dict):
|
||||
font = o['fontfamily']
|
||||
if o['bold']:
|
||||
font += ' B'
|
||||
if o['italic']:
|
||||
font += ' I'
|
||||
|
||||
code_s = self.settings.get('code_s', default=11, as_type=float)
|
||||
if code_s:
|
||||
p.setFont("Helvetica", code_s)
|
||||
code_x = self.settings.get('code_x', default=15, as_type=float)
|
||||
code_y = self.settings.get('code_y', default=120, as_type=float)
|
||||
p.drawString(code_x * units.mm, code_y * units.mm, op.secret)
|
||||
align_map = {
|
||||
'left': TA_LEFT,
|
||||
'center': TA_CENTER,
|
||||
'right': TA_RIGHT
|
||||
}
|
||||
style = ParagraphStyle(
|
||||
name=uuid.uuid4().hex,
|
||||
fontName=font,
|
||||
fontSize=float(o['fontsize']),
|
||||
leading=float(o['fontsize']),
|
||||
autoLeading="max",
|
||||
textColor=Color(o['color'][0] / 255, o['color'][1] / 255, o['color'][2] / 255),
|
||||
alignment=align_map[o['align']]
|
||||
)
|
||||
|
||||
attendee_s = self.settings.get('attendee_s', default=0, as_type=float)
|
||||
if attendee_s and op.attendee_name:
|
||||
p.setFont("Helvetica", attendee_s)
|
||||
attendee_x = self.settings.get('attendee_x', default=15, as_type=float)
|
||||
attendee_y = self.settings.get('attendee_y', default=90, as_type=float)
|
||||
p.drawString(attendee_x * units.mm, attendee_y * units.mm, op.attendee_name)
|
||||
p = Paragraph(self._get_text_content(op, order, o), style=style)
|
||||
p.wrapOn(canvas, float(o['width']) * mm, 1000 * mm)
|
||||
# p_size = p.wrap(float(o['width']) * mm, 1000 * mm)
|
||||
ad = getAscentDescent(font, float(o['fontsize']))
|
||||
p.drawOn(canvas, float(o['left']) * mm, float(o['bottom']) * mm - ad[1])
|
||||
|
||||
p.showPage()
|
||||
def _draw_page(self, canvas: Canvas, op: OrderPosition, order: Order):
|
||||
objs = self.override_layout or self.settings.get('layout', as_type=list) or self._legacy_layout()
|
||||
for o in objs:
|
||||
if o['type'] == "barcodearea":
|
||||
self._draw_barcodearea(canvas, op, o)
|
||||
elif o['type'] == "textarea":
|
||||
self._draw_textarea(canvas, op, order, o)
|
||||
|
||||
canvas.showPage()
|
||||
|
||||
def generate_order(self, order: Order):
|
||||
buffer = BytesIO()
|
||||
@@ -113,97 +153,198 @@ class PdfTicketOutput(BaseTicketOutput):
|
||||
from reportlab.pdfgen import canvas
|
||||
from reportlab.lib import pagesizes
|
||||
|
||||
pagesize = self.settings.get('pagesize', default='A4')
|
||||
if hasattr(pagesizes, pagesize):
|
||||
pagesize = getattr(pagesizes, pagesize)
|
||||
else:
|
||||
pagesize = pagesizes.A4
|
||||
orientation = self.settings.get('orientation', default='portrait')
|
||||
if hasattr(pagesizes, orientation):
|
||||
pagesize = getattr(pagesizes, orientation)(pagesize)
|
||||
# Doesn't matter as we'll overpaint it over a background later
|
||||
pagesize = pagesizes.A4
|
||||
|
||||
self._register_fonts()
|
||||
return canvas.Canvas(buffer, pagesize=pagesize)
|
||||
|
||||
def _render_with_background(self, buffer):
|
||||
def _render_with_background(self, buffer, title=_('Ticket')):
|
||||
from PyPDF2 import PdfFileWriter, PdfFileReader
|
||||
buffer.seek(0)
|
||||
new_pdf = PdfFileReader(buffer)
|
||||
output = PdfFileWriter()
|
||||
bg_file = self.settings.get('background', as_type=File)
|
||||
if isinstance(bg_file, File):
|
||||
if self.override_background:
|
||||
bgf = default_storage.open(self.override_background.name, "rb")
|
||||
elif isinstance(bg_file, File):
|
||||
bgf = default_storage.open(bg_file.name, "rb")
|
||||
else:
|
||||
bgf = open(finders.find('pretixpresale/pdf/ticket_default_a4.pdf'), "rb")
|
||||
bg_pdf = PdfFileReader(bgf)
|
||||
|
||||
for page in new_pdf.pages:
|
||||
bg_page = copy.copy(bg_pdf.getPage(0))
|
||||
bg_page.mergePage(page)
|
||||
output.addPage(bg_page)
|
||||
|
||||
output.addMetadata({
|
||||
'/Title': str(title),
|
||||
'/Creator': 'pretix',
|
||||
})
|
||||
outbuffer = BytesIO()
|
||||
output.write(outbuffer)
|
||||
outbuffer.seek(0)
|
||||
return outbuffer
|
||||
|
||||
@property
|
||||
def settings_form_fields(self) -> dict:
|
||||
return OrderedDict(
|
||||
list(super().settings_form_fields.items()) + [
|
||||
('paper_size',
|
||||
forms.ChoiceField(
|
||||
label=_('Paper size'),
|
||||
choices=(
|
||||
('A4', 'A4'),
|
||||
('A5', 'A5'),
|
||||
('B4', 'B4'),
|
||||
('B5', 'B5'),
|
||||
('letter', 'Letter'),
|
||||
('legal', 'Legal'),
|
||||
),
|
||||
required=False
|
||||
)),
|
||||
('orientation',
|
||||
forms.ChoiceField(
|
||||
label=_('Paper orientation'),
|
||||
choices=(
|
||||
('portrait', _('Portrait')),
|
||||
('landscape', _('Landscape')),
|
||||
),
|
||||
required=False
|
||||
)),
|
||||
('background',
|
||||
ExtFileField(
|
||||
label=_('Background PDF'),
|
||||
ext_whitelist=(".pdf", ),
|
||||
required=False
|
||||
)),
|
||||
('qr_x', forms.FloatField(label=_('QR-Code x position (mm)'), required=False)),
|
||||
('qr_y', forms.FloatField(label=_('QR-Code y position (mm)'), required=False)),
|
||||
('qr_s', forms.FloatField(label=_('QR-Code size (mm)'), required=False)),
|
||||
('code_x', forms.FloatField(label=_('Ticket code x position (mm)'), required=False)),
|
||||
('code_y', forms.FloatField(label=_('Ticket code y position (mm)'), required=False)),
|
||||
('code_s', forms.FloatField(label=_('Ticket code size (mm)'), required=False,
|
||||
help_text=_('Visible by default, set this to 0 to hide the element.'))),
|
||||
('order_x', forms.FloatField(label=_('Order x position (mm)'), required=False)),
|
||||
('order_y', forms.FloatField(label=_('Order y position (mm)'), required=False)),
|
||||
('order_s', forms.FloatField(label=_('Order size (mm)'), required=False,
|
||||
help_text=_('Visible by default, set this to 0 to hide the element.'))),
|
||||
('name_x', forms.FloatField(label=_('Product name x position (mm)'), required=False)),
|
||||
('name_y', forms.FloatField(label=_('Product name y position (mm)'), required=False)),
|
||||
('name_s', forms.FloatField(label=_('Product name size (mm)'), required=False,
|
||||
help_text=_('Visible by default, set this to 0 to hide the element.'))),
|
||||
('price_x', forms.FloatField(label=_('Price x position (mm)'), required=False)),
|
||||
('price_y', forms.FloatField(label=_('Price y position (mm)'), required=False)),
|
||||
('price_s', forms.FloatField(label=_('Price size (mm)'), required=False,
|
||||
help_text=_('Visible by default, set this to 0 to hide the element.'))),
|
||||
('event_x', forms.FloatField(label=_('Event name x position (mm)'), required=False)),
|
||||
('event_y', forms.FloatField(label=_('Event name y position (mm)'), required=False)),
|
||||
('event_s', forms.FloatField(label=_('Event name size (mm)'), required=False,
|
||||
help_text=_('Visible by default, set this to 0 to hide the element.'))),
|
||||
('attendee_x', forms.FloatField(label=_('Attendee name x position (mm)'), required=False)),
|
||||
('attendee_y', forms.FloatField(label=_('Attendee name y position (mm)'), required=False)),
|
||||
('attendee_s', forms.FloatField(label=_('Attendee name size (mm)'), required=False,
|
||||
help_text=_('Invisible by default, set this to a number greater than 0 '
|
||||
'to show.')))
|
||||
]
|
||||
)
|
||||
def settings_content_render(self, request: HttpRequest) -> str:
|
||||
"""
|
||||
When the event's administrator visits the event configuration
|
||||
page, this method is called. It may return HTML containing additional information
|
||||
that is displayed below the form fields configured in ``settings_form_fields``.
|
||||
"""
|
||||
template = get_template('pretixplugins/ticketoutputpdf/form.html')
|
||||
return template.render({
|
||||
'request': request
|
||||
})
|
||||
|
||||
def _legacy_layout(self):
|
||||
if self.settings.get('ticketoutput_pdf_background'):
|
||||
return self._migrate_from_old_settings()
|
||||
else:
|
||||
return self._default_layout()
|
||||
|
||||
def _default_layout(self):
|
||||
return [
|
||||
{"type": "textarea", "left": "17.50", "bottom": "274.60", "fontsize": "16.0", "color": [0, 0, 0, 1],
|
||||
"fontfamily": "Open Sans", "bold": False, "italic": False, "width": "175.00", "content": "event_name",
|
||||
"text": "Sample event name", "align": "left"},
|
||||
{"type": "textarea", "left": "17.50", "bottom": "262.90", "fontsize": "13.0", "color": [0, 0, 0, 1],
|
||||
"fontfamily": "Open Sans", "bold": False, "italic": False, "width": "110.00", "content": "itemvar",
|
||||
"text": "Sample product – sample variation", "align": "left"},
|
||||
{"type": "textarea", "left": "17.50", "bottom": "252.50", "fontsize": "13.0", "color": [0, 0, 0, 1],
|
||||
"fontfamily": "Open Sans", "bold": False, "italic": False, "width": "110.00", "content": "attendee_name",
|
||||
"text": "John Doe", "align": "left"},
|
||||
{"type": "textarea", "left": "17.50", "bottom": "242.10", "fontsize": "13.0", "color": [0, 0, 0, 1],
|
||||
"fontfamily": "Open Sans", "bold": False, "italic": False, "width": "110.00", "content": "event_date",
|
||||
"text": "May 31st, 2017", "align": "left"},
|
||||
{"type": "textarea", "left": "17.50", "bottom": "234.30", "fontsize": "13.0", "color": [0, 0, 0, 1],
|
||||
"fontfamily": "Open Sans", "bold": False, "italic": False, "width": "110.00", "content": "event_location",
|
||||
"text": "Random City", "align": "left"},
|
||||
{"type": "textarea", "left": "17.50", "bottom": "194.50", "fontsize": "13.0", "color": [0, 0, 0, 1],
|
||||
"fontfamily": "Open Sans", "bold": False, "italic": False, "width": "30.00", "content": "order",
|
||||
"text": "A1B2C", "align": "left"},
|
||||
{"type": "textarea", "left": "52.50", "bottom": "194.50", "fontsize": "13.0", "color": [0, 0, 0, 1],
|
||||
"fontfamily": "Open Sans", "bold": False, "italic": False, "width": "45.00", "content": "price",
|
||||
"text": "123.45 EUR", "align": "right"},
|
||||
{"type": "textarea", "left": "102.50", "bottom": "194.50", "fontsize": "13.0", "color": [0, 0, 0, 1],
|
||||
"fontfamily": "Open Sans", "bold": False, "italic": False, "width": "90.00", "content": "secret",
|
||||
"text": "tdmruoekvkpbv1o2mv8xccvqcikvr58u", "align": "left"},
|
||||
{"type": "barcodearea", "left": "130.40", "bottom": "204.50", "size": "64.00"}
|
||||
]
|
||||
|
||||
def _migrate_from_old_settings(self):
|
||||
l = []
|
||||
|
||||
event_s = self.settings.get('event_s', default=22, as_type=float)
|
||||
if event_s:
|
||||
l.append({
|
||||
'type': 'textarea',
|
||||
'fontfamily': 'Helvetica',
|
||||
'left': self.settings.get('event_x', default=15, as_type=float),
|
||||
'bottom': self.settings.get('event_y', default=235, as_type=float),
|
||||
'fontsize': event_s,
|
||||
'color': [0, 0, 0, 1],
|
||||
'bold': False,
|
||||
'italic': False,
|
||||
'width': 150,
|
||||
'content': 'event_name',
|
||||
'text': 'Sample event',
|
||||
'align': 'left'
|
||||
})
|
||||
|
||||
order_s = self.settings.get('order_s', default=17, as_type=float)
|
||||
if order_s:
|
||||
l.append({
|
||||
'type': 'textarea',
|
||||
'fontfamily': 'Helvetica',
|
||||
'left': self.settings.get('order_x', default=15, as_type=float),
|
||||
'bottom': self.settings.get('order_y', default=220, as_type=float),
|
||||
'fontsize': order_s,
|
||||
'color': [0, 0, 0, 1],
|
||||
'bold': False,
|
||||
'italic': False,
|
||||
'width': 150,
|
||||
'content': 'order',
|
||||
'text': 'AB1C2',
|
||||
'align': 'left'
|
||||
})
|
||||
|
||||
name_s = self.settings.get('name_s', default=17, as_type=float)
|
||||
if name_s:
|
||||
l.append({
|
||||
'type': 'textarea',
|
||||
'fontfamily': 'Helvetica',
|
||||
'left': self.settings.get('name_x', default=15, as_type=float),
|
||||
'bottom': self.settings.get('name_y', default=210, as_type=float),
|
||||
'fontsize': name_s,
|
||||
'color': [0, 0, 0, 1],
|
||||
'bold': False,
|
||||
'italic': False,
|
||||
'width': 150,
|
||||
'content': 'itemvar',
|
||||
'text': 'Sample Producs - XS',
|
||||
'align': 'left'
|
||||
})
|
||||
|
||||
price_s = self.settings.get('price_s', default=17, as_type=float)
|
||||
if price_s:
|
||||
l.append({
|
||||
'type': 'textarea',
|
||||
'fontfamily': 'Helvetica',
|
||||
'left': self.settings.get('price_x', default=15, as_type=float),
|
||||
'bottom': self.settings.get('price_y', default=200, as_type=float),
|
||||
'fontsize': price_s,
|
||||
'color': [0, 0, 0, 1],
|
||||
'bold': False,
|
||||
'italic': False,
|
||||
'width': 150,
|
||||
'content': 'price',
|
||||
'text': 'EUR 12,34',
|
||||
'align': 'left'
|
||||
})
|
||||
|
||||
qr_s = self.settings.get('qr_s', default=80, as_type=float)
|
||||
if qr_s:
|
||||
l.append({
|
||||
'type': 'barcodearea',
|
||||
'left': self.settings.get('qr_x', default=10, as_type=float),
|
||||
'bottom': self.settings.get('qr_y', default=120, as_type=float),
|
||||
'size': qr_s,
|
||||
})
|
||||
|
||||
code_s = self.settings.get('code_s', default=11, as_type=float)
|
||||
if code_s:
|
||||
l.append({
|
||||
'type': 'textarea',
|
||||
'fontfamily': 'Helvetica',
|
||||
'left': self.settings.get('code_x', default=15, as_type=float),
|
||||
'bottom': self.settings.get('code_y', default=120, as_type=float),
|
||||
'fontsize': code_s,
|
||||
'color': [0, 0, 0, 1],
|
||||
'bold': False,
|
||||
'italic': False,
|
||||
'width': 150,
|
||||
'content': 'secret',
|
||||
'text': 'asdsdgjfgbgkjdastjrxfdg',
|
||||
'align': 'left'
|
||||
})
|
||||
|
||||
attendee_s = self.settings.get('attendee_s', default=0, as_type=float)
|
||||
if attendee_s:
|
||||
l.append({
|
||||
'type': 'textarea',
|
||||
'fontfamily': 'Helvetica',
|
||||
'left': self.settings.get('attendee_x', default=15, as_type=float),
|
||||
'bottom': self.settings.get('attendee_y', default=90, as_type=float),
|
||||
'fontsize': attendee_s,
|
||||
'color': [0, 0, 0, 1],
|
||||
'bold': False,
|
||||
'italic': False,
|
||||
'width': 150,
|
||||
'content': 'attendee_name',
|
||||
'text': 'John Doe',
|
||||
'align': 'left'
|
||||
})
|
||||
|
||||
return l
|
||||
|
||||
11
src/pretix/plugins/ticketoutputpdf/urls.py
Normal file
11
src/pretix/plugins/ticketoutputpdf/urls.py
Normal file
@@ -0,0 +1,11 @@
|
||||
from django.conf.urls import url
|
||||
|
||||
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'),
|
||||
]
|
||||
138
src/pretix/plugins/ticketoutputpdf/views.py
Normal file
138
src/pretix/plugins/ticketoutputpdf/views.py
Normal file
@@ -0,0 +1,138 @@
|
||||
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 HttpResponse, HttpResponseBadRequest, JsonResponse
|
||||
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
|
||||
from pretix.control.permissions import EventPermissionRequiredMixin
|
||||
from pretix.control.views import ChartContainingView
|
||||
from pretix.helpers.database import rolledback_transaction
|
||||
from pretix.plugins.ticketoutputpdf.signals import get_fonts
|
||||
|
||||
from .ticketoutput import PdfTicketOutput
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EditorView(EventPermissionRequiredMixin, ChartContainingView, TemplateView):
|
||||
template_name = 'pretixplugins/ticketoutputpdf/index.html'
|
||||
permission = 'can_change_settings'
|
||||
accepted_formats = (
|
||||
'application/pdf',
|
||||
)
|
||||
maxfilesize = 1024 * 1024 * 10
|
||||
minfilesize = 10
|
||||
|
||||
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 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.pdf'
|
||||
c.type = 'application/pdf'
|
||||
c.file = fileobj
|
||||
c.save()
|
||||
c.refresh_from_db()
|
||||
return JsonResponse({
|
||||
"status": "ok",
|
||||
"id": c.id,
|
||||
"url": c.file.url
|
||||
})
|
||||
|
||||
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):
|
||||
item = request.event.items.create(name=_("Sample product"), default_price=42.23)
|
||||
|
||||
from pretix.base.models import Order
|
||||
order = request.event.orders.create(status=Order.STATUS_PENDING, datetime=now(),
|
||||
email='sample@pretix.eu',
|
||||
expires=now(), code="PREVIEW1234", total=119)
|
||||
|
||||
p = order.positions.create(item=item, attendee_name=_("John Doe"), price=item.default_price)
|
||||
|
||||
prov = PdfTicketOutput(request.event,
|
||||
override_layout=json.loads(request.POST.get("data")),
|
||||
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-Security-Policy'] = "style-src 'unsafe-inline'; object-src 'self'"
|
||||
resp['Content-Disposition'] = 'inline; filename="ticket-preview.{}"'.format(ftype)
|
||||
return resp
|
||||
elif "data" in request.POST:
|
||||
if cf:
|
||||
fexisting = request.event.settings.get('ticketoutput_pdf_layout', 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 = '%s-%s/%s/%s.%s.%s' % (
|
||||
'event', 'settings', self.request.event.pk, 'ticketoutput_pdf_layout', nonce, 'pdf'
|
||||
)
|
||||
newname = default_storage.save(fname, cf.file)
|
||||
request.event.settings.set('ticketoutput_pdf_background', 'file://' + newname)
|
||||
|
||||
request.event.settings.set('ticketoutput_pdf_layout', request.POST.get("data"))
|
||||
return JsonResponse({'status': 'ok'})
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
prov = PdfTicketOutput(self.request.event)
|
||||
ctx['fonts'] = get_fonts()
|
||||
ctx['layout'] = json.dumps(
|
||||
self.request.event.settings.get('ticketoutput_pdf_layout', 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
|
||||
Reference in New Issue
Block a user