Files
pretix_original/src/pretix/plugins/pretixdroid/views.py
Raphael Michel d0dfde382c Questions at check-in time (#745)
Questions at check-in time
2018-01-22 22:55:54 +01:00

487 lines
18 KiB
Python

import json
import logging
import urllib.parse
import dateutil.parser
from django.contrib import messages
from django.core.exceptions import ValidationError
from django.db import transaction
from django.db.models import Count, Max, OuterRef, Prefetch, Q, Subquery
from django.http import (
HttpResponseForbidden, HttpResponseNotFound, JsonResponse,
)
from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse
from django.utils.decorators import method_decorator
from django.utils.functional import cached_property
from django.utils.timezone import now
from django.utils.translation import ugettext_lazy as _
from django.views.decorators.csrf import csrf_exempt
from django.views.generic import TemplateView, View
from pretix.base.models import (
Checkin, Event, Order, OrderPosition, Question, QuestionOption,
)
from pretix.base.models.event import SubEvent
from pretix.control.permissions import EventPermissionRequiredMixin
from pretix.helpers.urls import build_absolute_uri
from pretix.multidomain.urlreverse import (
build_absolute_uri as event_absolute_uri,
)
from pretix.plugins.pretixdroid.forms import AppConfigurationForm
from pretix.plugins.pretixdroid.models import AppConfiguration
logger = logging.getLogger('pretix.plugins.pretixdroid')
API_VERSION = 3
class ConfigCodeView(EventPermissionRequiredMixin, TemplateView):
template_name = 'pretixplugins/pretixdroid/configuration_code.html'
permission = 'can_change_orders'
def get(self, request, **kwargs):
try:
self.object = self.request.event.appconfiguration_set.get(pk=kwargs.get("config"))
except AppConfiguration.DoesNotExist:
messages.error(request, _('The selected configuration does not exist.'))
return redirect(reverse('plugins:pretixdroid:config', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
}))
return super().get(request, **kwargs)
def get_context_data(self, **kwargs):
ctx = super().get_context_data()
url = build_absolute_uri('plugins:pretixdroid:api.redeem', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug
})
if self.object.subevent:
url = build_absolute_uri('plugins:pretixdroid:api.redeem', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
'subevent': self.object.subevent.pk
})
data = {
'version': API_VERSION,
'url': url[:-7], # the slice removes the redeem/ part at the end
'key': self.object.key,
'allow_search': self.object.allow_search,
'show_info': self.object.show_info
}
ctx['config'] = self.object
ctx['query'] = urllib.parse.urlencode(data, safe=':/')
ctx['qrdata'] = json.dumps(data)
return ctx
class ConfigView(EventPermissionRequiredMixin, TemplateView):
template_name = 'pretixplugins/pretixdroid/configuration.html'
permission = 'can_change_orders'
@cached_property
def add_form(self):
return AppConfigurationForm(
event=self.request.event,
instance=AppConfiguration(event=self.request.event),
data=self.request.POST if self.request.method == "POST" and "add" in self.request.POST else None
)
def post(self, request, *args, **kwargs):
if "add" in self.request.POST and self.add_form.is_valid():
self.add_form.save()
self.request.event.log_action('pretix.plugins.pretixdroid.config.added', user=self.request.user,
data=dict(self.add_form.cleaned_data))
return redirect(reverse('plugins:pretixdroid:config.code', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
'config': self.add_form.instance.pk
}))
elif "delete" in self.request.POST:
try:
ac = self.request.event.appconfiguration_set.get(pk=request.POST.get("delete"))
self.request.event.log_action('pretix.plugins.pretixdroid.config.deleted', user=self.request.user,
data={'id': ac.pk})
ac.delete()
messages.success(request, _('The selected configuration has been deleted.'))
except AppConfiguration.DoesNotExist:
messages.error(request, _('The selected configuration does not exist.'))
return redirect(reverse('plugins:pretixdroid:config', kwargs={
'organizer': self.request.event.organizer.slug,
'event': self.request.event.slug,
}))
else:
return self.get(request, *args, **kwargs)
def get_context_data(self, **kwargs):
ctx = super().get_context_data()
ctx['add_form'] = self.add_form
ctx['configs'] = self.request.event.appconfiguration_set.select_related('list').prefetch_related('items')
return ctx
class ApiView(View):
@method_decorator(csrf_exempt)
def dispatch(self, request, **kwargs):
try:
self.event = Event.objects.get(
slug=self.kwargs['event'],
organizer__slug=self.kwargs['organizer']
)
except Event.DoesNotExist:
return HttpResponseNotFound('Unknown event')
try:
self.config = self.event.appconfiguration_set.get(key=request.GET.get("key", "-unset-"))
except AppConfiguration.DoesNotExist:
return HttpResponseForbidden('Invalid key')
self.subevent = None
if self.event.has_subevents:
if self.config.list.subevent:
self.subevent = self.config.list.subevent
if 'subevent' in kwargs and kwargs['subevent'] != str(self.subevent.pk):
return HttpResponseForbidden('Invalid subevent selected.')
elif 'subevent' in kwargs:
self.subevent = get_object_or_404(SubEvent, event=self.event, pk=kwargs['subevent'])
else:
return HttpResponseForbidden('No subevent selected.')
else:
if 'subevent' in kwargs:
return HttpResponseForbidden('Subevents not enabled.')
return super().dispatch(request, **kwargs)
class ApiRedeemView(ApiView):
def _save_answers(self, op, answers, given_answers):
for q, a in given_answers.items():
if not a:
if q in answers:
answers[q].delete()
else:
continue
if isinstance(a, QuestionOption):
if q in answers:
qa = answers[q]
qa.answer = str(a.answer)
qa.save()
qa.options.clear()
else:
qa = op.answers.create(question=q, answer=str(a.answer))
qa.options.add(a)
elif isinstance(a, list):
if q in answers:
qa = answers[q]
qa.answer = ", ".join([str(o) for o in a])
qa.save()
qa.options.clear()
else:
qa = op.answers.create(question=q, answer=", ".join([str(o) for o in a]))
qa.options.add(*a)
else:
if q in answers:
qa = answers[q]
qa.answer = str(a)
qa.save()
else:
op.answers.create(question=q, answer=str(a))
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
}
if 'datetime' in request.POST:
dt = dateutil.parser.parse(request.POST.get('datetime'))
else:
dt = now()
try:
with transaction.atomic():
created = False
op = OrderPosition.objects.select_related(
'item', 'variation', 'order', 'addon_to'
).prefetch_related(
'item__questions',
Prefetch(
'item__questions',
queryset=Question.objects.filter(ask_during_checkin=True),
to_attr='checkin_questions'
),
'answers'
).get(
order__event=self.event, secret=secret, subevent=self.subevent
)
answers = {a.question: a for a in op.answers.all()}
require_answers = []
given_answers = {}
for q in op.item.checkin_questions:
if 'answer_{}'.format(q.pk) in request.POST:
try:
given_answers[q] = q.clean_answer(request.POST.get('answer_{}'.format(q.pk)))
continue
except ValidationError:
pass
if q in answers:
continue
require_answers.append(serialize_question(q))
self._save_answers(op, answers, given_answers)
if not self.config.list.all_products and op.item_id not in [i.pk for i in self.config.list.limit_products.all()]:
response['status'] = 'error'
response['reason'] = 'product'
elif not self.config.all_items and op.item_id not in [i.pk for i in self.config.items.all()]:
response['status'] = 'error'
response['reason'] = 'product'
elif require_answers and not force and request.POST.get('questions_supported'):
response['status'] = 'incomplete'
response['questions'] = require_answers
elif op.order.status == Order.STATUS_PAID or force:
ci, created = Checkin.objects.get_or_create(position=op, list=self.config.list, defaults={
'datetime': dt,
'nonce': nonce,
})
else:
response['status'] = 'error'
response['reason'] = 'unpaid'
if 'status' not in response:
if created or (nonce and nonce == ci.nonce):
response['status'] = 'ok'
if created:
op.order.log_action('pretix.plugins.pretixdroid.scan', data={
'position': op.id,
'positionid': op.positionid,
'first': True,
'forced': op.order.status != Order.STATUS_PAID,
'datetime': dt,
'list': self.config.list.pk
})
else:
if force:
response['status'] = 'ok'
else:
response['status'] = 'error'
response['reason'] = 'already_redeemed'
op.order.log_action('pretix.plugins.pretixdroid.scan', data={
'position': op.id,
'positionid': op.positionid,
'first': False,
'forced': force,
'datetime': dt,
'list': self.config.list.pk
})
response['data'] = serialize_op(op, redeemed=op.order.status == Order.STATUS_PAID or force)
except OrderPosition.DoesNotExist:
response['status'] = 'error'
response['reason'] = 'unknown_ticket'
return JsonResponse(response)
def serialize_question(q, items=False):
d = {
'id': q.pk,
'type': q.type,
'question': str(q.question),
'required': q.required,
'position': q.position,
'options': [
{
'id': o.pk,
'answer': str(o.answer)
} for o in q.options.all()
] if q.type in ('C', 'M') else []
}
if items:
d['items'] = [i.pk for i in q.items.all()]
return d
def serialize_op(op, redeemed):
name = op.attendee_name
if not name and op.addon_to:
name = op.addon_to.attendee_name
if not name:
try:
name = op.order.invoice_address.name
except:
pass
return {
'secret': op.secret,
'order': op.order.code,
'item': str(op.item),
'item_id': op.item_id,
'variation': str(op.variation) if op.variation else None,
'variation_id': op.variation_id,
'attendee_name': name,
'attention': op.item.checkin_attention,
'redeemed': redeemed,
'paid': op.order.status == Order.STATUS_PAID,
}
class ApiSearchView(ApiView):
def get(self, request, **kwargs):
query = request.GET.get('query', '!INVALID!')
response = {
'version': API_VERSION
}
if len(query) >= 4:
cqs = Checkin.objects.filter(
position_id=OuterRef('pk'),
list_id=self.config.list.pk
).order_by().values('position_id').annotate(
m=Max('datetime')
).values('m')
qs = OrderPosition.objects.filter(
order__event=self.event,
subevent=self.config.list.subevent
).annotate(
last_checked_in=Subquery(cqs)
).select_related('item', 'variation', 'order', 'order__invoice_address', 'addon_to')
if not self.config.list.all_products:
qs = qs.filter(item__in=self.config.list.limit_products.values_list('id', flat=True))
if not self.config.all_items:
qs = qs.filter(item__in=self.config.items.all())
if not self.config.allow_search:
ops = qs.filter(
Q(secret__istartswith=query)
)[:25]
else:
ops = qs.filter(
Q(secret__istartswith=query) | Q(attendee_name__icontains=query) | Q(order__code__istartswith=query)
| Q(order__invoice_address__name__icontains=query)
)[:25]
response['results'] = [serialize_op(op, bool(op.last_checked_in)) for op in ops]
else:
response['results'] = []
return JsonResponse(response)
class ApiDownloadView(ApiView):
def get(self, request, **kwargs):
response = {
'version': API_VERSION
}
cqs = Checkin.objects.filter(
position_id=OuterRef('pk'),
list_id=self.config.list.pk
).order_by().values('position_id').annotate(
m=Max('datetime')
).values('m')
qs = OrderPosition.objects.filter(
order__event=self.event,
order__status=Order.STATUS_PAID,
subevent=self.config.list.subevent
).annotate(
last_checked_in=Subquery(cqs)
).select_related('item', 'variation', 'order', 'addon_to')
if not self.config.list.all_products:
qs = qs.filter(item__in=self.config.list.limit_products.values_list('id', flat=True))
if not self.config.all_items:
qs = qs.filter(item__in=self.config.items.all())
response['results'] = [serialize_op(op, bool(op.last_checked_in)) for op in qs]
questions = self.event.questions.filter(ask_during_checkin=True).prefetch_related('items', 'options')
response['questions'] = [serialize_question(q, items=True) for q in questions]
return JsonResponse(response)
class ApiStatusView(ApiView):
def get(self, request, **kwargs):
cqs = Checkin.objects.filter(
position__order__event=self.event, position__subevent=self.subevent,
position__order__status=Order.STATUS_PAID,
list=self.config.list
)
pqs = OrderPosition.objects.filter(
order__event=self.event, order__status=Order.STATUS_PAID, subevent=self.subevent,
)
if not self.config.list.all_products:
pqs = pqs.filter(item__in=self.config.list.limit_products.values_list('id', flat=True))
ev = self.subevent or self.event
response = {
'version': API_VERSION,
'event': {
'name': str(ev.name),
'list': self.config.list.name,
'slug': self.event.slug,
'organizer': {
'name': str(self.event.organizer),
'slug': self.event.organizer.slug
},
'subevent': self.subevent.pk if self.subevent else str(self.event),
'date_from': ev.date_from,
'date_to': ev.date_to,
'timezone': self.event.settings.timezone,
'url': event_absolute_uri(self.event, 'presale:event.index')
},
'checkins': cqs.count(),
'total': pqs.count()
}
op_by_item = {
p['item']: p['cnt']
for p in pqs.order_by().values('item').annotate(cnt=Count('id'))
}
op_by_variation = {
p['variation']: p['cnt']
for p in pqs.order_by().values('variation').annotate(cnt=Count('id'))
}
c_by_item = {
p['position__item']: p['cnt']
for p in cqs.order_by().values('position__item').annotate(cnt=Count('id'))
}
c_by_variation = {
p['position__variation']: p['cnt']
for p in cqs.order_by().values('position__variation').annotate(cnt=Count('id'))
}
response['items'] = []
for item in self.event.items.order_by('pk').prefetch_related('variations'):
i = {
'id': item.pk,
'name': str(item),
'admission': item.admission,
'checkins': c_by_item.get(item.pk, 0),
'total': op_by_item.get(item.pk, 0),
'variations': []
}
for var in item.variations.all():
i['variations'].append({
'id': var.pk,
'name': str(var),
'checkins': c_by_variation.get(var.pk, 0),
'total': op_by_variation.get(var.pk, 0),
})
response['items'].append(i)
return JsonResponse(response)