From e26e180d56adbec868f5b3ed8c6ab711b9e33e98 Mon Sep 17 00:00:00 2001 From: David Hotham Date: Sat, 1 Jul 2023 23:16:55 +0100 Subject: [PATCH] more REST-y image upload --- hunt/apiviews.py | 40 ++++++++++++++++-------- hunt/third_party/__init__.py | 0 hunt/third_party/apimixin.py | 59 ------------------------------------ upload.py | 12 +++++--- 4 files changed, 36 insertions(+), 75 deletions(-) delete mode 100644 hunt/third_party/__init__.py delete mode 100644 hunt/third_party/apimixin.py diff --git a/hunt/apiviews.py b/hunt/apiviews.py index ad8b08f..228b14c 100644 --- a/hunt/apiviews.py +++ b/hunt/apiviews.py @@ -1,16 +1,16 @@ from __future__ import annotations +import json from typing import TYPE_CHECKING, Generic, TypeVar from uuid import uuid4 from rest_framework import serializers, status, viewsets from rest_framework.decorators import action -from rest_framework.parsers import FormParser, MultiPartParser +from rest_framework.parsers import MultiPartParser from rest_framework.response import Response from hunt.constants import HINTS_PER_LEVEL from hunt.models import Hint, Level -from hunt.third_party.apimixin import AllowPUTAsCreateMixin T = TypeVar("T", bound="Model") if TYPE_CHECKING: @@ -51,27 +51,43 @@ class Meta: ] -class LevelViewSet(AllowPUTAsCreateMixin, ModelViewSet[Level]): +class LevelViewSet(ModelViewSet[Level]): queryset = Level.objects.all().order_by("number") serializer_class = LevelSerializer - http_method_names = ["delete", "get", "head", "patch", "put"] # noqa: RUF012 + http_method_names = [ # noqa: RUF012 + "delete", + "get", + "head", + "patch", + "post", + "put", + ] @action( detail=True, - methods=["put"], - url_path="hints/(?P\\d+)", - parser_classes=[FormParser, MultiPartParser], + methods=["post"], + url_path="hint", + parser_classes=[MultiPartParser], ) - def save_hint(self, request: Request, pk: str, number: str) -> Response: - # Check that it's a sensible hint. - if int(number) >= HINTS_PER_LEVEL: + def save_hint(self, request: Request, pk: str) -> Response: + try: + data = request.data["data"] + details = json.loads(data) + number = details["number"] + except (KeyError, ValueError): + return Response( + "hint number not provided", status=status.HTTP_400_BAD_REQUEST + ) + + if number >= HINTS_PER_LEVEL: return Response( f"Hint {number} is too high", status=status.HTTP_400_BAD_REQUEST ) # Check that we have a file, and that it seems to be an image. - upload = next(iter(request.data.values()), None) - if upload is None: + try: + upload = request.data["file"] + except KeyError: return Response("no file attached", status=status.HTTP_400_BAD_REQUEST) extension = EXTENSIONS.get(upload.content_type) diff --git a/hunt/third_party/__init__.py b/hunt/third_party/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/hunt/third_party/apimixin.py b/hunt/third_party/apimixin.py deleted file mode 100644 index c291713..0000000 --- a/hunt/third_party/apimixin.py +++ /dev/null @@ -1,59 +0,0 @@ -from __future__ import annotations - -from typing import TYPE_CHECKING, Any, TypeAlias - -from django.http import Http404 -from rest_framework import status -from rest_framework.request import clone_request -from rest_framework.response import Response - -if TYPE_CHECKING: - from django.db.models import Model - from rest_framework.request import Request - from rest_framework.viewsets import GenericViewSet - - _Base: TypeAlias = GenericViewSet[Model] -else: - _Base: TypeAlias = object - - -# https://gist.github.com/tomchristie/a2ace4577eff2c603b1b -class AllowPUTAsCreateMixin(_Base): - """ - The following mixin class may be used in order to support PUT-as-create - behavior for incoming requests. - """ - - def update( - self, - request: Request, - *args: Any, # noqa: arg0001 - **kwargs: Any, - ) -> Response: - partial = kwargs.pop("partial", False) - instance = self.get_object_or_none() - serializer = self.get_serializer(instance, data=request.data, partial=partial) - serializer.is_valid(raise_exception=True) - - if instance is None: - lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field - lookup_value = self.kwargs[lookup_url_kwarg] - extra_kwargs = {self.lookup_field: lookup_value} - serializer.save(**extra_kwargs) - return Response(serializer.data, status=status.HTTP_201_CREATED) - - serializer.save() - return Response(serializer.data) - - def partial_update(self, request: Request, *args: Any, **kwargs: Any) -> Response: - kwargs["partial"] = True - return self.update(request, *args, **kwargs) - - def get_object_or_none(self) -> Any: - try: - return self.get_object() - except Http404: - if self.request.method == "PUT": - self.check_permissions(clone_request(self.request, "POST")) - else: - raise diff --git a/upload.py b/upload.py index db16b1e..d3d92c4 100755 --- a/upload.py +++ b/upload.py @@ -58,7 +58,7 @@ def upload_hint(level: int, hint: int, image: Path) -> None: Upload a hint. :param level: The level that the hint belongs to. The level must already exist. - :param number: The hint number. Zero-indexed. + :param hint: The hint number. Zero-indexed. :param image: The file containing the hint. """ suffix = image.suffix @@ -66,12 +66,16 @@ def upload_hint(level: int, hint: int, image: Path) -> None: if content_type is None: raise RuntimeError(f"unrecognized suffix: {suffix}") - url = f"{SERVER}/api/levels/{level}/hints/{hint}" + url = f"{SERVER}/api/levels/{level}/hint" + payload = {"number": hint} with image.open("rb") as f: - r = requests.put( + r = requests.post( url, auth=(USERNAME, PASSWORD), - files={"hint": (image.name, f, content_type)}, + files={ # type: ignore[arg-type] + "file": (image.name, f, content_type), + "data": (None, json.dumps(payload), "application/json"), + }, timeout=5, )