mirror of
https://github.com/pretix/pretix.git
synced 2026-08-05 09:57:49 +00:00
Merge pull request #1017 from pretix/deviceauth
Authentication scheme for devices
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
Authentication
|
||||
==============
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
tokenauth
|
||||
oauth
|
||||
deviceauth
|
||||
@@ -0,0 +1,137 @@
|
||||
.. _`rest-deviceauth`:
|
||||
|
||||
Device authentication
|
||||
=====================
|
||||
|
||||
Initializing a new device
|
||||
-------------------------
|
||||
|
||||
Users can create new devices in the "Device" section of their organizer settings. When creating
|
||||
a new device, users can specify a list of events the device is allowed to access. After a new
|
||||
device is created, users will be presented initialization instructions, consisting of an URL
|
||||
and an initialization token. They will also be shown as a QR code with the following contents::
|
||||
|
||||
{"handshake_version": 1, "url": "https://pretix.eu", "token": "kpp4jn8g2ynzonp6"}
|
||||
|
||||
Your application should be able to scan a QR code of this type, or allow to enter the URL and the
|
||||
initialization token manually. The handshake version is not used for manual initialization. When a
|
||||
QR code is scanned with a higher handshake version than you support, you should reject the request
|
||||
and prompt the user to update the client application.
|
||||
|
||||
After your application received the token, you need to call the initialization endpoint to obtain
|
||||
a proper API token. At this point, you need to identify the name and version of your application,
|
||||
as well as the type of underlying hardware. Example:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/v1/device/initialize HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"token": "kpp4jn8g2ynzonp6",
|
||||
"hardware_brand": "Samsung",
|
||||
"hardware_model": "Galaxy S",
|
||||
"software_brand": "pretixdroid",
|
||||
"software_version": "4.0.0"
|
||||
}
|
||||
|
||||
Every initialization token can only be used once. On success, you will receive a response containing
|
||||
information on your device as well as your API token:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"organizer": "foo",
|
||||
"device_id": 5,
|
||||
"unique_serial": "HHZ9LW9JWP390VFZ",
|
||||
"api_token": "1kcsh572fonm3hawalrncam4l1gktr2rzx25a22l8g9hx108o9oi0rztpcvwnfnd",
|
||||
"name": "Bar"
|
||||
}
|
||||
|
||||
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.
|
||||
|
||||
In case of an error, the response will look like this:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
HTTP/1.1 400 Bad Request
|
||||
Content-Type: application/json
|
||||
|
||||
{"token":["This initialization token has already been used."]}
|
||||
|
||||
|
||||
Performing API requests
|
||||
-----------------------
|
||||
|
||||
You need to include the API token with every request to pretix' API in the ``Authorization`` header
|
||||
like the following:
|
||||
|
||||
.. sourcecode:: http
|
||||
:emphasize-lines: 3
|
||||
|
||||
GET /api/v1/organizers/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Authorization: Device 1kcsh572fonm3hawalrncam4l1gktr2rzx25a22l8g9hx108o9oi0rztpcvwnfnd
|
||||
|
||||
Updating the software version
|
||||
-----------------------------
|
||||
|
||||
If your application is updated, we ask you to tell the server about the new version in use. You can do this at the
|
||||
following endpoint:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/v1/device/update HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Content-Type: application/json
|
||||
Authorization: Device 1kcsh572fonm3hawalrncam4l1gktr2rzx25a22l8g9hx108o9oi0rztpcvwnfnd
|
||||
|
||||
{
|
||||
"hardware_brand": "Samsung",
|
||||
"hardware_model": "Galaxy S",
|
||||
"software_brand": "pretixdroid",
|
||||
"software_version": "4.1.0"
|
||||
}
|
||||
|
||||
Creating a new API key
|
||||
----------------------
|
||||
|
||||
If you think your API key might have leaked or just want to be extra cautious, the API allows you to create a new key.
|
||||
The old API key will be invalid immediately. A request for a new key looks like this:
|
||||
|
||||
.. sourcecode:: http
|
||||
|
||||
POST /api/v1/device/roll HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Authorization: Device 1kcsh572fonm3hawalrncam4l1gktr2rzx25a22l8g9hx108o9oi0rztpcvwnfnd
|
||||
|
||||
The response will look like the response to the initialization request.
|
||||
|
||||
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.
|
||||
|
||||
Permissions
|
||||
-----------
|
||||
|
||||
Device authentication is currently hardcoded to grant the following permissions:
|
||||
|
||||
* View event meta data and products etc.
|
||||
* View and change orders
|
||||
|
||||
Devices cannot change events or products and cannot access vouchers.
|
||||
+14
-38
@@ -9,44 +9,20 @@ with pretix' REST API, such as authentication, pagination and similar definition
|
||||
Authentication
|
||||
--------------
|
||||
|
||||
If you're building an application for end users, we strongly recommend that you use our
|
||||
:ref:`OAuth-based authentication progress <rest-oauth>`. However, for simpler needs, you
|
||||
can also go with static API tokens that you can create on a per-team basis (see below).
|
||||
To access the API, you need to present valid authentication credentials. pretix currently
|
||||
supports the following authorization schemes:
|
||||
|
||||
You need to include the API token with every request to pretix' API in the ``Authorization`` header
|
||||
like the following:
|
||||
|
||||
.. sourcecode:: http
|
||||
:emphasize-lines: 3
|
||||
|
||||
GET /api/v1/organizers/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Authorization: Token e1l6gq2ye72thbwkacj7jbri7a7tvxe614ojv8ybureain92ocub46t5gab5966k
|
||||
|
||||
.. note:: The API currently also supports authentication via browser sessions, i.e. the
|
||||
same way that you authenticate with pretix when using the browser interface.
|
||||
Using this type of authentication is *not* officially supported for use by
|
||||
third-party clients and might change or be removed at any time. We plan on
|
||||
adding OAuth2 support in the future for user-level authentication. If you want
|
||||
to use session authentication, be sure to comply with Django's `CSRF policies`_.
|
||||
|
||||
Obtaining an API token
|
||||
----------------------
|
||||
|
||||
To authenticate your API requests, you need to obtain an API token. You can create a
|
||||
token in the pretix web interface on the level of organizer teams. Create a new team
|
||||
or choose an existing team that has the level of permissions the token should have and
|
||||
create a new token using the form below the list of team members:
|
||||
|
||||
.. image:: img/token_form.png
|
||||
:class: screenshot
|
||||
|
||||
You can enter a description for the token to distinguish from other tokens later on.
|
||||
Once you click "Add", you will be provided with an API token in the success message.
|
||||
Copy this token, as you won't be able to retrieve it again.
|
||||
|
||||
.. image:: img/token_success.png
|
||||
:class: screenshot
|
||||
* :ref:`rest-tokenauth`: This is the simplest way and recommended for server-side applications
|
||||
that interact with pretix without user interaction.
|
||||
* :ref:`rest-oauth`: This is the recommended way to use if you write a third-party application
|
||||
that users can connect with their pretix account. It provides the best user experience, but
|
||||
requires user interaction and slightly more implementation effort.
|
||||
* :ref:`rest-deviceauth`: This is the recommended way if you build apps or hardware devices that can
|
||||
connect to pretix, e.g. for processing check-ins or to sell tickets offline. It provides a way
|
||||
to uniquely identify devices and allows for a quick configuration flow inside your software.
|
||||
* Authentication using browser sessions: This is used by the pretix web interface and it is *not*
|
||||
officially supported for use by third-party applications. It might change or be removed at any
|
||||
time without prior notice. If you use it, you need to comply with Django's `CSRF policies`_.
|
||||
|
||||
Permissions
|
||||
-----------
|
||||
@@ -204,4 +180,4 @@ as the string values ``true`` and ``false``.
|
||||
If the ``ordering`` parameter is documented for a resource, you can use it to sort the result set by one of the allowed
|
||||
fields. Prepend a ``-`` to the field name to reverse the sort order.
|
||||
|
||||
.. _CSRF policies: https://docs.djangoproject.com/en/1.11/ref/csrf/#ajax
|
||||
.. _CSRF policies: https://docs.djangoproject.com/en/1.11/ref/csrf/#ajax
|
||||
|
||||
+1
-1
@@ -14,5 +14,5 @@ in functionality over time.
|
||||
:maxdepth: 2
|
||||
|
||||
fundamentals
|
||||
oauth
|
||||
auth
|
||||
resources/index
|
||||
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
.. _`rest-oauth`:
|
||||
|
||||
OAuth support / "Connect with pretix"
|
||||
=====================================
|
||||
OAuth authentication / "Connect with pretix"
|
||||
============================================
|
||||
|
||||
In addition to static tokens, pretix supports `OAuth2`_-based authentication starting with
|
||||
pretix 1.16. This allows you to put a "Connect with pretix" button into your website or tool
|
||||
@@ -168,4 +168,4 @@ pretix user interface.
|
||||
|
||||
.. _OAuth2: https://en.wikipedia.org/wiki/OAuth
|
||||
.. _OAuth2 Simplified: https://aaronparecki.com/oauth-2-simplified/
|
||||
.. _HTTP Basic authentication: https://en.wikipedia.org/wiki/Basic_access_authentication
|
||||
.. _HTTP Basic authentication: https://en.wikipedia.org/wiki/Basic_access_authentication
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
.. _`rest-tokenauth`:
|
||||
|
||||
Token-based authentication
|
||||
==========================
|
||||
|
||||
Obtaining an API token
|
||||
----------------------
|
||||
|
||||
To authenticate your API requests with Tokens, you need to obtain a team-level API token.
|
||||
You can create a token in the pretix web interface on the level of organizer teams. Create
|
||||
a new team or choose an existing team that has the level of permissions the token should
|
||||
have and create a new token using the form below the list of team members:
|
||||
|
||||
.. image:: img/token_form.png
|
||||
:class: screenshot
|
||||
|
||||
You can enter a description for the token to distinguish from other tokens later on.
|
||||
Once you click "Add", you will be provided with an API token in the success message.
|
||||
Copy this token, as you won't be able to retrieve it again.
|
||||
|
||||
.. image:: img/token_success.png
|
||||
:class: screenshot
|
||||
|
||||
Using an API token
|
||||
------------------
|
||||
|
||||
You need to include the API token with every request to pretix' API in the ``Authorization`` header
|
||||
like the following:
|
||||
|
||||
.. sourcecode:: http
|
||||
:emphasize-lines: 3
|
||||
|
||||
GET /api/v1/organizers/ HTTP/1.1
|
||||
Host: pretix.eu
|
||||
Authorization: Token e1l6gq2ye72thbwkacj7jbri7a7tvxe614ojv8ybureain92ocub46t5gab5966k
|
||||
|
||||
@@ -23,6 +23,7 @@ cronjob
|
||||
cryptographic
|
||||
debian
|
||||
deduplication
|
||||
deprovision
|
||||
discoverable
|
||||
django
|
||||
dockerfile
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from rest_framework import exceptions
|
||||
from rest_framework.authentication import TokenAuthentication
|
||||
|
||||
from pretix.base.models import Device
|
||||
|
||||
|
||||
class DeviceTokenAuthentication(TokenAuthentication):
|
||||
model = Device
|
||||
keyword = 'Device'
|
||||
|
||||
def authenticate_credentials(self, key):
|
||||
model = self.get_model()
|
||||
try:
|
||||
device = model.objects.select_related('organizer').get(api_token=key)
|
||||
except model.DoesNotExist:
|
||||
raise exceptions.AuthenticationFailed('Invalid token.')
|
||||
|
||||
if not device.initialized:
|
||||
raise exceptions.AuthenticationFailed('Device has not been initialized.')
|
||||
|
||||
if not device.api_token:
|
||||
raise exceptions.AuthenticationFailed('Device access has been revoked.')
|
||||
|
||||
return AnonymousUser(), device
|
||||
@@ -1,7 +1,7 @@
|
||||
from rest_framework.permissions import SAFE_METHODS, BasePermission
|
||||
|
||||
from pretix.api.models import OAuthAccessToken
|
||||
from pretix.base.models import Event
|
||||
from pretix.base.models import Device, Event
|
||||
from pretix.base.models.organizer import Organizer, TeamAPIToken
|
||||
from pretix.helpers.security import (
|
||||
SessionInvalid, SessionReauthRequired, assert_session_valid,
|
||||
@@ -9,10 +9,9 @@ from pretix.helpers.security import (
|
||||
|
||||
|
||||
class EventPermission(BasePermission):
|
||||
model = TeamAPIToken
|
||||
|
||||
def has_permission(self, request, view):
|
||||
if not request.user.is_authenticated and not isinstance(request.auth, TeamAPIToken):
|
||||
if not request.user.is_authenticated and not isinstance(request.auth, (Device, TeamAPIToken)):
|
||||
return False
|
||||
|
||||
if request.method not in SAFE_METHODS and hasattr(view, 'write_permission'):
|
||||
@@ -31,7 +30,7 @@ class EventPermission(BasePermission):
|
||||
except SessionReauthRequired:
|
||||
return False
|
||||
|
||||
perm_holder = (request.auth if isinstance(request.auth, TeamAPIToken)
|
||||
perm_holder = (request.auth if isinstance(request.auth, (Device, TeamAPIToken))
|
||||
else request.user)
|
||||
if 'event' in request.resolver_match.kwargs and 'organizer' in request.resolver_match.kwargs:
|
||||
request.event = Event.objects.filter(
|
||||
|
||||
@@ -7,7 +7,8 @@ from rest_framework import routers
|
||||
from pretix.api.views import cart
|
||||
|
||||
from .views import (
|
||||
checkin, event, item, oauth, order, organizer, voucher, waitinglist,
|
||||
checkin, device, event, item, oauth, order, organizer, voucher,
|
||||
waitinglist,
|
||||
)
|
||||
|
||||
router = routers.DefaultRouter()
|
||||
@@ -66,4 +67,8 @@ urlpatterns = [
|
||||
url(r"^oauth/authorize$", oauth.AuthorizationView.as_view(), name="authorize"),
|
||||
url(r"^oauth/token$", oauth.TokenView.as_view(), name="token"),
|
||||
url(r"^oauth/revoke_token$", oauth.RevokeTokenView.as_view(), name="revoke-token"),
|
||||
url(r"^device/initialize$", device.InitializeView.as_view(), name="device.initialize"),
|
||||
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/revoke$", device.RevokeKeyView.as_view(), name="device.revoke"),
|
||||
]
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import logging
|
||||
|
||||
from django.utils.timezone import now
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from pretix.api.auth.device import DeviceTokenAuthentication
|
||||
from pretix.base.models import Device
|
||||
from pretix.base.models.devices import generate_api_token
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class InitializationRequestSerializer(serializers.Serializer):
|
||||
token = serializers.CharField(max_length=190)
|
||||
hardware_brand = serializers.CharField(max_length=190)
|
||||
hardware_model = serializers.CharField(max_length=190)
|
||||
software_brand = serializers.CharField(max_length=190)
|
||||
software_version = serializers.CharField(max_length=190)
|
||||
|
||||
|
||||
class UpdateRequestSerializer(serializers.Serializer):
|
||||
hardware_brand = serializers.CharField(max_length=190)
|
||||
hardware_model = serializers.CharField(max_length=190)
|
||||
software_brand = serializers.CharField(max_length=190)
|
||||
software_version = serializers.CharField(max_length=190)
|
||||
|
||||
|
||||
class DeviceSerializer(serializers.ModelSerializer):
|
||||
organizer = serializers.SlugRelatedField(slug_field='slug', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Device
|
||||
fields = [
|
||||
'organizer', 'device_id', 'unique_serial', 'api_token',
|
||||
'name'
|
||||
]
|
||||
|
||||
|
||||
class InitializeView(APIView):
|
||||
authentication_classes = tuple()
|
||||
permission_classes = tuple()
|
||||
|
||||
def post(self, request, format=None):
|
||||
serializer = InitializationRequestSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
try:
|
||||
device = Device.objects.get(initialization_token=serializer.validated_data.get('token'))
|
||||
except Device.DoesNotExist:
|
||||
raise ValidationError({'token': ['Unknown initialization token.']})
|
||||
|
||||
if device.initialized:
|
||||
raise ValidationError({'token': ['This initialization token has already been used.']})
|
||||
|
||||
device.initialized = now()
|
||||
device.hardware_brand = serializer.validated_data.get('hardware_brand')
|
||||
device.hardware_model = serializer.validated_data.get('hardware_model')
|
||||
device.software_brand = serializer.validated_data.get('software_brand')
|
||||
device.software_version = serializer.validated_data.get('software_version')
|
||||
device.api_token = generate_api_token()
|
||||
device.save()
|
||||
|
||||
device.log_action('pretix.device.initialized', data=serializer.validated_data, auth=device)
|
||||
|
||||
serializer = DeviceSerializer(device)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
class UpdateView(APIView):
|
||||
authentication_classes = (DeviceTokenAuthentication,)
|
||||
|
||||
def post(self, request, format=None):
|
||||
serializer = UpdateRequestSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
device = request.auth
|
||||
device.hardware_brand = serializer.validated_data.get('hardware_brand')
|
||||
device.hardware_model = serializer.validated_data.get('hardware_model')
|
||||
device.software_brand = serializer.validated_data.get('software_brand')
|
||||
device.software_version = serializer.validated_data.get('software_version')
|
||||
device.save()
|
||||
device.log_action('pretix.device.updated', data=serializer.validated_data, auth=device)
|
||||
|
||||
serializer = DeviceSerializer(device)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
class RollKeyView(APIView):
|
||||
authentication_classes = (DeviceTokenAuthentication,)
|
||||
|
||||
def post(self, request, format=None):
|
||||
device = request.auth
|
||||
device.api_token = generate_api_token()
|
||||
device.save()
|
||||
device.log_action('pretix.device.keyroll', auth=device)
|
||||
|
||||
serializer = DeviceSerializer(device)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
class RevokeKeyView(APIView):
|
||||
authentication_classes = (DeviceTokenAuthentication,)
|
||||
|
||||
def post(self, request, format=None):
|
||||
device = request.auth
|
||||
device.api_token = None
|
||||
device.save()
|
||||
device.log_action('pretix.device.revoked', auth=device)
|
||||
|
||||
serializer = DeviceSerializer(device)
|
||||
return Response(serializer.data)
|
||||
@@ -12,7 +12,9 @@ from pretix.api.serializers.event import (
|
||||
TaxRuleSerializer,
|
||||
)
|
||||
from pretix.api.views import ConditionalListView
|
||||
from pretix.base.models import Event, ItemCategory, TaxRule, TeamAPIToken
|
||||
from pretix.base.models import (
|
||||
Device, Event, ItemCategory, TaxRule, TeamAPIToken,
|
||||
)
|
||||
from pretix.base.models.event import SubEvent
|
||||
from pretix.helpers.dicts import merge_dicts
|
||||
|
||||
@@ -73,7 +75,7 @@ class EventViewSet(viewsets.ModelViewSet):
|
||||
filterset_class = EventFilter
|
||||
|
||||
def get_queryset(self):
|
||||
if isinstance(self.request.auth, TeamAPIToken):
|
||||
if isinstance(self.request.auth, (TeamAPIToken, Device)):
|
||||
qs = self.request.auth.get_events_with_any_permission()
|
||||
elif self.request.user.is_authenticated:
|
||||
qs = self.request.user.get_events_with_any_permission(self.request).filter(
|
||||
@@ -224,7 +226,7 @@ class SubEventViewSet(ConditionalListView, viewsets.ReadOnlyModelViewSet):
|
||||
def get_queryset(self):
|
||||
if getattr(self.request, 'event', None):
|
||||
qs = self.request.event.subevents
|
||||
elif isinstance(self.request.auth, TeamAPIToken):
|
||||
elif isinstance(self.request.auth, (TeamAPIToken, Device)):
|
||||
qs = SubEvent.objects.filter(
|
||||
event__organizer=self.request.organizer,
|
||||
event__in=self.request.auth.get_events_with_any_permission()
|
||||
|
||||
@@ -42,7 +42,7 @@ class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
ordering_fields = ('id', 'position')
|
||||
ordering = ('position', 'id')
|
||||
filterset_class = ItemFilter
|
||||
permission = 'can_change_items'
|
||||
permission = None
|
||||
write_permission = 'can_change_items'
|
||||
|
||||
def get_queryset(self):
|
||||
@@ -92,7 +92,7 @@ class ItemVariationViewSet(viewsets.ModelViewSet):
|
||||
filter_backends = (DjangoFilterBackend, OrderingFilter,)
|
||||
ordering_fields = ('id', 'position')
|
||||
ordering = ('id',)
|
||||
permission = 'can_change_items'
|
||||
permission = None
|
||||
write_permission = 'can_change_items'
|
||||
|
||||
def get_queryset(self):
|
||||
@@ -154,7 +154,7 @@ class ItemAddOnViewSet(viewsets.ModelViewSet):
|
||||
filter_backends = (DjangoFilterBackend, OrderingFilter,)
|
||||
ordering_fields = ('id', 'position')
|
||||
ordering = ('id',)
|
||||
permission = 'can_change_items'
|
||||
permission = None
|
||||
write_permission = 'can_change_items'
|
||||
|
||||
def get_queryset(self):
|
||||
@@ -210,7 +210,7 @@ class ItemCategoryViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
filterset_class = ItemCategoryFilter
|
||||
ordering_fields = ('id', 'position')
|
||||
ordering = ('position', 'id')
|
||||
permission = 'can_change_items'
|
||||
permission = None
|
||||
write_permission = 'can_change_items'
|
||||
|
||||
def get_queryset(self):
|
||||
@@ -264,7 +264,8 @@ class QuestionViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
filterset_class = QuestionFilter
|
||||
ordering_fields = ('id', 'position')
|
||||
ordering = ('position', 'id')
|
||||
permission = 'can_change_items'
|
||||
permission = None
|
||||
write_permission = 'can_change_items'
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.event.questions.prefetch_related('options').all()
|
||||
@@ -307,7 +308,7 @@ class QuestionOptionViewSet(viewsets.ModelViewSet):
|
||||
filter_backends = (DjangoFilterBackend, OrderingFilter,)
|
||||
ordering_fields = ('id', 'position')
|
||||
ordering = ('position',)
|
||||
permission = 'can_change_items'
|
||||
permission = None
|
||||
write_permission = 'can_change_items'
|
||||
|
||||
def get_queryset(self):
|
||||
@@ -362,7 +363,7 @@ class QuotaViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
filterset_class = QuotaFilter
|
||||
ordering_fields = ('id', 'size')
|
||||
ordering = ('id',)
|
||||
permission = 'can_change_items'
|
||||
permission = None
|
||||
write_permission = 'can_change_items'
|
||||
|
||||
def get_queryset(self):
|
||||
|
||||
@@ -25,7 +25,7 @@ from pretix.api.serializers.order import (
|
||||
OrderRefundSerializer, OrderSerializer,
|
||||
)
|
||||
from pretix.base.models import (
|
||||
Invoice, Order, OrderPayment, OrderPosition, OrderRefund, Quota,
|
||||
Device, Invoice, Order, OrderPayment, OrderPosition, OrderRefund, Quota,
|
||||
TeamAPIToken,
|
||||
)
|
||||
from pretix.base.payment import PaymentException
|
||||
@@ -177,6 +177,7 @@ class OrderViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
|
||||
order,
|
||||
user=request.user if request.user.is_authenticated else None,
|
||||
api_token=request.auth if isinstance(request.auth, TeamAPIToken) else None,
|
||||
device=request.auth if isinstance(request.auth, Device) else None,
|
||||
oauth_application=request.auth.application if isinstance(request.auth, OAuthAccessToken) else None,
|
||||
send_mail=send_mail
|
||||
)
|
||||
@@ -191,7 +192,7 @@ class OrderViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
|
||||
approve_order(
|
||||
order,
|
||||
user=request.user if request.user.is_authenticated else None,
|
||||
auth=request.auth if isinstance(request.auth, (TeamAPIToken, OAuthAccessToken)) else None,
|
||||
auth=request.auth if isinstance(request.auth, (Device, TeamAPIToken, OAuthAccessToken)) else None,
|
||||
send_mail=send_mail,
|
||||
)
|
||||
except Quota.QuotaExceededException as e:
|
||||
@@ -210,7 +211,7 @@ class OrderViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
|
||||
deny_order(
|
||||
order,
|
||||
user=request.user if request.user.is_authenticated else None,
|
||||
auth=request.auth if isinstance(request.auth, (TeamAPIToken, OAuthAccessToken)) else None,
|
||||
auth=request.auth if isinstance(request.auth, (Device, TeamAPIToken, OAuthAccessToken)) else None,
|
||||
send_mail=send_mail,
|
||||
comment=comment,
|
||||
)
|
||||
@@ -267,7 +268,7 @@ class OrderViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
|
||||
mark_order_refunded(
|
||||
order,
|
||||
user=request.user if request.user.is_authenticated else None,
|
||||
api_token=(request.auth if isinstance(request.auth, TeamAPIToken) else None),
|
||||
auth=(request.auth if isinstance(request.auth, (TeamAPIToken, OAuthAccessToken, Device)) else None),
|
||||
)
|
||||
return self.retrieve(request, [], **kwargs)
|
||||
|
||||
|
||||
@@ -23,5 +23,7 @@ class OrganizerViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
)
|
||||
else:
|
||||
return Organizer.objects.filter(pk__in=self.request.user.teams.values_list('organizer', flat=True))
|
||||
elif hasattr(self.request.auth, 'organizer_id'):
|
||||
return Organizer.objects.filter(pk=self.request.auth.organizer_id)
|
||||
else:
|
||||
return Organizer.objects.filter(pk=self.request.auth.team.organizer_id)
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
# Generated by Django 2.1 on 2018-09-12 10:35
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
import pretix.base.models.devices
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0098_auto_20180731_1243_squashed_0100_item_require_approval'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Device',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('device_id', models.PositiveIntegerField()),
|
||||
('unique_serial', models.CharField(default=pretix.base.models.devices.generate_serial, max_length=190, unique=True)),
|
||||
('initialization_token', models.CharField(default=pretix.base.models.devices.generate_initialization_token, max_length=190, unique=True)),
|
||||
('api_token', models.CharField(max_length=190, null=True, unique=True)),
|
||||
('all_events', models.BooleanField(default=False, verbose_name='All events (including newly created ones)')),
|
||||
('name', models.CharField(max_length=190, verbose_name='Name')),
|
||||
('created', models.DateTimeField(auto_now_add=True, verbose_name='Setup date')),
|
||||
('initialized', models.DateTimeField(null=True, verbose_name='Initialization date')),
|
||||
('hardware_brand', models.CharField(blank=True, max_length=190, null=True)),
|
||||
('hardware_model', models.CharField(blank=True, max_length=190, null=True)),
|
||||
('software_brand', models.CharField(blank=True, max_length=190, null=True)),
|
||||
('software_version', models.CharField(blank=True, max_length=190, null=True)),
|
||||
('limit_events', models.ManyToManyField(blank=True, to='pretixbase.Event', verbose_name='Limit to events')),
|
||||
('organizer', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='devices', to='pretixbase.Organizer')),
|
||||
],
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='device',
|
||||
unique_together={('organizer', 'device_id')},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='logentry',
|
||||
name='device',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='pretixbase.Device'),
|
||||
),
|
||||
]
|
||||
@@ -2,6 +2,7 @@ from ..settings import GlobalSettingsObject_SettingsStore
|
||||
from .auth import U2FDevice, User
|
||||
from .base import CachedFile, LoggedModel, cachedfile_name
|
||||
from .checkin import Checkin, CheckinList
|
||||
from .devices import Device
|
||||
from .event import (
|
||||
Event, Event_SettingsStore, EventLock, EventMetaProperty, EventMetaValue,
|
||||
RequiredAction, SubEvent, SubEventMetaValue, generate_invite_token,
|
||||
|
||||
@@ -47,6 +47,7 @@ class LoggingMixin:
|
||||
"""
|
||||
from .log import LogEntry
|
||||
from .event import Event
|
||||
from .devices import Device
|
||||
from pretix.api.models import OAuthAccessToken, OAuthApplication
|
||||
from .organizer import TeamAPIToken
|
||||
from ..notifications import get_all_notification_types
|
||||
@@ -67,6 +68,8 @@ class LoggingMixin:
|
||||
kwargs['oauth_application'] = auth
|
||||
elif isinstance(auth, TeamAPIToken):
|
||||
kwargs['api_token'] = auth
|
||||
elif isinstance(auth, Device):
|
||||
kwargs['device'] = auth
|
||||
elif isinstance(api_token, TeamAPIToken):
|
||||
kwargs['api_token'] = api_token
|
||||
|
||||
@@ -96,4 +99,4 @@ class LoggedModel(models.Model, LoggingMixin):
|
||||
|
||||
return LogEntry.objects.filter(
|
||||
content_type=ContentType.objects.get_for_model(type(self)), object_id=self.pk
|
||||
).select_related('user', 'event', 'oauth_application', 'api_token')
|
||||
).select_related('user', 'event', 'oauth_application', 'api_token', 'device')
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
import string
|
||||
|
||||
from django.db import models
|
||||
from django.db.models import Max
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from pretix.base.models import LoggedModel
|
||||
|
||||
|
||||
def generate_serial():
|
||||
serial = get_random_string(allowed_chars='ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', length=16)
|
||||
while Device.objects.filter(unique_serial=serial).exists():
|
||||
serial = get_random_string(allowed_chars='ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', length=16)
|
||||
return serial
|
||||
|
||||
|
||||
def generate_initialization_token():
|
||||
token = get_random_string(length=16, allowed_chars=string.ascii_lowercase + string.digits)
|
||||
while Device.objects.filter(initialization_token=token).exists():
|
||||
token = get_random_string(length=16, allowed_chars=string.ascii_lowercase + string.digits)
|
||||
return token
|
||||
|
||||
|
||||
def generate_api_token():
|
||||
token = get_random_string(length=64, allowed_chars=string.ascii_lowercase + string.digits)
|
||||
while Device.objects.filter(api_token=token).exists():
|
||||
token = get_random_string(length=64, allowed_chars=string.ascii_lowercase + string.digits)
|
||||
return token
|
||||
|
||||
|
||||
class Device(LoggedModel):
|
||||
organizer = models.ForeignKey(
|
||||
'pretixbase.Organizer',
|
||||
on_delete=models.PROTECT,
|
||||
related_name='devices'
|
||||
)
|
||||
device_id = models.PositiveIntegerField()
|
||||
unique_serial = models.CharField(max_length=190, default=generate_serial, unique=True)
|
||||
initialization_token = models.CharField(max_length=190, default=generate_initialization_token, unique=True)
|
||||
api_token = models.CharField(max_length=190, unique=True, null=True)
|
||||
all_events = models.BooleanField(default=False, verbose_name=_("All events (including newly created ones)"))
|
||||
limit_events = models.ManyToManyField('Event', verbose_name=_("Limit to events"), blank=True)
|
||||
name = models.CharField(
|
||||
max_length=190,
|
||||
verbose_name=_('Name')
|
||||
)
|
||||
created = models.DateTimeField(
|
||||
auto_now_add=True,
|
||||
verbose_name=_('Setup date')
|
||||
)
|
||||
initialized = models.DateTimeField(
|
||||
verbose_name=_('Initialization date'),
|
||||
null=True,
|
||||
)
|
||||
hardware_brand = models.CharField(
|
||||
max_length=190,
|
||||
null=True, blank=True
|
||||
)
|
||||
hardware_model = models.CharField(
|
||||
max_length=190,
|
||||
null=True, blank=True
|
||||
)
|
||||
software_brand = models.CharField(
|
||||
max_length=190,
|
||||
null=True, blank=True
|
||||
)
|
||||
software_version = models.CharField(
|
||||
max_length=190,
|
||||
null=True, blank=True
|
||||
)
|
||||
|
||||
class Meta:
|
||||
unique_together = (('organizer', 'device_id'),)
|
||||
|
||||
def __str__(self):
|
||||
return '#{} ({} {})'.format(
|
||||
self.device_id, self.hardware_brand, self.hardware_model
|
||||
)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.device_id:
|
||||
self.device_id = (self.organizer.devices.aggregate(m=Max('device_id'))['m'] or 0) + 1
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def permission_set(self) -> set:
|
||||
return {
|
||||
'can_view_orders',
|
||||
'can_change_orders',
|
||||
}
|
||||
|
||||
def get_event_permission_set(self, organizer, event) -> set:
|
||||
"""
|
||||
Gets a set of permissions (as strings) that a token holds for a particular event
|
||||
|
||||
:param organizer: The organizer of the event
|
||||
:param event: The event to check
|
||||
:return: set of permissions
|
||||
"""
|
||||
has_event_access = (self.all_events and organizer == self.organizer) or (
|
||||
event in self.limit_events.all()
|
||||
)
|
||||
return self.permission_set() if has_event_access else set()
|
||||
|
||||
def get_organizer_permission_set(self, organizer) -> set:
|
||||
"""
|
||||
Gets a set of permissions (as strings) that a token holds for a particular organizer
|
||||
|
||||
:param organizer: The organizer of the event
|
||||
:return: set of permissions
|
||||
"""
|
||||
return self.permission_set() if self.organizer == organizer else set()
|
||||
|
||||
def has_event_permission(self, organizer, event, perm_name=None, request=None) -> bool:
|
||||
"""
|
||||
Checks if this token is part of a team that grants access of type ``perm_name``
|
||||
to the event ``event``.
|
||||
|
||||
:param organizer: The organizer of the event
|
||||
:param event: The event to check
|
||||
:param perm_name: The permission, e.g. ``can_change_teams``
|
||||
:param request: This parameter is ignored and only defined for compatibility reasons.
|
||||
:return: bool
|
||||
"""
|
||||
has_event_access = (self.all_events and organizer == self.organizer) or (
|
||||
event in self.limit_events.all()
|
||||
)
|
||||
if isinstance(perm_name, (tuple, list)):
|
||||
return has_event_access and any(p in self.permission_set() for p in perm_name)
|
||||
return has_event_access and (not perm_name or perm_name in self.permission_set())
|
||||
|
||||
def has_organizer_permission(self, organizer, perm_name=None, request=None):
|
||||
"""
|
||||
Checks if this token is part of a team that grants access of type ``perm_name``
|
||||
to the organizer ``organizer``.
|
||||
|
||||
:param organizer: The organizer to check
|
||||
:param perm_name: The permission, e.g. ``can_change_teams``
|
||||
:param request: This parameter is ignored and only defined for compatibility reasons.
|
||||
:return: bool
|
||||
"""
|
||||
if isinstance(perm_name, (tuple, list)):
|
||||
return organizer == self.organizer and any(p in self.permission_set() for p in perm_name)
|
||||
return organizer == self.organizer and (not perm_name or perm_name in self.permission_set())
|
||||
|
||||
def get_events_with_any_permission(self):
|
||||
"""
|
||||
Returns a queryset of events the token has any permissions to.
|
||||
|
||||
:return: Iterable of Events
|
||||
"""
|
||||
if self.all_events:
|
||||
return self.organizer.events.all()
|
||||
else:
|
||||
return self.limit_events.all()
|
||||
@@ -276,6 +276,18 @@ class Event(EventMixin, LoggedModel):
|
||||
else:
|
||||
return super().presale_has_ended
|
||||
|
||||
def delete_all_orders(self, really=False):
|
||||
from .orders import OrderRefund, OrderPayment, OrderPosition, OrderFee
|
||||
|
||||
if not really:
|
||||
raise TypeError("Pass really=True as a parameter.")
|
||||
|
||||
OrderPosition.objects.all().delete(order__event=self)
|
||||
OrderFee.objects.all().delete(order__event=self)
|
||||
OrderPayment.objects.all().delete(order__event=self)
|
||||
OrderRefund.objects.all().delete(order__event=self)
|
||||
self.orders.all().delete()
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
obj = super().save(*args, **kwargs)
|
||||
self.cache.clear()
|
||||
|
||||
@@ -41,6 +41,7 @@ class LogEntry(models.Model):
|
||||
datetime = models.DateTimeField(auto_now_add=True, db_index=True)
|
||||
user = models.ForeignKey('User', null=True, blank=True, on_delete=models.PROTECT)
|
||||
api_token = models.ForeignKey('TeamAPIToken', null=True, blank=True, on_delete=models.PROTECT)
|
||||
device = models.ForeignKey('Device', null=True, blank=True, on_delete=models.PROTECT)
|
||||
oauth_application = models.ForeignKey('pretixapi.OAuthApplication', null=True, blank=True, on_delete=models.PROTECT)
|
||||
event = models.ForeignKey('Event', null=True, blank=True, on_delete=models.SET_NULL)
|
||||
action_type = models.CharField(max_length=255)
|
||||
|
||||
@@ -21,7 +21,7 @@ from pretix.base.i18n import (
|
||||
LazyCurrencyNumber, LazyDate, LazyLocaleException, LazyNumber, language,
|
||||
)
|
||||
from pretix.base.models import (
|
||||
CartPosition, Event, Item, ItemVariation, Order, OrderPayment,
|
||||
CartPosition, Device, Event, Item, ItemVariation, Order, OrderPayment,
|
||||
OrderPosition, Quota, User, Voucher,
|
||||
)
|
||||
from pretix.base.models.event import SubEvent
|
||||
@@ -307,7 +307,7 @@ def deny_order(order, comment='', user=None, send_mail: bool=True, auth=None):
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, oauth_application=None):
|
||||
def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device=None, oauth_application=None):
|
||||
"""
|
||||
Mark this order as canceled
|
||||
:param order: The order to change
|
||||
@@ -319,6 +319,8 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, oauth_
|
||||
user = User.objects.get(pk=user)
|
||||
if isinstance(api_token, int):
|
||||
api_token = TeamAPIToken.objects.get(pk=api_token)
|
||||
if isinstance(device, int):
|
||||
device = Device.objects.get(pk=device)
|
||||
if isinstance(oauth_application, int):
|
||||
oauth_application = OAuthApplication.objects.get(pk=oauth_application)
|
||||
with order.event.lock():
|
||||
@@ -327,7 +329,7 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, oauth_
|
||||
order.status = Order.STATUS_CANCELED
|
||||
order.save()
|
||||
|
||||
order.log_action('pretix.event.order.canceled', user=user, auth=api_token or oauth_application)
|
||||
order.log_action('pretix.event.order.canceled', user=user, auth=api_token or oauth_application or device)
|
||||
i = order.invoices.filter(is_cancellation=False).last()
|
||||
if i:
|
||||
generate_cancellation(i)
|
||||
@@ -1299,10 +1301,11 @@ def perform_order(self, event: str, payment_provider: str, positions: List[str],
|
||||
|
||||
|
||||
@app.task(base=ProfiledTask, bind=True, max_retries=5, default_retry_delay=1, throws=(OrderError,))
|
||||
def cancel_order(self, order: int, user: int=None, send_mail: bool=True, api_token=None, oauth_application=None):
|
||||
def cancel_order(self, order: int, user: int=None, send_mail: bool=True, api_token=None, oauth_application=None,
|
||||
device=None):
|
||||
try:
|
||||
try:
|
||||
return _cancel_order(order, user, send_mail, api_token, oauth_application)
|
||||
return _cancel_order(order, user, send_mail, api_token, device, oauth_application)
|
||||
except LockTimeoutException:
|
||||
self.retry()
|
||||
except (MaxRetriesExceededError, LockTimeoutException):
|
||||
|
||||
@@ -6,7 +6,7 @@ from django.utils.translation import ugettext_lazy as _
|
||||
from i18nfield.forms import I18nFormField, I18nTextarea
|
||||
|
||||
from pretix.base.forms import I18nModelForm, SettingsForm
|
||||
from pretix.base.models import Organizer, Team
|
||||
from pretix.base.models import Device, Organizer, Team
|
||||
from pretix.control.forms import ExtFileField, MultipleLanguagesWidget
|
||||
from pretix.multidomain.models import KnownDomain
|
||||
from pretix.presale.style import get_fonts
|
||||
@@ -107,7 +107,7 @@ class TeamForm(forms.ModelForm):
|
||||
data = super().clean()
|
||||
if self.instance.pk and not data['can_change_teams']:
|
||||
if not self.instance.organizer.teams.exclude(pk=self.instance.pk).filter(
|
||||
can_change_teams=True, members__isnull=False
|
||||
can_change_teams=True, members__isnull=False
|
||||
).exists():
|
||||
raise ValidationError(_('The changes could not be saved because there would be no remaining team with '
|
||||
'the permission to change teams and permissions.'))
|
||||
@@ -115,6 +115,23 @@ class TeamForm(forms.ModelForm):
|
||||
return data
|
||||
|
||||
|
||||
class DeviceForm(forms.ModelForm):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
organizer = kwargs.pop('organizer')
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['limit_events'].queryset = organizer.events.all()
|
||||
|
||||
class Meta:
|
||||
model = Device
|
||||
fields = ['name', 'all_events', 'limit_events']
|
||||
widgets = {
|
||||
'limit_events': forms.CheckboxSelectMultiple(attrs={
|
||||
'data-inverse-dependency': '#id_all_events'
|
||||
}),
|
||||
}
|
||||
|
||||
|
||||
class OrganizerSettingsForm(SettingsForm):
|
||||
|
||||
organizer_info_text = I18nFormField(
|
||||
|
||||
@@ -274,6 +274,12 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
|
||||
'pretix.subevent.quota.added': pgettext_lazy('subevent', 'A quota has been added to the event date.'),
|
||||
'pretix.subevent.quota.changed': pgettext_lazy('subevent', 'A quota has been changed on the event date.'),
|
||||
'pretix.subevent.quota.deleted': pgettext_lazy('subevent', 'A quota has been removed from the event date.'),
|
||||
'pretix.device.created': _('The device has been created.'),
|
||||
'pretix.device.changed': _('The device has been changed.'),
|
||||
'pretix.device.revoked': _('Access of the device has been revoked.'),
|
||||
'pretix.device.initialized': _('The device has been initialized.'),
|
||||
'pretix.device.keyroll': _('The access token of the device has been regenerated.'),
|
||||
'pretix.device.updated': _('The device has notified the server of an hardware or software update.'),
|
||||
}
|
||||
|
||||
data = json.loads(logentry.data)
|
||||
|
||||
@@ -167,6 +167,9 @@
|
||||
<br><span class="fa fa-plug fa-fw"></span>
|
||||
{{ log.oauth_application.name }}
|
||||
{% endif %}
|
||||
{% elif log.device %}
|
||||
<span class="fa fa-mobile fa-fw"></span>
|
||||
{{ log.device.name }}
|
||||
{% elif log.api_token %}
|
||||
<span class="fa fa-key fa-fw"></span>
|
||||
{{ log.api_token.name }}
|
||||
|
||||
@@ -54,6 +54,9 @@
|
||||
<br><span class="fa fa-plug fa-fw"></span>
|
||||
{{ log.oauth_application.name }}
|
||||
{% endif %}
|
||||
{% elif log.device %}
|
||||
<span class="fa fa-mobile fa-fw"></span>
|
||||
{{ log.device.name }}
|
||||
{% elif log.api_token %}
|
||||
<span class="fa fa-key fa-fw"></span>
|
||||
{{ log.api_token.name }}
|
||||
|
||||
@@ -19,6 +19,9 @@
|
||||
<span class="fa fa-plug fa-fw"></span>
|
||||
{{ log.oauth_application.name }}
|
||||
{% endif %}
|
||||
{% elif log.device %}
|
||||
<span class="fa fa-mobile fa-fw"></span>
|
||||
{{ log.device.name }}
|
||||
{% elif log.api_token %}
|
||||
<span class="fa fa-key fa-fw"></span>
|
||||
{{ log.api_token.name }}
|
||||
|
||||
@@ -33,6 +33,13 @@
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if 'can_change_organizer_settings' in request.orgapermset %}
|
||||
<li {% if "organizer.device" in url_name %}class="active"{% endif %}>
|
||||
<a href="{% url "control:organizer.devices" organizer=organizer.slug %}">
|
||||
{% trans "Devices" %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% for nav in nav_organizer %}
|
||||
<li {% if nav.active %}class="active"{% endif %}>
|
||||
<a href="{{ nav.url }}">
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
{% extends "pretixcontrol/organizers/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load staticfiles %}
|
||||
{% load bootstrap3 %}
|
||||
{% block inner %}
|
||||
<legend>{% trans "Connect to device:" %} {{ device.name }}</legend>
|
||||
|
||||
<div>
|
||||
<ol>
|
||||
<li>{% trans "Open the app that you want to connect and optionally reset it to the original state." %}</li>
|
||||
<li>{% trans "Scan the following configuration code:" %}<br><br>
|
||||
<script type="text/json" data-replace-with-qr>{{ qrdata|safe }}</script><br>
|
||||
{% trans "If your app/device does not support scanning a QR code, you can also enter the following information:" %}
|
||||
<br>
|
||||
<strong>{% trans "System URL:" %}</strong> {{ settings.SITE_URL }}<br>
|
||||
<strong>{% trans "Token:" %}</strong> {{ device.initialization_token }}
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="alert alert-warning">
|
||||
<strong>
|
||||
{% blocktrans trimmed %}
|
||||
Please note that this is a new feature that currently only works for beta-stage software, such as
|
||||
pretixPOS. pretixdroid 1.x and pretixdesk 0.x are not supported by this feature. Future versions of
|
||||
pretixdroid and pretixdesk will be supported through this menu.
|
||||
{% endblocktrans %}
|
||||
</strong>
|
||||
<br><br>
|
||||
{% blocktrans trimmed %}
|
||||
To set up pretixdroid or pretixdesk, please go to the <strong>Check-in devices</strong> section of an event.
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
<a href="{% url "control:organizer.devices" organizer=request.organizer.slug %}"
|
||||
class="btn btn-default"><i class="fa fa-arrow-left"></i>
|
||||
{% trans "Device overview" %}
|
||||
</a>
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/devices.js" %}"></script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,41 @@
|
||||
{% extends "pretixcontrol/organizers/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block inner %}
|
||||
{% if device %}
|
||||
<legend>{% trans "Device:" %} {{ device.name }}</legend>
|
||||
{% else %}
|
||||
<legend>{% trans "Connect a new device" %}</legend>
|
||||
{% endif %}
|
||||
<form class="form-horizontal" action="" method="post">
|
||||
{% if device %}
|
||||
<div class="row">
|
||||
<div class="col-xs-12 col-lg-10">
|
||||
{% endif %}
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form_errors form %}
|
||||
{% bootstrap_field form.name layout="control" %}
|
||||
{% bootstrap_field form.all_events layout="control" %}
|
||||
{% bootstrap_field form.limit_events layout="control" %}
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{% if device %}
|
||||
</div>
|
||||
<div class="col-xs-12 col-lg-2">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
{% trans "Device history" %}
|
||||
</h3>
|
||||
</div>
|
||||
{% include "pretixcontrol/includes/logs.html" with obj=device %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,21 @@
|
||||
{% extends "pretixcontrol/organizers/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block inner %}
|
||||
<h2>{% trans "Revoke device access:" %} {{ device.name }}</h2>
|
||||
<form action="" method="post" class="form-horizontal">
|
||||
{% csrf_token %}
|
||||
<p>
|
||||
<strong>{% blocktrans %}Are you sure you want remove access for this device?{% endblocktrans %}</strong>
|
||||
{% trans "All data of this device will stay available, but you can't use the device any more." %}
|
||||
</p>
|
||||
<div class="form-group submit-group">
|
||||
<a href="{% url "control:organizer.devices" organizer=request.organizer.slug%}" class="btn btn-default btn-cancel">
|
||||
{% trans "Cancel" %}
|
||||
</a>
|
||||
<button type="submit" class="btn btn-danger btn-save">
|
||||
{% trans "Revoke" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,116 @@
|
||||
{% extends "pretixcontrol/organizers/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block inner %}
|
||||
<legend>
|
||||
{% trans "Connected devices" %}
|
||||
</legend>
|
||||
<div class="alert alert-info">
|
||||
{% blocktrans trimmed %}
|
||||
This menu allows you to connect hardware devices such as box office terminals or scanning terminals to
|
||||
your account.
|
||||
{% endblocktrans %}
|
||||
<strong>
|
||||
{% blocktrans trimmed %}
|
||||
Please note that this is a new feature that currently only works for beta-stage software, such as
|
||||
pretixPOS. pretixdroid 1.x and pretixdesk 0.x are not supported by this feature. Future versions of
|
||||
pretixdroid and pretixdesk will be supported through this menu.
|
||||
{% endblocktrans %}
|
||||
</strong>
|
||||
<br><br>
|
||||
{% blocktrans trimmed %}
|
||||
To set up pretixdroid or pretixdesk, please go to the <strong>Check-in devices</strong> section of an event.
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
{% if devices|length == 0 %}
|
||||
<div class="empty-collection">
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
You haven't connected any hardware devices yet.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
|
||||
<a href="{% url "control:organizer.device.add" organizer=request.organizer.slug %}"
|
||||
class="btn btn-primary btn-lg"><i class="fa fa-plus"></i> {% trans "Connect a device" %}</a>
|
||||
</div>
|
||||
{% else %}
|
||||
<p>
|
||||
<a href="{% url "control:organizer.device.add" organizer=request.organizer.slug %}"
|
||||
class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Connect a device" %}</a>
|
||||
</p>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-condensed table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Device ID" %}</th>
|
||||
<th>{% trans "Name" %}</th>
|
||||
<th>{% trans "Hardware model" %}</th>
|
||||
<th>{% trans "Software" %}</th>
|
||||
<th>{% trans "Setup date" %}</th>
|
||||
<th>{% trans "Events" %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for d in devices %}
|
||||
<tr>
|
||||
<td>
|
||||
{{ d.device_id }}
|
||||
</td>
|
||||
<td>
|
||||
{% if d.initialized and not d.api_token %}<del>{% endif %}
|
||||
{{ d.name }}
|
||||
{% if d.initialized and not d.api_token %}</del>{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{{ d.hardware_brand|default_if_none:"" }} {{ d.hardware_model|default_if_none:"" }}
|
||||
</td>
|
||||
<td>
|
||||
{{ d.software_brand|default_if_none:"" }} {{ d.software_version|default_if_none:"" }}
|
||||
</td>
|
||||
<td>
|
||||
{% if d.initialized %}
|
||||
{{ d.initialized|date:"SHORT_DATETIME_FORMAT" }}
|
||||
{% else %}
|
||||
<em>{% trans "Not yet initialized" %}</em>
|
||||
{% endif %}
|
||||
{% if d.initialized and not d.api_token %}
|
||||
<span class="label label-danger">{% trans "Revoked" %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if d.all_events %}
|
||||
{% trans "All" %}
|
||||
{% else %}
|
||||
<ul>
|
||||
{% for e in d.limit_events.all %}
|
||||
<li>
|
||||
<a href="{% url "control:event.index" organizer=request.organizer.slug event=e.slug %}">
|
||||
{{ e }}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
{% if not d.initialized %}
|
||||
<a href="{% url "control:organizer.device.connect" organizer=request.organizer.slug device=d.id %}"
|
||||
class="btn btn-primary btn-sm"><i class="fa fa-link"></i>
|
||||
{% trans "Connect" %}</a>
|
||||
{% elif d.api_token %}
|
||||
<a href="{% url "control:organizer.device.revoke" organizer=request.organizer.slug device=d.id %}"
|
||||
class="btn btn-default btn-sm">
|
||||
{% trans "Revoke access" %}</a>
|
||||
{% endif %}
|
||||
<a href="{% url "control:organizer.device.edit" organizer=request.organizer.slug device=d.id %}"
|
||||
class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% include "pretixcontrol/pagination.html" %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -68,6 +68,15 @@ urlpatterns = [
|
||||
url(r'^organizer/(?P<organizer>[^/]+)/edit$', organizer.OrganizerUpdate.as_view(), name='organizer.edit'),
|
||||
url(r'^organizer/(?P<organizer>[^/]+)/settings/display$', organizer.OrganizerDisplaySettings.as_view(),
|
||||
name='organizer.display'),
|
||||
url(r'^organizer/(?P<organizer>[^/]+)/devices$', organizer.DeviceListView.as_view(), name='organizer.devices'),
|
||||
url(r'^organizer/(?P<organizer>[^/]+)/device/add$', organizer.DeviceCreateView.as_view(),
|
||||
name='organizer.device.add'),
|
||||
url(r'^organizer/(?P<organizer>[^/]+)/device/(?P<device>[^/]+)/edit$', organizer.DeviceUpdateView.as_view(),
|
||||
name='organizer.device.edit'),
|
||||
url(r'^organizer/(?P<organizer>[^/]+)/device/(?P<device>[^/]+)/connect$', organizer.DeviceConnectView.as_view(),
|
||||
name='organizer.device.connect'),
|
||||
url(r'^organizer/(?P<organizer>[^/]+)/device/(?P<device>[^/]+)/revoke$', organizer.DeviceRevokeView.as_view(),
|
||||
name='organizer.device.revoke'),
|
||||
url(r'^organizer/(?P<organizer>[^/]+)/teams$', organizer.TeamListView.as_view(), name='organizer.teams'),
|
||||
url(r'^organizer/(?P<organizer>[^/]+)/team/add$', organizer.TeamCreateView.as_view(), name='organizer.team.add'),
|
||||
url(r'^organizer/(?P<organizer>[^/]+)/team/(?P<team>[^/]+)/$', organizer.TeamMemberView.as_view(),
|
||||
|
||||
@@ -254,7 +254,8 @@ def event_index(request, organizer, event):
|
||||
|
||||
can_change_orders = request.user.has_event_permission(request.organizer, request.event, 'can_change_orders',
|
||||
request=request)
|
||||
qs = request.event.logentry_set.all().select_related('user', 'content_type', 'api_token', 'oauth_application').order_by('-datetime')
|
||||
qs = request.event.logentry_set.all().select_related('user', 'content_type', 'api_token', 'oauth_application',
|
||||
'device').order_by('-datetime')
|
||||
qs = qs.exclude(action_type__in=OVERVIEW_BLACKLIST)
|
||||
if not request.user.has_event_permission(request.organizer, request.event, 'can_view_orders', request=request):
|
||||
qs = qs.exclude(content_type=ContentType.objects.get_for_model(Order))
|
||||
|
||||
@@ -875,7 +875,7 @@ class EventLog(EventPermissionRequiredMixin, ListView):
|
||||
|
||||
def get_queryset(self):
|
||||
qs = self.request.event.logentry_set.all().select_related(
|
||||
'user', 'content_type', 'api_token', 'oauth_application'
|
||||
'user', 'content_type', 'api_token', 'oauth_application', 'device'
|
||||
).order_by('-datetime')
|
||||
qs = qs.exclude(action_type__in=OVERVIEW_BLACKLIST)
|
||||
if not self.request.user.has_event_permission(self.request.organizer, self.request.event, 'can_view_orders',
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import json
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.core.files import File
|
||||
from django.db import transaction
|
||||
from django.db.models import Count
|
||||
from django.forms import inlineformset_factory
|
||||
from django.http import JsonResponse
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.urls import reverse
|
||||
from django.utils.functional import cached_property
|
||||
@@ -13,14 +17,14 @@ from django.views.generic import (
|
||||
CreateView, DeleteView, DetailView, FormView, ListView, UpdateView,
|
||||
)
|
||||
|
||||
from pretix.base.models import Organizer, Team, TeamInvite, User
|
||||
from pretix.base.models import Device, Organizer, Team, TeamInvite, User
|
||||
from pretix.base.models.event import EventMetaProperty
|
||||
from pretix.base.models.organizer import TeamAPIToken
|
||||
from pretix.base.services.mail import SendMailException, mail
|
||||
from pretix.control.forms.filter import OrganizerFilterForm
|
||||
from pretix.control.forms.organizer import (
|
||||
EventMetaPropertyForm, OrganizerDisplaySettingsForm, OrganizerForm,
|
||||
OrganizerSettingsForm, OrganizerUpdateForm, TeamForm,
|
||||
DeviceForm, EventMetaPropertyForm, OrganizerDisplaySettingsForm,
|
||||
OrganizerForm, OrganizerSettingsForm, OrganizerUpdateForm, TeamForm,
|
||||
)
|
||||
from pretix.control.permissions import OrganizerPermissionRequiredMixin
|
||||
from pretix.control.signals import nav_organizer
|
||||
@@ -576,3 +580,139 @@ class TeamMemberView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin,
|
||||
'organizer': self.request.organizer.slug,
|
||||
'team': self.object.pk
|
||||
})
|
||||
|
||||
|
||||
class DeviceListView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, ListView):
|
||||
model = Device
|
||||
template_name = 'pretixcontrol/organizers/devices.html'
|
||||
permission = 'can_change_organizer_settings'
|
||||
context_object_name = 'devices'
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.organizer.devices.prefetch_related('limit_events')
|
||||
|
||||
|
||||
class DeviceCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, CreateView):
|
||||
model = Device
|
||||
template_name = 'pretixcontrol/organizers/device_edit.html'
|
||||
permission = 'can_change_organizer_settings'
|
||||
form_class = DeviceForm
|
||||
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs['organizer'] = self.request.organizer
|
||||
return kwargs
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('control:organizer.device.connect', kwargs={
|
||||
'organizer': self.request.organizer.slug,
|
||||
'device': self.object.pk
|
||||
})
|
||||
|
||||
def form_valid(self, form):
|
||||
form.instance.organizer = self.request.organizer
|
||||
ret = super().form_valid(form)
|
||||
form.instance.log_action('pretix.device.created', user=self.request.user, data={
|
||||
k: getattr(self.object, k) if k != 'limit_events' else [e.id for e in getattr(self.object, k).all()]
|
||||
for k in form.changed_data
|
||||
})
|
||||
return ret
|
||||
|
||||
def form_invalid(self, form):
|
||||
messages.error(self.request, _('Your changes could not be saved.'))
|
||||
return super().form_invalid(form)
|
||||
|
||||
|
||||
class DeviceUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, UpdateView):
|
||||
model = Device
|
||||
template_name = 'pretixcontrol/organizers/device_edit.html'
|
||||
permission = 'can_change_organizer_settings'
|
||||
context_object_name = 'device'
|
||||
form_class = DeviceForm
|
||||
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs['organizer'] = self.request.organizer
|
||||
return kwargs
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
return get_object_or_404(Device, organizer=self.request.organizer, pk=self.kwargs.get('device'))
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('control:organizer.devices', kwargs={
|
||||
'organizer': self.request.organizer.slug,
|
||||
})
|
||||
|
||||
def form_valid(self, form):
|
||||
if form.has_changed():
|
||||
self.object.log_action('pretix.device.changed', user=self.request.user, data={
|
||||
k: getattr(self.object, k) if k != 'limit_events' else [e.id for e in getattr(self.object, k).all()]
|
||||
for k in form.changed_data
|
||||
})
|
||||
messages.success(self.request, _('Your changes have been saved.'))
|
||||
return super().form_valid(form)
|
||||
|
||||
def form_invalid(self, form):
|
||||
messages.error(self.request, _('Your changes could not be saved.'))
|
||||
return super().form_invalid(form)
|
||||
|
||||
|
||||
class DeviceConnectView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, DetailView):
|
||||
model = Device
|
||||
template_name = 'pretixcontrol/organizers/device_connect.html'
|
||||
permission = 'can_change_organizer_settings'
|
||||
context_object_name = 'device'
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
return get_object_or_404(Device, organizer=self.request.organizer, pk=self.kwargs.get('device'))
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
if 'ajax' in request.GET:
|
||||
return JsonResponse({
|
||||
'initialized': bool(self.object.initialized)
|
||||
})
|
||||
if self.object.initialized:
|
||||
messages.success(request, _('This device has been set up successfully.'))
|
||||
return redirect(reverse('control:organizer.devices', kwargs={
|
||||
'organizer': self.request.organizer.slug,
|
||||
}))
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['qrdata'] = json.dumps({
|
||||
'handshake_version': 1,
|
||||
'url': settings.SITE_URL,
|
||||
'token': self.object.initialization_token,
|
||||
})
|
||||
return ctx
|
||||
|
||||
|
||||
class DeviceRevokeView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin, DetailView):
|
||||
model = Device
|
||||
template_name = 'pretixcontrol/organizers/device_revoke.html'
|
||||
permission = 'can_change_organizer_settings'
|
||||
context_object_name = 'device'
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
return get_object_or_404(Device, organizer=self.request.organizer, pk=self.kwargs.get('device'))
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
if not self.object.api_token:
|
||||
messages.success(request, _('This device currently does not have access.'))
|
||||
return redirect(reverse('control:organizer.devices', kwargs={
|
||||
'organizer': self.request.organizer.slug,
|
||||
}))
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
self.object = self.get_object()
|
||||
self.object.api_token = None
|
||||
self.object.save()
|
||||
self.object.log_action('pretix.device.revoked', user=self.request.user)
|
||||
messages.success(request, _('Access for this device has been revoked.'))
|
||||
return redirect(reverse('control:organizer.devices', kwargs={
|
||||
'organizer': self.request.organizer.slug,
|
||||
}))
|
||||
|
||||
@@ -5,6 +5,7 @@ from rest_framework.exceptions import PermissionDenied
|
||||
from rest_framework.mixins import CreateModelMixin
|
||||
from rest_framework.response import Response
|
||||
|
||||
from pretix.base.models import Device
|
||||
from pretix.base.models.organizer import TeamAPIToken
|
||||
|
||||
from .models import BankImportJob, BankTransaction
|
||||
@@ -68,7 +69,7 @@ class BankImportJobViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
|
||||
return serializer.save()
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
perm_holder = (request.auth if isinstance(request.auth, TeamAPIToken) else request.user)
|
||||
perm_holder = (request.auth if isinstance(request.auth, (Device, TeamAPIToken)) else request.user)
|
||||
if not perm_holder.has_organizer_permission(request.organizer, 'can_change_orders'):
|
||||
raise PermissionDenied('Invalid set of permissions')
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
|
||||
@@ -278,6 +278,7 @@ REST_FRAMEWORK = {
|
||||
'PAGE_SIZE': 50,
|
||||
'DEFAULT_AUTHENTICATION_CLASSES': (
|
||||
'pretix.api.auth.token.TeamTokenAuthentication',
|
||||
'pretix.api.auth.device.DeviceTokenAuthentication',
|
||||
'rest_framework.authentication.SessionAuthentication',
|
||||
'oauth2_provider.contrib.rest_framework.OAuth2Authentication',
|
||||
),
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
/*globals $, Morris, gettext, RRule, RRuleSet*/
|
||||
|
||||
$(function () {
|
||||
var update = function () {
|
||||
$.getJSON(location.href + '?ajax=true', {}, function (data) {
|
||||
if (data.initialized) {
|
||||
location.reload();
|
||||
} else {
|
||||
window.setTimeout(update, 500);
|
||||
}
|
||||
});
|
||||
};
|
||||
window.setTimeout(update, 500);
|
||||
});
|
||||
@@ -366,6 +366,16 @@ var form_handlers = function (el) {
|
||||
el.find("input[name=basics-slug]").bind("keyup keydown change", function () {
|
||||
$(this).closest(".form-group").find(".slug-length").toggle($(this).val().length > 16);
|
||||
});
|
||||
|
||||
el.find("script[data-replace-with-qr]").each(function () {
|
||||
var $div = $("<div>");
|
||||
$div.insertBefore($(this));
|
||||
$div.qrcode(
|
||||
{
|
||||
text: $(this).html()
|
||||
}
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
$(function () {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
from datetime import datetime
|
||||
|
||||
import pytest
|
||||
from django.utils.timezone import now
|
||||
from pytz import UTC
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from pretix.base.models import Event, Organizer, Team, User
|
||||
from pretix.base.models import Device, Event, Organizer, Team, User
|
||||
from pretix.base.models.devices import generate_api_token
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -69,6 +71,17 @@ def team(organizer):
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def device(organizer):
|
||||
return Device.objects.create(
|
||||
organizer=organizer,
|
||||
all_events=True,
|
||||
name='Foo',
|
||||
initialized=now(),
|
||||
api_token=generate_api_token()
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def user():
|
||||
return User.objects.create_user('dummy@dummy.dummy', 'dummy')
|
||||
@@ -96,6 +109,12 @@ def token_client(client, team):
|
||||
return client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def device_client(client, device):
|
||||
client.credentials(HTTP_AUTHORIZATION='Device ' + device.api_token)
|
||||
return client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def subevent(event, meta_prop):
|
||||
event.has_subevents = True
|
||||
|
||||
@@ -52,3 +52,27 @@ def test_token_auth_inactive(client, team):
|
||||
client.credentials(HTTP_AUTHORIZATION='Token ' + t.token)
|
||||
resp = client.get('/api/v1/organizers/')
|
||||
assert resp.status_code == 401
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_device_invalid(client):
|
||||
client.credentials(HTTP_AUTHORIZATION='Device ABCDE')
|
||||
resp = client.get('/api/v1/organizers/')
|
||||
assert resp.status_code == 401
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_device_auth_valid(client, device):
|
||||
client.credentials(HTTP_AUTHORIZATION='Device ' + device.api_token)
|
||||
resp = client.get('/api/v1/organizers/')
|
||||
assert resp.status_code == 200
|
||||
assert len(resp.data['results']) == 1
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_device_auth_revoked(client, device):
|
||||
client.credentials(HTTP_AUTHORIZATION='Device ' + device.api_token)
|
||||
device.api_token = None
|
||||
device.save()
|
||||
resp = client.get('/api/v1/organizers/')
|
||||
assert resp.status_code == 401
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
import pytest
|
||||
|
||||
from pretix.base.models import Device
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def new_device(organizer):
|
||||
return Device.objects.create(
|
||||
name="Foo",
|
||||
all_events=True,
|
||||
organizer=organizer
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_initialize_required_fields(client, new_device: Device):
|
||||
resp = client.post('/api/v1/device/initialize')
|
||||
assert resp.status_code == 400
|
||||
assert resp.data == {
|
||||
'token': ['This field is required.'],
|
||||
'hardware_brand': ['This field is required.'],
|
||||
'hardware_model': ['This field is required.'],
|
||||
'software_brand': ['This field is required.'],
|
||||
'software_version': ['This field is required.'],
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_initialize_unknown_token(client, new_device: Device):
|
||||
resp = client.post('/api/v1/device/initialize', {
|
||||
'token': 'aaa',
|
||||
'hardware_brand': 'Samsung',
|
||||
'hardware_model': 'Galaxy S',
|
||||
'software_brand': 'pretixdroid',
|
||||
'software_version': '4.0.0'
|
||||
})
|
||||
assert resp.status_code == 400
|
||||
assert resp.data == {'token': ['Unknown initialization token.']}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_initialize_used_token(client, device: Device):
|
||||
resp = client.post('/api/v1/device/initialize', {
|
||||
'token': device.initialization_token,
|
||||
'hardware_brand': 'Samsung',
|
||||
'hardware_model': 'Galaxy S',
|
||||
'software_brand': 'pretixdroid',
|
||||
'software_version': '4.0.0'
|
||||
})
|
||||
assert resp.status_code == 400
|
||||
assert resp.data == {'token': ['This initialization token has already been used.']}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_initialize_valid_token(client, new_device: Device):
|
||||
resp = client.post('/api/v1/device/initialize', {
|
||||
'token': new_device.initialization_token,
|
||||
'hardware_brand': 'Samsung',
|
||||
'hardware_model': 'Galaxy S',
|
||||
'software_brand': 'pretixdroid',
|
||||
'software_version': '4.0.0'
|
||||
})
|
||||
assert resp.status_code == 200
|
||||
assert resp.data['organizer'] == 'dummy'
|
||||
assert resp.data['name'] == 'Foo'
|
||||
assert 'device_id' in resp.data
|
||||
assert 'unique_serial' in resp.data
|
||||
assert 'api_token' in resp.data
|
||||
new_device.refresh_from_db()
|
||||
assert new_device.api_token
|
||||
assert new_device.initialized
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_update_required_fields(device_client, device: Device):
|
||||
resp = device_client.post('/api/v1/device/update')
|
||||
assert resp.status_code == 400
|
||||
assert resp.data == {
|
||||
'hardware_brand': ['This field is required.'],
|
||||
'hardware_model': ['This field is required.'],
|
||||
'software_brand': ['This field is required.'],
|
||||
'software_version': ['This field is required.'],
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_update_required_auth(client, token_client, device: Device):
|
||||
resp = client.post('/api/v1/device/update', {
|
||||
'hardware_brand': 'Samsung',
|
||||
'hardware_model': 'Galaxy S',
|
||||
'software_brand': 'pretixdroid',
|
||||
'software_version': '5.0.0'
|
||||
})
|
||||
assert resp.status_code == 401
|
||||
resp = token_client.post('/api/v1/device/update', {
|
||||
'hardware_brand': 'Samsung',
|
||||
'hardware_model': 'Galaxy S',
|
||||
'software_brand': 'pretixdroid',
|
||||
'software_version': '5.0.0'
|
||||
})
|
||||
assert resp.status_code == 401
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_update_valid_fields(device_client, device: Device):
|
||||
resp = device_client.post('/api/v1/device/update', {
|
||||
'hardware_brand': 'Samsung',
|
||||
'hardware_model': 'Galaxy S',
|
||||
'software_brand': 'pretixdroid',
|
||||
'software_version': '5.0.0'
|
||||
})
|
||||
assert resp.status_code == 200
|
||||
device.refresh_from_db()
|
||||
assert device.software_version == '5.0.0'
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_keyroll_required_auth(client, token_client, device: Device):
|
||||
resp = client.post('/api/v1/device/roll', {})
|
||||
assert resp.status_code == 401
|
||||
resp = token_client.post('/api/v1/device/roll', {})
|
||||
assert resp.status_code == 401
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_keyroll_valid(device_client, device: Device):
|
||||
token = device.api_token
|
||||
resp = device_client.post('/api/v1/device/roll')
|
||||
assert resp.status_code == 200
|
||||
device.refresh_from_db()
|
||||
assert device.api_token
|
||||
assert device.api_token != token
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_revoke_required_auth(client, token_client, device: Device):
|
||||
resp = client.post('/api/v1/device/revoke', {})
|
||||
assert resp.status_code == 401
|
||||
resp = token_client.post('/api/v1/device/revoke', {})
|
||||
assert resp.status_code == 401
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_revoke_valid(device_client, device: Device):
|
||||
resp = device_client.post('/api/v1/device/revoke')
|
||||
assert resp.status_code == 200
|
||||
device.refresh_from_db()
|
||||
assert not device.api_token
|
||||
@@ -6,18 +6,18 @@ from django.test import override_settings
|
||||
from pretix.base.models import Organizer
|
||||
|
||||
event_urls = [
|
||||
'categories/',
|
||||
'invoices/',
|
||||
'items/',
|
||||
'orders/',
|
||||
'orderpositions/',
|
||||
'questions/',
|
||||
'quotas/',
|
||||
'vouchers/',
|
||||
'subevents/',
|
||||
'taxrules/',
|
||||
'waitinglistentries/',
|
||||
'checkinlists/',
|
||||
(None, 'categories/'),
|
||||
('can_view_orders', 'invoices/'),
|
||||
(None, 'items/'),
|
||||
('can_view_orders', 'orders/'),
|
||||
('can_view_orders', 'orderpositions/'),
|
||||
(None, 'questions/'),
|
||||
(None, 'quotas/'),
|
||||
('can_view_vouchers', 'vouchers/'),
|
||||
(None, 'subevents/'),
|
||||
(None, 'taxrules/'),
|
||||
('can_view_orders', 'waitinglistentries/'),
|
||||
('can_view_orders', 'checkinlists/'),
|
||||
]
|
||||
|
||||
event_permission_sub_urls = [
|
||||
@@ -36,38 +36,63 @@ event_permission_sub_urls = [
|
||||
('patch', 'can_change_orders', 'waitinglistentries/1/', 404),
|
||||
('put', 'can_change_orders', 'waitinglistentries/1/', 404),
|
||||
('post', 'can_change_orders', 'waitinglistentries/1/send_voucher/', 404),
|
||||
('get', 'can_change_items', 'categories/', 200),
|
||||
('get', 'can_change_items', 'items/', 200),
|
||||
('get', 'can_change_items', 'questions/', 200),
|
||||
('get', 'can_change_items', 'quotas/', 200),
|
||||
('get', None, 'categories/', 200),
|
||||
('get', None, 'items/', 200),
|
||||
('get', None, 'questions/', 200),
|
||||
('get', None, 'quotas/', 200),
|
||||
('post', 'can_change_items', 'items/', 400),
|
||||
('get', None, 'items/1/', 404),
|
||||
('put', 'can_change_items', 'items/1/', 404),
|
||||
('patch', 'can_change_items', 'items/1/', 404),
|
||||
('delete', 'can_change_items', 'items/1/', 404),
|
||||
('post', 'can_change_items', 'categories/', 400),
|
||||
('get', None, 'categories/1/', 404),
|
||||
('put', 'can_change_items', 'categories/1/', 404),
|
||||
('patch', 'can_change_items', 'categories/1/', 404),
|
||||
('delete', 'can_change_items', 'categories/1/', 404),
|
||||
('post', 'can_change_items', 'items/1/variations/', 404),
|
||||
('get', None, 'items/1/variations/', 404),
|
||||
('get', None, 'items/1/variations/1/', 404),
|
||||
('put', 'can_change_items', 'items/1/variations/1/', 404),
|
||||
('patch', 'can_change_items', 'items/1/variations/1/', 404),
|
||||
('delete', 'can_change_items', 'items/1/variations/1/', 404),
|
||||
('get', None, 'items/1/addons/', 404),
|
||||
('get', None, 'items/1/addons/1/', 404),
|
||||
('post', 'can_change_items', 'items/1/addons/', 404),
|
||||
('put', 'can_change_items', 'items/1/addons/1/', 404),
|
||||
('patch', 'can_change_items', 'items/1/addons/1/', 404),
|
||||
('delete', 'can_change_items', 'items/1/addons/1/', 404),
|
||||
('get', None, 'subevents/', 200),
|
||||
('get', None, 'subevents/1/', 404),
|
||||
('get', None, 'taxrules/', 200),
|
||||
('get', None, 'taxrules/1/', 404),
|
||||
('post', 'can_change_event_settings', 'taxrules/', 400),
|
||||
('put', 'can_change_event_settings', 'taxrules/1/', 404),
|
||||
('patch', 'can_change_event_settings', 'taxrules/1/', 404),
|
||||
('delete', 'can_change_event_settings', 'taxrules/1/', 404),
|
||||
('get', 'can_view_vouchers', 'vouchers/', 200),
|
||||
('get', 'can_view_vouchers', 'vouchers/1/', 404),
|
||||
('post', 'can_change_vouchers', 'vouchers/', 400),
|
||||
('put', 'can_change_vouchers', 'vouchers/1/', 404),
|
||||
('patch', 'can_change_vouchers', 'vouchers/1/', 404),
|
||||
('delete', 'can_change_vouchers', 'vouchers/1/', 404),
|
||||
('get', None, 'quotas/', 200),
|
||||
('get', None, 'quotas/1/', 404),
|
||||
('post', 'can_change_items', 'quotas/', 400),
|
||||
('put', 'can_change_items', 'quotas/1/', 404),
|
||||
('patch', 'can_change_items', 'quotas/1/', 404),
|
||||
('delete', 'can_change_items', 'quotas/1/', 404),
|
||||
('get', None, 'questions/', 200),
|
||||
('get', None, 'questions/1/', 404),
|
||||
('post', 'can_change_items', 'questions/', 400),
|
||||
('put', 'can_change_items', 'questions/1/', 404),
|
||||
('patch', 'can_change_items', 'questions/1/', 404),
|
||||
('delete', 'can_change_items', 'questions/1/', 404),
|
||||
('get', None, 'questions/1/options/', 404),
|
||||
('get', None, 'questions/1/options/1/', 404),
|
||||
('put', 'can_change_items', 'questions/1/options/1/', 404),
|
||||
('patch', 'can_change_items', 'questions/1/options/1/', 404),
|
||||
('delete', 'can_change_items', 'questions/1/options/1/', 404),
|
||||
('post', 'can_change_orders', 'orders/', 400),
|
||||
('post', 'can_change_orders', 'orders/ABC12/mark_paid/', 404),
|
||||
('post', 'can_change_orders', 'orders/ABC12/mark_pending/', 404),
|
||||
@@ -92,6 +117,10 @@ event_permission_sub_urls = [
|
||||
('patch', 'can_change_event_settings', 'checkinlists/1/', 404),
|
||||
('delete', 'can_change_event_settings', 'checkinlists/1/', 404),
|
||||
('post', 'can_create_events', 'clone/', 400),
|
||||
('get', 'can_view_orders', 'cartpositions/', 200),
|
||||
('get', 'can_view_orders', 'cartpositions/1/', 404),
|
||||
('post', 'can_change_orders', 'cartpositions/', 400),
|
||||
('delete', 'can_change_orders', 'cartpositions/1/', 404),
|
||||
]
|
||||
|
||||
|
||||
@@ -127,6 +156,13 @@ def test_organizer_not_allowed(token_client, organizer):
|
||||
assert resp.status_code == 403
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_organizer_not_allowed_device(device_client, organizer):
|
||||
o2 = Organizer.objects.create(slug='o2', name='Organizer 2')
|
||||
resp = device_client.get('/api/v1/organizers/{}/events/'.format(o2.slug))
|
||||
assert resp.status_code == 403
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_organizer_not_existing(token_client, organizer):
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/'.format('o2'))
|
||||
@@ -138,33 +174,65 @@ def test_organizer_not_existing(token_client, organizer):
|
||||
def test_event_allowed_all_events(token_client, team, organizer, event, url):
|
||||
team.all_events = True
|
||||
team.save()
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/{}'.format(organizer.slug, event.slug, url))
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/{}'.format(organizer.slug, event.slug, url[1]))
|
||||
assert resp.status_code == 200
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize("url", event_urls)
|
||||
def test_event_allowed_all_events_device(device_client, device, organizer, event, url):
|
||||
resp = device_client.get('/api/v1/organizers/{}/events/{}/{}'.format(organizer.slug, event.slug, url[1]))
|
||||
if url[0] is None or url[0] in device.permission_set():
|
||||
assert resp.status_code == 200
|
||||
else:
|
||||
assert resp.status_code == 403
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize("url", event_urls)
|
||||
def test_event_allowed_limit_events(token_client, organizer, team, event, url):
|
||||
team.all_events = False
|
||||
team.save()
|
||||
team.limit_events.add(event)
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/{}'.format(organizer.slug, event.slug, url))
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/{}'.format(organizer.slug, event.slug, url[1]))
|
||||
assert resp.status_code == 200
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize("url", event_urls)
|
||||
def test_event_allowed_limit_events_device(device_client, organizer, device, event, url):
|
||||
device.all_events = False
|
||||
device.save()
|
||||
device.limit_events.add(event)
|
||||
resp = device_client.get('/api/v1/organizers/{}/events/{}/{}'.format(organizer.slug, event.slug, url[1]))
|
||||
if url[0] is None or url[0] in device.permission_set():
|
||||
assert resp.status_code == 200
|
||||
else:
|
||||
assert resp.status_code == 403
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize("url", event_urls)
|
||||
def test_event_not_allowed(token_client, organizer, team, event, url):
|
||||
team.all_events = False
|
||||
team.save()
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/{}'.format(organizer.slug, event.slug, url))
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/{}'.format(organizer.slug, event.slug, url[1]))
|
||||
assert resp.status_code == 403
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize("url", event_urls)
|
||||
def test_event_not_allowed_device(device_client, organizer, device, event, url):
|
||||
device.all_events = False
|
||||
device.save()
|
||||
resp = device_client.get('/api/v1/organizers/{}/events/{}/{}'.format(organizer.slug, event.slug, url[1]))
|
||||
assert resp.status_code == 403
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize("url", event_urls)
|
||||
def test_event_not_existing(token_client, organizer, url, event):
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/{}'.format(organizer.slug, event.slug, url))
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/{}'.format(organizer.slug, event.slug, url[1]))
|
||||
assert resp.status_code == 403
|
||||
|
||||
|
||||
@@ -172,7 +240,8 @@ def test_event_not_existing(token_client, organizer, url, event):
|
||||
@pytest.mark.parametrize("urlset", event_permission_sub_urls)
|
||||
def test_token_event_subresources_permission_allowed(token_client, team, organizer, event, urlset):
|
||||
team.all_events = True
|
||||
setattr(team, urlset[1], True)
|
||||
if urlset[1]:
|
||||
setattr(team, urlset[1], True)
|
||||
team.save()
|
||||
resp = getattr(token_client, urlset[0])('/api/v1/organizers/{}/events/{}/{}'.format(
|
||||
organizer.slug, event.slug, urlset[2]))
|
||||
@@ -182,8 +251,11 @@ def test_token_event_subresources_permission_allowed(token_client, team, organiz
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize("urlset", event_permission_sub_urls)
|
||||
def test_token_event_subresources_permission_not_allowed(token_client, team, organizer, event, urlset):
|
||||
team.all_events = True
|
||||
setattr(team, urlset[1], False)
|
||||
if urlset[1] is None:
|
||||
team.all_events = False
|
||||
else:
|
||||
team.all_events = True
|
||||
setattr(team, urlset[1], False)
|
||||
team.save()
|
||||
resp = getattr(token_client, urlset[0])('/api/v1/organizers/{}/events/{}/{}'.format(
|
||||
organizer.slug, event.slug, urlset[2]))
|
||||
@@ -313,3 +385,17 @@ def test_update_session_activity(user_client, team, organizer, event):
|
||||
assert response.status_code == 200
|
||||
|
||||
assert user_client.session['pretix_auth_last_used'] > t1
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize("urlset", event_permission_sub_urls)
|
||||
def test_device_subresource_permission_check(device_client, device, organizer, event, urlset):
|
||||
resp = getattr(device_client, urlset[0])('/api/v1/organizers/{}/events/{}/{}'.format(
|
||||
organizer.slug, event.slug, urlset[2]))
|
||||
if urlset[1] is None or urlset[1] in device.permission_set():
|
||||
assert resp.status_code == urlset[3]
|
||||
else:
|
||||
if urlset[3] == 404:
|
||||
assert resp.status_code == 403
|
||||
else:
|
||||
assert resp.status_code in (404, 403)
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import pytest
|
||||
from django.utils.timezone import now
|
||||
|
||||
from pretix.base.models import Device, Event, Organizer, Team, User
|
||||
from pretix.base.models.devices import generate_api_token
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def organizer():
|
||||
return Organizer.objects.create(name='Dummy', slug='dummy')
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def event(organizer):
|
||||
event = Event.objects.create(
|
||||
organizer=organizer, name='Dummy', slug='dummy',
|
||||
date_from=now()
|
||||
)
|
||||
return event
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def device(organizer):
|
||||
return organizer.devices.create(name='Cashdesk')
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def admin_user(admin_team):
|
||||
u = User.objects.create_user('dummy@dummy.dummy', 'dummy')
|
||||
admin_team.members.add(u)
|
||||
return u
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def admin_team(organizer):
|
||||
return Team.objects.create(organizer=organizer, can_change_organizer_settings=True, name='Admin team')
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_list_of_devices(event, admin_user, client, device):
|
||||
client.login(email='dummy@dummy.dummy', password='dummy')
|
||||
resp = client.get('/control/organizer/dummy/devices')
|
||||
assert 'Cashdesk' in resp.rendered_content
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_create_device(event, admin_user, admin_team, client):
|
||||
client.login(email='dummy@dummy.dummy', password='dummy')
|
||||
resp = client.post('/control/organizer/dummy/device/add', {
|
||||
'name': 'Foo',
|
||||
'limit_events': str(event.pk),
|
||||
}, follow=True)
|
||||
d = Device.objects.last()
|
||||
assert d.name == 'Foo'
|
||||
assert not d.all_events
|
||||
assert list(d.limit_events.all()) == [event]
|
||||
assert d.initialization_token in resp.content.decode()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_update_device(event, admin_user, admin_team, device, client):
|
||||
client.login(email='dummy@dummy.dummy', password='dummy')
|
||||
client.post('/control/organizer/dummy/device/{}/edit'.format(device.pk), {
|
||||
'name': 'Cashdesk 2',
|
||||
'limit_events': str(event.pk),
|
||||
}, follow=True)
|
||||
device.refresh_from_db()
|
||||
assert device.name == 'Cashdesk 2'
|
||||
assert not device.all_events
|
||||
assert list(device.limit_events.all()) == [event]
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_revoke_device(event, admin_user, admin_team, device, client):
|
||||
client.login(email='dummy@dummy.dummy', password='dummy')
|
||||
device.api_token = generate_api_token()
|
||||
device.initialized = now()
|
||||
device.save()
|
||||
|
||||
client.get('/control/organizer/dummy/device/{}/revoke'.format(device.pk))
|
||||
client.post('/control/organizer/dummy/device/{}/revoke'.format(device.pk), {}, follow=True)
|
||||
device.refresh_from_db()
|
||||
assert device.api_token is None
|
||||
@@ -131,6 +131,11 @@ organizer_urls = [
|
||||
'organizer/abc/team/1/edit',
|
||||
'organizer/abc/team/1/delete',
|
||||
'organizer/abc/team/add',
|
||||
'organizer/abc/devices',
|
||||
'organizer/abc/device/add',
|
||||
'organizer/abc/device/1/edit',
|
||||
'organizer/abc/device/1/connect',
|
||||
'organizer/abc/device/1/revoke',
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -98,6 +98,8 @@ def logged_in_client(client, event):
|
||||
('/control/organizers/', 200),
|
||||
('/control/organizers/add', 200),
|
||||
('/control/organizer/{orga}/edit', 200),
|
||||
('/control/organizer/{orga}/teams', 200),
|
||||
('/control/organizer/{orga}/devices', 200),
|
||||
|
||||
('/control/events/', 200),
|
||||
('/control/events/add', 200),
|
||||
|
||||
Reference in New Issue
Block a user