Add event selection endpoint (#1827)

* Add event selection endpoint

* Minor fixes

* Add filter by gate
This commit is contained in:
Raphael Michel
2020-10-24 19:20:07 +02:00
committed by GitHub
parent 3865063b12
commit 987597b298
12 changed files with 515 additions and 7 deletions

View File

@@ -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&current_subevent=42&current_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
}

View File

@@ -96,6 +96,7 @@ presale
pretix pretix
pretixSCAN pretixSCAN
pretixdroid pretixdroid
pretixPOS
pretixpresale pretixpresale
prometheus prometheus
proxied proxied

View File

@@ -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'),

View File

@@ -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"),
] ]

View File

@@ -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)

View 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'),
),
]

View File

@@ -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

View File

@@ -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,
} }

View File

@@ -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">

View File

@@ -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 %}

View File

@@ -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 %}

View 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&current_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&current_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&current_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&current_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&current_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&current_subevent={se1.pk}')
assert resp.status_code == 200
resp = device_client.get(f'/api/v1/device/eventselection?current_event=e1&current_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&current_checkinlist={cl1.pk}&current_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&current_checkinlist={cl1.pk}&current_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&current_checkinlist={cl1.pk}&current_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