diff --git a/config/api_router.py b/config/api_router.py index e741253..ae44f3f 100644 --- a/config/api_router.py +++ b/config/api_router.py @@ -5,6 +5,7 @@ from dicom.api.views import ( RetrieveUpdateDeleteCircleApi, RetrieveUpdateDeleteDicomApi, RetrieveUpdateDeletePolygonApi, + SmartFileUploadApi, ) from django.urls import include, path from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView @@ -26,6 +27,7 @@ urlpatterns = [ include( [ path("", ListCreateDicomApi.as_view(), name="list_create_dicom"), + path("upload", SmartFileUploadApi.as_view(), name="upload_dicom_api"), path( "", RetrieveUpdateDeleteDicomApi.as_view(), diff --git a/config/settings/base.py b/config/settings/base.py index ad9848a..0a4c53b 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -142,6 +142,10 @@ MIDDLEWARE = [ "django.middleware.clickjacking.XFrameOptionsMiddleware", ] +FILE_UPLOAD_HANDLERS = [ + "django.core.files.uploadhandler.TemporaryFileUploadHandler", +] + # STATIC # ------------------------------------------------------------------------------ # https://docs.djangoproject.com/en/dev/ref/settings/#static-root diff --git a/image_markuper/dicom/api/serializers.py b/image_markuper/dicom/api/serializers.py index d24d5a3..6bc70c9 100644 --- a/image_markuper/dicom/api/serializers.py +++ b/image_markuper/dicom/api/serializers.py @@ -112,3 +112,7 @@ class CircleSerializer(serializers.ModelSerializer): obj.radius = validated_data["radius"] obj.save() return obj + + +class SmartFileUploadSerializer(serializers.ModelSerializer): + file = serializers.FileField() diff --git a/image_markuper/dicom/api/views.py b/image_markuper/dicom/api/views.py index e8dac7f..7876861 100644 --- a/image_markuper/dicom/api/views.py +++ b/image_markuper/dicom/api/views.py @@ -1,9 +1,13 @@ from drf_spectacular.utils import extend_schema -from rest_framework import generics +from rest_framework import generics, status +from rest_framework.exceptions import ValidationError from rest_framework.generics import get_object_or_404 from rest_framework.parsers import FormParser, MultiPartParser +from rest_framework.response import Response +from rest_framework.views import APIView from ..models import Circle, Dicom, Polygon +from ..services import process_files from .serializers import ( CircleSerializer, DicomSerializer, @@ -19,18 +23,6 @@ class ListCreateDicomApi(generics.ListCreateAPIView): def get_queryset(self): return Dicom.objects.filter(user=self.request.user) - @extend_schema( - operation_id="upload_file", - request={ - "multipart/form-data": { - "type": "object", - "properties": {"file": {"type": "string", "format": "binary"}}, - } - }, - ) - def post(self, request, *args, **kwargs): - return self.create(request, *args, **kwargs) - class RetrieveUpdateDeleteDicomApi(generics.RetrieveUpdateDestroyAPIView): def get_queryset(self): @@ -80,3 +72,16 @@ class RetrieveUpdateDeleteCircleApi(generics.RetrieveUpdateDestroyAPIView): @extend_schema(description="Note: coordinated are dropped on update") def put(self, request, *args, **kwargs): return self.update(request, *args, **kwargs) + + +class SmartFileUploadApi(APIView): + parser_classes = [MultiPartParser, FormParser] + + def post(self, request): + if "file" not in request.data: + raise ValidationError("no files") + d_list = process_files(request.FILES.getlist("file"), request.user) + return Response( + DicomSerializer(d_list.files.all(), many=True).data, + status=status.HTTP_201_CREATED, + ) diff --git a/image_markuper/dicom/models/__init__.py b/image_markuper/dicom/models/__init__.py index 4b57082..d099365 100644 --- a/image_markuper/dicom/models/__init__.py +++ b/image_markuper/dicom/models/__init__.py @@ -1,3 +1,3 @@ # flake8: noqa -from .base import Dicom +from .base import Dicom, ListOfDicom from .blocks import BaseShape, Circle, Coordinate, Polygon diff --git a/image_markuper/dicom/models/base.py b/image_markuper/dicom/models/base.py index f10a5d0..ad4ef0b 100644 --- a/image_markuper/dicom/models/base.py +++ b/image_markuper/dicom/models/base.py @@ -6,6 +6,10 @@ from utils.files import media_upload_path User = get_user_model() +class ListOfDicom(models.Model): + pass + + class Dicom(models.Model): class PathologyType(models.IntegerChoices): no_pathology = 0, "Без патологий" @@ -17,7 +21,10 @@ class Dicom(models.Model): file = models.FileField(upload_to=media_upload_path) uploaded = models.DateTimeField(auto_now_add=True) - pathology_type = models.IntegerField(choices=PathologyType.choices) + pathology_type = models.IntegerField(choices=PathologyType.choices, default=0) + list = models.ForeignKey( + ListOfDicom, related_name="files", null=True, on_delete=models.SET_NULL + ) def __str__(self): return self.file.name diff --git a/image_markuper/dicom/models/blocks.py b/image_markuper/dicom/models/blocks.py index 6245208..3f93f34 100644 --- a/image_markuper/dicom/models/blocks.py +++ b/image_markuper/dicom/models/blocks.py @@ -19,8 +19,8 @@ class BaseShape(PolymorphicModel): class Coordinate(models.Model): - x = models.IntegerField() - y = models.IntegerField() + x = models.FloatField() + y = models.FloatField() shape = models.ForeignKey( to=BaseShape, diff --git a/image_markuper/dicom/services.py b/image_markuper/dicom/services.py new file mode 100644 index 0000000..3675266 --- /dev/null +++ b/image_markuper/dicom/services.py @@ -0,0 +1,39 @@ +import glob +import os +import shutil +import zipfile +from pathlib import Path + +import magic +from dicom.models import Dicom, ListOfDicom +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() + 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) + elif "Zip" in content_type: + dit_path = f"/tmp/{generate_charset(10)}" + os.mkdir(dit_path) + with zipfile.ZipFile(file.temporary_file_path(), "r") as zip_ref: + zip_ref.extractall(dit_path) + files = glob.glob(dit_path + "/**/*", recursive=True) + + for file_in_d in files: + if not os.path.isdir(file_in_d): + content_type = magic.from_file(file_in_d) + if content_type == "DICOM medical imaging data": + path = Path(file_in_d) + with path.open(mode="rb") as f: + Dicom.objects.create( + file=File(f, name=file_in_d.split("/")[-1]), + list=d_list, + user=user, + ) + shutil.rmtree(dit_path) + return d_list diff --git a/requirements/base.txt b/requirements/base.txt index 531a40d..22a1d09 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -1,4 +1,5 @@ pytz==2022.5 # https://github.com/stub42/pytz +python-magic==0.4.27 python-slugify==6.1.2 # https://github.com/un33k/python-slugify Pillow==9.2.0 # https://github.com/python-pillow/Pillow argon2-cffi==21.3.0 # https://github.com/hynek/argon2_cffi