diff --git a/atlas/api.py b/atlas/api.py index 9d9f36d5..f10a057e 100644 --- a/atlas/api.py +++ b/atlas/api.py @@ -4,7 +4,7 @@ from django.shortcuts import get_object_or_404 from django.urls import reverse from ninja import ModelSchema, Router, Schema, Field -from django.db import transaction +from django.db import transaction, IntegrityError # from .decorators import user_is_author_or_rapid_checker from django.core.exceptions import PermissionDenied @@ -109,7 +109,6 @@ def clear_dicoms(request): dicoms = UncategorisedDicom.objects.filter(user=request.user) dicoms.delete() - print(dicoms) return True @@ -163,28 +162,31 @@ def import_dicoms_helper(request, case_id: int | None = None): tags = data[series_uid][0][1] - # Check if series with the id already exists (in which case we just add to htat) - if Series.objects.filter( - series_instance_uid=tags["SeriesInstanceUID"] - ).exists(): - series = Series.objects.get(series_instance_uid=tags["SeriesInstanceUID"]) - else: - series = Series( - modality=modality, - description=tags["SeriesDescription"], - series_instance_uid=tags["SeriesInstanceUID"], - ) - - if tags["StudyDescription"]: - examination, created = Examination.objects.get_or_create( - examination=tags["StudyDescription"] + try: + with transaction.atomic(): + series, created = Series.objects.get_or_create( + series_instance_uid=tags["SeriesInstanceUID"], + defaults={ + "modality": modality, + "description": tags["SeriesDescription"], + } ) - if created: - examination.modality = modality - examination.save() - series.examination = examination + if created and tags["StudyDescription"]: + examination, created_exam = Examination.objects.get_or_create( + examination=tags["StudyDescription"] + ) + if created_exam: + examination.modality = modality + examination.save() + series.examination = examination + series.save() + except IntegrityError: + # If a race condition still occurs, fetch the existing series + pass - series.save() + + if 'series' not in locals(): + series = Series.objects.get(series_instance_uid=tags["SeriesInstanceUID"]) # We might only want to add the author during creation.... @@ -192,8 +194,11 @@ def import_dicoms_helper(request, case_id: int | None = None): if case_id is not None: case_object = get_object_or_404(Case, pk=case_id) - if not series.case.contains(case_object): + try: series.case.add(case_object) + except IntegrityError: + # The relation already exists, safe to ignore + pass for dicom, dicom_tags in data[series_uid]: series_image = SeriesImage( @@ -238,7 +243,6 @@ def import_dicoms_case(request, case_id: int): @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) @@ -261,7 +265,6 @@ def series_remove_duplicate_images(request, series_id: int): @router.get("/series_truncate/{series_id}/{start}/{end}/", auth=django_auth) def series_truncate(request, series_id: int, start: int, end: int): - print(start, end) series = get_object_or_404(Series, pk=series_id) if not series.check_user_can_edit(request.user): @@ -272,7 +275,6 @@ def series_truncate(request, series_id: int, start: int, end: int): if n >= start and n <= end: continue else: - print(n, image) image.removed = True image.image = None image.save() @@ -291,7 +293,6 @@ def get_cases_user(request): @router.get("/check_image_hash/{hash}", auth=django_auth) def check_image_hash(request, hash: str): try: - print(hash) series_image = SeriesImage.objects.get(image_blake3_hash=hash) data = { "status": "success", @@ -372,11 +373,8 @@ def series_split_by_tag(request, series_id: int, dicom_tag: str): # Split teh associated images for image in series.images.all(): - print(image) ds = image.get_dicom_data() - # print("2a", ds[dicom_tag]) - if dicom_tag in ds: val = ds[dicom_tag].value diff --git a/atlas/decorators.py b/atlas/decorators.py index 16c6b4fa..55091d1a 100755 --- a/atlas/decorators.py +++ b/atlas/decorators.py @@ -80,6 +80,8 @@ def user_is_collection_author_or_atlas_editor(function): def wrap(request, *args, **kwargs): if "exam_id" in kwargs: atlas = CaseCollection.objects.get(pk=kwargs["exam_id"]) + elif "collection_id" in kwargs: + atlas = CaseCollection.objects.get(pk=kwargs["collection_id"]) else: atlas = CaseCollection.objects.get(pk=kwargs["pk"]) if ( diff --git a/atlas/migrations/0071_casedetail_default_viewerstate.py b/atlas/migrations/0071_casedetail_default_viewerstate.py new file mode 100644 index 00000000..f673798b --- /dev/null +++ b/atlas/migrations/0071_casedetail_default_viewerstate.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.4 on 2025-06-16 12:52 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('atlas', '0070_seriesfinding_annotation_json_3d_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='casedetail', + name='default_viewerstate', + field=models.JSONField(blank=True, help_text='Default viewer state for the case', null=True), + ), + ] diff --git a/atlas/models.py b/atlas/models.py index 7ea85f44..f80894e1 100644 --- a/atlas/models.py +++ b/atlas/models.py @@ -541,10 +541,21 @@ class Case(models.Model, AuthorMixin, QuestionMixin): return True return False - + def get_series_images_nested(self, as_json=True): + """ + Returns a list of lists, where each inner list contains the image URLs + for a single series in this case, in the order of the series. + """ + images = [ + [image.image.url for image in series.images.all()] + for series in self.series.all() + ] - + if as_json: + return json.dumps(images) + else: + return images def extract_image_dicom_json_from_ds(ds, url, image_index): @@ -1107,6 +1118,12 @@ class CaseDetail(models.Model): # TODO add feedback for questions #question_feedback = models.JSONField(null=True, blank=True) + default_viewerstate = models.JSONField( + null=True, + blank=True, + help_text="Default viewer state for the case" + ) + sort_order = models.IntegerField(default=1000) class Meta: @@ -1129,6 +1146,9 @@ class CaseDetail(models.Model): except UserReportAnswer.DoesNotExist: return None + def default_viewerstate_string(self): + return json.dumps(self.default_viewerstate) if self.default_viewerstate else "" + class CasePrior(models.Model): case_detail = models.ForeignKey(CaseDetail, on_delete=models.CASCADE) prior_case = models.ForeignKey(Case, on_delete=models.CASCADE, related_name="prior_case") diff --git a/atlas/templates/atlas/case_display_block.html b/atlas/templates/atlas/case_display_block.html index 92555369..a49e29b9 100755 --- a/atlas/templates/atlas/case_display_block.html +++ b/atlas/templates/atlas/case_display_block.html @@ -440,8 +440,7 @@ }, 200); document.getElementById("reload-finding").addEventListener("click", function () { - importAnnotations_root(annotationjson3d) - importViewerState_root(viewerState); + window.mountDicomViewers(); }); // Initialize the DICOM viewer with the images and annotations diff --git a/atlas/templates/atlas/collection_case_display_setup.html b/atlas/templates/atlas/collection_case_display_setup.html new file mode 100644 index 00000000..0d36ba7b --- /dev/null +++ b/atlas/templates/atlas/collection_case_display_setup.html @@ -0,0 +1,68 @@ +{% extends 'atlas/exams.html' %} + + + +{% block content %} +
Click on the 'Load' button to open the case in a the linked window. Loading subsequent cases/series will replace the first case in the window.
It is also possible to open a single series by clicking on the 'Load' button next to the individual series.
Once opened the case details will be show on the right hand side of the page.
+ +Awating load request
+ This page will display series when loaded from other pages. +