diff --git a/src/pretix/plugins/pretixdroid/__init__.py b/src/pretix/plugins/pretixdroid/__init__.py new file mode 100644 index 000000000..a3073b114 --- /dev/null +++ b/src/pretix/plugins/pretixdroid/__init__.py @@ -0,0 +1,22 @@ +from django.apps import AppConfig +from django.utils.translation import ugettext_lazy as _ + +from pretix.base.plugins import PluginType + + +class PretixdroidApp(AppConfig): + name = 'pretix.plugins.pretixdroid' + verbose_name = _("pretixdroid API") + + class PretixPluginMeta: + type = PluginType.ADMINFEATURE + name = _("pretixdroid API") + author = _("the pretix team") + version = '1.0.0' + description = _("This plugin allows you to use the pretixdroid Android app for your event.") + + def ready(self): + from . import signals # NOQA + + +default_app_config = 'pretix.plugins.pretixdroid.PretixdroidApp' diff --git a/src/pretix/plugins/pretixdroid/signals.py b/src/pretix/plugins/pretixdroid/signals.py new file mode 100644 index 000000000..c24ced06c --- /dev/null +++ b/src/pretix/plugins/pretixdroid/signals.py @@ -0,0 +1,36 @@ +from django.core.urlresolvers import resolve, reverse +from django.dispatch import receiver +from django.template import Context +from django.template.loader import get_template +from django.utils.translation import ugettext_lazy as _ + +from pretix.control.signals import html_head, nav_event + + +@receiver(nav_event, dispatch_uid="pretixdroid_nav") +def control_nav_import(sender, request=None, **kwargs): + url = resolve(request.path_info) + if not request.eventperm.can_change_orders: + return [] + return [ + { + 'label': _('pretixdroid'), + 'url': reverse('plugins:pretixdroid:config', kwargs={ + 'event': request.event.slug, + 'organizer': request.event.organizer.slug, + }), + 'active': (url.namespace == 'plugins:pretixdroid' and url.url_name == 'config'), + 'icon': 'android', + } + ] + + +@receiver(html_head, dispatch_uid="pretixdroid_html_head") +def html_head_presale(sender, request=None, **kwargs): + url = resolve(request.path_info) + if url.namespace == 'plugins:pretixdroid': + template = get_template('pretixplugins/pretixdroid/control_head.html') + ctx = Context({}) + return template.render(ctx) + else: + return "" diff --git a/src/pretix/plugins/pretixdroid/static/pretixplugins/pretixdroid/jquery.qrcode.min.js b/src/pretix/plugins/pretixdroid/static/pretixplugins/pretixdroid/jquery.qrcode.min.js new file mode 100644 index 000000000..fe9680e6c --- /dev/null +++ b/src/pretix/plugins/pretixdroid/static/pretixplugins/pretixdroid/jquery.qrcode.min.js @@ -0,0 +1,28 @@ +(function(r){r.fn.qrcode=function(h){var s;function u(a){this.mode=s;this.data=a}function o(a,c){this.typeNumber=a;this.errorCorrectLevel=c;this.modules=null;this.moduleCount=0;this.dataCache=null;this.dataList=[]}function q(a,c){if(void 0==a.length)throw Error(a.length+"/"+c);for(var d=0;da||this.moduleCount<=a||0>c||this.moduleCount<=c)throw Error(a+","+c);return this.modules[a][c]},getModuleCount:function(){return this.moduleCount},make:function(){if(1>this.typeNumber){for(var a=1,a=1;40>a;a++){for(var c=p.getRSBlocks(a,this.errorCorrectLevel),d=new t,b=0,e=0;e=d;d++)if(!(-1>=a+d||this.moduleCount<=a+d))for(var b=-1;7>=b;b++)-1>=c+b||this.moduleCount<=c+b||(this.modules[a+d][c+b]= +0<=d&&6>=d&&(0==b||6==b)||0<=b&&6>=b&&(0==d||6==d)||2<=d&&4>=d&&2<=b&&4>=b?!0:!1)},getBestMaskPattern:function(){for(var a=0,c=0,d=0;8>d;d++){this.makeImpl(!0,d);var b=j.getLostPoint(this);if(0==d||a>b)a=b,c=d}return c},createMovieClip:function(a,c,d){a=a.createEmptyMovieClip(c,d);this.make();for(c=0;c=f;f++)for(var i=-2;2>=i;i++)this.modules[b+f][e+i]=-2==f||2==f||-2==i||2==i||0==f&&0==i?!0:!1}},setupTypeNumber:function(a){for(var c= +j.getBCHTypeNumber(this.typeNumber),d=0;18>d;d++){var b=!a&&1==(c>>d&1);this.modules[Math.floor(d/3)][d%3+this.moduleCount-8-3]=b}for(d=0;18>d;d++)b=!a&&1==(c>>d&1),this.modules[d%3+this.moduleCount-8-3][Math.floor(d/3)]=b},setupTypeInfo:function(a,c){for(var d=j.getBCHTypeInfo(this.errorCorrectLevel<<3|c),b=0;15>b;b++){var e=!a&&1==(d>>b&1);6>b?this.modules[b][8]=e:8>b?this.modules[b+1][8]=e:this.modules[this.moduleCount-15+b][8]=e}for(b=0;15>b;b++)e=!a&&1==(d>>b&1),8>b?this.modules[8][this.moduleCount- +b-1]=e:9>b?this.modules[8][15-b-1+1]=e:this.modules[8][15-b-1]=e;this.modules[this.moduleCount-8][8]=!a},mapData:function(a,c){for(var d=-1,b=this.moduleCount-1,e=7,f=0,i=this.moduleCount-1;0g;g++)if(null==this.modules[b][i-g]){var n=!1;f>>e&1));j.getMask(c,b,i-g)&&(n=!n);this.modules[b][i-g]=n;e--; -1==e&&(f++,e=7)}b+=d;if(0>b||this.moduleCount<=b){b-=d;d=-d;break}}}};o.PAD0=236;o.PAD1=17;o.createData=function(a,c,d){for(var c=p.getRSBlocks(a, +c),b=new t,e=0;e8*a)throw Error("code length overflow. ("+b.getLengthInBits()+">"+8*a+")");for(b.getLengthInBits()+4<=8*a&&b.put(0,4);0!=b.getLengthInBits()%8;)b.putBit(!1);for(;!(b.getLengthInBits()>=8*a);){b.put(o.PAD0,8);if(b.getLengthInBits()>=8*a)break;b.put(o.PAD1,8)}return o.createBytes(b,c)};o.createBytes=function(a,c){for(var d= +0,b=0,e=0,f=Array(c.length),i=Array(c.length),g=0;g>>=1;return c},getPatternPosition:function(a){return j.PATTERN_POSITION_TABLE[a-1]},getMask:function(a,c,d){switch(a){case 0:return 0==(c+d)%2;case 1:return 0==c%2;case 2:return 0==d%3;case 3:return 0==(c+d)%3;case 4:return 0==(Math.floor(c/2)+Math.floor(d/3))%2;case 5:return 0==c*d%2+c*d%3;case 6:return 0==(c*d%2+c*d%3)%2;case 7:return 0==(c*d%3+(c+d)%2)%2;default:throw Error("bad maskPattern:"+ +a);}},getErrorCorrectPolynomial:function(a){for(var c=new q([1],0),d=0;dc)switch(a){case 1:return 10;case 2:return 9;case s:return 8;case 8:return 8;default:throw Error("mode:"+a);}else if(27>c)switch(a){case 1:return 12;case 2:return 11;case s:return 16;case 8:return 10;default:throw Error("mode:"+a);}else if(41>c)switch(a){case 1:return 14;case 2:return 13;case s:return 16;case 8:return 12;default:throw Error("mode:"+ +a);}else throw Error("type:"+c);},getLostPoint:function(a){for(var c=a.getModuleCount(),d=0,b=0;b=g;g++)if(!(0>b+g||c<=b+g))for(var h=-1;1>=h;h++)0>e+h||c<=e+h||0==g&&0==h||i==a.isDark(b+g,e+h)&&f++;5a)throw Error("glog("+a+")");return l.LOG_TABLE[a]},gexp:function(a){for(;0>a;)a+=255;for(;256<=a;)a-=255;return l.EXP_TABLE[a]},EXP_TABLE:Array(256), +LOG_TABLE:Array(256)},m=0;8>m;m++)l.EXP_TABLE[m]=1<m;m++)l.EXP_TABLE[m]=l.EXP_TABLE[m-4]^l.EXP_TABLE[m-5]^l.EXP_TABLE[m-6]^l.EXP_TABLE[m-8];for(m=0;255>m;m++)l.LOG_TABLE[l.EXP_TABLE[m]]=m;q.prototype={get:function(a){return this.num[a]},getLength:function(){return this.num.length},multiply:function(a){for(var c=Array(this.getLength()+a.getLength()-1),d=0;d +this.getLength()-a.getLength())return this;for(var c=l.glog(this.get(0))-l.glog(a.get(0)),d=Array(this.getLength()),b=0;b>>7-a%8&1)},put:function(a,c){for(var d=0;d>>c-d-1&1))},getLengthInBits:function(){return this.length},putBit:function(a){var c=Math.floor(this.length/8);this.buffer.length<=c&&this.buffer.push(0);a&&(this.buffer[c]|=128>>>this.length%8);this.length++}};"string"===typeof h&&(h={text:h});h=r.extend({},{render:"canvas",width:256,height:256,typeNumber:-1, +correctLevel:2,background:"#ffffff",foreground:"#000000"},h);return this.each(function(){var a;if("canvas"==h.render){a=new o(h.typeNumber,h.correctLevel);a.addData(h.text);a.make();var c=document.createElement("canvas");c.width=h.width;c.height=h.height;for(var d=c.getContext("2d"),b=h.width/a.getModuleCount(),e=h.height/a.getModuleCount(),f=0;f").css("width",h.width+"px").css("height",h.height+"px").css("border","0px").css("border-collapse","collapse").css("background-color",h.background);d=h.width/a.getModuleCount();b=h.height/a.getModuleCount();for(e=0;e").css("height",b+"px").appendTo(c);for(i=0;i").css("width", +d+"px").css("background-color",a.isDark(e,i)?h.foreground:h.background).appendTo(f)}}a=c;jQuery(a).appendTo(this)})}})(jQuery); diff --git a/src/pretix/plugins/pretixdroid/templates/pretixplugins/pretixdroid/configuration.html b/src/pretix/plugins/pretixdroid/templates/pretixplugins/pretixdroid/configuration.html new file mode 100644 index 000000000..438cceec9 --- /dev/null +++ b/src/pretix/plugins/pretixdroid/templates/pretixplugins/pretixdroid/configuration.html @@ -0,0 +1,19 @@ +{% extends "pretixcontrol/event/base.html" %} +{% load i18n %} +{% load bootstrap3 %} +{% block title %}{% trans "pretixdroid configuration" %}{% endblock %} +{% block content %} +

{% trans "pretixdroid configuration" %}

+

{% blocktrans trimmed %} + pretixdroid is an Android app that you can use to control tickets at the entrance of your event. + 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/templates/pretixplugins/pretixdroid/control_head.html b/src/pretix/plugins/pretixdroid/templates/pretixplugins/pretixdroid/control_head.html new file mode 100644 index 000000000..baebd880c --- /dev/null +++ b/src/pretix/plugins/pretixdroid/templates/pretixplugins/pretixdroid/control_head.html @@ -0,0 +1,3 @@ +{% load staticfiles %} + + diff --git a/src/pretix/plugins/pretixdroid/urls.py b/src/pretix/plugins/pretixdroid/urls.py new file mode 100644 index 000000000..5d9ae844d --- /dev/null +++ b/src/pretix/plugins/pretixdroid/urls.py @@ -0,0 +1,10 @@ +from django.conf.urls import url + +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'), +] diff --git a/src/pretix/plugins/pretixdroid/views.py b/src/pretix/plugins/pretixdroid/views.py new file mode 100644 index 000000000..5df600c50 --- /dev/null +++ b/src/pretix/plugins/pretixdroid/views.py @@ -0,0 +1,68 @@ +import json +import logging +import random +import string + +from django.http import ( + HttpResponseForbidden, HttpResponseNotFound, JsonResponse, +) +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 + +logger = logging.getLogger('pretix.plugins.pretixdroid') + + +class ConfigView(EventPermissionRequiredMixin, TemplateView): + template_name = 'pretixplugins/pretixdroid/configuration.html' + permission = 'can_change_orders' + + def get_context_data(self, **kwargs): + ctx = super().get_context_data() + key = self.request.event.settings.get('pretixdroid_key') + if not key or 'flush_key' in self.request.GET: + key = ''.join( + random.SystemRandom().choice(string.ascii_uppercase + string.ascii_lowercase + string.digits) for _ in + range(32)) + self.request.event.settings.set('pretixdroid_key', key) + + ctx['qrdata'] = json.dumps({ + 'version': 1, + 'url': build_absolute_uri('plugins:pretixdroid:api', kwargs={ + 'organizer': self.request.event.organizer.slug, + 'event': self.request.event.slug + }), + 'key': key + }) + return ctx + + +class ApiView(View): + def get(self, request, **kwargs): + try: + event = Event.objects.current.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', '')): + return HttpResponseForbidden('Invalid key') + + ops = OrderPosition.objects.current.filter( + order__event=event, order__status=Order.STATUS_PAID, + ).select_related('item', 'variation') + data = [ + { + 'id': op.identity, + '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}) diff --git a/src/pretix/settings.py b/src/pretix/settings.py index 2eb5ec667..694c4f210 100644 --- a/src/pretix/settings.py +++ b/src/pretix/settings.py @@ -139,6 +139,7 @@ INSTALLED_APPS = ( 'pretix.plugins.ticketoutputpdf', 'pretix.plugins.sendmail', 'pretix.plugins.statistics', + 'pretix.plugins.pretixdroid', 'easy_thumbnails', )