diff --git a/doc/plugins/pretixdroid.rst b/doc/plugins/pretixdroid.rst index 89990c9a02..c9840f13fe 100644 --- a/doc/plugins/pretixdroid.rst +++ b/doc/plugins/pretixdroid.rst @@ -29,6 +29,10 @@ uses to communicate with the pretix server. You can optionally include the additional parameter ``force`` to indicate that the request should be logged regardless of previous check-ins for the same ticket. This might be useful if you made the entry decision offline. + You can optionally include the additional parameter ``nonce`` with a globally unique random value to identify this + check-in. This is meant to be used to prevent duplicate check-ins when you are just retrying after a connection + failure. + **Example successful response**: .. sourcecode:: http diff --git a/src/pretix/base/migrations/0059_checkin_nonce.py b/src/pretix/base/migrations/0059_checkin_nonce.py new file mode 100644 index 0000000000..c9287b2231 --- /dev/null +++ b/src/pretix/base/migrations/0059_checkin_nonce.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-05-04 07:06 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('pretixbase', '0058_auto_20170429_1020'), + ] + + operations = [ + migrations.AddField( + model_name='checkin', + name='nonce', + field=models.CharField(blank=True, max_length=190, null=True), + ), + ] diff --git a/src/pretix/base/models/checkin.py b/src/pretix/base/models/checkin.py index 3911d60094..37808779c2 100644 --- a/src/pretix/base/models/checkin.py +++ b/src/pretix/base/models/checkin.py @@ -8,3 +8,4 @@ class Checkin(models.Model): """ position = models.ForeignKey('pretixbase.OrderPosition', related_name='checkins') datetime = models.DateTimeField(default=now) + nonce = models.CharField(max_length=190, null=True, blank=True) diff --git a/src/pretix/plugins/pretixdroid/views.py b/src/pretix/plugins/pretixdroid/views.py index 564ce3791f..f610d1066a 100644 --- a/src/pretix/plugins/pretixdroid/views.py +++ b/src/pretix/plugins/pretixdroid/views.py @@ -70,6 +70,7 @@ class ApiRedeemView(ApiView): def post(self, request, **kwargs): secret = request.POST.get('secret', '!INVALID!') force = request.POST.get('force', 'false') in ('true', 'True') + nonce = request.POST.get('nonce') response = { 'version': API_VERSION } @@ -86,24 +87,25 @@ class ApiRedeemView(ApiView): order__event=self.event, secret=secret ) if op.order.status == Order.STATUS_PAID: - ci, created = Checkin.objects.get_or_create(position=op) - if created and 'datetime' in request.POST: - ci.datetime = dt - ci.save() + ci, created = Checkin.objects.get_or_create(position=op, defaults={ + 'datetime': dt, + 'nonce': nonce, + }) else: response['status'] = 'error' response['reason'] = 'unpaid' if 'status' not in response: - if created: + if created or (nonce and nonce == ci.nonce): response['status'] = 'ok' - op.order.log_action('pretix.plugins.pretixdroid.scan', data={ - 'position': op.id, - 'positionid': op.positionid, - 'first': True, - 'forced': False, - 'datetime': dt, - }) + if created: + op.order.log_action('pretix.plugins.pretixdroid.scan', data={ + 'position': op.id, + 'positionid': op.positionid, + 'first': True, + 'forced': False, + 'datetime': dt, + }) else: if force: response['status'] = 'ok' diff --git a/src/tests/plugins/test_pretixdroid.py b/src/tests/plugins/test_pretixdroid.py index 75cca02826..8e87ad55c2 100644 --- a/src/tests/plugins/test_pretixdroid.py +++ b/src/tests/plugins/test_pretixdroid.py @@ -85,6 +85,22 @@ def test_only_once(client, env): assert jdata['reason'] == 'already_redeemed' +@pytest.mark.django_db +def test_reupload_same_nonce(client, env): + env[0].settings.set('pretixdroid_key', 'abcdefg') + + resp = client.post('/pretixdroid/api/%s/%s/redeem/?key=%s' % (env[0].organizer.slug, env[0].slug, 'abcdefg'), + data={'secret': '1234', 'nonce': 'fooobar'}) + jdata = json.loads(resp.content.decode("utf-8")) + assert jdata['version'] == 2 + assert jdata['status'] == 'ok' + resp = client.post('/pretixdroid/api/%s/%s/redeem/?key=%s' % (env[0].organizer.slug, env[0].slug, 'abcdefg'), + data={'secret': '1234', 'nonce': 'fooobar'}) + jdata = json.loads(resp.content.decode("utf-8")) + assert jdata['status'] == 'ok' + assert Checkin.objects.count() == 1 + + @pytest.mark.django_db def test_forced_multiple(client, env): env[0].settings.set('pretixdroid_key', 'abcdefg')