diff --git a/config/api_router.py b/config/api_router.py index ce8228d..e741253 100644 --- a/config/api_router.py +++ b/config/api_router.py @@ -1,4 +1,11 @@ -from dicom.api.views import ListCreateDicomApi, RetrieveUpdateDeleteDicomApi +from dicom.api.views import ( + CreateCircleApi, + CreatePolygonApi, + ListCreateDicomApi, + RetrieveUpdateDeleteCircleApi, + RetrieveUpdateDeleteDicomApi, + RetrieveUpdateDeletePolygonApi, +) from django.urls import include, path from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView from users.api.views import RegisterView @@ -24,6 +31,33 @@ urlpatterns = [ RetrieveUpdateDeleteDicomApi.as_view(), name="get_update_delete_dicom", ), + path( + "/polygon", + CreatePolygonApi.as_view(), + name="create_polygon", + ), + path( + "/circle", + CreateCircleApi.as_view(), + name="create_circle", + ), + ] + ), + ), + path( + "shapes/", + include( + [ + path( + "polygon/", + RetrieveUpdateDeletePolygonApi.as_view(), + name="get_update_delete_polygon", + ), + path( + "circle/", + RetrieveUpdateDeleteCircleApi.as_view(), + name="get_update_delete_circle", + ), ] ), ), diff --git a/config/settings/base.py b/config/settings/base.py index 10904c6..ad9848a 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -309,8 +309,6 @@ REST_FRAMEWORK = { "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", } -# django-cors-headers - https://github.com/adamchainz/django-cors-headers#setup -CORS_URLS_REGEX = r"^/api/.*$" # By Default swagger ui is available only to admin user(s). You can change permission classes to change that # See more configuration options at https://drf-spectacular.readthedocs.io/en/latest/settings.html#settings SPECTACULAR_SETTINGS = { diff --git a/image_markuper/dicom/api/serializers.py b/image_markuper/dicom/api/serializers.py index 6164c58..9966824 100644 --- a/image_markuper/dicom/api/serializers.py +++ b/image_markuper/dicom/api/serializers.py @@ -1,5 +1,22 @@ -from dicom.models import Dicom +from dicom.models import Circle, Coordinate, Dicom, Polygon +from drf_spectacular.utils import extend_schema_field from rest_framework import serializers +from rest_framework.generics import get_object_or_404 + + +def create_coordinate(coordinates, obj): + for coordinate in coordinates: + Coordinate.objects.create( + x=coordinate["x"], + y=coordinate["y"], + shape=obj, + ) + + +class CoordinateSerializer(serializers.ModelSerializer): + class Meta: + model = Coordinate + fields = ["x", "y"] class ListDicomSerializer(serializers.ModelSerializer): @@ -16,9 +33,82 @@ class ListDicomSerializer(serializers.ModelSerializer): return Dicom.objects.create(**validated_data, user=self.context["request"].user) +class BaseShapeSerializer(serializers.Serializer): + type = serializers.ChoiceField(choices=["circle", "polygon"]) + image_number = serializers.IntegerField() + coordinates = CoordinateSerializer(many=True) + + class DicomSerializer(serializers.ModelSerializer): file = serializers.FileField() + shapes = serializers.SerializerMethodField("get_dicom_shapes") + + @extend_schema_field(field=BaseShapeSerializer) + def get_dicom_shapes(self, obj): + return [x.serialize_self() for x in obj.shapes.all()] class Meta: model = Dicom - fields = ["file", "uploaded"] + fields = ["file", "uploaded", "shapes"] + + +class PolygonSerializer(serializers.ModelSerializer): + coordinates = CoordinateSerializer(many=True) + + class Meta: + model = Polygon + fields = ["id", "image_number", "coordinates"] + extra_kwargs = {"id": {"read_only": True}} + + def create(self, validated_data): + if "coordinates" not in validated_data: + raise serializers.ValidationError + dicom = get_object_or_404( + Dicom, slug=self.context["request"].parser_context["kwargs"]["slug"] + ) + polygon = Polygon.objects.create( + dicom=dicom, image_number=validated_data["image_number"] + ) + + create_coordinate(validated_data["coordinates"], polygon) + return polygon + + def update(self, obj: Circle, validated_data): + Coordinate.objects.filter(shape=obj).delete() + create_coordinate(validated_data["coordinates"], obj) + return obj + + +class CircleSerializer(serializers.ModelSerializer): + coordinates = CoordinateSerializer(many=True) + + class Meta: + model = Circle + fields = ["id", "image_number", "radius", "coordinates"] + extra_kwargs = {"id": {"read_only": True}} + + def create(self, validated_data): + if ( + "coordinates" not in validated_data + and len(validated_data["coordinates"]) > 1 + ): + raise serializers.ValidationError + dicom = get_object_or_404( + Dicom, slug=self.context["request"].parser_context["kwargs"]["slug"] + ) + circle = Circle.objects.create( + dicom=dicom, + image_number=validated_data["image_number"], + radius=validated_data["radius"], + ) + + create_coordinate(validated_data["coordinates"], circle) + return circle + + def update(self, obj: Circle, validated_data): + Coordinate.objects.filter(shape=obj).delete() + create_coordinate(validated_data["coordinates"], obj) + if "radius" in validated_data: + obj.radius = validated_data["radius"] + obj.save() + return obj diff --git a/image_markuper/dicom/api/views.py b/image_markuper/dicom/api/views.py index 9c248b1..e8dac7f 100644 --- a/image_markuper/dicom/api/views.py +++ b/image_markuper/dicom/api/views.py @@ -1,9 +1,15 @@ from drf_spectacular.utils import extend_schema from rest_framework import generics +from rest_framework.generics import get_object_or_404 from rest_framework.parsers import FormParser, MultiPartParser -from ..models import Dicom -from .serializers import DicomSerializer, ListDicomSerializer +from ..models import Circle, Dicom, Polygon +from .serializers import ( + CircleSerializer, + DicomSerializer, + ListDicomSerializer, + PolygonSerializer, +) class ListCreateDicomApi(generics.ListCreateAPIView): @@ -34,3 +40,43 @@ class RetrieveUpdateDeleteDicomApi(generics.RetrieveUpdateDestroyAPIView): parser_classes = [MultiPartParser, FormParser] lookup_field = "slug" + + +class CreatePolygonApi(generics.CreateAPIView): + serializer_class = PolygonSerializer + + +class CreateCircleApi(generics.CreateAPIView): + serializer_class = CircleSerializer + + +class RetrieveUpdateDeletePolygonApi(generics.RetrieveUpdateDestroyAPIView): + serializer_class = PolygonSerializer + + def get_object(self): + return get_object_or_404( + Polygon, id=self.request.parser_context["kwargs"]["id"] + ) + + @extend_schema(description="Note: coordinated are dropped on update") + def put(self, request, *args, **kwargs): + return self.update(request, *args, **kwargs) + + @extend_schema(description="Note: coordinated are dropped on update") + def patch(self, request, *args, **kwargs): + return self.partial_update(request, *args, **kwargs) + + +class RetrieveUpdateDeleteCircleApi(generics.RetrieveUpdateDestroyAPIView): + serializer_class = CircleSerializer + + def get_object(self): + return get_object_or_404(Circle, id=self.request.parser_context["kwargs"]["id"]) + + @extend_schema(description="Note: coordinated are dropped on update") + def patch(self, request, *args, **kwargs): + return self.partial_update(request, *args, **kwargs) + + @extend_schema(description="Note: coordinated are dropped on update") + def put(self, request, *args, **kwargs): + return self.update(request, *args, **kwargs) diff --git a/image_markuper/dicom/models/__init__.py b/image_markuper/dicom/models/__init__.py new file mode 100644 index 0000000..4b57082 --- /dev/null +++ b/image_markuper/dicom/models/__init__.py @@ -0,0 +1,3 @@ +# flake8: noqa +from .base import Dicom +from .blocks import BaseShape, Circle, Coordinate, Polygon diff --git a/image_markuper/dicom/models.py b/image_markuper/dicom/models/base.py similarity index 100% rename from image_markuper/dicom/models.py rename to image_markuper/dicom/models/base.py diff --git a/image_markuper/dicom/models/blocks.py b/image_markuper/dicom/models/blocks.py new file mode 100644 index 0000000..6245208 --- /dev/null +++ b/image_markuper/dicom/models/blocks.py @@ -0,0 +1,60 @@ +from dicom.models import Dicom +from django.db import models +from polymorphic.models import PolymorphicModel + + +class BaseShape(PolymorphicModel): + dicom = models.ForeignKey(Dicom, related_name="shapes", on_delete=models.CASCADE) + image_number = models.IntegerField() + + def serialize_self(self): + raise NotImplementedError + + @property + def coordinates(self) -> [(int, int)]: + return self.shape_coordinates.all().values("x", "y") + + def __str__(self): + return self.dicom.file.name + + +class Coordinate(models.Model): + x = models.IntegerField() + y = models.IntegerField() + + shape = models.ForeignKey( + to=BaseShape, + null=False, + blank=False, + on_delete=models.CASCADE, + related_name="shape_coordinates", + ) + + +class Circle(BaseShape): + radius = models.IntegerField() + + def serialize_self(self): + return { + "id": self.id, + "type": "circle", + "image_number": self.image_number, + "radius": self.radius, + "coordinates": self.coordinates, + } + + def __str__(self): + return f"circle on {self.dicom.file.name}" + + +class Polygon(BaseShape): + def serialize_self(self): + return { + "id": self.id, + "type": "polygon", + "image_number": self.image_number, + "coordinates": self.coordinates, + } + + def __str__(self): + return f"polygon on {self.dicom.file.name}"