major fixes and updates

This commit is contained in:
Alexander Karpov 2022-11-01 12:20:43 +03:00
parent 8c53cb6745
commit dcac066a18
9 changed files with 328 additions and 88 deletions

View File

@ -1,11 +1,19 @@
from dicom.api.views import (
AddDicomProjectApi,
CreateCircleApi,
CreateFreeHandApi,
CreateRoiApi,
CreateRulerApi,
DeleteDicomProjectApi,
ListCreateDicomApi,
ListCreateProjectApi,
ListUpdateDicomImageNumberApi,
RetrieveUpdateDeleteCircleApi,
RetrieveUpdateDeleteDicomApi,
RetrieveUpdateDeleteFreeHandApi,
RetrieveUpdateDeleteProjectApi,
RetrieveUpdateDeleteRoiApi,
RetrieveUpdateDeleteRulerApi,
SmartFileUploadApi,
)
from django.urls import include, path
@ -39,11 +47,21 @@ urlpatterns = [
CreateRoiApi.as_view(),
name="create_roi",
),
path(
"<str:slug>/free_hand",
CreateFreeHandApi.as_view(),
name="create_free_hand",
),
path(
"<str:slug>/circle",
CreateCircleApi.as_view(),
name="create_circle",
),
path(
"<str:slug>/ruler",
CreateRulerApi.as_view(),
name="create_ruler",
),
path(
"<str:slug>/<int:layer>",
ListUpdateDicomImageNumberApi.as_view(),
@ -61,11 +79,44 @@ urlpatterns = [
RetrieveUpdateDeleteRoiApi.as_view(),
name="get_update_delete_roi",
),
path(
"free_hand/<int:id>",
RetrieveUpdateDeleteFreeHandApi.as_view(),
name="get_update_delete_free_hand",
),
path(
"circle/<int:id>",
RetrieveUpdateDeleteCircleApi.as_view(),
name="get_update_delete_circle",
),
path(
"ruler/<int:id>",
RetrieveUpdateDeleteRulerApi.as_view(),
name="get_update_delete_ruler",
),
]
),
),
path(
"project/",
include(
[
path("", ListCreateProjectApi.as_view(), name="list_create_project"),
path(
"<str:slug>",
RetrieveUpdateDeleteProjectApi.as_view(),
name="get_update_delete_project",
),
path(
"<str:slug>/upload",
AddDicomProjectApi.as_view(),
name="add_dicom_api",
),
path(
"<str:slug>/<str:dicom_slug>",
DeleteDicomProjectApi.as_view(),
name="delete_dicom_api",
),
]
),
),

View File

@ -1,6 +1,7 @@
from dicom.models import Circle, Dicom, Roi
from dicom.models import Circle, Dicom, FreeHand, Roi
from django.contrib import admin
admin.site.register(Dicom)
admin.site.register(Circle)
admin.site.register(Roi)
admin.site.register(FreeHand)

View File

@ -1,18 +1,19 @@
from dicom.models import Circle, Coordinate, Dicom, Roi
from dicom.models import (
BaseShape,
Circle,
Coordinate,
Dicom,
FreeHand,
Project,
Roi,
Ruler,
)
from dicom.services import create_coordinate
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
@ -34,13 +35,43 @@ class ListDicomSerializer(serializers.ModelSerializer):
class BaseShapeSerializer(serializers.Serializer):
type = serializers.ChoiceField(choices=["circle", "roi"])
model = BaseShape
type = serializers.ChoiceField(choices=["circle", "roi", "free_hand"])
image_number = serializers.IntegerField()
coordinates = CoordinateSerializer(many=True)
def create(self, validated_data):
if self.model.max_coordinates:
if len(validated_data["coordinates"]) > self.model.max_coordinates:
raise serializers.ValidationError
if self.model.min_coordinates:
if len(validated_data["coordinates"]) < self.model.min_coordinates:
raise serializers.ValidationError
dicom = get_object_or_404(
Dicom, slug=self.context["request"].parser_context["kwargs"]["slug"]
)
obj = self.model.objects.create(
dicom=dicom, image_number=validated_data["image_number"]
)
create_coordinate(validated_data["coordinates"], obj)
return obj
def update(self, obj, validated_data):
Coordinate.objects.filter(shape=obj).delete()
if self.model.max_coordinates:
if len(validated_data["coordinates"]) > self.model.max_coordinates:
raise serializers.ValidationError
if self.model.min_coordinates:
if len(validated_data["coordinates"]) < self.model.min_coordinates:
raise serializers.ValidationError
create_coordinate(validated_data["coordinates"], obj)
return obj
class BaseShapeLayerSerializer(serializers.Serializer):
type = serializers.ChoiceField(choices=["circle", "roi"])
type = serializers.ChoiceField(choices=["circle", "roi", "free_hand"])
radius = serializers.FloatField(required=False)
coordinates = CoordinateSerializer(many=True)
@ -58,31 +89,34 @@ class DicomSerializer(serializers.ModelSerializer):
fields = ["file", "uploaded", "pathology_type", "shapes"]
class RoiSerializer(serializers.ModelSerializer):
class RoiSerializer(BaseShapeSerializer, serializers.ModelSerializer):
coordinates = CoordinateSerializer(many=True)
model = Roi
class Meta:
model = Roi
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"]
)
roi = Roi.objects.create(
dicom=dicom, image_number=validated_data["image_number"]
)
create_coordinate(validated_data["coordinates"], roi)
return roi
class FreeHandSerializer(BaseShapeSerializer, serializers.ModelSerializer):
coordinates = CoordinateSerializer(many=True)
model = FreeHand
def update(self, obj: Circle, validated_data):
Coordinate.objects.filter(shape=obj).delete()
create_coordinate(validated_data["coordinates"], obj)
return obj
class Meta:
model = FreeHand
fields = ["id", "image_number", "coordinates"]
extra_kwargs = {"id": {"read_only": True}}
class RulerSerializer(BaseShapeSerializer, serializers.ModelSerializer):
coordinates = CoordinateSerializer(many=True)
model = Ruler
class Meta:
model = FreeHand
fields = ["id", "image_number", "coordinates"]
extra_kwargs = {"id": {"read_only": True}}
class CircleSerializer(serializers.ModelSerializer):
@ -122,3 +156,24 @@ class CircleSerializer(serializers.ModelSerializer):
class SmartFileUploadSerializer(serializers.Serializer):
file = serializers.FileField()
class ListProjectSerializer(serializers.ModelSerializer):
url = serializers.HyperlinkedIdentityField(
view_name="get_update_delete_project", lookup_field="slug"
)
class Meta:
model = Project
fields = ["url", "created"]
extra_kwargs = {
"created": {"read_only": True},
}
class ProjectSerializer(serializers.ModelSerializer):
files = ListDicomSerializer(many=True)
class Meta:
model = Project
fields = ["files", "created"]

View File

@ -5,15 +5,19 @@ from rest_framework.generics import GenericAPIView, get_object_or_404
from rest_framework.parsers import FormParser, MultiPartParser
from rest_framework.response import Response
from ..models import Circle, Dicom, Roi
from ..models import Circle, Dicom, Project, Roi
from ..services import process_files
from .serializers import (
BaseShapeLayerSerializer,
BaseShapeSerializer,
CircleSerializer,
DicomSerializer,
FreeHandSerializer,
ListDicomSerializer,
ListProjectSerializer,
ProjectSerializer,
RoiSerializer,
RulerSerializer,
SmartFileUploadSerializer,
create_coordinate,
)
@ -41,55 +45,110 @@ class CreateRoiApi(generics.CreateAPIView):
serializer_class = RoiSerializer
class CreateFreeHandApi(generics.CreateAPIView):
serializer_class = FreeHandSerializer
class CreateCircleApi(generics.CreateAPIView):
serializer_class = CircleSerializer
class RetrieveUpdateDeleteRoiApi(generics.RetrieveUpdateDestroyAPIView):
class CreateRulerApi(generics.CreateAPIView):
serializer_class = RulerSerializer
class RetrieveUpdateDeleteBaseShape(generics.RetrieveUpdateDestroyAPIView):
def get_object(self):
return get_object_or_404(
self.serializer_class.Meta.model,
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 RetrieveUpdateDeleteRoiApi(RetrieveUpdateDeleteBaseShape):
serializer_class = RoiSerializer
def get_object(self):
return get_object_or_404(Roi, 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 RetrieveUpdateDeleteFreeHandApi(RetrieveUpdateDeleteBaseShape):
serializer_class = FreeHandSerializer
class RetrieveUpdateDeleteCircleApi(generics.RetrieveUpdateDestroyAPIView):
class RetrieveUpdateDeleteCircleApi(RetrieveUpdateDeleteBaseShape):
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)
class RetrieveUpdateDeleteRulerApi(RetrieveUpdateDeleteBaseShape):
serializer_class = CircleSerializer
class SmartFileUploadApi(GenericAPIView):
parser_classes = [MultiPartParser, FormParser]
serializer_class = SmartFileUploadSerializer
@extend_schema(responses={201: DicomSerializer(many=True)})
@extend_schema(responses={201: ListDicomSerializer(many=True)})
def post(self, request):
if "file" not in request.data:
raise ValidationError("no files")
d_list = process_files(request.FILES.getlist("file"), request.user)
project = process_files(
request.FILES.getlist("file"),
request.user,
)
return Response(
DicomSerializer(d_list.files.all(), many=True).data,
ListDicomSerializer(project.files.all(), many=True).data,
status=status.HTTP_201_CREATED,
)
class AddDicomProjectApi(GenericAPIView):
parser_classes = [MultiPartParser, FormParser]
serializer_class = SmartFileUploadSerializer
@extend_schema(
operation_id="add_dicom_to_project",
responses={201: ListDicomSerializer(many=True)},
)
def post(self, request, slug):
if "file" not in request.data:
raise ValidationError("no files")
get_object_or_404(Project, slug=slug)
project = process_files(
request.FILES.getlist("file"),
request.user,
slug,
)
return Response(
ListDicomSerializer(project.files.all(), many=True).data,
status=status.HTTP_201_CREATED,
)
class DeleteDicomProjectApi(GenericAPIView):
serializer_class = SmartFileUploadSerializer
@extend_schema(
operation_id="add_dicom_to_project",
request=None,
responses={200: ListDicomSerializer(many=True)},
)
def delete(self, request, slug, dicom_slug):
project = get_object_or_404(Project, slug=slug)
project.files.filter(slug=dicom_slug).delete()
return Response(
ListDicomSerializer(
project.files.all(), many=True, context={"request": request}
).data,
status=status.HTTP_200_OK,
)
class ListUpdateDicomImageNumberApi(GenericAPIView):
serializer_class = BaseShapeSerializer(many=True)
@ -134,3 +193,27 @@ class ListUpdateDicomImageNumberApi(GenericAPIView):
)
]
return Response(shapes, status=status.HTTP_200_OK)
@extend_schema(
request=None,
responses={204: None},
operation_id="delete_dicom_layer",
)
def delete(self, request, slug, layer):
dicom = get_object_or_404(Dicom, slug=slug)
dicom.shapes.filter(image_number=layer).delete()
return Response(status=status.HTTP_204_NO_CONTENT)
class ListCreateProjectApi(generics.ListCreateAPIView):
serializer_class = ListProjectSerializer
def get_queryset(self):
return Project.objects.filter(user=self.request.user)
class RetrieveUpdateDeleteProjectApi(generics.RetrieveUpdateDestroyAPIView):
serializer_class = ProjectSerializer
queryset = Project.objects.all()
lookup_field = "slug"

View File

@ -1,3 +1,3 @@
# flake8: noqa
from .base import Dicom, ListOfDicom
from .blocks import BaseShape, Circle, Coordinate, Roi
from .base import Dicom, Project
from .shapes import BaseShape, Circle, Coordinate, FreeHand, Roi, Ruler

View File

@ -6,8 +6,14 @@ from utils.files import media_upload_path
User = get_user_model()
class ListOfDicom(models.Model):
pass
class Project(models.Model):
user = models.ForeignKey(User, related_name="projects", on_delete=models.CASCADE)
slug = models.SlugField(max_length=10)
created = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"{self.user.username}'s project"
class Dicom(models.Model):
@ -22,8 +28,8 @@ class Dicom(models.Model):
uploaded = models.DateTimeField(auto_now_add=True)
pathology_type = models.IntegerField(choices=PathologyType.choices, default=0)
list = models.ForeignKey(
ListOfDicom, related_name="files", null=True, on_delete=models.SET_NULL
project = models.ForeignKey(
Project, related_name="files", null=True, on_delete=models.SET_NULL
)
def __str__(self):

View File

@ -4,11 +4,26 @@ from polymorphic.models import PolymorphicModel
class BaseShape(PolymorphicModel):
TYPE = "no_type"
min_coordinates = None
max_coordinates = None
dicom = models.ForeignKey(Dicom, related_name="shapes", on_delete=models.CASCADE)
image_number = models.IntegerField()
def serialize_self(self):
raise NotImplementedError
return {
"id": self.id,
"type": self.TYPE,
"image_number": self.image_number,
"coordinates": self.coordinates,
}
def serialize_self_without_layer(self):
return {
"id": self.id,
"type": self.TYPE,
"coordinates": self.coordinates,
}
@property
def coordinates(self) -> [(int, int)]:
@ -33,6 +48,7 @@ class Coordinate(models.Model):
class Circle(BaseShape):
radius = models.FloatField()
max_coordinates = 1
def serialize_self(self):
return {
@ -56,21 +72,23 @@ class Circle(BaseShape):
class Roi(BaseShape):
def serialize_self(self):
return {
"id": self.id,
"type": "roi",
"image_number": self.image_number,
"coordinates": self.coordinates,
}
def serialize_self_without_layer(self):
return {
"id": self.id,
"type": "roi",
"radius": self.radius,
"coordinates": self.coordinates,
}
TYPE = "roi"
def __str__(self):
return f"Roi on {self.dicom.file.name}"
class FreeHand(BaseShape):
TYPE = "free_hand"
def __str__(self):
return f"FreeHand on {self.dicom.file.name}"
class Ruler(BaseShape):
TYPE = "ruler"
max_coordinates = 2
min_coordinates = 2
def __str__(self):
return f"Ruler on {self.dicom.file.name}"

View File

@ -5,18 +5,23 @@ import zipfile
from pathlib import Path
import magic
from dicom.models import Dicom, ListOfDicom
from dicom.models import Coordinate, Dicom, Project
from django.core.files import File
from django.core.files.uploadedfile import InMemoryUploadedFile, TemporaryUploadedFile
from utils.generators import generate_charset
def process_files(files: list[TemporaryUploadedFile | InMemoryUploadedFile], user):
d_list = ListOfDicom.objects.create()
def process_files(
files: list[TemporaryUploadedFile | InMemoryUploadedFile], user, slug=None
):
if slug:
project = Project.objects.get(slug=slug)
else:
project = Project.objects.create(user=user)
for file in files:
content_type = magic.from_file(file.temporary_file_path())
if content_type == "DICOM medical imaging data":
Dicom.objects.create(file=file, list=d_list, user=user)
Dicom.objects.create(file=file, project=project, user=user)
elif "Zip" in content_type:
dit_path = f"/tmp/{generate_charset(10)}"
os.mkdir(dit_path)
@ -32,8 +37,17 @@ def process_files(files: list[TemporaryUploadedFile | InMemoryUploadedFile], use
with path.open(mode="rb") as f:
Dicom.objects.create(
file=File(f, name=file_in_d.split("/")[-1]),
list=d_list,
project=project,
user=user,
)
shutil.rmtree(dit_path)
return d_list
return project
def create_coordinate(coordinates, obj):
for coordinate in coordinates:
Coordinate.objects.create(
x=coordinate["x"],
y=coordinate["y"],
shape=obj,
)

View File

@ -1,12 +1,24 @@
from dicom.models import Dicom
from django.db.models.signals import pre_save
from dicom.models import Dicom, Project
from django.db.models.signals import post_save
from django.dispatch import receiver
from utils.generators import generate_charset
@receiver(pre_save, sender=Dicom)
def create_dicom(sender, instance: Dicom, **kwargs):
slug = generate_charset(5)
while Dicom.objects.filter(slug=slug):
@receiver(post_save, sender=Project)
def create_project(sender, instance: Project, created, **kwargs):
if created:
slug = generate_charset(5)
instance.slug = slug
while Project.objects.filter(slug=slug):
slug = generate_charset(5)
instance.slug = slug
instance.save()
@receiver(post_save, sender=Dicom)
def create_dicom(sender, instance: Dicom, created, **kwargs):
if created:
slug = generate_charset(5)
while Dicom.objects.filter(slug=slug):
slug = generate_charset(5)
instance.slug = slug
instance.save()