forked from CGM_Public/pretix_original
Add event selection endpoint (#1827)
* Add event selection endpoint * Minor fixes * Add filter by gate
This commit is contained in:
@@ -49,11 +49,15 @@ information on your device as well as your API token:
|
|||||||
"device_id": 5,
|
"device_id": 5,
|
||||||
"unique_serial": "HHZ9LW9JWP390VFZ",
|
"unique_serial": "HHZ9LW9JWP390VFZ",
|
||||||
"api_token": "1kcsh572fonm3hawalrncam4l1gktr2rzx25a22l8g9hx108o9oi0rztpcvwnfnd",
|
"api_token": "1kcsh572fonm3hawalrncam4l1gktr2rzx25a22l8g9hx108o9oi0rztpcvwnfnd",
|
||||||
"name": "Bar"
|
"name": "Bar",
|
||||||
|
"gate": {
|
||||||
|
"id": 3,
|
||||||
|
"name": "South entrance"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Please make sure that you store this ``api_token`` value. We also recommend storing your device ID, your assigned
|
Please make sure that you store this ``api_token`` value. We also recommend storing your device ID, your assigned
|
||||||
``unique_serial``, and the ``organizer`` you have access to, but that's up to you.
|
``unique_serial``, and the ``organizer`` you have access to, but that's up to you. ``gate`` might be ``null``.
|
||||||
|
|
||||||
In case of an error, the response will look like this:
|
In case of an error, the response will look like this:
|
||||||
|
|
||||||
@@ -98,6 +102,8 @@ following endpoint:
|
|||||||
"software_version": "4.1.0"
|
"software_version": "4.1.0"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
You will receive a response equivalent to the response of your initialization request.
|
||||||
|
|
||||||
Creating a new API key
|
Creating a new API key
|
||||||
----------------------
|
----------------------
|
||||||
|
|
||||||
@@ -126,12 +132,65 @@ invalidate your API key. There is no way to reverse this operation.
|
|||||||
|
|
||||||
This can also be done by the user through the web interface.
|
This can also be done by the user through the web interface.
|
||||||
|
|
||||||
Permissions
|
Permissions & security profiles
|
||||||
-----------
|
-------------------------------
|
||||||
|
|
||||||
Device authentication is currently hardcoded to grant the following permissions:
|
Device authentication is currently hardcoded to grant the following permissions:
|
||||||
|
|
||||||
* View event meta data and products etc.
|
* View event meta data and products etc.
|
||||||
* View and change orders
|
* View orders
|
||||||
|
* Change orders
|
||||||
|
* Manage gift cards
|
||||||
|
|
||||||
Devices cannot change events or products and cannot access vouchers.
|
Devices cannot change events or products and cannot access vouchers.
|
||||||
|
|
||||||
|
Additionally, when creating a device through the user interface or API, a user can specify a "security profile" for
|
||||||
|
the device. These include an allow list of specific API calls that may be made by the device. pretix ships with security
|
||||||
|
policies for official pretix apps like pretixSCAN and pretixPOS.
|
||||||
|
|
||||||
|
Removing a device
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
If you want implement a way to to deprovision a device in your software, you can call the ``revoke`` endpoint to
|
||||||
|
invalidate your API key. There is no way to reverse this operation.
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
POST /api/v1/device/revoke HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Authorization: Device 1kcsh572fonm3hawalrncam4l1gktr2rzx25a22l8g9hx108o9oi0rztpcvwnfnd
|
||||||
|
|
||||||
|
This can also be done by the user through the web interface.
|
||||||
|
|
||||||
|
Event selection
|
||||||
|
---------------
|
||||||
|
|
||||||
|
In most cases, your application should allow the user to select the event and check-in list they work with manually
|
||||||
|
from a list. However, in some cases it is required to automatically configure the device for the correct event, for
|
||||||
|
example in a kiosk-like situation where nobody is operating the device. In this case, the app can query the server
|
||||||
|
for a suggestion which event should be used. You can also submit the configuration that is currently in use via
|
||||||
|
query parameters:
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
GET /api/v1/device/eventselection?current_event=democon¤t_subevent=42¤t_checkinlist=542 HTTP/1.1
|
||||||
|
Host: pretix.eu
|
||||||
|
Authorization: Device 1kcsh572fonm3hawalrncam4l1gktr2rzx25a22l8g9hx108o9oi0rztpcvwnfnd
|
||||||
|
|
||||||
|
You can get three response codes:
|
||||||
|
|
||||||
|
* ``304`` The server things you already selected a good event
|
||||||
|
* ``404`` The server has not found a suggestion for you
|
||||||
|
* ``200`` The server suggests a new event (body see below)
|
||||||
|
|
||||||
|
.. sourcecode:: http
|
||||||
|
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"event": "democon",
|
||||||
|
"subevent": 23,
|
||||||
|
"checkinlist": 5
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ presale
|
|||||||
pretix
|
pretix
|
||||||
pretixSCAN
|
pretixSCAN
|
||||||
pretixdroid
|
pretixdroid
|
||||||
|
pretixPOS
|
||||||
pretixpresale
|
pretixpresale
|
||||||
prometheus
|
prometheus
|
||||||
proxied
|
proxied
|
||||||
|
|||||||
@@ -22,8 +22,10 @@ class PretixScanSecurityProfile(AllowListSecurityProfile):
|
|||||||
verbose_name = _('pretixSCAN')
|
verbose_name = _('pretixSCAN')
|
||||||
allowlist = (
|
allowlist = (
|
||||||
('GET', 'api-v1:version'),
|
('GET', 'api-v1:version'),
|
||||||
|
('GET', 'api-v1:device.eventselection'),
|
||||||
('POST', 'api-v1:device.update'),
|
('POST', 'api-v1:device.update'),
|
||||||
('POST', 'api-v1:device.revoke'),
|
('POST', 'api-v1:device.revoke'),
|
||||||
|
('POST', 'api-v1:device.roll'),
|
||||||
('GET', 'api-v1:event-list'),
|
('GET', 'api-v1:event-list'),
|
||||||
('GET', 'api-v1:event-detail'),
|
('GET', 'api-v1:event-detail'),
|
||||||
('GET', 'api-v1:subevent-list'),
|
('GET', 'api-v1:subevent-list'),
|
||||||
@@ -48,8 +50,10 @@ class PretixScanNoSyncSecurityProfile(AllowListSecurityProfile):
|
|||||||
verbose_name = _('pretixSCAN (kiosk mode, online only)')
|
verbose_name = _('pretixSCAN (kiosk mode, online only)')
|
||||||
allowlist = (
|
allowlist = (
|
||||||
('GET', 'api-v1:version'),
|
('GET', 'api-v1:version'),
|
||||||
|
('GET', 'api-v1:device.eventselection'),
|
||||||
('POST', 'api-v1:device.update'),
|
('POST', 'api-v1:device.update'),
|
||||||
('POST', 'api-v1:device.revoke'),
|
('POST', 'api-v1:device.revoke'),
|
||||||
|
('POST', 'api-v1:device.roll'),
|
||||||
('GET', 'api-v1:event-list'),
|
('GET', 'api-v1:event-list'),
|
||||||
('GET', 'api-v1:event-detail'),
|
('GET', 'api-v1:event-detail'),
|
||||||
('GET', 'api-v1:subevent-list'),
|
('GET', 'api-v1:subevent-list'),
|
||||||
@@ -72,8 +76,10 @@ class PretixPosSecurityProfile(AllowListSecurityProfile):
|
|||||||
verbose_name = _('pretixPOS')
|
verbose_name = _('pretixPOS')
|
||||||
allowlist = (
|
allowlist = (
|
||||||
('GET', 'api-v1:version'),
|
('GET', 'api-v1:version'),
|
||||||
|
('GET', 'api-v1:device.eventselection'),
|
||||||
('POST', 'api-v1:device.update'),
|
('POST', 'api-v1:device.update'),
|
||||||
('POST', 'api-v1:device.revoke'),
|
('POST', 'api-v1:device.revoke'),
|
||||||
|
('POST', 'api-v1:device.roll'),
|
||||||
('GET', 'api-v1:event-list'),
|
('GET', 'api-v1:event-list'),
|
||||||
('GET', 'api-v1:event-detail'),
|
('GET', 'api-v1:event-detail'),
|
||||||
('GET', 'api-v1:subevent-list'),
|
('GET', 'api-v1:subevent-list'),
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ urlpatterns = [
|
|||||||
url(r"^device/update$", device.UpdateView.as_view(), name="device.update"),
|
url(r"^device/update$", device.UpdateView.as_view(), name="device.update"),
|
||||||
url(r"^device/roll$", device.RollKeyView.as_view(), name="device.roll"),
|
url(r"^device/roll$", device.RollKeyView.as_view(), name="device.roll"),
|
||||||
url(r"^device/revoke$", device.RevokeKeyView.as_view(), name="device.revoke"),
|
url(r"^device/revoke$", device.RevokeKeyView.as_view(), name="device.revoke"),
|
||||||
|
url(r"^device/eventselection$", device.EventSelectionView.as_view(), name="device.eventselection"),
|
||||||
url(r"^me$", user.MeView.as_view(), name="user.me"),
|
url(r"^me$", user.MeView.as_view(), name="user.me"),
|
||||||
url(r"^version$", version.VersionView.as_view(), name="version"),
|
url(r"^version$", version.VersionView.as_view(), name="version"),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from django.db.models import Exists, OuterRef, Q
|
||||||
|
from django.db.models.functions import Coalesce
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers, status
|
||||||
from rest_framework.exceptions import ValidationError
|
from rest_framework.exceptions import ValidationError
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
|
|
||||||
from pretix.api.auth.device import DeviceTokenAuthentication
|
from pretix.api.auth.device import DeviceTokenAuthentication
|
||||||
from pretix.base.models import Device
|
from pretix.base.models import CheckinList, Device, SubEvent
|
||||||
from pretix.base.models.devices import Gate, generate_api_token
|
from pretix.base.models.devices import Gate, generate_api_token
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -122,3 +124,156 @@ class RevokeKeyView(APIView):
|
|||||||
|
|
||||||
serializer = DeviceSerializer(device)
|
serializer = DeviceSerializer(device)
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
|
||||||
|
class EventSelectionView(APIView):
|
||||||
|
authentication_classes = (DeviceTokenAuthentication,)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def base_event_qs(self):
|
||||||
|
qs = self.request.auth.organizer.events.annotate(
|
||||||
|
first_date=Coalesce('date_admission', 'date_from'),
|
||||||
|
last_date=Coalesce('date_to', 'date_from'),
|
||||||
|
).filter(
|
||||||
|
live=True,
|
||||||
|
has_subevents=False
|
||||||
|
).order_by('first_date')
|
||||||
|
if self.request.auth.gate:
|
||||||
|
has_cl = CheckinList.objects.filter(
|
||||||
|
event=OuterRef('pk'),
|
||||||
|
gates__in=[self.request.auth.gate]
|
||||||
|
)
|
||||||
|
qs = qs.annotate(has_cl=Exists(has_cl)).filter(has_cl=True)
|
||||||
|
return qs
|
||||||
|
|
||||||
|
@property
|
||||||
|
def base_subevent_qs(self):
|
||||||
|
qs = SubEvent.objects.annotate(
|
||||||
|
first_date=Coalesce('date_admission', 'date_from'),
|
||||||
|
last_date=Coalesce('date_to', 'date_from'),
|
||||||
|
).filter(
|
||||||
|
event__organizer=self.request.auth.organizer,
|
||||||
|
event__live=True,
|
||||||
|
active=True,
|
||||||
|
).select_related('event').order_by('first_date')
|
||||||
|
if self.request.auth.gate:
|
||||||
|
has_cl = CheckinList.objects.filter(
|
||||||
|
Q(subevent__isnull=True) | Q(subevent=OuterRef('pk')),
|
||||||
|
event_id=OuterRef('event_id'),
|
||||||
|
gates__in=[self.request.auth.gate]
|
||||||
|
)
|
||||||
|
qs = qs.annotate(has_cl=Exists(has_cl)).filter(has_cl=True)
|
||||||
|
return qs
|
||||||
|
|
||||||
|
def get(self, request, format=None):
|
||||||
|
device = request.auth
|
||||||
|
current_event = None
|
||||||
|
current_subevent = None
|
||||||
|
if 'current_event' in request.query_params:
|
||||||
|
current_event = device.organizer.events.filter(slug=request.query_params['current_event']).first()
|
||||||
|
if current_event and 'current_subevent' in request.query_params:
|
||||||
|
current_subevent = current_event.subevents.filter(pk=request.query_params['current_subevent']).first()
|
||||||
|
if current_event and current_event.has_subevents and not current_subevent:
|
||||||
|
current_event = None
|
||||||
|
|
||||||
|
if current_event:
|
||||||
|
current_ev = current_subevent or current_event
|
||||||
|
current_ev_start = current_ev.date_admission or current_ev.date_from
|
||||||
|
tz = current_event.timezone
|
||||||
|
if current_ev.date_to and current_ev_start < now() < current_ev.date_to:
|
||||||
|
# The event that is selected is currently running. Good enough.
|
||||||
|
return Response(status=status.HTTP_304_NOT_MODIFIED)
|
||||||
|
|
||||||
|
# The event that is selected is not currently running. We cannot rely on all events having a proper end date.
|
||||||
|
# In any case, we'll need to decide between the event that last started (and might still be running) and the
|
||||||
|
# event that starts next (and might already be letting people in), so let's get these two!
|
||||||
|
last_started_ev = self.base_event_qs.filter(first_date__lte=now()).last() or self.base_subevent_qs.filter(
|
||||||
|
first_date__lte=now()).last()
|
||||||
|
|
||||||
|
upcoming_event = self.base_event_qs.filter(first_date__gt=now()).first()
|
||||||
|
upcoming_subevent = self.base_subevent_qs.filter(first_date__gt=now()).first()
|
||||||
|
if upcoming_event and upcoming_subevent:
|
||||||
|
if upcoming_event.first_date > upcoming_subevent.first_date:
|
||||||
|
upcoming_ev = upcoming_subevent
|
||||||
|
else:
|
||||||
|
upcoming_ev = upcoming_event
|
||||||
|
else:
|
||||||
|
upcoming_ev = upcoming_event or upcoming_subevent
|
||||||
|
|
||||||
|
if not upcoming_ev and not last_started_ev:
|
||||||
|
# Ooops, no events here
|
||||||
|
return Response(status=status.HTTP_404_NOT_FOUND)
|
||||||
|
elif upcoming_ev and not last_started_ev:
|
||||||
|
# No event running, so let's take the next one
|
||||||
|
return self._suggest_event(current_event, upcoming_ev)
|
||||||
|
elif last_started_ev and not upcoming_ev:
|
||||||
|
# No event upcoming, so let's take the next one
|
||||||
|
return self._suggest_event(current_event, last_started_ev)
|
||||||
|
|
||||||
|
if last_started_ev.date_to and now() < last_started_ev.date_to:
|
||||||
|
# The event that last started is currently running. Good enough.
|
||||||
|
return self._suggest_event(current_event, last_started_ev)
|
||||||
|
|
||||||
|
if not current_event:
|
||||||
|
tz = (upcoming_event or last_started_ev).timezone
|
||||||
|
|
||||||
|
lse_d = last_started_ev.date_from.astimezone(tz).date()
|
||||||
|
upc_d = upcoming_ev.date_from.astimezone(tz).date()
|
||||||
|
now_d = now().astimezone(tz).date()
|
||||||
|
if lse_d == now_d and upc_d != now_d:
|
||||||
|
# Last event was today, next is tomorrow, stick with today
|
||||||
|
return self._suggest_event(current_event, last_started_ev)
|
||||||
|
elif lse_d != now_d and upc_d == now_d:
|
||||||
|
# Last event was yesterday, next is today, stick with today
|
||||||
|
return self._suggest_event(current_event, upcoming_ev)
|
||||||
|
|
||||||
|
# Both last and next event are today, we switch over in the middle
|
||||||
|
if now() > last_started_ev.last_date + (upcoming_ev.first_date - last_started_ev.last_date) / 2:
|
||||||
|
return self._suggest_event(current_event, upcoming_ev)
|
||||||
|
else:
|
||||||
|
return self._suggest_event(current_event, last_started_ev)
|
||||||
|
|
||||||
|
def _suggest_event(self, current_event, ev):
|
||||||
|
current_checkinlist = None
|
||||||
|
if current_event and 'current_checkinlist' in self.request.query_params:
|
||||||
|
current_checkinlist = current_event.checkin_lists.filter(
|
||||||
|
pk=self.request.query_params['current_checkinlist']
|
||||||
|
).first()
|
||||||
|
if isinstance(ev, SubEvent):
|
||||||
|
checkinlist_qs = ev.event.checkin_lists.filter(Q(subevent__isnull=True) | Q(subevent=ev))
|
||||||
|
else:
|
||||||
|
checkinlist_qs = ev.checkin_lists
|
||||||
|
|
||||||
|
if self.request.auth.gate:
|
||||||
|
checkinlist_qs = checkinlist_qs.filter(gates__in=[self.request.auth.gate])
|
||||||
|
|
||||||
|
checkinlist = None
|
||||||
|
if current_checkinlist:
|
||||||
|
checkinlist = checkinlist_qs.filter(Q(name=current_checkinlist.name) | Q(pk=current_checkinlist.pk)).first()
|
||||||
|
if not checkinlist:
|
||||||
|
checkinlist = checkinlist_qs.first()
|
||||||
|
r = {
|
||||||
|
'event': {
|
||||||
|
'slug': ev.event.slug if isinstance(ev, SubEvent) else ev.slug,
|
||||||
|
'name': str(ev.event.name) if isinstance(ev, SubEvent) else str(ev.name),
|
||||||
|
},
|
||||||
|
'subevent': ev.pk if isinstance(ev, SubEvent) else None,
|
||||||
|
'checkinlist': checkinlist.pk if checkinlist else None,
|
||||||
|
}
|
||||||
|
|
||||||
|
if r == {
|
||||||
|
'event': {
|
||||||
|
'slug': current_event.slug if current_event else None,
|
||||||
|
'name': str(current_event.name) if current_event else None,
|
||||||
|
},
|
||||||
|
'subevent': (
|
||||||
|
int(self.request.query_params.get('current_subevent'))
|
||||||
|
if self.request.query_params.get('current_subevent') else None
|
||||||
|
),
|
||||||
|
'checkinlist': (
|
||||||
|
int(self.request.query_params.get('current_checkinlist'))
|
||||||
|
if self.request.query_params.get('current_checkinlist') else None
|
||||||
|
),
|
||||||
|
}:
|
||||||
|
return Response(status=status.HTTP_304_NOT_MODIFIED)
|
||||||
|
return Response(r)
|
||||||
|
|||||||
18
src/pretix/base/migrations/0169_checkinlist_gates.py
Normal file
18
src/pretix/base/migrations/0169_checkinlist_gates.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 3.0.10 on 2020-10-24 15:55
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('pretixbase', '0168_auto_20201023_1447'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='checkinlist',
|
||||||
|
name='gates',
|
||||||
|
field=models.ManyToManyField(to='pretixbase.Gate'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -21,6 +21,11 @@ class CheckinList(LoggedModel):
|
|||||||
default=False,
|
default=False,
|
||||||
help_text=_('With this option, people will be able to check in even if the '
|
help_text=_('With this option, people will be able to check in even if the '
|
||||||
'order have not been paid.'))
|
'order have not been paid.'))
|
||||||
|
gates = models.ManyToManyField(
|
||||||
|
'Gate', verbose_name=_("Gates"), blank=True,
|
||||||
|
help_text=_("Does not have any effect for the validation of tickets, only for the automatic configuration of "
|
||||||
|
"check-in devices.")
|
||||||
|
)
|
||||||
allow_entry_after_exit = models.BooleanField(
|
allow_entry_after_exit = models.BooleanField(
|
||||||
verbose_name=_('Allow re-entering after an exit scan'),
|
verbose_name=_('Allow re-entering after an exit scan'),
|
||||||
default=True
|
default=True
|
||||||
|
|||||||
@@ -44,6 +44,11 @@ class CheckinListForm(forms.ModelForm):
|
|||||||
widget=forms.CheckboxSelectMultiple
|
widget=forms.CheckboxSelectMultiple
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if not self.event.organizer.gates.exists():
|
||||||
|
del self.fields['gates']
|
||||||
|
else:
|
||||||
|
self.fields['gates'].queryset = self.event.organizer.gates.all()
|
||||||
|
|
||||||
if self.event.has_subevents:
|
if self.event.has_subevents:
|
||||||
self.fields['subevent'].queryset = self.event.subevents.all()
|
self.fields['subevent'].queryset = self.event.subevents.all()
|
||||||
self.fields['subevent'].widget = Select2(
|
self.fields['subevent'].widget = Select2(
|
||||||
@@ -73,17 +78,22 @@ class CheckinListForm(forms.ModelForm):
|
|||||||
'allow_multiple_entries',
|
'allow_multiple_entries',
|
||||||
'allow_entry_after_exit',
|
'allow_entry_after_exit',
|
||||||
'rules',
|
'rules',
|
||||||
|
'gates',
|
||||||
'exit_all_at',
|
'exit_all_at',
|
||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
'limit_products': forms.CheckboxSelectMultiple(attrs={
|
'limit_products': forms.CheckboxSelectMultiple(attrs={
|
||||||
'data-inverse-dependency': '<[name$=all_products]'
|
'data-inverse-dependency': '<[name$=all_products]'
|
||||||
}),
|
}),
|
||||||
|
'gates': forms.CheckboxSelectMultiple(attrs={
|
||||||
|
'class': 'scrolling-multiple-choice'
|
||||||
|
}),
|
||||||
'auto_checkin_sales_channels': forms.CheckboxSelectMultiple(),
|
'auto_checkin_sales_channels': forms.CheckboxSelectMultiple(),
|
||||||
'exit_all_at': forms.TimeInput(attrs={'class': 'timepickerfield'}),
|
'exit_all_at': forms.TimeInput(attrs={'class': 'timepickerfield'}),
|
||||||
}
|
}
|
||||||
field_classes = {
|
field_classes = {
|
||||||
'limit_products': SafeModelMultipleChoiceField,
|
'limit_products': SafeModelMultipleChoiceField,
|
||||||
|
'gates': SafeModelMultipleChoiceField,
|
||||||
'subevent': SafeModelChoiceField,
|
'subevent': SafeModelChoiceField,
|
||||||
'exit_all_at': NextTimeField,
|
'exit_all_at': NextTimeField,
|
||||||
}
|
}
|
||||||
@@ -96,6 +106,11 @@ class SimpleCheckinListForm(forms.ModelForm):
|
|||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self.fields['limit_products'].queryset = self.event.items.all()
|
self.fields['limit_products'].queryset = self.event.items.all()
|
||||||
|
|
||||||
|
if not self.event.organizer.gates.exists():
|
||||||
|
del self.fields['gates']
|
||||||
|
else:
|
||||||
|
self.fields['gates'].queryset = self.event.organizer.gates.all()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = CheckinList
|
model = CheckinList
|
||||||
localized_fields = '__all__'
|
localized_fields = '__all__'
|
||||||
@@ -105,13 +120,18 @@ class SimpleCheckinListForm(forms.ModelForm):
|
|||||||
'limit_products',
|
'limit_products',
|
||||||
'include_pending',
|
'include_pending',
|
||||||
'allow_entry_after_exit',
|
'allow_entry_after_exit',
|
||||||
|
'gates',
|
||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
'limit_products': forms.CheckboxSelectMultiple(attrs={
|
'limit_products': forms.CheckboxSelectMultiple(attrs={
|
||||||
'data-inverse-dependency': '<[name$=all_products]'
|
'data-inverse-dependency': '<[name$=all_products]'
|
||||||
}),
|
}),
|
||||||
|
'gates': forms.CheckboxSelectMultiple(attrs={
|
||||||
|
'class': 'scrolling-multiple-choice'
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
field_classes = {
|
field_classes = {
|
||||||
'limit_products': SafeModelMultipleChoiceField,
|
'limit_products': SafeModelMultipleChoiceField,
|
||||||
'subevent': SafeModelChoiceField,
|
'subevent': SafeModelChoiceField,
|
||||||
|
'gates': SafeModelMultipleChoiceField,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,6 +60,9 @@
|
|||||||
{% bootstrap_field form.allow_entry_after_exit layout="control" %}
|
{% bootstrap_field form.allow_entry_after_exit layout="control" %}
|
||||||
{% bootstrap_field form.exit_all_at layout="control" %}
|
{% bootstrap_field form.exit_all_at layout="control" %}
|
||||||
{% bootstrap_field form.auto_checkin_sales_channels layout="control" %}
|
{% bootstrap_field form.auto_checkin_sales_channels layout="control" %}
|
||||||
|
{% if form.gates %}
|
||||||
|
{% bootstrap_field form.gates layout="control" %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<h3>{% trans "Custom check-in rule" %}</h3>
|
<h3>{% trans "Custom check-in rule" %}</h3>
|
||||||
<div id="rules-editor" class="form-inline">
|
<div id="rules-editor" class="form-inline">
|
||||||
|
|||||||
@@ -509,6 +509,9 @@
|
|||||||
{% bootstrap_field form.all_products layout="control" %}
|
{% bootstrap_field form.all_products layout="control" %}
|
||||||
{% bootstrap_field form.limit_products layout="control" %}
|
{% bootstrap_field form.limit_products layout="control" %}
|
||||||
{% bootstrap_field form.allow_entry_after_exit layout="control" %}
|
{% bootstrap_field form.allow_entry_after_exit layout="control" %}
|
||||||
|
{% if form.gates %}
|
||||||
|
{% bootstrap_field form.gates layout="control" %}
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -538,6 +541,9 @@
|
|||||||
{% bootstrap_field cl_formset.empty_form.all_products layout="control" %}
|
{% bootstrap_field cl_formset.empty_form.all_products layout="control" %}
|
||||||
{% bootstrap_field cl_formset.empty_form.limit_products layout="control" %}
|
{% bootstrap_field cl_formset.empty_form.limit_products layout="control" %}
|
||||||
{% bootstrap_field cl_formset.empty_form.allow_entry_after_exit layout="control" %}
|
{% bootstrap_field cl_formset.empty_form.allow_entry_after_exit layout="control" %}
|
||||||
|
{% if cl_formset.empty_form.gates %}
|
||||||
|
{% bootstrap_field cl_formset.empty_form.gates layout="control" %}
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endescapescript %}
|
{% endescapescript %}
|
||||||
|
|||||||
@@ -193,6 +193,9 @@
|
|||||||
{% bootstrap_field form.all_products layout="control" %}
|
{% bootstrap_field form.all_products layout="control" %}
|
||||||
{% bootstrap_field form.limit_products layout="control" %}
|
{% bootstrap_field form.limit_products layout="control" %}
|
||||||
{% bootstrap_field form.allow_entry_after_exit layout="control" %}
|
{% bootstrap_field form.allow_entry_after_exit layout="control" %}
|
||||||
|
{% if form.gates %}
|
||||||
|
{% bootstrap_field form.gates layout="control" %}
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -222,6 +225,9 @@
|
|||||||
{% bootstrap_field cl_formset.empty_form.all_products layout="control" %}
|
{% bootstrap_field cl_formset.empty_form.all_products layout="control" %}
|
||||||
{% bootstrap_field cl_formset.empty_form.limit_products layout="control" %}
|
{% bootstrap_field cl_formset.empty_form.limit_products layout="control" %}
|
||||||
{% bootstrap_field cl_formset.empty_form.allow_entry_after_exit layout="control" %}
|
{% bootstrap_field cl_formset.empty_form.allow_entry_after_exit layout="control" %}
|
||||||
|
{% if cl_formset.empty_form.gates %}
|
||||||
|
{% bootstrap_field cl_formset.empty_form.gates layout="control" %}
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endescapescript %}
|
{% endescapescript %}
|
||||||
|
|||||||
228
src/tests/api/test_device_event_selection.py
Normal file
228
src/tests/api/test_device_event_selection.py
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import pytz
|
||||||
|
from django_scopes import scopes_disabled
|
||||||
|
from freezegun import freeze_time
|
||||||
|
|
||||||
|
tz = pytz.timezone("Asia/Tokyo")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_no_events(device_client, device):
|
||||||
|
resp = device_client.get(f'/api/v1/device/eventselection?current_event=e1')
|
||||||
|
assert resp.status_code == 404
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_choose_between_events(device_client, device):
|
||||||
|
with scopes_disabled():
|
||||||
|
e1 = device.organizer.events.create(
|
||||||
|
name="Event", slug="e1", live=True,
|
||||||
|
date_from=tz.localize(datetime(2020, 1, 10, 14, 0)),
|
||||||
|
date_to=tz.localize(datetime(2020, 1, 10, 15, 0)),
|
||||||
|
)
|
||||||
|
cl1 = e1.checkin_lists.create(name="Same name")
|
||||||
|
e2 = device.organizer.events.create(
|
||||||
|
name="Event", slug="e2", live=True,
|
||||||
|
date_from=tz.localize(datetime(2020, 1, 10, 16, 0)),
|
||||||
|
date_to=tz.localize(datetime(2020, 1, 10, 17, 0)),
|
||||||
|
)
|
||||||
|
e2.checkin_lists.create(name="Other name")
|
||||||
|
cl2 = e2.checkin_lists.create(name="Same name")
|
||||||
|
e2.checkin_lists.create(name="Yet another name")
|
||||||
|
tomorrow = device.organizer.events.create(
|
||||||
|
name="Event", slug="tomorrow", live=True,
|
||||||
|
date_from=tz.localize(datetime(2020, 1, 11, 15, 0)),
|
||||||
|
date_to=tz.localize(datetime(2020, 1, 11, 16, 0)),
|
||||||
|
)
|
||||||
|
cl3 = tomorrow.checkin_lists.create(name="Just any name")
|
||||||
|
for e in device.organizer.events.all():
|
||||||
|
e.settings.timezone = "Asia/Tokyo"
|
||||||
|
|
||||||
|
# Keep current when still running
|
||||||
|
with freeze_time("2020-01-10T14:30:00+09:00"):
|
||||||
|
resp = device_client.get(f'/api/v1/device/eventselection?current_event=e1¤t_checkinlist={cl1.pk}')
|
||||||
|
assert resp.status_code == 304
|
||||||
|
with freeze_time("2020-01-10T16:30:00+09:00"):
|
||||||
|
resp = device_client.get(f'/api/v1/device/eventselection?current_event=e1')
|
||||||
|
assert resp.status_code == 200
|
||||||
|
resp = device_client.get(f'/api/v1/device/eventselection?current_event=e2')
|
||||||
|
assert resp.status_code == 304
|
||||||
|
|
||||||
|
# Next one only
|
||||||
|
with freeze_time("2020-01-10T12:30:00+09:00"):
|
||||||
|
resp = device_client.get(f'/api/v1/device/eventselection')
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.data['event']['slug'] == 'e1'
|
||||||
|
|
||||||
|
# Last one only
|
||||||
|
with freeze_time("2020-01-10T17:30:00+09:00"):
|
||||||
|
resp = device_client.get(f'/api/v1/device/eventselection')
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.data['event']['slug'] == 'e2'
|
||||||
|
|
||||||
|
# Running one
|
||||||
|
with freeze_time("2020-01-10T14:30:00+09:00"):
|
||||||
|
resp = device_client.get(f'/api/v1/device/eventselection')
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.data['event']['slug'] == 'e1'
|
||||||
|
with freeze_time("2020-01-10T16:01:00+09:00"):
|
||||||
|
resp = device_client.get(f'/api/v1/device/eventselection?current_event=e1¤t_checkinlist={cl1.pk}')
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.data['event']['slug'] == 'e2'
|
||||||
|
assert resp.data['checkinlist'] == cl2.pk
|
||||||
|
|
||||||
|
# Prefer the one on the same day
|
||||||
|
with freeze_time("2020-01-10T23:59:00+09:00"):
|
||||||
|
resp = device_client.get(f'/api/v1/device/eventselection?current_event=e1¤t_checkinlist={cl1.pk}')
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.data['event']['slug'] == 'e2'
|
||||||
|
assert resp.data['checkinlist'] == cl2.pk
|
||||||
|
with freeze_time("2020-01-11T01:00:00+09:00"):
|
||||||
|
resp = device_client.get(f'/api/v1/device/eventselection?current_event=e1¤t_checkinlist={cl1.pk}')
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.data['event']['slug'] == 'tomorrow'
|
||||||
|
assert resp.data['checkinlist'] == cl3.pk
|
||||||
|
|
||||||
|
# Switch at half-time
|
||||||
|
with freeze_time("2020-01-10T15:29:00+09:00"):
|
||||||
|
resp = device_client.get(f'/api/v1/device/eventselection')
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.data['event']['slug'] == 'e1'
|
||||||
|
with freeze_time("2020-01-10T15:31:00+09:00"):
|
||||||
|
resp = device_client.get(f'/api/v1/device/eventselection')
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.data['event']['slug'] == 'e2'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_choose_between_subevents(device_client, device):
|
||||||
|
with scopes_disabled():
|
||||||
|
e = device.organizer.events.create(
|
||||||
|
name="Event", slug="e1", live=True,
|
||||||
|
date_from=tz.localize(datetime(2020, 1, 10, 14, 0)),
|
||||||
|
has_subevents=True,
|
||||||
|
)
|
||||||
|
e.settings.timezone = "Asia/Tokyo"
|
||||||
|
se1 = e.subevents.create(
|
||||||
|
name="Event", active=True,
|
||||||
|
date_from=tz.localize(datetime(2020, 1, 10, 14, 0)),
|
||||||
|
date_to=tz.localize(datetime(2020, 1, 10, 15, 0)),
|
||||||
|
)
|
||||||
|
cl1 = e.checkin_lists.create(name="Same name", subevent=se1)
|
||||||
|
se2 = e.subevents.create(
|
||||||
|
name="Event", active=True,
|
||||||
|
date_from=tz.localize(datetime(2020, 1, 10, 16, 0)),
|
||||||
|
date_to=tz.localize(datetime(2020, 1, 10, 17, 0)),
|
||||||
|
)
|
||||||
|
cl2 = e.checkin_lists.create(name="Same name", subevent=se2)
|
||||||
|
cl3 = e.checkin_lists.create(name="Other name")
|
||||||
|
e.checkin_lists.create(name="Yet another name", subevent=se2)
|
||||||
|
se_tomorrow = e.subevents.create(
|
||||||
|
name="Event", active=True,
|
||||||
|
date_from=tz.localize(datetime(2020, 1, 11, 15, 0)),
|
||||||
|
date_to=tz.localize(datetime(2020, 1, 11, 16, 0)),
|
||||||
|
)
|
||||||
|
with freeze_time("2020-01-10T14:30:00+09:00"):
|
||||||
|
resp = device_client.get(f'/api/v1/device/eventselection?current_event=e1¤t_subevent={se1.pk}')
|
||||||
|
assert resp.status_code == 304
|
||||||
|
with freeze_time("2020-01-10T16:30:00+09:00"):
|
||||||
|
resp = device_client.get(f'/api/v1/device/eventselection?current_event=e1¤t_subevent={se1.pk}')
|
||||||
|
assert resp.status_code == 200
|
||||||
|
resp = device_client.get(f'/api/v1/device/eventselection?current_event=e1¤t_subevent={se2.pk}')
|
||||||
|
assert resp.status_code == 304
|
||||||
|
|
||||||
|
# Next one only
|
||||||
|
with freeze_time("2020-01-10T12:30:00+09:00"):
|
||||||
|
resp = device_client.get(f'/api/v1/device/eventselection')
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.data['event']['slug'] == 'e1'
|
||||||
|
assert resp.data['subevent'] == se1.pk
|
||||||
|
|
||||||
|
# Last one only
|
||||||
|
with freeze_time("2020-01-10T17:30:00+09:00"):
|
||||||
|
resp = device_client.get(f'/api/v1/device/eventselection')
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.data['event']['slug'] == 'e1'
|
||||||
|
assert resp.data['subevent'] == se2.pk
|
||||||
|
|
||||||
|
# Running one
|
||||||
|
with freeze_time("2020-01-10T14:30:00+09:00"):
|
||||||
|
resp = device_client.get(f'/api/v1/device/eventselection')
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.data['event']['slug'] == 'e1'
|
||||||
|
assert resp.data['subevent'] == se1.pk
|
||||||
|
with freeze_time("2020-01-10T16:01:00+09:00"):
|
||||||
|
resp = device_client.get(
|
||||||
|
f'/api/v1/device/eventselection?current_event=e1¤t_checkinlist={cl1.pk}¤t_subevent={se1.pk}')
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.data['event']['slug'] == 'e1'
|
||||||
|
assert resp.data['subevent'] == se2.pk
|
||||||
|
assert resp.data['checkinlist'] == cl2.pk
|
||||||
|
|
||||||
|
# Prefer the one on the same day
|
||||||
|
with freeze_time("2020-01-10T23:59:00+09:00"):
|
||||||
|
resp = device_client.get(f'/api/v1/device/eventselection')
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.data['event']['slug'] == 'e1'
|
||||||
|
assert resp.data['subevent'] == se2.pk
|
||||||
|
with freeze_time("2020-01-11T01:00:00+09:00"):
|
||||||
|
resp = device_client.get(
|
||||||
|
f'/api/v1/device/eventselection?current_event=e1¤t_checkinlist={cl1.pk}¤t_subevent={se1.pk}')
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.data['event']['slug'] == 'e1'
|
||||||
|
assert resp.data['subevent'] == se_tomorrow.pk
|
||||||
|
assert resp.data['checkinlist'] == cl3.pk
|
||||||
|
|
||||||
|
# Switch at half-time
|
||||||
|
with freeze_time("2020-01-10T15:29:00+09:00"):
|
||||||
|
resp = device_client.get(f'/api/v1/device/eventselection')
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.data['event']['slug'] == 'e1'
|
||||||
|
assert resp.data['subevent'] == se1.pk
|
||||||
|
with freeze_time("2020-01-10T15:31:00+09:00"):
|
||||||
|
resp = device_client.get(f'/api/v1/device/eventselection')
|
||||||
|
assert resp.data['event']['slug'] == 'e1'
|
||||||
|
assert resp.data['subevent'] == se2.pk
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_require_gate(device_client, device):
|
||||||
|
with scopes_disabled():
|
||||||
|
g = device.organizer.gates.create(name="Gate 1")
|
||||||
|
device.gate = g
|
||||||
|
device.save()
|
||||||
|
e = device.organizer.events.create(
|
||||||
|
name="Event", slug="e1", live=True,
|
||||||
|
date_from=tz.localize(datetime(2020, 1, 10, 14, 0)),
|
||||||
|
has_subevents=True,
|
||||||
|
)
|
||||||
|
e.settings.timezone = "Asia/Tokyo"
|
||||||
|
se0 = e.subevents.create(
|
||||||
|
name="Event", active=True,
|
||||||
|
date_from=tz.localize(datetime(2020, 1, 10, 9, 0)),
|
||||||
|
date_to=tz.localize(datetime(2020, 1, 10, 10, 0)),
|
||||||
|
)
|
||||||
|
e.subevents.create(
|
||||||
|
name="Event", active=True,
|
||||||
|
date_from=tz.localize(datetime(2020, 1, 10, 14, 0)),
|
||||||
|
date_to=tz.localize(datetime(2020, 1, 10, 15, 0)),
|
||||||
|
)
|
||||||
|
cl1 = e.checkin_lists.create(name="Same name", subevent=se0)
|
||||||
|
se2 = e.subevents.create(
|
||||||
|
name="Event", active=True,
|
||||||
|
date_from=tz.localize(datetime(2020, 1, 10, 16, 0)),
|
||||||
|
date_to=tz.localize(datetime(2020, 1, 10, 17, 0)),
|
||||||
|
)
|
||||||
|
e.checkin_lists.create(name="Same name", subevent=se2)
|
||||||
|
cl3 = e.checkin_lists.create(name="Other name", subevent=se2)
|
||||||
|
cl3.gates.add(g)
|
||||||
|
|
||||||
|
with freeze_time("2020-01-10T11:00:00+09:00"):
|
||||||
|
resp = device_client.get(
|
||||||
|
f'/api/v1/device/eventselection?current_event=e1¤t_checkinlist={cl1.pk}¤t_subevent={se0.pk}')
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert resp.data['event']['slug'] == 'e1'
|
||||||
|
assert resp.data['subevent'] == se2.pk
|
||||||
|
assert resp.data['checkinlist'] == cl3.pk
|
||||||
Reference in New Issue
Block a user