From 854fb1218e7a4e4872d6bb37341a60a1e1638864 Mon Sep 17 00:00:00 2001 From: Raphael Michel Date: Tue, 30 Aug 2016 12:57:06 +0200 Subject: [PATCH] pretixdroid: Completely new API --- .../pretixdroid/migrations/0001_initial.py | 26 +++++++ .../pretixdroid/migrations/__init__.py | 0 src/pretix/plugins/pretixdroid/models.py | 6 ++ .../pretixplugins/pretixdroid/pretixdroid.js | 2 +- .../pretixdroid/configuration.html | 3 + src/pretix/plugins/pretixdroid/urls.py | 4 +- src/pretix/plugins/pretixdroid/views.py | 69 ++++++++++++++----- 7 files changed, 91 insertions(+), 19 deletions(-) create mode 100644 src/pretix/plugins/pretixdroid/migrations/0001_initial.py create mode 100644 src/pretix/plugins/pretixdroid/migrations/__init__.py create mode 100644 src/pretix/plugins/pretixdroid/models.py diff --git a/src/pretix/plugins/pretixdroid/migrations/0001_initial.py b/src/pretix/plugins/pretixdroid/migrations/0001_initial.py new file mode 100644 index 000000000..9bbf831fd --- /dev/null +++ b/src/pretix/plugins/pretixdroid/migrations/0001_initial.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.9.7 on 2016-08-30 10:09 +from __future__ import unicode_literals + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('pretixbase', '0033_auto_20160821_2222'), + ] + + operations = [ + migrations.CreateModel( + name='Checkin', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('datetime', models.DateTimeField(auto_now_add=True)), + ('position', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='pretixdroid_checkins', to='pretixbase.OrderPosition')), + ], + ), + ] diff --git a/src/pretix/plugins/pretixdroid/migrations/__init__.py b/src/pretix/plugins/pretixdroid/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/pretix/plugins/pretixdroid/models.py b/src/pretix/plugins/pretixdroid/models.py new file mode 100644 index 000000000..e588e5927 --- /dev/null +++ b/src/pretix/plugins/pretixdroid/models.py @@ -0,0 +1,6 @@ +from django.db import models + + +class Checkin(models.Model): + position = models.ForeignKey('pretixbase.OrderPosition', related_name='pretixdroid_checkins') + datetime = models.DateTimeField(auto_now_add=True) diff --git a/src/pretix/plugins/pretixdroid/static/pretixplugins/pretixdroid/pretixdroid.js b/src/pretix/plugins/pretixdroid/static/pretixplugins/pretixdroid/pretixdroid.js index d8edf4279..66f5b5e71 100644 --- a/src/pretix/plugins/pretixdroid/static/pretixplugins/pretixdroid/pretixdroid.js +++ b/src/pretix/plugins/pretixdroid/static/pretixplugins/pretixdroid/pretixdroid.js @@ -1,7 +1,7 @@ $(function () { jQuery('#qrcodeCanvas').qrcode( { - text: '{{ qrdata|safe }}' + text: $("#qrdata").html() } ); }); diff --git a/src/pretix/plugins/pretixdroid/templates/pretixplugins/pretixdroid/configuration.html b/src/pretix/plugins/pretixdroid/templates/pretixplugins/pretixdroid/configuration.html index d25ee9157..d7cfa8d96 100644 --- a/src/pretix/plugins/pretixdroid/templates/pretixplugins/pretixdroid/configuration.html +++ b/src/pretix/plugins/pretixdroid/templates/pretixplugins/pretixdroid/configuration.html @@ -10,6 +10,9 @@ If you try to configure the app, it will ask you to scan the QR code below. {% endblocktrans %}

+ {% trans "Reset authentication token" %} {% endblock %} diff --git a/src/pretix/plugins/pretixdroid/urls.py b/src/pretix/plugins/pretixdroid/urls.py index 5d9ae844d..1036d468c 100644 --- a/src/pretix/plugins/pretixdroid/urls.py +++ b/src/pretix/plugins/pretixdroid/urls.py @@ -5,6 +5,6 @@ from . import views urlpatterns = [ url(r'^control/event/(?P[^/]+)/(?P[^/]+)/pretixdroid/', views.ConfigView.as_view(), name='config'), - url(r'^pretixdroid/api/(?P[^/]+)/(?P[^/]+)/', views.ApiView.as_view(), - name='api'), + url(r'^pretixdroid/api/(?P[^/]+)/(?P[^/]+)/redeem/', views.ApiRedeemView.as_view(), + name='api.redeem'), ] diff --git a/src/pretix/plugins/pretixdroid/views.py b/src/pretix/plugins/pretixdroid/views.py index ae41f3638..d510e5f39 100644 --- a/src/pretix/plugins/pretixdroid/views.py +++ b/src/pretix/plugins/pretixdroid/views.py @@ -2,17 +2,22 @@ import json import logging import string +from django.db import transaction from django.http import ( HttpResponseForbidden, HttpResponseNotFound, JsonResponse, ) from django.utils.crypto import get_random_string +from django.utils.decorators import method_decorator +from django.views.decorators.csrf import csrf_exempt from django.views.generic import TemplateView, View from pretix.base.models import Event, Order, OrderPosition from pretix.control.permissions import EventPermissionRequiredMixin from pretix.helpers.urls import build_absolute_uri +from pretix.plugins.pretixdroid.models import Checkin logger = logging.getLogger('pretix.plugins.pretixdroid') +API_VERSION = 2 class ConfigView(EventPermissionRequiredMixin, TemplateView): @@ -23,44 +28,76 @@ class ConfigView(EventPermissionRequiredMixin, TemplateView): ctx = super().get_context_data() key = self.request.event.settings.get('pretixdroid_key') if not key or 'flush_key' in self.request.GET: - key = get_random_string(length=32, allowed_chars=string.ascii_uppercase + string.ascii_lowercase + string.digits) + key = get_random_string(length=32, + allowed_chars=string.ascii_uppercase + string.ascii_lowercase + string.digits) self.request.event.settings.set('pretixdroid_key', key) ctx['qrdata'] = json.dumps({ - 'version': 1, - 'url': build_absolute_uri('plugins:pretixdroid:api', kwargs={ + 'version': API_VERSION, + 'url': build_absolute_uri('plugins:pretixdroid:api.redeem', kwargs={ 'organizer': self.request.event.organizer.slug, 'event': self.request.event.slug - }), + })[:-7], # the slice removes the redeem/ part at the end 'key': key }) return ctx class ApiView(View): - def get(self, request, **kwargs): + + @method_decorator(csrf_exempt) + def dispatch(self, request, **kwargs): try: - event = Event.objects.get( + self.event = Event.objects.get( slug=self.kwargs['event'], organizer__slug=self.kwargs['organizer'] ) except Event.DoesNotExist: return HttpResponseNotFound('Unknown event') - if (not event.settings.get('pretixdroid_key') - or event.settings.get('pretixdroid_key') != request.GET.get('key', '')): + if (not self.event.settings.get('pretixdroid_key') + or self.event.settings.get('pretixdroid_key') != request.GET.get('key', '')): return HttpResponseForbidden('Invalid key') - ops = OrderPosition.objects.filter( - order__event=event, order__status=Order.STATUS_PAID, - ).select_related('item', 'variation') - data = [ - { + return super().dispatch(request, **kwargs) + + +class ApiRedeemView(ApiView): + def post(self, request, **kwargs): + secret = request.POST.get('secret', '!INVALID!') + response = { + 'version': API_VERSION + } + + try: + with transaction.atomic(): + created = False + op = OrderPosition.objects.select_related('item', 'variation', 'order').get( + order__event=self.event, secret=secret + ) + if op.order.status == Order.STATUS_PAID: + ci, created = Checkin.objects.get_or_create(position=op) + else: + response['status'] = 'error' + response['reason'] = 'unpaid' + + if 'status' not in response: + if created: + response['status'] = 'ok' + else: + response['status'] = 'error' + response['reason'] = 'already_redeemed' + + response['data'] = { 'secret': op.secret, + 'order': op.order.code, 'item': str(op.item), 'variation': str(op.variation) if op.variation else None, 'attendee_name': op.attendee_name } - for op in ops - ] - return JsonResponse({'data': data, 'version': 1}) + + except OrderPosition.DoesNotExist: + response['status'] = 'error' + response['reason'] = 'unknown_ticket' + + return JsonResponse(response)