Files
pretix_cgo/src/pretix/plugins/pretixdroid/views.py
2018-05-18 12:15:32 +02:00

431 lines
16 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.models import Count, Max, OuterRef, 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
from pretix.base.models.event import SubEvent
from pretix.base.services.checkin import (
CheckInError, RequiredQuestionsError, perform_checkin,
)
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 post(self, request, **kwargs):
secret = request.POST.get('secret', '!INVALID!')
force = request.POST.get('force', 'false') in ('true', 'True')
ignore_unpaid = request.POST.get('ignore_unpaid', '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:
op = OrderPosition.objects.get(order__event=self.event, secret=secret, subevent=self.subevent)
except OrderPosition.DoesNotExist:
response['status'] = 'error'
response['reason'] = 'unknown_ticket'
else:
given_answers = {}
for q in op.item.questions.filter(ask_during_checkin=True):
if 'answer_{}'.format(q.pk) in request.POST:
try:
given_answers[q] = q.clean_answer(request.POST.get('answer_{}'.format(q.pk)))
except ValidationError:
pass
try:
if not self.config.all_items and op.item_id not in [i.pk for i in self.config.items.all()]:
raise CheckInError('', 'product')
perform_checkin(
op=op,
clist=self.config.list,
given_answers=given_answers,
force=force,
ignore_unpaid=ignore_unpaid,
nonce=nonce,
datetime=dt,
questions_supported=bool(request.POST.get('questions_supported'))
)
except RequiredQuestionsError as e:
response['status'] = 'incomplete'
response['questions'] = [serialize_question(q) for q in e.questions]
except CheckInError as e:
response['status'] = 'error'
response['reason'] = e.code
else:
response['status'] = 'ok'
response['data'] = serialize_op(op, redeemed=op.order.status == Order.STATUS_PAID or force,
clist=self.config.list)
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, clist):
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
checkin_allowed = (
op.order.status == Order.STATUS_PAID
or (
op.order.status == Order.STATUS_PENDING
and clist.include_pending
)
)
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 or op.order.checkin_attention,
'redeemed': redeemed,
'paid': op.order.status == Order.STATUS_PAID,
'checkin_allowed': checkin_allowed,
'addons_text': ", ".join([
'{} - {}'.format(p.item, p.variation) if p.variation else str(p.item)
for p in op.addons.all()
])
}
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').prefetch_related(
'addons', 'addons__item', 'addons__variation'
)
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(addon_to__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), self.config.list) 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__in=[Order.STATUS_PAID] + ([Order.STATUS_PENDING] if self.config.list.include_pending else
[]),
subevent=self.config.list.subevent
).annotate(
last_checked_in=Subquery(cqs)
).select_related('item', 'variation', 'order', 'addon_to').prefetch_related(
'addons', 'addons__item', 'addons__variation'
)
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), self.config.list) 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__in=[Order.STATUS_PAID] + ([Order.STATUS_PENDING] if
self.config.list.include_pending else []),
list=self.config.list
)
pqs = OrderPosition.objects.filter(
order__event=self.event,
order__status__in=[Order.STATUS_PAID] + ([Order.STATUS_PENDING] if self.config.list.include_pending else
[]),
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)