diff --git a/doc/api/resources/checkinlists.rst b/doc/api/resources/checkinlists.rst index 0c044b6393..d3ccff25bd 100644 --- a/doc/api/resources/checkinlists.rst +++ b/doc/api/resources/checkinlists.rst @@ -375,6 +375,7 @@ Order position endpoints "secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w", "addon_to": null, "subevent": null, + "pseudonymization_id": "MQLJvANO3B", "checkins": [ { "list": 1, @@ -467,6 +468,7 @@ Order position endpoints "secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w", "addon_to": null, "subevent": null, + "pseudonymization_id": "MQLJvANO3B", "checkins": [ { "list": 1, diff --git a/doc/api/resources/orders.rst b/doc/api/resources/orders.rst index c5a6443a73..967ed7a2b5 100644 --- a/doc/api/resources/orders.rst +++ b/doc/api/resources/orders.rst @@ -130,6 +130,7 @@ tax_rule integer The ID of the u secret string Secret code printed on the tickets for validation addon_to integer Internal ID of the position this position is an add-on for (or ``null``) subevent integer ID of the date inside an event series this position belongs to (or ``null``). +pseudonymization_id string A random ID, e.g. for use in lead scanning apps checkins list of objects List of check-ins with this ticket ├ list integer Internal ID of the check-in list └ datetime datetime Time of check-in @@ -156,6 +157,10 @@ answers list of objects Answers to user The attributes ``answers.question_identifier`` and ``answers.option_identifiers`` have been added. +.. versionchanged:: 1.16 + + The attribute ``pseudonymization_id`` has been added. + Order endpoints --------------- @@ -235,6 +240,7 @@ Order endpoints "secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w", "addon_to": null, "subevent": null, + "pseudonymization_id": "MQLJvANO3B", "checkins": [ { "list": 44, @@ -349,6 +355,7 @@ Order endpoints "secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w", "addon_to": null, "subevent": null, + "pseudonymization_id": "MQLJvANO3B", "checkins": [ { "list": 44, @@ -847,6 +854,7 @@ Order position endpoints "tax_rule": null, "tax_value": "0.00", "secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w", + "pseudonymization_id": "MQLJvANO3B", "addon_to": null, "subevent": null, "checkins": [ @@ -939,6 +947,7 @@ Order position endpoints "secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w", "addon_to": null, "subevent": null, + "pseudonymization_id": "MQLJvANO3B", "checkins": [ { "list": 44, diff --git a/src/pretix/api/serializers/order.py b/src/pretix/api/serializers/order.py index 0169a73089..393670f10e 100644 --- a/src/pretix/api/serializers/order.py +++ b/src/pretix/api/serializers/order.py @@ -128,7 +128,7 @@ class OrderPositionSerializer(I18nAwareModelSerializer): model = OrderPosition fields = ('id', 'order', 'positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_email', 'voucher', 'tax_rate', 'tax_value', 'secret', 'addon_to', 'subevent', 'checkins', 'downloads', - 'answers', 'tax_rule') + 'answers', 'tax_rule', 'pseudonymization_id') class OrderFeeSerializer(I18nAwareModelSerializer): diff --git a/src/pretix/base/migrations/0093_auto_20180528_1432.py b/src/pretix/base/migrations/0093_auto_20180528_1432.py new file mode 100644 index 0000000000..3f0c9c84fa --- /dev/null +++ b/src/pretix/base/migrations/0093_auto_20180528_1432.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.13 on 2018-05-28 14:32 +from __future__ import unicode_literals + +from django.db import migrations, models +from django.utils.crypto import get_random_string + + +def set_pids(apps, schema_editor): + OrderPosition = apps.get_model('pretixbase', 'OrderPosition') # noqa + taken = set() + charset = list('ABCDEFGHJKLMNPQRSTUVWXYZ3789') + for op in OrderPosition.objects.iterator(): + while True: + code = get_random_string(length=10, allowed_chars=charset) + if code not in taken: + op.pseudonymization_id = code + taken.add(code) + break + op.save(update_fields=['pseudonymization_id']) + + +class Migration(migrations.Migration): + + dependencies = [ + ('pretixbase', '0092_auto_20180511_1224'), + ] + + operations = [ + migrations.AddField( + model_name='orderposition', + name='pseudonymization_id', + field=models.CharField(db_index=True, max_length=16, null=True, unique=True), + ), + migrations.RunPython( + set_pids, + migrations.RunPython.noop, + ), + migrations.AlterField( + model_name='orderposition', + name='pseudonymization_id', + field=models.CharField(db_index=True, default='', max_length=16, unique=True), + preserve_default=False, + ), + ] diff --git a/src/pretix/base/models/orders.py b/src/pretix/base/models/orders.py index 6ed7943524..a5b609c446 100644 --- a/src/pretix/base/models/orders.py +++ b/src/pretix/base/models/orders.py @@ -835,6 +835,11 @@ class OrderPosition(AbstractPosition): verbose_name=_('Tax value') ) secret = models.CharField(max_length=64, default=generate_position_secret, db_index=True) + pseudonymization_id = models.CharField( + max_length=16, + unique=True, + db_index=True + ) class Meta: verbose_name = _("Order position") @@ -916,8 +921,24 @@ class OrderPosition(AbstractPosition): if self.pk is None: while OrderPosition.objects.filter(secret=self.secret).exists(): self.secret = generate_position_secret() + + if not self.pseudonymization_id: + self.assign_pseudonymization_id() + return super().save(*args, **kwargs) + def assign_pseudonymization_id(self): + # This omits some character pairs completely because they are hard to read even on screens (1/I and O/0) + # and includes only one of two characters for some pairs because they are sometimes hard to distinguish in + # handwriting (2/Z, 4/A, 5/S, 6/G). This allows for better detection e.g. in incoming wire transfers that + # might include OCR'd handwritten text + charset = list('ABCDEFGHJKLMNPQRSTUVWXYZ3789') + while True: + code = get_random_string(length=10, allowed_chars=charset) + if not OrderPosition.objects.filter(pseudonymization_id=code).exists(): + self.pseudonymization_id = code + return + class CartPosition(AbstractPosition): """ diff --git a/src/pretix/base/pdf.py b/src/pretix/base/pdf.py index b070228993..6bfd1a1142 100644 --- a/src/pretix/base/pdf.py +++ b/src/pretix/base/pdf.py @@ -211,8 +211,14 @@ class Renderer: pdfmetrics.registerFont(TTFont(family + ' B I', finders.find(styles['bolditalic']['truetype']))) def _draw_barcodearea(self, canvas: Canvas, op: OrderPosition, o: dict): + content = o.get('content', 'secret') + if content == 'secret': + content = op.secret + elif content == 'pseudonymization_id': + content = op.pseudonymization_id + reqs = float(o['size']) * mm - qrw = QrCodeWidget(op.secret, barLevel='H', barHeight=reqs, barWidth=reqs) + qrw = QrCodeWidget(content, barLevel='H', barHeight=reqs, barWidth=reqs) d = Drawing(reqs, reqs) d.add(qrw) qr_x = float(o['left']) * mm diff --git a/src/pretix/control/templates/pretixcontrol/pdf/index.html b/src/pretix/control/templates/pretixcontrol/pdf/index.html index bea593bf6e..17d7eec75d 100644 --- a/src/pretix/control/templates/pretixcontrol/pdf/index.html +++ b/src/pretix/control/templates/pretixcontrol/pdf/index.html @@ -317,9 +317,15 @@ {% trans "Text" %} - + diff --git a/src/pretix/static/pretixcontrol/js/ui/editor.js b/src/pretix/static/pretixcontrol/js/ui/editor.js index 6f0a4cd287..17ba02bf0d 100644 --- a/src/pretix/static/pretixcontrol/js/ui/editor.js +++ b/src/pretix/static/pretixcontrol/js/ui/editor.js @@ -18,7 +18,11 @@ fabric.Barcodearea = fabric.util.createClass(fabric.Rect, { ctx.font = '16px Helvetica'; ctx.fillStyle = '#fff'; - ctx.fillText(gettext('QR Code'), -this.width / 2, -this.height / 2 + 20); + if (this.content === "pseudonymization_id") { + ctx.fillText(gettext('Lead Scan QR'), -this.width / 2, -this.height / 2 + 20); + } else { + ctx.fillText(gettext('Check-in QR'), -this.width / 2, -this.height / 2 + 20); + } }, }); fabric.Barcodearea.fromObject = function (object, callback, forceAsync) { @@ -112,7 +116,8 @@ var editor = { 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) + size: editor._px2mm(o.height * o.scaleY).toFixed(2), + content: o.content, }); } } @@ -122,6 +127,7 @@ var editor = { _add_from_data: function (d) { if (d.type === "barcodearea") { o = editor._add_qrcode(); + o.content = d.content; o.scaleToHeight(editor._mm2px(d.size)); } else if (d.type === "textarea" || o.type === "text") { o = editor._add_text(); @@ -418,6 +424,7 @@ var editor = { lockRotation: true, lockUniScaling: true, fill: '#666', + content: $(this).attr("data-content"), }); rect.setControlsVisibility({'mtr': false}); editor.fabric.add(rect); @@ -645,7 +652,7 @@ var editor = { editor.$fcv = $("#fabric-canvas"); editor.$cva = $("#editor-canvas-area"); editor._load_pdf(); - $("#editor-add-qrcode").click(editor._add_qrcode); + $("#editor-add-qrcode, #editor-add-qrcode-lead").click(editor._add_qrcode); $("#editor-add-text").click(editor._add_text); editor.$cva.get(0).tabIndex = 1000; editor.$cva.on("keydown", editor._on_keydown); diff --git a/src/tests/api/test_checkin.py b/src/tests/api/test_checkin.py index 73b0526451..c69ded5d3c 100644 --- a/src/tests/api/test_checkin.py +++ b/src/tests/api/test_checkin.py @@ -51,7 +51,8 @@ def order(event, item, other_item, taxrule): variation=None, price=Decimal("23"), attendee_name="Peter", - secret="z3fsn8jyufm5kpk768q69gkbyr5f4h6w" + secret="z3fsn8jyufm5kpk768q69gkbyr5f4h6w", + pseudonymization_id="ABCDEFGHKL", ) OrderPosition.objects.create( order=o, @@ -60,7 +61,8 @@ def order(event, item, other_item, taxrule): variation=None, price=Decimal("23"), attendee_name="Michael", - secret="sf4HZG73fU6kwddgjg2QOusFbYZwVKpK" + secret="sf4HZG73fU6kwddgjg2QOusFbYZwVKpK", + pseudonymization_id="BACDEFGHKL", ) return o @@ -83,7 +85,8 @@ TEST_ORDERPOSITION1_RES = { "checkins": [], "downloads": [], "answers": [], - "subevent": None + "subevent": None, + "pseudonymization_id": "ABCDEFGHKL", } TEST_ORDERPOSITION2_RES = { @@ -104,7 +107,8 @@ TEST_ORDERPOSITION2_RES = { "checkins": [], "downloads": [], "answers": [], - "subevent": None + "subevent": None, + "pseudonymization_id": "BACDEFGHKL", } TEST_LIST_RES = { diff --git a/src/tests/api/test_orders.py b/src/tests/api/test_orders.py index 03a410e1a4..a165689b43 100644 --- a/src/tests/api/test_orders.py +++ b/src/tests/api/test_orders.py @@ -76,7 +76,8 @@ def order(event, item, taxrule, question): variation=None, price=Decimal("23"), attendee_name="Peter", - secret="z3fsn8jyufm5kpk768q69gkbyr5f4h6w" + secret="z3fsn8jyufm5kpk768q69gkbyr5f4h6w", + pseudonymization_id="ABCDEFGHKL", ) op.answers.create(question=question, answer='S') return o @@ -97,6 +98,7 @@ TEST_ORDERPOSITION_RES = { "tax_rule": None, "secret": "z3fsn8jyufm5kpk768q69gkbyr5f4h6w", "addon_to": None, + "pseudonymization_id": "ABCDEFGHKL", "checkins": [], "downloads": [], "answers": [