diff --git a/src/pretix/plugins/pretixdroid/__init__.py b/src/pretix/plugins/pretixdroid/__init__.py index 7736d89419..604dd7949c 100644 --- a/src/pretix/plugins/pretixdroid/__init__.py +++ b/src/pretix/plugins/pretixdroid/__init__.py @@ -17,8 +17,5 @@ class PretixdroidApp(AppConfig): category = 'INTEGRATION' description = _("This plugin allows you to use the pretixdroid and pretixdesk apps for your event.") - def ready(self): - from . import signals # NOQA - default_app_config = 'pretix.plugins.pretixdroid.PretixdroidApp' diff --git a/src/pretix/plugins/pretixdroid/forms.py b/src/pretix/plugins/pretixdroid/forms.py deleted file mode 100644 index 6e3a0016f8..0000000000 --- a/src/pretix/plugins/pretixdroid/forms.py +++ /dev/null @@ -1,43 +0,0 @@ -from django import forms -from django.urls import reverse -from django.utils.translation import gettext_lazy as _ -from django_scopes.forms import ( - SafeModelChoiceField, SafeModelMultipleChoiceField, -) - -from pretix.control.forms.widgets import Select2 -from pretix.plugins.pretixdroid.models import AppConfiguration - - -class AppConfigurationForm(forms.ModelForm): - class Meta: - model = AppConfiguration - fields = ('all_items', 'items', 'list', 'show_info', 'allow_search', 'app') - widgets = { - 'items': forms.CheckboxSelectMultiple(attrs={ - 'data-inverse-dependency': '#id_all_items' - }), - 'app': forms.RadioSelect - } - field_classes = { - 'items': SafeModelMultipleChoiceField, - 'list': SafeModelChoiceField, - } - - def __init__(self, **kwargs): - self.event = kwargs.pop('event') - super().__init__(**kwargs) - self.fields['items'].queryset = self.event.items.all() - self.fields['list'].queryset = self.event.checkin_lists.all() - self.fields['list'].widget = Select2( - attrs={ - 'data-model-select2': 'generic', - 'data-select2-url': reverse('control:event.orders.checkinlists.select2', kwargs={ - 'event': self.event.slug, - 'organizer': self.event.organizer.slug, - }), - 'data-placeholder': _('Check-in list') - } - ) - self.fields['list'].widget.choices = self.fields['list'].choices - self.fields['list'].required = True diff --git a/src/pretix/plugins/pretixdroid/migrations/0004_delete_appconfiguration.py b/src/pretix/plugins/pretixdroid/migrations/0004_delete_appconfiguration.py new file mode 100644 index 0000000000..38b1d8e2f0 --- /dev/null +++ b/src/pretix/plugins/pretixdroid/migrations/0004_delete_appconfiguration.py @@ -0,0 +1,16 @@ +# Generated by Django 3.0.9 on 2020-10-06 16:23 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('pretixdroid', '0003_appconfiguration_squashed_0005_auto_20180106_2122'), + ] + + operations = [ + migrations.DeleteModel( + name='AppConfiguration', + ), + ] diff --git a/src/pretix/plugins/pretixdroid/models.py b/src/pretix/plugins/pretixdroid/models.py deleted file mode 100644 index 266076d22f..0000000000 --- a/src/pretix/plugins/pretixdroid/models.py +++ /dev/null @@ -1,36 +0,0 @@ -import string - -from django.db import models -from django.utils.crypto import get_random_string -from django.utils.translation import gettext_lazy as _ - - -class AppConfiguration(models.Model): - event = models.ForeignKey('pretixbase.Event', on_delete=models.CASCADE) - key = models.CharField(max_length=190, db_index=True) - all_items = models.BooleanField(default=True, verbose_name=_('Can scan all products')) - items = models.ManyToManyField('pretixbase.Item', blank=True, verbose_name=_('Can scan these products')) - show_info = models.BooleanField(default=True, verbose_name=_('Show information'), - help_text=_('If disabled, the device can not see how many tickets exist and how ' - 'many are already scanned. pretixdroid 1.6 or pretixdesk only.')) - allow_search = models.BooleanField(default=True, verbose_name=_('Search allowed'), - help_text=_('If disabled, the device can not search for attendees by name. ' - 'pretixdroid 1.6 or pretixdesk only.')) - app = models.CharField(max_length=190, verbose_name=_('Scan software'), default='pretixdroid', choices=( - ('pretixdroid', _('pretixdroid – for Android smartphones')), - ('pretixdesk', _('pretixdesk – for desktop computers')), - )) - list = models.ForeignKey( - 'pretixbase.CheckinList', on_delete=models.CASCADE, verbose_name=_('Check-in list') - ) - - @property - def subevent(self): - return self.list.subevent - - def save(self, **kwargs): - if not self.key: - self.key = get_random_string( - length=32, allowed_chars=string.ascii_uppercase + string.ascii_lowercase + string.digits - ) - return super().save(**kwargs) diff --git a/src/pretix/plugins/pretixdroid/signals.py b/src/pretix/plugins/pretixdroid/signals.py deleted file mode 100644 index a2a9e8c5e7..0000000000 --- a/src/pretix/plugins/pretixdroid/signals.py +++ /dev/null @@ -1,38 +0,0 @@ - -from django.dispatch import receiver -from django.urls import resolve, reverse -from django.utils.translation import gettext_lazy as _ - -from pretix.base.signals import logentry_display -from pretix.control.logdisplay import _display_checkin -from pretix.control.signals import 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.user.has_event_permission(request.organizer, request.event, 'can_change_orders', request=request): - return [] - return [ - { - 'label': _('Check-in devices'), - 'url': reverse('plugins:pretixdroid:config', kwargs={ - 'event': request.event.slug, - 'organizer': request.event.organizer.slug, - }), - 'parent': reverse('control:event.orders.checkinlists', kwargs={ - 'event': request.event.slug, - 'organizer': request.event.organizer.slug, - }), - 'active': (url.namespace == 'plugins:pretixdroid' and url.url_name == 'config'), - 'icon': 'mobile', - } - ] - - -@receiver(signal=logentry_display, dispatch_uid="pretixdroid_logentry_display") -def pretixcontrol_logentry_display(sender, logentry, **kwargs): - if logentry.action_type != 'pretix.plugins.pretixdroid.scan': - return - - return _display_checkin(sender, logentry) diff --git a/src/pretix/plugins/pretixdroid/static/pretixplugins/pretixdroid/play_store_en.png b/src/pretix/plugins/pretixdroid/static/pretixplugins/pretixdroid/play_store_en.png deleted file mode 100644 index 1fa74c1b37..0000000000 Binary files a/src/pretix/plugins/pretixdroid/static/pretixplugins/pretixdroid/play_store_en.png and /dev/null differ diff --git a/src/pretix/plugins/pretixdroid/static/pretixplugins/pretixdroid/pretixdroid.js b/src/pretix/plugins/pretixdroid/static/pretixplugins/pretixdroid/pretixdroid.js deleted file mode 100644 index 66f5b5e717..0000000000 --- a/src/pretix/plugins/pretixdroid/static/pretixplugins/pretixdroid/pretixdroid.js +++ /dev/null @@ -1,7 +0,0 @@ -$(function () { - jQuery('#qrcodeCanvas').qrcode( - { - 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 deleted file mode 100644 index 2dd453042a..0000000000 --- a/src/pretix/plugins/pretixdroid/templates/pretixplugins/pretixdroid/configuration.html +++ /dev/null @@ -1,127 +0,0 @@ -{% extends "pretixcontrol/event/base.html" %} -{% load i18n %} -{% load bootstrap3 %} -{% load static %} -{% block title %}{% trans "Check-in device configuration" %}{% endblock %} -{% block content %} -

{% trans "Check-in device configuration" %}

- - {% if not configs or "create" in request.GET %} -
-

{% trans "We've got a new app!" %}

-

- {% blocktrans trimmed %} - We've retired pretixdesk and pretixdroid in favor of our new app pretixSCAN that works on all major - platforms, allows convenient switching between events, has better performance when dealing with large - events and supports printing badges. We suggest that you switch to pretixSCAN for your events, but you - can continue using pretixdesk for at least all of 2019, if you like. - {% endblocktrans %} -

-
-
-

{% trans "Our new app: pretixSCAN" %}

- -

{% trans "Available on Android, iOS, Windows, and Linux." %}

-

{% trans "Configuration is available in your organizer account's device list." %}

- - {% trans "Switch to my device list" %} - -
-
-

{% trans "Our old apps: pretixdesk and pretixdroid" %}

- -

{% trans "Available on Android, Windows, and Linux." %}

-

{% trans "Scroll down to create a configuration" %}

-
-
-
- {% endif %} - - {% if not configs or "create" in request.GET %} -

{% trans "Create app configuration" %}

-

- {% blocktrans trimmed %} - To start scanning tickets with our apps, first create a configuration code here: - {% endblocktrans %} -

-
- {% csrf_token %} - {% bootstrap_form_errors add_form %} - {% bootstrap_field add_form.list layout="horizontal" %} - {% bootstrap_field add_form.all_items layout="horizontal" %} - {% bootstrap_field add_form.items layout="horizontal" %} - {% bootstrap_field add_form.show_info layout="horizontal" %} - {% bootstrap_field add_form.allow_search layout="horizontal" %} - {% bootstrap_field add_form.app layout="horizontal" %} -
-
- -
-
-
- {% endif %} - - - {% if configs and "create" not in request.GET %} -

{% trans "Existing app configurations" %}

- - {% trans "Create a new configuration" %} - -
- {% csrf_token %} - - - - - - - - - - - - - {% for ac in configs %} - - - - - - - - - {% endfor %} - -
{% trans "ID" %}{% trans "Check-in list" %}{% trans "Items" %}{% trans "Show info" %}{% trans "Allow search" %}
- {% if ac.app == "pretixdroid" %} - - {% elif ac.app == "pretixdesk" %} - - {% endif %} - {{ ac.key|slice:"0:8" }}… - - {{ ac.list }} - - {% if ac.all_items %} - {% trans "All" %} - {% else %} - {% for item in ac.items.all %} - {{ item.name }} - {% if forloop.revcounter0 > 0 %}
{% endif %} - {% endfor %} - {% endif %} -
{% if ac.show_info %}{% trans "Yes" %}{% else %}{% trans "No" %}{% endif %}{% if ac.allow_search %}{% trans "Yes" %}{% else %}{% trans "No" %}{% endif %} - - {% trans "Configure device" %} - - -
-
- {% endif %} -{% endblock %} - diff --git a/src/pretix/plugins/pretixdroid/templates/pretixplugins/pretixdroid/configuration_code.html b/src/pretix/plugins/pretixdroid/templates/pretixplugins/pretixdroid/configuration_code.html deleted file mode 100644 index 43fc3577c0..0000000000 --- a/src/pretix/plugins/pretixdroid/templates/pretixplugins/pretixdroid/configuration_code.html +++ /dev/null @@ -1,73 +0,0 @@ -{% extends "pretixcontrol/event/base.html" %} -{% load i18n %} -{% load bootstrap3 %} -{% load static %} -{% block title %}{% trans "Device configuration" %}{% endblock %} -{% block content %} - {% if config.app == "pretixdroid" %} -

- {% trans "pretixdroid configuration" %} - - {% trans "Back to overview" %} - -

-

{% trans "1. Download app" %}

-

- - 
-    {% trans - -

-

- - {% blocktrans trimmed %} - Android, Google Play and the Google Play logo are trademarks of Google Inc. - {% endblocktrans %} - -

-

{% trans "2. Scan code" %}

-
- -

{% trans "3. Start scanning tickets" %}

- - {% else %} -

- {% trans "pretixdesk configuration" %} - - {% trans "Back to overview" %} - -

-

{% trans "1. Download pretixdesk" %}

-

- - {% trans "Open download page" %} - -

-

{% trans "2. Connect device" %}

-

- - {% trans "Connect with pretixdesk" %} - -

-

- {% blocktrans trimmed %} - If this link does not open the pretixdesk application or if you want to set the application up on a - separate device, copy the following code and paste it into the application: - {% endblocktrans %} -

-

- -

-

{% trans "3. Start scanning tickets" %}

- - {% if request.event.testmode %} -
- {% trans "Test mode orders will only be scanned if you scan online. If you scan in asynchronous mode, test mode orders won't be there." %} -
- {% endif %} - {% endif %} -{% endblock %} - diff --git a/src/pretix/plugins/pretixdroid/urls.py b/src/pretix/plugins/pretixdroid/urls.py deleted file mode 100644 index 789a5387df..0000000000 --- a/src/pretix/plugins/pretixdroid/urls.py +++ /dev/null @@ -1,24 +0,0 @@ -from django.conf.urls import include, url - -from . import views - -pretixdroid_api_patterns = [ - url(r'^redeem/', views.ApiRedeemView.as_view(), - name='api.redeem'), - url(r'^search/', views.ApiSearchView.as_view(), - name='api.search'), - url(r'^download/', views.ApiDownloadView.as_view(), - name='api.download'), - url(r'^status/', views.ApiStatusView.as_view(), - name='api.status'), -] - -urlpatterns = [ - url(r'^control/event/(?P[^/]+)/(?P[^/]+)/pretixdroid/$', views.ConfigView.as_view(), - name='config'), - url(r'^control/event/(?P[^/]+)/(?P[^/]+)/pretixdroid/(?P\d+)/$', - views.ConfigCodeView.as_view(), name='config.code'), - url(r'^pretixdroid/api/(?P[^/]+)/(?P[^/]+)/(?P\d+)/', - include(pretixdroid_api_patterns)), - url(r'^pretixdroid/api/(?P[^/]+)/(?P[^/]+)/', include(pretixdroid_api_patterns)), -] diff --git a/src/pretix/plugins/pretixdroid/views.py b/src/pretix/plugins/pretixdroid/views.py deleted file mode 100644 index 6cc7ec8286..0000000000 --- a/src/pretix/plugins/pretixdroid/views.py +++ /dev/null @@ -1,433 +0,0 @@ -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 gettext_lazy as _ -from django.views.decorators.csrf import csrf_exempt -from django.views.generic import TemplateView, View -from django_scopes import scope, scopes_disabled - -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): - with scopes_disabled(): - try: - self.event = Event.objects.get( - slug=self.kwargs['event'], - organizer__slug=self.kwargs['organizer'] - ) - except Event.DoesNotExist: - return HttpResponseNotFound('Unknown event') - with scope(organizer=self.event.organizer): - 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_cached__icontains=query) - | Q(addon_to__attendee_name_cached__icontains=query) - | Q(order__code__istartswith=query) - | Q(order__invoice_address__name_cached__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 - []), - order__testmode=False, - 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)