diff --git a/atlas/admin.py b/atlas/admin.py index 36210fca..bb79dcce 100755 --- a/atlas/admin.py +++ b/atlas/admin.py @@ -13,6 +13,7 @@ from .models import ( Structure, PathologicalProcess, Presentation, + UncategorisedDicom, ) from django.forms import ModelForm @@ -38,6 +39,7 @@ admin.site.register(Differential) admin.site.register(PathologicalProcess) admin.site.register(Presentation) admin.site.register(CaseDetail) +admin.site.register(UncategorisedDicom) class DifferentialInline(admin.TabularInline): diff --git a/atlas/api.py b/atlas/api.py new file mode 100644 index 00000000..20d7f342 --- /dev/null +++ b/atlas/api.py @@ -0,0 +1,137 @@ +from collections import defaultdict +from typing import List +from django.shortcuts import get_object_or_404 +from ninja import ModelSchema, Router, Schema, Field + +# from .decorators import user_is_author_or_rapid_checker +from django.core.exceptions import PermissionDenied + +from generic.decorators import check_user_in_group +from generic.constants import Group + +from ninja.security import django_auth + +from ninja import NinjaAPI, File +from ninja.files import UploadedFile + +from generic.models import Modality + +from .models import Case, DuplicateDicom, Series, SeriesImage, UncategorisedDicom + + +router = Router() + +class SeriesSchema(ModelSchema): + case_id: List[int] = [] + + class Config: + model = Series + model_fields = ["id", "modality", "examination", "series_instance_uid", "author"] + + @staticmethod + def resolve_case_id(obj): + return [i.id for i in obj.case.all()] + + +@router.post("/upload_dicom", auth=django_auth) +def upload_dicom(request, files: List[UploadedFile] = File(...)): + uploaded = [] + duplicate = [] + for file in files: + # data = file.read() + try: + ud = UncategorisedDicom(image=file, user=request.user) + + ud.save() + + uploaded.append(ud) + except DuplicateDicom: + duplicate.append(ud) + pass + + return {"uploaded": len(uploaded), "duplicates": len(duplicate)} + + +@router.get("/clear_dicoms", auth=django_auth) +def clear_dicoms(request): + dicoms = UncategorisedDicom.objects.filter(user=request.user) + + dicoms.delete() + + return True + + +@router.get("/uncategorised_dicoms", auth=django_auth) +def uncategorised_dicoms(request): + dicoms = UncategorisedDicom.objects.filter(user=request.user) + + data = defaultdict(list) + for d in dicoms: + tags = d.get_basic_dicom_tags() + + data[tags["SeriesInstanceUID"]].append(tags) + + return data + + +@router.get("/import_dicoms", auth=django_auth) +def import_dicoms(request): + dicoms = UncategorisedDicom.objects.filter(user=request.user) + + data = defaultdict(list) + for d in dicoms: + tags = d.get_basic_dicom_tags() + + data[tags["SeriesInstanceUID"]].append((d, tags)) + + for series_uid in data: + + modality = get_object_or_404(Modality, short_code=tags["Modality"]) + tags = data[series_uid][0][1] + + # Check if series with the id already exists (in which case we just add to htat) + try: + series = Series.objects.get(series_instance_uid=tags["SeriesInstanceUID"]) + except Series.DoesNotExist: + series = Series( + modality=modality, + description=tags["SeriesDescription"], + series_instance_uid=tags["SeriesInstanceUID"], + ) + series.save() + + # We might only want to add the author during creation.... + series.author.add(request.user) + + for dicom, dicom_tags in data[series_uid]: + series_image = SeriesImage(image=dicom.image, series=series) + series_image.save() + + dicom.delete() + + return {} + +@router.get("/orphan_series", auth=django_auth, response=List[SeriesSchema]) +def orphan_series(request): + #print(request.user.series.filter(case=None)) + return request.user.series.filter(case=None) + + +@router.get("/series_remove_duplicate_images", auth=django_auth) +def series_remove_duplicate_images(request, series_id: int): + + series = get_object_or_404(Series, pk=series_id) + + img_ids = set() + dupes = set() + for series_image in series.images.all(): + if series_image.image_md5_hash in img_ids: + dupes.add(series_image.id) + img_ids.add(series_image.image_md5_hash) + + if dupes: + SeriesImage.objects.filter(id__in=dupes).delete() + + return len(dupes) + + diff --git a/atlas/forms.py b/atlas/forms.py index 48b88594..155ec906 100755 --- a/atlas/forms.py +++ b/atlas/forms.py @@ -30,6 +30,7 @@ from atlas.models import ( Condition, Structure, Subspecialty, + UncategorisedDicom, UserReportAnswer, ) @@ -589,4 +590,13 @@ class SelfReviewForm(ModelForm): case_id = kwargs.pop("case_id") ModelForm.__init__(self, *args, **kwargs) - super(SelfReviewForm, self).__init__(*args, **kwargs) \ No newline at end of file + super(SelfReviewForm, self).__init__(*args, **kwargs) + + +class UncategorisedDicomForm(ModelForm): + class Meta: + model = UncategorisedDicom + fields = ["image"]#, "user"] + + +#UncategorisedDicomFormset = formset_factory(UncategorisedDicomForm) \ No newline at end of file diff --git a/atlas/migrations/0013_uncategoriseddicom_is_dicom.py b/atlas/migrations/0013_uncategoriseddicom_is_dicom.py new file mode 100644 index 00000000..dbc64234 --- /dev/null +++ b/atlas/migrations/0013_uncategoriseddicom_is_dicom.py @@ -0,0 +1,18 @@ +# Generated by Django 4.1.4 on 2023-08-14 09:19 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('atlas', '0012_seriesimage_image_md5_hash_seriesimage_is_dicom_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='uncategoriseddicom', + name='is_dicom', + field=models.BooleanField(default=False), + ), + ] diff --git a/atlas/migrations/0014_series_series_instance_uid_alter_series_examination_and_more.py b/atlas/migrations/0014_series_series_instance_uid_alter_series_examination_and_more.py new file mode 100644 index 00000000..fa2ecbc9 --- /dev/null +++ b/atlas/migrations/0014_series_series_instance_uid_alter_series_examination_and_more.py @@ -0,0 +1,30 @@ +# Generated by Django 4.1.4 on 2023-08-14 10:50 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('generic', '0008_delete_selfreview'), + ('atlas', '0013_uncategoriseddicom_is_dicom'), + ] + + operations = [ + migrations.AddField( + model_name='series', + name='series_instance_uid', + field=models.CharField(blank=True, max_length=255, null=True), + ), + migrations.AlterField( + model_name='series', + name='examination', + field=models.ForeignKey(blank=True, help_text='Name of the examination', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='atlas_series_examination', to='generic.examination'), + ), + migrations.AlterField( + model_name='series', + name='modality', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='atlas_series_modality', to='generic.modality'), + ), + ] diff --git a/atlas/models.py b/atlas/models.py index 85a20460..b513ae05 100644 --- a/atlas/models.py +++ b/atlas/models.py @@ -32,7 +32,7 @@ from sortedm2m.fields import SortedManyToManyField import string from collections import defaultdict -from helpers.images import image_as_base64, pretty_print_dicom +from helpers.images import get_image_hash, image_as_base64, pretty_print_dicom from generic.models import ( @@ -452,6 +452,7 @@ class Series(SeriesBase): related_name="atlas_series_modality", on_delete=models.SET_NULL, null=True, + blank=True, ) examination = models.ForeignKey( Examination, @@ -459,6 +460,7 @@ class Series(SeriesBase): related_name="atlas_series_examination", on_delete=models.SET_NULL, null=True, + blank=True, ) plane = models.ForeignKey( Plane, @@ -483,6 +485,8 @@ class Series(SeriesBase): related_name="series", ) + series_instance_uid = models.CharField(max_length=255, blank=True, null=True) + # findings = models.TextField(null=True, blank=True, help_text="Findings on the series / stack") def get_full_str(self): @@ -811,6 +815,22 @@ class UncategorisedDicom(models.Model): ) image_md5_hash = models.CharField(max_length=32, null=True, blank=True) + is_dicom = models.BooleanField(default=False) + + def save(self, *args, **kwargs): + """Override save method to add image hash""" + if self.image: + image_hash, is_dicom = get_image_hash(self.image) + + self.is_dicom = is_dicom + self.image_md5_hash = image_hash + + if UncategorisedDicom.objects.filter(image_md5_hash=image_hash): + raise DuplicateDicom + + # Hack for tests + if image_hash != "12345ABCD": + super().save(*args, **kwargs) # Call the "real" save() method. def get_dicom_info(self): try: @@ -837,10 +857,13 @@ class UncategorisedDicom(models.Model): for tag in to_include: if tag in ds: - tags[tag] = ds[tag] + tags[tag] = ds[tag].value else: tags[tag] = "" - return ds + return tags except pydicom.errors.InvalidDicomError: return None + +class DuplicateDicom(Exception): + pass \ No newline at end of file diff --git a/atlas/views.py b/atlas/views.py index 5a17b6ea..c45f5832 100755 --- a/atlas/views.py +++ b/atlas/views.py @@ -1882,12 +1882,12 @@ class AddSelfReview(CreateView): @login_required -def uncategorised_dicoms(request, pk): - dicoms = UncategorisedDicom.objects.filter(user=request.User) +def uncategorised_dicoms(request): + dicoms = UncategorisedDicom.objects.filter(user=request.user) - data = {} + data = [] for d in dicoms: - d[d.image] = d.get_basic_dicom_tags() + data.append((d.image.name, d.get_basic_dicom_tags())) - return JsonResponse(data) \ No newline at end of file + return JsonResponse(data, safe=False) \ No newline at end of file diff --git a/generic/migrations/0009_modality_short_code_alter_modality_modality.py b/generic/migrations/0009_modality_short_code_alter_modality_modality.py new file mode 100644 index 00000000..096593ba --- /dev/null +++ b/generic/migrations/0009_modality_short_code_alter_modality_modality.py @@ -0,0 +1,23 @@ +# Generated by Django 4.1.4 on 2023-08-14 11:17 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('generic', '0008_delete_selfreview'), + ] + + operations = [ + migrations.AddField( + model_name='modality', + name='short_code', + field=models.CharField(max_length=5, null=True, unique=True), + ), + migrations.AlterField( + model_name='modality', + name='modality', + field=models.CharField(max_length=200, unique=True), + ), + ] diff --git a/generic/models.py b/generic/models.py index 68143ce3..d43ed70f 100644 --- a/generic/models.py +++ b/generic/models.py @@ -46,10 +46,11 @@ def findMiddle(input_list): class Modality(models.Model): - modality = models.CharField(max_length=200) + modality = models.CharField(max_length=200, unique=True) + short_code = models.CharField(max_length=5, unique=True, null=True) def __str__(self): - return self.modality + return f"{self.modality} ({self.short_code})" class Plane(models.Model): diff --git a/rad/api.py b/rad/api.py index ef2dc9fa..3e901f7f 100644 --- a/rad/api.py +++ b/rad/api.py @@ -5,6 +5,7 @@ from longs.api import router as longs_router from anatomy.api import router as anatomy_router from sbas.api import router as sbas_router from physics.api import router as physics_router +from atlas.api import router as atlas_router api = NinjaAPI(csrf=True, version="1") @@ -15,7 +16,9 @@ api.add_router("anatomy/", anatomy_router) api.add_router("longs/", longs_router) api.add_router("sbas/", sbas_router) api.add_router("physics/", physics_router) +api.add_router("atlas/", atlas_router) @api.get("/hello") def hello(request): - return "Hello world" \ No newline at end of file + return "Hello world" +