diff --git a/doc/api/resources/devices.rst b/doc/api/resources/devices.rst new file mode 100644 index 0000000000..9b72fc56b9 --- /dev/null +++ b/doc/api/resources/devices.rst @@ -0,0 +1,220 @@ +.. spelling:: fullname + +.. _`rest-devices`: + +Devices +======= + +See also :ref:`rest-deviceauth`. + +Device resource +---------------- + +The device resource contains the following public fields: + +.. rst-class:: rest-resource-table + +===================================== ========================== ======================================================= +Field Type Description +===================================== ========================== ======================================================= +device_id integer Internal ID of the device within this organizer +unique_serial string Unique identifier of this device +name string Device name +all_events boolean Whether this device has access to all events +limit_events list List of event slugs this device has access to +hardware_brand string Device hardware manufacturer (read-only) +hardware_model string Device hardware model (read-only) +software_brand string Device software product (read-only) +software_version string Device software version (read-only) +created datetime Creation time +initialized datetime Time of initialization (or ``null``) +initialization_token string Token for initialization +revoked boolean Whether this device no longer has access +===================================== ========================== ======================================================= + +Device endpoints +---------------- + +.. http:get:: /api/v1/organizers/(organizer)/devices/ + + Returns a list of all devices within a given organizer. + + **Example request**: + + .. sourcecode:: http + + GET /api/v1/organizers/bigevents/devices/ HTTP/1.1 + Host: pretix.eu + Accept: application/json, text/javascript + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Vary: Accept + Content-Type: application/json + + { + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "device_id": 1, + "unique_serial": "UOS3GNZ27O39V3QS", + "initialization_token": "frkso3m2w58zuw70", + "all_events": false, + "limit_events": [ + "museum" + ], + "revoked": false, + "name": "Scanner", + "created": "2020-09-18T14:17:40.971519Z", + "initialized": "2020-09-18T14:17:44.190021Z", + "hardware_brand": "Zebra", + "hardware_model": "TC25", + "software_brand": "pretixSCAN", + "software_version": "1.5.1" + } + ] + } + + :query integer page: The page number in case of a multi-page result set, default is 1 + :param organizer: The ``slug`` field of the organizer to fetch + :statuscode 200: no error + :statuscode 401: Authentication failure + :statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource. + +.. http:get:: /api/v1/organizers/(organizer)/devices/(device_id)/ + + Returns information on one device, identified by its ID. + + **Example request**: + + .. sourcecode:: http + + GET /api/v1/organizers/bigevents/devices/1/ HTTP/1.1 + Host: pretix.eu + Accept: application/json, text/javascript + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Vary: Accept + Content-Type: application/json + + { + "device_id": 1, + "unique_serial": "UOS3GNZ27O39V3QS", + "initialization_token": "frkso3m2w58zuw70", + "all_events": false, + "limit_events": [ + "museum" + ], + "revoked": false, + "name": "Scanner", + "created": "2020-09-18T14:17:40.971519Z", + "initialized": "2020-09-18T14:17:44.190021Z", + "hardware_brand": "Zebra", + "hardware_model": "TC25", + "software_brand": "pretixSCAN", + "software_version": "1.5.1" + } + + :param organizer: The ``slug`` field of the organizer to fetch + :param device_id: The ``device_id`` field of the device to fetch + :statuscode 200: no error + :statuscode 401: Authentication failure + :statuscode 403: The requested organizer does not exist **or** you have no permission to view this resource. + +.. http:post:: /api/v1/organizers/(organizer)/devices/ + + Creates a new device + + **Example request**: + + .. sourcecode:: http + + POST /api/v1/organizers/bigevents/devices/ HTTP/1.1 + Host: pretix.eu + Accept: application/json, text/javascript + Content-Type: application/json + + { + "name": "Scanner", + "all_events": true, + "limit_events": [], + } + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 201 Created + Vary: Accept + Content-Type: application/json + + { + "device_id": 1, + "unique_serial": "UOS3GNZ27O39V3QS", + "initialization_token": "frkso3m2w58zuw70", + "all_events": true, + "limit_events": [], + "revoked": false, + "name": "Scanner", + "created": "2020-09-18T14:17:40.971519Z", + "initialized": null + "hardware_brand": null, + "hardware_model": null, + "software_brand": null, + "software_version": null + } + + :param organizer: The ``slug`` field of the organizer to create a device for + :statuscode 201: no error + :statuscode 400: The device could not be created due to invalid submitted data. + :statuscode 401: Authentication failure + :statuscode 403: The requested organizer does not exist **or** you have no permission to create this resource. + +.. http:patch:: /api/v1/organizers/(organizer)/devices/(device_id)/ + + Update a device. + + **Example request**: + + .. sourcecode:: http + + PATCH /api/v1/organizers/bigevents/devices/1/ HTTP/1.1 + Host: pretix.eu + Accept: application/json, text/javascript + Content-Type: application/json + Content-Length: 94 + + { + "name": "Foo" + } + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Vary: Accept + Content-Type: application/json + + { + "id": 1, + "name": "Foo", + ... + } + + :param organizer: The ``slug`` field of the organizer to modify + :param device_id: The ``device_id`` field of the deviec to modify + :statuscode 200: no error + :statuscode 400: The device could not be modified due to invalid submitted data + :statuscode 401: Authentication failure + :statuscode 403: The requested organizer does not exist **or** you have no permission to change this resource. + diff --git a/doc/api/resources/index.rst b/doc/api/resources/index.rst index 2fe0a481dd..6cba34aa07 100644 --- a/doc/api/resources/index.rst +++ b/doc/api/resources/index.rst @@ -24,6 +24,7 @@ Resources and endpoints giftcards carts teams + devices webhooks seatingplans billing_invoices diff --git a/src/pretix/api/serializers/organizer.py b/src/pretix/api/serializers/organizer.py index 5c02491e94..869b7863ed 100644 --- a/src/pretix/api/serializers/organizer.py +++ b/src/pretix/api/serializers/organizer.py @@ -9,7 +9,8 @@ from pretix.api.serializers.i18n import I18nAwareModelSerializer from pretix.api.serializers.order import CompatibleJSONField from pretix.base.auth import get_auth_backends from pretix.base.models import ( - GiftCard, Organizer, SeatingPlan, Team, TeamAPIToken, TeamInvite, User, + Device, GiftCard, Organizer, SeatingPlan, Team, TeamAPIToken, TeamInvite, + User, ) from pretix.base.models.seating import SeatingPlanLayoutValidator from pretix.base.services.mail import SendMailException, mail @@ -66,9 +67,6 @@ class EventSlugField(serializers.SlugRelatedField): class TeamSerializer(serializers.ModelSerializer): limit_events = EventSlugField(slug_field='slug', many=True) - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - class Meta: model = Team fields = ( @@ -86,6 +84,28 @@ class TeamSerializer(serializers.ModelSerializer): return data +class DeviceSerializer(serializers.ModelSerializer): + limit_events = EventSlugField(slug_field='slug', many=True) + device_id = serializers.IntegerField(read_only=True) + unique_serial = serializers.CharField(read_only=True) + hardware_brand = serializers.CharField(read_only=True) + hardware_model = serializers.CharField(read_only=True) + software_brand = serializers.CharField(read_only=True) + software_version = serializers.CharField(read_only=True) + created = serializers.DateTimeField(read_only=True) + revoked = serializers.BooleanField(read_only=True) + initialized = serializers.DateTimeField(read_only=True) + initialization_token = serializers.DateTimeField(read_only=True) + + class Meta: + model = Device + fields = ( + 'device_id', 'unique_serial', 'initialization_token', 'all_events', 'limit_events', + 'revoked', 'name', 'created', 'initialized', 'hardware_brand', 'hardware_model', + 'software_brand', 'software_version' + ) + + class TeamInviteSerializer(serializers.ModelSerializer): class Meta: model = TeamInvite diff --git a/src/pretix/api/urls.py b/src/pretix/api/urls.py index 930ed59855..a8a3d8b64f 100644 --- a/src/pretix/api/urls.py +++ b/src/pretix/api/urls.py @@ -21,6 +21,7 @@ orga_router.register(r'webhooks', webhooks.WebHookViewSet) orga_router.register(r'seatingplans', organizer.SeatingPlanViewSet) orga_router.register(r'giftcards', organizer.GiftCardViewSet) orga_router.register(r'teams', organizer.TeamViewSet) +orga_router.register(r'devices', organizer.DeviceViewSet) team_router = routers.DefaultRouter() team_router.register(r'members', organizer.TeamMemberViewSet) diff --git a/src/pretix/api/views/organizer.py b/src/pretix/api/views/organizer.py index 1c5d79df2b..532991d9ea 100644 --- a/src/pretix/api/views/organizer.py +++ b/src/pretix/api/views/organizer.py @@ -6,20 +6,22 @@ from django.shortcuts import get_object_or_404 from django.utils.functional import cached_property from django_filters.rest_framework import DjangoFilterBackend, FilterSet from django_scopes import scopes_disabled -from rest_framework import filters, serializers, status, viewsets +from rest_framework import filters, mixins, serializers, status, viewsets from rest_framework.decorators import action from rest_framework.exceptions import MethodNotAllowed, PermissionDenied from rest_framework.mixins import CreateModelMixin, DestroyModelMixin from rest_framework.response import Response +from rest_framework.viewsets import GenericViewSet from pretix.api.models import OAuthAccessToken from pretix.api.serializers.organizer import ( - GiftCardSerializer, OrganizerSerializer, SeatingPlanSerializer, - TeamAPITokenSerializer, TeamInviteSerializer, TeamMemberSerializer, - TeamSerializer, + DeviceSerializer, GiftCardSerializer, OrganizerSerializer, + SeatingPlanSerializer, TeamAPITokenSerializer, TeamInviteSerializer, + TeamMemberSerializer, TeamSerializer, ) from pretix.base.models import ( - GiftCard, Organizer, SeatingPlan, Team, TeamAPIToken, TeamInvite, User, + Device, GiftCard, Organizer, SeatingPlan, Team, TeamAPIToken, TeamInvite, + User, ) from pretix.helpers.dicts import merge_dicts @@ -353,3 +355,44 @@ class TeamAPITokenViewSet(CreateModelMixin, DestroyModelMixin, viewsets.ReadOnly serializer = self.get_serializer_class()(instance) headers = self.get_success_headers(serializer.data) return Response(serializer.data, status=status.HTTP_200_OK, headers=headers) + + +class DeviceViewSet(mixins.CreateModelMixin, + mixins.RetrieveModelMixin, + mixins.UpdateModelMixin, + mixins.ListModelMixin, + GenericViewSet): + serializer_class = DeviceSerializer + queryset = Device.objects.none() + permission = 'can_change_organizer_settings' + write_permission = 'can_change_organizer_settings' + lookup_field = 'device_id' + + def get_queryset(self): + return self.request.organizer.devices.order_by('pk') + + def get_serializer_context(self): + ctx = super().get_serializer_context() + ctx['organizer'] = self.request.organizer + return ctx + + @transaction.atomic() + def perform_create(self, serializer): + inst = serializer.save(organizer=self.request.organizer) + inst.log_action( + 'pretix.device.created', + user=self.request.user, + auth=self.request.auth, + data=merge_dicts(self.request.data, {'id': inst.pk}) + ) + + @transaction.atomic() + def perform_update(self, serializer): + inst = serializer.save() + inst.log_action( + 'pretix.device.changed', + user=self.request.user, + auth=self.request.auth, + data=self.request.data + ) + return inst diff --git a/src/tests/api/test_permissions.py b/src/tests/api/test_permissions.py index 3d6c2329c4..50a97da969 100644 --- a/src/tests/api/test_permissions.py +++ b/src/tests/api/test_permissions.py @@ -144,6 +144,11 @@ org_permission_sub_urls = [ ('get', 'can_manage_gift_cards', 'giftcards/1/', 404), ('put', 'can_manage_gift_cards', 'giftcards/1/', 404), ('patch', 'can_manage_gift_cards', 'giftcards/1/', 404), + ('get', 'can_change_organizer_settings', 'devices/', 200), + ('post', 'can_change_organizer_settings', 'devices/', 400), + ('get', 'can_change_organizer_settings', 'devices/1/', 404), + ('put', 'can_change_organizer_settings', 'devices/1/', 404), + ('patch', 'can_change_organizer_settings', 'devices/1/', 404), ('get', 'can_change_teams', 'teams/', 200), ('post', 'can_change_teams', 'teams/', 400), ('get', 'can_change_teams', 'teams/{team_id}/', 200),