diff --git a/.gitattributes b/.gitattributes index ae2fa07cf9..3fdddca17b 100644 --- a/.gitattributes +++ b/.gitattributes @@ -4,7 +4,10 @@ src/static/typeahead/* linguist-vendored src/static/moment/* linguist-vendored src/static/datetimepicker/* linguist-vendored src/static/colorpicker/* linguist-vendored +src/static/fileupload/* linguist-vendored src/static/charts/* linguist-vendored +src/pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/fabric.* linguist-vendored +src/pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/pdf.* linguist-vendored # Denote all files that are truly binary and should not be modified. *.eot binary diff --git a/src/pretix/base/models/event.py b/src/pretix/base/models/event.py index 4c216ece1b..3316418364 100644 --- a/src/pretix/base/models/event.py +++ b/src/pretix/base/models/event.py @@ -137,7 +137,7 @@ class Event(LoggedModel): return [] return self.plugins.split(",") - def get_date_from_display(self, tz=None) -> str: + def get_date_from_display(self, tz=None, show_times=True) -> str: """ Returns a formatted string containing the start date of the event with respect to the current locale and to the ``show_times`` setting. @@ -145,7 +145,17 @@ class Event(LoggedModel): tz = tz or pytz.timezone(self.settings.timezone) return _date( self.date_from.astimezone(tz), - "DATETIME_FORMAT" if self.settings.show_times else "DATE_FORMAT" + "DATETIME_FORMAT" if self.settings.show_times and show_times else "DATE_FORMAT" + ) + + def get_time_from_display(self, tz=None) -> str: + """ + Returns a formatted string containing the start time of the event, ignoring + the ``show_times`` setting. + """ + tz = tz or pytz.timezone(self.settings.timezone) + return _date( + self.date_from.astimezone(tz), "TIME_FORMAT" ) def get_date_to_display(self, tz=None) -> str: diff --git a/src/pretix/control/templates/pretixcontrol/base.html b/src/pretix/control/templates/pretixcontrol/base.html index 73dddb0729..a87d1ccac8 100644 --- a/src/pretix/control/templates/pretixcontrol/base.html +++ b/src/pretix/control/templates/pretixcontrol/base.html @@ -36,6 +36,8 @@ + + {% endcompress %} {{ html_head|safe }} diff --git a/src/pretix/plugins/ticketoutputpdf/signals.py b/src/pretix/plugins/ticketoutputpdf/signals.py index 0dfb943f1b..256be80e90 100644 --- a/src/pretix/plugins/ticketoutputpdf/signals.py +++ b/src/pretix/plugins/ticketoutputpdf/signals.py @@ -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 diff --git a/src/pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.css b/src/pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.css new file mode 100644 index 0000000000..32b9cba717 --- /dev/null +++ b/src/pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.css @@ -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; +} diff --git a/src/pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js b/src/pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js new file mode 100644 index 0000000000..e85bafc2db --- /dev/null +++ b/src/pretix/plugins/ticketoutputpdf/static/pretixplugins/ticketoutputpdf/editor.js @@ -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("