mirror of
https://github.com/pretix/pretix.git
synced 2026-05-04 15:04:03 +00:00
Remove pretixdroid code
This commit is contained in:
@@ -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'
|
||||
|
||||
@@ -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
|
||||
@@ -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',
|
||||
),
|
||||
]
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 14 KiB |
@@ -1,7 +0,0 @@
|
||||
$(function () {
|
||||
jQuery('#qrcodeCanvas').qrcode(
|
||||
{
|
||||
text: $("#qrdata").html()
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -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 %}
|
||||
<h1>{% trans "Check-in device configuration" %}</h1>
|
||||
|
||||
{% if not configs or "create" in request.GET %}
|
||||
<div class="alert alert-info pretixscan-migration">
|
||||
<h2 class="mar">{% trans "We've got a new app!" %}</h2>
|
||||
<p>
|
||||
{% 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 %}
|
||||
</p>
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-sm-12 text-center">
|
||||
<h3>{% trans "Our new app: pretixSCAN" %}</h3>
|
||||
<img src="{% static "pretixbase/img/pretixscan.svg" %}">
|
||||
<p>{% trans "Available on Android, iOS, Windows, and Linux." %}</p>
|
||||
<p>{% trans "Configuration is available in your organizer account's device list." %}</p>
|
||||
<a href="{% url "control:organizer.devices" organizer=request.organizer.slug %}"
|
||||
class="btn btn-primary btn-lg">
|
||||
{% trans "Switch to my device list" %}
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-6 col-sm-12 text-center">
|
||||
<h3>{% trans "Our old apps: pretixdesk and pretixdroid" %}</h3>
|
||||
<img src="{% static "pretixbase/img/pretixdroid.png" %}">
|
||||
<p>{% trans "Available on Android, Windows, and Linux." %}</p>
|
||||
<p><strong>{% trans "Scroll down to create a configuration" %}</strong></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if not configs or "create" in request.GET %}
|
||||
<h2>{% trans "Create app configuration" %}</h2>
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
To start scanning tickets with our apps, first create a configuration code here:
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<form action="?add" method="post" class="form-horizontal">
|
||||
{% 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" %}
|
||||
<div class="form-group">
|
||||
<div class="col-md-offset-3 col-md-9">
|
||||
<button type="submit" class="btn btn-primary btn-save" name="add" value="1">
|
||||
{% trans "Create configuration" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% if configs and "create" not in request.GET %}
|
||||
<h2>{% trans "Existing app configurations" %}</h2>
|
||||
<a href="?create=1" class="btn btn-default">
|
||||
{% trans "Create a new configuration" %}
|
||||
</a>
|
||||
<form action="?" method="post" class="form-horizontal">
|
||||
{% csrf_token %}
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "ID" %}</th>
|
||||
<th>{% trans "Check-in list" %}</th>
|
||||
<th>{% trans "Items" %}</th>
|
||||
<th>{% trans "Show info" %}</th>
|
||||
<th>{% trans "Allow search" %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for ac in configs %}
|
||||
<tr>
|
||||
<td>
|
||||
{% if ac.app == "pretixdroid" %}
|
||||
<span class="fa fa-fw fa-android"></span>
|
||||
{% elif ac.app == "pretixdesk" %}
|
||||
<span class="fa fa-fw fa-desktop"></span>
|
||||
{% endif %}
|
||||
{{ ac.key|slice:"0:8" }}…
|
||||
</td>
|
||||
<td>
|
||||
<a href="{% url "control:event.orders.checkinlists.show" organizer=request.event.organizer.slug event=request.event.slug list=ac.list.id %}">{{ ac.list }}</a>
|
||||
</td>
|
||||
<td>
|
||||
{% if ac.all_items %}
|
||||
{% trans "All" %}
|
||||
{% else %}
|
||||
{% for item in ac.items.all %}
|
||||
<a href="{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=item.id %}">{{ item.name }}</a>
|
||||
{% if forloop.revcounter0 > 0 %}<br>{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{% if ac.show_info %}{% trans "Yes" %}{% else %}{% trans "No" %}{% endif %}</td>
|
||||
<td>{% if ac.allow_search %}{% trans "Yes" %}{% else %}{% trans "No" %}{% endif %}</td>
|
||||
<td class="text-right">
|
||||
<a href="{% url "plugins:pretixdroid:config.code" organizer=request.event.organizer.slug event=request.event.slug config=ac.pk %}" class="btn btn-default">
|
||||
<span class="fa fa-qrcode"></span> {% trans "Configure device" %}
|
||||
</a>
|
||||
<button class="btn btn-danger" name="delete" value="{{ ac.pk }}">
|
||||
<span class="fa fa-trash"></span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -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" %}
|
||||
<h1>
|
||||
{% trans "pretixdroid configuration" %}
|
||||
<a href="{% url "plugins:pretixdroid:config" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default">
|
||||
{% trans "Back to overview" %}
|
||||
</a>
|
||||
</h1>
|
||||
<h2>{% trans "1. Download app" %}</h2>
|
||||
<p>
|
||||
<a href="http://play.google.com/store/apps/details?id=eu.pretix.pretixdroid">
|
||||
<img src="{% static "pretixplugins/pretixdroid/play_store_en.png" %}" alt="
|
||||
{% trans "Download the app from the Google Play Store" %}" height="70">
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
<small>
|
||||
{% blocktrans trimmed %}
|
||||
Android, Google Play and the Google Play logo are trademarks of Google Inc.
|
||||
{% endblocktrans %}
|
||||
</small>
|
||||
</p>
|
||||
<h2>{% trans "2. Scan code" %}</h2>
|
||||
<div id="qrcodeCanvas"></div>
|
||||
<script type="text/json" id="qrdata">
|
||||
{{ qrdata|safe }}
|
||||
|
||||
</script>
|
||||
<h2>{% trans "3. Start scanning tickets" %}</h2>
|
||||
<script type="text/javascript" src="{% static "pretixplugins/pretixdroid/pretixdroid.js" %}"></script>
|
||||
{% else %}
|
||||
<h1>
|
||||
{% trans "pretixdesk configuration" %}
|
||||
<a href="{% url "plugins:pretixdroid:config" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default">
|
||||
{% trans "Back to overview" %}
|
||||
</a>
|
||||
</h1>
|
||||
<h2>{% trans "1. Download pretixdesk" %}</h2>
|
||||
<p>
|
||||
<a href="https://pretix.eu/about/en/desk" class="btn btn-lg btn-primary" target="_blank">
|
||||
{% trans "Open download page" %}
|
||||
</a>
|
||||
</p>
|
||||
<h2>{% trans "2. Connect device" %}</h2>
|
||||
<p>
|
||||
<a href="pretixdesk://setup?{{ query }}" class="btn btn-lg btn-primary">
|
||||
{% trans "Connect with pretixdesk" %}
|
||||
</a>
|
||||
</p>
|
||||
<p>
|
||||
{% 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 %}
|
||||
</p>
|
||||
<p>
|
||||
<input type="text" class="form-control" value="pretixdesk://setup?{{ query }}" readonly>
|
||||
</p>
|
||||
<h2>{% trans "3. Start scanning tickets" %}</h2>
|
||||
<script type="text/javascript" src="{% static "pretixplugins/pretixdroid/pretixdroid.js" %}"></script>
|
||||
{% if request.event.testmode %}
|
||||
<div class="alert-info">
|
||||
{% 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." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -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<organizer>[^/]+)/(?P<event>[^/]+)/pretixdroid/$', views.ConfigView.as_view(),
|
||||
name='config'),
|
||||
url(r'^control/event/(?P<organizer>[^/]+)/(?P<event>[^/]+)/pretixdroid/(?P<config>\d+)/$',
|
||||
views.ConfigCodeView.as_view(), name='config.code'),
|
||||
url(r'^pretixdroid/api/(?P<organizer>[^/]+)/(?P<event>[^/]+)/(?P<subevent>\d+)/',
|
||||
include(pretixdroid_api_patterns)),
|
||||
url(r'^pretixdroid/api/(?P<organizer>[^/]+)/(?P<event>[^/]+)/', include(pretixdroid_api_patterns)),
|
||||
]
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user