diff --git a/atlas/forms.py b/atlas/forms.py
index 93c1fb57..14c11a58 100755
--- a/atlas/forms.py
+++ b/atlas/forms.py
@@ -67,6 +67,9 @@ from atlas.helpers import get_cases_available_to_user
from typing import Any
+from helpers.cimar import CimarAPI, NotFoundError
+from rad.settings import CIMAR_USERNAME, CIMAR_PASSWORD
+
class CaseSelect(Select):
template_name = "atlas/case_select_widget.html"
@@ -363,6 +366,7 @@ class CaseForm(ModelForm):
"open_access",
"previous_case",
"diagnostic_certainty",
+ "cimar_uuid"
),
)
@@ -396,6 +400,7 @@ class CaseForm(ModelForm):
"open_access",
"previous_case",
"diagnostic_certainty",
+ "cimar_uuid"
]
# fields = ['question', 'findings', 'subspecialty', 'references']
widgets = {
@@ -417,6 +422,18 @@ class CaseForm(ModelForm):
"pathological_process": CheckboxSelectMultiple(),
"previous_case": CaseSelect(),
}
+
+ def clean_cimar_uuid(self):
+ cimar_uuid = self.cleaned_data["cimar_uuid"]
+ if cimar_uuid:
+ with CimarAPI(username=CIMAR_USERNAME, password=CIMAR_PASSWORD) as api:
+ try:
+ study = api.get_study_by_uuid(cimar_uuid)
+
+ except NotFoundError:
+ raise ValidationError("Study not found in CIMAR")
+
+ return cimar_uuid
def save(self, commit=True):
# Get the unsaved Case instance
diff --git a/atlas/migrations/0067_case_cimar_uuid.py b/atlas/migrations/0067_case_cimar_uuid.py
new file mode 100644
index 00000000..ca739f7b
--- /dev/null
+++ b/atlas/migrations/0067_case_cimar_uuid.py
@@ -0,0 +1,18 @@
+# Generated by Django 5.1.4 on 2025-02-10 13:07
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('atlas', '0066_case_total_images_series_size'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='case',
+ name='cimar_uuid',
+ field=models.CharField(blank=True, max_length=255, null=True),
+ ),
+ ]
diff --git a/atlas/models.py b/atlas/models.py
index c95ca0e9..7a0cd5f1 100644
--- a/atlas/models.py
+++ b/atlas/models.py
@@ -46,6 +46,7 @@ from generic.models import (
CidUser,
CidUserExam,
CidUserGroup,
+ CimarCase,
ExamOrCollectionGenericBase,
ExamUserStatus,
Examination,
@@ -392,6 +393,8 @@ class Case(models.Model, AuthorMixin, QuestionMixin):
total_images_series_size = models.BigIntegerField(null=True, blank=True)
+ cimar_uuid = models.CharField(max_length=255, null=True, blank=True)
+
def get_app_name(self):
return "atlas"
@@ -404,6 +407,14 @@ class Case(models.Model, AuthorMixin, QuestionMixin):
# return f"{self.pk}: {self.title}"
return format_html("{} ", self.get_absolute_url(), str(self))
+ def get_cimar_case(self):
+ return get_object_or_404(CimarCase, uuid=self.cimar_uuid)
+
+ def get_cimar_case_details(self):
+ cimar_case = self.get_cimar_case()
+ return cimar_case.study_details
+
+
# def get_base_template(self):
# def get_edit_template_links(self):
@@ -427,6 +438,7 @@ class Case(models.Model, AuthorMixin, QuestionMixin):
def get_case_dicom_json(self, case_title_as_patient_name=True, priors=None):
series_json = []
+ series: Series
for series in self.series.all():
series_json.append(series.get_series_dicom_json())
@@ -506,6 +518,8 @@ class Case(models.Model, AuthorMixin, QuestionMixin):
return size
+
+
diff --git a/atlas/templates/atlas/case_display_block.html b/atlas/templates/atlas/case_display_block.html
index 249a1def..47f50d01 100755
--- a/atlas/templates/atlas/case_display_block.html
+++ b/atlas/templates/atlas/case_display_block.html
@@ -19,7 +19,65 @@
{% endif %}
-
View case in OHIF (new tab)
+
+
+ {% if case.cimar_uuid %}
+
+
Details
+
+ Case uuid: {{case.cimar_uuid}}
+
+ {{case.get_cimar_case.viewer_link}}?{{cimar_sid}}
+
+Open Modal
+
+
+
+ Viewer
+
+
+
+ Study details
+ {{ case.get_cimar_case.get_pretty_study_details }}
+
+
+ Study schema
+ {{ case.get_cimar_case.get_pretty_study_schema }}
+
+
+ Study series
+
+ Load Series
+
+
+ Refresh
+
+
+
+
+
+ {% else %}
+
View case in OHIF (new tab)
+ {% endif %}
+
{{ case.created_date|date:"d/m/Y" }}
diff --git a/atlas/urls.py b/atlas/urls.py
index 3663564b..a228afba 100755
--- a/atlas/urls.py
+++ b/atlas/urls.py
@@ -378,6 +378,7 @@ urlpatterns = [
# path("all_questions/", views.all_questions, name="all_questions"),
path("case/
/scrap", views.atlas_scrap, name="case_scrap"),
path("case//delete", views.CaseDelete.as_view(), name="case_delete"),
+ path("case//push_to_cimar", views.push_case_to_cimar, name="push_case_to_cimar"),
path("case/create/", views.AtlasCreate.as_view(), name="case_create"),
path(
"case/collection//create",
diff --git a/atlas/views.py b/atlas/views.py
index 9ba8a1a1..d2e5ef0f 100755
--- a/atlas/views.py
+++ b/atlas/views.py
@@ -22,6 +22,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMix
from django.views.generic import View
from django.views.generic.detail import DetailView
import pydicom
+from pydicom.uid import generate_uid
from generic.mixins import SuperuserRequiredMixin
from django.views.generic.edit import CreateView, UpdateView, DeleteView, FormView
@@ -33,8 +34,9 @@ from django.urls import reverse_lazy, reverse
from django.http import Http404, JsonResponse
from django.http import HttpResponseRedirect, HttpResponse
-from generic.models import CidUser, CidUserExam
+from generic.models import CidUser, CidUserExam, CimarCase
from atlas.helpers import get_cases_available_to_user
+from rad.settings import REMOTE_URL, CIMAR_USERNAME, CIMAR_PASSWORD
from .forms import (
AddCollectionToCaseForm,
@@ -159,6 +161,8 @@ import reversion
import os
import difflib
+from helpers.cimar import CimarAPI
+
logger = logging.getLogger(__name__)
@@ -195,7 +199,7 @@ def case_detail(request, pk):
# raise PermissionDenied
# logging.debug(atlas.subspecialty.first().name.all())
- return render(request, "atlas/case_detail.html", {"case": case})
+ return render(request, "atlas/case_detail.html", {"case": case, "cimar_sid":request.user.userprofile.cimar_sid})
@login_required
@user_is_author_or_atlas_series_checker_or_atlas_marker_or_open_access
@@ -955,6 +959,9 @@ class AtlasCreateBase(RevisionMixin, LoginRequiredMixin):
form.instance.author.add(self.request.user.id)
+ # Check Cimar Case
+ #CimarAPI().get_study_by_uuid(self.object.cimar_uuid)
+
context = self.get_context_data(form=form)
series_formset = context["series_formset"]
casedifferential_formset = context["casedifferential_formset"]
@@ -2390,6 +2397,7 @@ def collection_case_view(request, pk, case_number):
"previous": previous,
"next": next,
"answer": answer,
+ "cimar_sid":request.user.userprofile.cimar_sid
},
)
@@ -2419,6 +2427,7 @@ def collection_case_dicom_json(request, exam_id, case_id):
return JsonResponse(case_detail.case.get_case_dicom_json(priors=priors))
+
def case_dicom_json(request, pk):
case = get_object_or_404(Case, pk=pk)
@@ -2971,3 +2980,29 @@ def collection_reset_answers(request, exam_id: int):
else:
raise Http404("Invalid request")
+
+def push_case_to_cimar(request, case_id):
+ case = get_object_or_404(Case, pk=case_id)
+
+ api = CimarAPI()
+ api.login(username=CIMAR_USERNAME, password=CIMAR_PASSWORD)
+
+ # We use the same study_uid for all images in a case
+ study_uid = generate_uid()
+
+ for series in case.get_series():
+ for image in series.images.all():
+ data = api.upload_dicom(image.image.path, study_uid=study_uid)
+
+
+ cimar_uuid = api.get_study_by_study_uid(data["study_uid"])["uuid"]
+
+ case.cimar_uuid = cimar_uuid
+
+ case.save()
+
+ cimar_case, created = CimarCase.objects.get_or_create(uuid=cimar_uuid)
+
+ cimar_case.refresh_study()
+
+ return HttpResponse("Success")
diff --git a/generic/migrations/0021_userprofile_cimar_sid.py b/generic/migrations/0021_userprofile_cimar_sid.py
new file mode 100644
index 00000000..80890663
--- /dev/null
+++ b/generic/migrations/0021_userprofile_cimar_sid.py
@@ -0,0 +1,18 @@
+# Generated by Django 5.1.4 on 2025-02-10 12:47
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('generic', '0020_remove_examuserstatus_share_with_supervisor_and_more'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='userprofile',
+ name='cimar_sid',
+ field=models.CharField(blank=True, max_length=100),
+ ),
+ ]
diff --git a/generic/migrations/0022_cimarcase.py b/generic/migrations/0022_cimarcase.py
new file mode 100644
index 00000000..e5ebd10c
--- /dev/null
+++ b/generic/migrations/0022_cimarcase.py
@@ -0,0 +1,22 @@
+# Generated by Django 5.1.4 on 2025-02-10 13:46
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('generic', '0021_userprofile_cimar_sid'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='CimarCase',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('uuid', models.CharField(max_length=255, unique=True)),
+ ('viewer_link', models.URLField(blank=True)),
+ ('study_details', models.JSONField(blank=True)),
+ ],
+ ),
+ ]
diff --git a/generic/migrations/0023_cimarcase_valid_uuid_alter_cimarcase_study_details.py b/generic/migrations/0023_cimarcase_valid_uuid_alter_cimarcase_study_details.py
new file mode 100644
index 00000000..c46b09d2
--- /dev/null
+++ b/generic/migrations/0023_cimarcase_valid_uuid_alter_cimarcase_study_details.py
@@ -0,0 +1,23 @@
+# Generated by Django 5.1.4 on 2025-02-17 10:26
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('generic', '0022_cimarcase'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='cimarcase',
+ name='valid_uuid',
+ field=models.BooleanField(default=False),
+ ),
+ migrations.AlterField(
+ model_name='cimarcase',
+ name='study_details',
+ field=models.JSONField(blank=True, default=dict),
+ ),
+ ]
diff --git a/generic/migrations/0024_cimarcase_series_data_cimarcase_study_schema.py b/generic/migrations/0024_cimarcase_series_data_cimarcase_study_schema.py
new file mode 100644
index 00000000..6c36690d
--- /dev/null
+++ b/generic/migrations/0024_cimarcase_series_data_cimarcase_study_schema.py
@@ -0,0 +1,23 @@
+# Generated by Django 5.1.4 on 2025-02-17 11:49
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('generic', '0023_cimarcase_valid_uuid_alter_cimarcase_study_details'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='cimarcase',
+ name='series_data',
+ field=models.JSONField(blank=True, default=dict),
+ ),
+ migrations.AddField(
+ model_name='cimarcase',
+ name='study_schema',
+ field=models.JSONField(blank=True, default=dict),
+ ),
+ ]
diff --git a/generic/models.py b/generic/models.py
index 4275b514..4a98d739 100644
--- a/generic/models.py
+++ b/generic/models.py
@@ -7,6 +7,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin
from django.db import models
from django.forms import ValidationError
from django.http import Http404, HttpRequest
+from django.shortcuts import render
from django.utils import timezone
from django.core.validators import MaxValueValidator, MinValueValidator
from smtplib import SMTPException
@@ -29,7 +30,8 @@ from django.db.models.signals import post_save
from django.dispatch import receiver
from generic.mixins import AuthorMixin, QuestionMixin
from helpers.images import get_image_hash, pretty_print_dicom
-from rad.settings import REMOTE_URL
+from helpers.cimar import CimarAPI, STORAGE_API_URL
+from rad.settings import REMOTE_URL, CIMAR_USERNAME, CIMAR_PASSWORD
from django.utils.html import format_html
from easy_thumbnails.files import get_thumbnailer
@@ -45,6 +47,34 @@ from django.forms.utils import from_current_timezone, to_current_timezone
from loguru import logger
+from pygments import highlight
+from pygments.lexers import JsonLexer
+from pygments.formatters import HtmlFormatter
+
+from django.contrib import admin
+from django.utils.safestring import mark_safe
+from django.template.loader import render_to_string
+
+
+def get_pretty_json(data):
+ """Function to display pretty version of our data"""
+ if not data:
+ return "No data"
+
+ # Convert the data to sorted, indented JSON
+ response = json.dumps(data, sort_keys=True, indent=2)
+
+ # Get the Pygments formatter
+ formatter = HtmlFormatter(style='colorful')
+
+ # Highlight the data
+ response = highlight(response, JsonLexer(), formatter)
+
+ # Get the stylesheet
+ style = " "
+
+ # Safe the output
+ return mark_safe(style + response)
def findMiddle(input_list):
"""Returns the middle element of a list"""
@@ -1690,6 +1720,8 @@ class UserProfile(models.Model):
username = property(getusername)
+ cimar_sid = models.CharField(max_length=100, blank=True)
+
def get_exams(self):
available_exams = defaultdict(list)
@@ -1826,4 +1858,89 @@ class ExamCollection(models.Model, AuthorMixin):
# settings.AUTH_USER_MODEL,
# blank=True,
# help_text="Author/Manager(s) of the exam collection",
-# )
\ No newline at end of file
+# )
+
+class CimarCase(models.Model):
+ uuid = models.CharField(max_length=255, unique=True)
+
+ viewer_link = models.URLField(blank=True)
+
+ study_details = models.JSONField(blank=True, default=dict)
+
+ study_schema = models.JSONField(blank=True, default=dict)
+
+ series_data = models.JSONField(blank=True, default=dict)
+
+ valid_uuid = models.BooleanField(default=False)
+
+ def load_series_data(self, api=None):
+ if api is None:
+ api = CimarAPI()
+ api.login(username=CIMAR_USERNAME, password=CIMAR_PASSWORD)
+
+
+
+
+ def refresh_study(self):
+ api = CimarAPI()
+ api.login(username=CIMAR_USERNAME, password=CIMAR_PASSWORD)
+
+ self.study_details = api.get_study_by_uuid(self.uuid)
+ self.study_schema = api.get_study_schema_by_uuid(self.uuid)
+
+ self.viewer_link = api.get_study_viewer_url_by_uuid(self.uuid)
+
+ series_data = []
+
+ for series in self.study_schema["series"]:
+ series_data.append({
+ "series_id" : series["id"],
+ "series_description" : series["description"],
+ "image_count": len(series["images"]),
+ "thumbnail": self.get_series_image_thumbnail_link(series)
+
+ }
+ )
+
+ self.series_data = {}
+ self.series_data["series"] = series_data
+
+ self.valid_uuid = True
+ self.save()
+
+ def __str__(self):
+ return f"{self.uuid} ({self.valid_uuid})"
+
+ def get_user_viewer_link(self, user):
+ return self.viewer_link + f"?sid={user.userprofile.cimar_sid}"
+
+
+ def get_pretty_study_details(self):
+ """Function to display pretty version of our data"""
+ return get_pretty_json(self.study_details)
+
+ def get_pretty_study_schema(self):
+ """Function to display pretty version of our data"""
+ return get_pretty_json(self.study_schema)
+
+ def get_pretty_study_series(self):
+ """Function to display pretty version of our data"""
+ return get_pretty_json(self.series_data)
+
+ #def get_series_details_block(self, cimar_sid=None):
+ # if "series" in self.series_data:
+ # return render_to_string("cimar_series.html", {"series_data": self.series_data["series"], cimar_sid: cimar_sid})
+ #
+ # return "No series data available"
+
+ # return render_to_string("cimar_series.html", {"series_data": self.series_data["series"]})
+
+
+
+ def get_series_image_thumbnail_link(self,series):
+
+ middle_point = len(series["images"]) // 2
+
+ image = series["images"][middle_point]
+
+ return STORAGE_API_URL + f"/study/{self.study_details["storage_namespace"]}/{self.study_details["study_uid"]}/image/{image["id"]}/version/{image["version"]}/frame/0/thumbnail"
diff --git a/generic/urls.py b/generic/urls.py
index a7336577..7fd1572f 100755
--- a/generic/urls.py
+++ b/generic/urls.py
@@ -243,6 +243,13 @@ urlpatterns = [
views.toggle_share_with_supervisor,
name="toggle_share_with_supervisor",
),
+ path(
+ "cimar_case//refresh",
+ views.cimar_case_refresh,
+ name="cimar_case_refresh"
+ ),
+
+
]
diff --git a/generic/views.py b/generic/views.py
index 7dfec8fe..8938a82b 100644
--- a/generic/views.py
+++ b/generic/views.py
@@ -84,6 +84,7 @@ from .models import (
CidUser,
CidUserExam,
CidUserGroup,
+ CimarCase,
ExamBase,
ExamCollection,
ExamUserStatus,
@@ -4084,4 +4085,17 @@ def exam_collection_add_marker(request, collection_id, exam_type):
collection.add_marker_to_exams(users, exam_types=(exam_type,))
- return HttpResponse("Marker added to exams")
\ No newline at end of file
+ return HttpResponse("Marker added to exams")
+
+
+def cimar_case_refresh(request, uuid):
+ try:
+ CimarCase.objects.get(uuid=uuid).refresh_study()
+ except CimarCase.DoesNotExist:
+ CimarCase(uuid=uuid).save()
+
+
+
+ return HttpResponse("Case refreshed")
+
+
\ No newline at end of file
diff --git a/helpers/cimar.py b/helpers/cimar.py
new file mode 100644
index 00000000..f05dc5b8
--- /dev/null
+++ b/helpers/cimar.py
@@ -0,0 +1,284 @@
+import json
+import requests
+from PIL import Image
+import io
+
+#def fetch_and_print_articles(api_url):
+# response = requests.get(api_url)
+#
+# if response.status_code == 200:
+# articles = response.json().get('articles', [])
+#
+# for index, article in enumerate(articles[:3], start=1):
+# print(f"Article {index}:\n{json.dumps(article, sort_keys=True, indent=4)}\n")
+# else:
+# print(f"Error: {response.status_code}")
+
+#API_KEY = '3805f6bbabcb42b3a0c08a489baf603d'
+#api_endpoint = f"https://newsapi.org/v2/top-headlines?country=us&category=business&apiKey={API_KEY}"
+
+
+
+
+API_URL = "https://cloud.cimar.co.uk/api/v3"
+STORAGE_API_URL = 'https://storelpu.cimar.co.uk' + "/api/v3/storage"
+
+class CimarAPI:
+
+ def __init__(self, sid=None, username=None, password=None):
+ self.sid = sid
+ self.context = False
+ self.username = username
+ self.password = password
+
+ # Default namespace to save studies to
+ self.namespace = "eff0ca95-6ce6-443c-80dd-1136c8a58e4d"
+
+ def get_storage(self, endpoint ):
+ pass
+
+ def post(self, endpoint, data=None, add_sid=True):
+ if data is None:
+ data = {}
+ if add_sid:
+ if self.sid is None:
+ raise NoSession("No session ID")
+ data["sid"] = self.sid
+
+ print(f"{data=}")
+
+ r = requests.post(API_URL + endpoint, data=data)
+
+
+ if r.status_code == 412:
+ match r.json():
+ case {"status": "ERROR", "error_type": "NOT_FOUND"}:
+ raise NotFoundError("Study not found")
+ case {"status": "ERROR", "error_type": error}:
+ raise APIError(error)
+ if r.status_code == 401:
+ raise InvalidCredentials("Invalid credentials")
+
+ print(r.status_code)
+ print(r.content)
+
+ return r.json()
+
+
+ def login(self, username=None, password=None):
+ if username is None:
+ username = self.username
+ if password is None:
+ password = self.password
+
+ data={"login": username, "password": password}
+ r = requests.post(API_URL + "/session/login", data=data)
+
+ if r.status_code == 412:
+ raise InvalidCredentials()
+
+ self.sid = r.json()["sid"]
+
+ return self.sid
+
+ def check_login_status(self):
+ try:
+ data = self.post("/session/user")
+ except InvalidCredentials:
+ return False
+
+ return data["status"] == "OK"
+
+ def logout(self):
+ data = self.post("/session/logout")
+
+
+ def __enter__(self):
+ if self.sid is None:
+ self.login()
+
+ self.context = True
+
+ return self
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ if self.context:
+ self.logout()
+ self.sid = None
+
+ def list_studies(self):
+ return self.post("/study/list")
+
+ def get_study_image_thumbnail(self, study_json):
+ schema = self.get_study_schema(study_json)
+
+ image = schema["series"][0]["images"][0]
+
+ r = requests.get(STORAGE_API_URL + f"/study/{study_json["storage_namespace"]}/{study_json["study_uid"]}/image/{image["id"]}/version/{image["version"]}/frame/0/thumbnail", params={"sid": self.sid})
+
+ print(r.status_code)
+
+ img = Image.open(io.BytesIO(r.content))
+
+ print(r.json())
+
+# def get_series_image_thumbnail_link(self, study_json, series):
+#
+# middle_point = len(series["images"]) // 2
+#
+# image = series["images"][middle_point]
+#
+# return requests.Request("GET",STORAGE_API_URL + f"/study/{study_json["storage_namespace"]}/{study_json["study_uid"]}/image/{image["id"]}/version/{image["version"]}/frame/0/thumbnail").prepare()
+#
+
+ def get_diagnostic_image(self, study_json, image_id, version, frame):
+ r = requests.get(STORAGE_API_URL + f"/study/{study_json["storage_namespace"]}/{study_json["study_uid"]}/image/{image_id}/version/{version}/frame/{frame}/diagnostic", params={"sid": self.sid})
+ print(r.status_code)
+
+ img = Image.open(io.BytesIO(r.content))
+
+ print(r.json())
+
+ def get_study_by_uuid(self, uuid):
+ return self.post("/study/get", data={"uuid": uuid})
+
+ def get_study_viewer_url_by_uuid(self, uuid):
+ return self.get_study_by_uuid(uuid)["viewer_link"]
+
+
+ def get_study_schema_by_uuid(self, uuid):
+ study_json = self.get_study_by_uuid(uuid)
+ return self.get_study_schema(study_json)
+
+ def get_study_by_study_uid(self, study_uid):
+ return self.post("/study/get", data={"study_uid": study_uid, "storage_namespace": self.namespace, "phi_namespace": self.namespace})
+
+ def get_study_schema(self, study_json):
+ r = requests.get(STORAGE_API_URL + f"/study/{study_json["storage_namespace"]}/{study_json["study_uid"]}/schema", params={"sid": self.sid})
+
+ return r.json()
+
+ def get_series_data(self, cimar_study_json):
+ series_json = []
+ for series in cimar_study_json["series"]:
+
+ instances = []
+ for image in series["images"]:
+ p= requests.Request("GET", STORAGE_API_URL + f"/study/{cimar_study_json["namespace"]}/{cimar_study_json["study_uid"]}/image/{image["id"]}/version/{image["version"]}/frame/0/diagnostic", params={"sid": self.sid}).prepare()
+ instances.append({"metadata": {}, "url": f"{p.url}"})
+
+
+
+ new_json = {
+ "SeriesInstanceUID": series["series_uid"],
+ "SeriesDescription": series["description"],
+ "Modality": series["modality"],
+ "instances": instances
+ }
+
+ series_json.append(new_json)
+ return series_json
+
+ def get_study_dicom_json(self, cimar_study_json):
+
+ series_json = []
+ for series in cimar_study_json["series"]:
+
+ instances = []
+ for image in series["images"]:
+ p= requests.Request("GET", STORAGE_API_URL + f"/study/{cimar_study_json["namespace"]}/{cimar_study_json["study_uid"]}/image/{image["id"]}/version/{image["version"]}/frame/0/diagnostic", params={"sid": self.sid}).prepare()
+ instances.append({"metadata": {}, "url": f"dicomweb:{p.url}"})
+
+
+
+ new_json = {
+ "SeriesInstanceUID": series["series_uid"],
+ "SeriesDescription": series["description"],
+ "Modality": series["modality"],
+ "instances": instances
+ }
+
+ series_json.append(new_json)
+
+ studies_json = {
+ "studies": [
+ {
+ "StudyInstanceUID": "1.3.6.1.4.1.14519.5.2.1.6279.6001.298806137288633453246975630178",
+ #"StudyDate": "20000101",
+ "StudyTime": "",
+ "PatientName": "Patient^Name",
+ "PatientID": "LIDC-IDRI-0001",
+ "AccessionNumber": "",
+ "PatientAge": "",
+ "PatientSex": "",
+ "series": series_json,
+ }
+ ]
+
+ }
+
+ return studies_json
+
+ def get_session_user(self):
+ return self.post("/session/user")
+
+
+ def upload_dicom(self, file, study_uid=""):
+
+ if study_uid:
+ study_uid = f"&study_uid={study_uid}"
+
+ #data = {}
+
+ #data["sid"] = self.sid
+ #data["study_uid"] = "1.2.840.113773.2.10070.7173.20210208144916.37"
+ #data["image_uid"]= "1.2.840.113773.4.10070.7173.20210208144916.39"
+
+ files = open(file, "rb")
+ #headers = {'content-type': 'application/dicom'}
+ r = requests.post(STORAGE_API_URL + f"/namespace/{self.namespace}/image?sid={self.sid}{study_uid}", data=files)#, headers=headers)
+ print(r)
+ return r.json()
+
+
+#CimarAdminApi = CimarAPI().login("")
+
+
+
+
+if __name__ == "__main__":
+ with CimarAPI(username="ross.kruger@nhs.net", password="") as api:
+ print("***")
+ #data = api.get_session_user()
+ #data = api.upload_dicom()
+
+
+ data = api.get_study_by_study_uid("2.1159655940009962407176401939062805636618")
+ print(data)
+ pass
+ #print(api.sid)
+ ##studies = api.list_studies()
+ ##test_study = studies["studies"][0]
+
+ ##schema = api.get_study_schema(test_study)
+
+ #schema = api.get_study_schema_by_uuid("c9417b53-87aa-4c40-aca0-3fa34c225536")
+
+ #api.get_study_dicom_json(schema)
+
+ #print(schema)
+ ##img = api.get_study_image_thumbnail(test_study)
+
+
+
+class NoSession(Exception):
+ pass
+
+class InvalidCredentials(Exception):
+ pass
+
+class NotFoundError(Exception):
+ pass
+
+class APIError(Exception):
+ pass
\ No newline at end of file
diff --git a/rad/settings.py b/rad/settings.py
index 9a1faf64..5adc73ea 100644
--- a/rad/settings.py
+++ b/rad/settings.py
@@ -315,6 +315,9 @@ STATICFILES_DIRS = (
os.path.join(BASE_DIR, "rad", "static"), # That's it!!
)
+CIMAR_USERNAME = ""
+CIMAR_PASSWORD = ""
+
try:
from .settings_local import *
except ImportError as e:
diff --git a/rad/urls.py b/rad/urls.py
index 01ac6500..3840139d 100644
--- a/rad/urls.py
+++ b/rad/urls.py
@@ -158,6 +158,28 @@ urlpatterns = [
path("ac/", autocomplete_urls),
path('psutil/', psutil_urlpatterns(), name='psutil'),
path("cimar/", views.cimar, name="cimar"),
+ path(
+ "cimar/study//dicom_json",
+ views.cimar_study_dicom_json,
+ name="cimar_study_dicom_json",
+ ),
+ path(
+ "cimar/study//viewer",
+ views.cimar_study_viewer,
+ name="cimar_study_viewer",
+ ),
+ path(
+ "cimar/study//viewer/embed",
+ views.cimar_study_viewer_embed,
+ name="cimar_study_viewer_embed",
+ ),
+ path(
+ "cimar/study//series_block",
+ views.cimar_study_series_block,
+ name="cimar_study_series_block",
+ ),
+
+ path("cimar/login/", views.cimar_login, name="cimar_login"),
]
diff --git a/rad/views.py b/rad/views.py
index 79c9a191..7f404561 100644
--- a/rad/views.py
+++ b/rad/views.py
@@ -48,6 +48,7 @@ from anatomy.models import AnatomyQuestion
from anatomy.models import Answer as AnatomyAnswer
from rad.filters import UserListFilter
+from helpers.cimar import CimarAPI, InvalidCredentials, NoSession
from rapids.models import UserAnswer as RapidsUserAnswer
from rapids.models import Exam as RapidsExam
from rapids.models import Rapid
@@ -65,7 +66,7 @@ from rapids.views import GenericExamViews as RapidsExamsViews
from longs.views import GenericExamViews as LongsExamViews
from generic.forms import QuestionNoteForm
-from generic.models import CidUser, ExamCollection, QuestionNote, Supervisor, UserGrades, UserProfile
+from generic.models import CidUser, CimarCase, ExamCollection, QuestionNote, Supervisor, UserGrades, UserProfile
from django_filters.views import FilterView
@@ -919,3 +920,65 @@ def request_cid_details(request):
return HttpResponse(
f"Candidate details will be sent to the requested email (if it is found in the system). If you have not received an email after a few minutes you may need to contact {settings.CONTACT_EMAIL} "
)
+
+
+def cimar_study_dicom_json(request, uuid="c9417b53-87aa-4c40-aca0-3fa34c225536"):
+ with CimarAPI(request.user.userprofile.cimar_sid) as api:
+ print(api.sid)
+ #studies = api.list_studies()
+ #test_study = studies["studies"][0]
+
+ #schema = api.get_study_schema(test_study)
+
+ schema = api.get_study_schema_by_uuid("c9417b53-87aa-4c40-aca0-3fa34c225536")
+
+ return JsonResponse(api.get_study_dicom_json(schema))
+
+def cimar_study_viewer(request, uuid="c9417b53-87aa-4c40-aca0-3fa34c225536"):
+
+ case = get_object_or_404(CimarCase, uuid=uuid)
+
+ viewer_link = case.viewer_link + f"?sid={request.user.userprofile.cimar_sid}"
+ return render(request, "cimar_embed.html", {"viewer_link": viewer_link})
+
+def cimar_study_viewer_embed(request, uuid="c9417b53-87aa-4c40-aca0-3fa34c225536"):
+
+ case = get_object_or_404(CimarCase, uuid=uuid)
+
+ viewer_link = case.viewer_link + f"?sid={request.user.userprofile.cimar_sid}"
+ return render(request, "cimar_embed.html#embed", {"viewer_link": viewer_link})
+
+def cimar_study_series_block(request, uuid):
+
+ case = get_object_or_404(CimarCase, uuid=uuid)
+
+ return render(request, "cimar_series.html", {"series_data": case.series_data["series"], "cimar_sid": request.user.userprofile.cimar_sid})
+
+def cimar_login(request):
+
+ if request.htmx:
+ username = request.POST.get("username")
+ password = request.POST.get("password")
+
+
+
+ if not username and not password:
+ return HttpResponse("Please fill in login details")
+
+ try:
+ sid = CimarAPI().login(username, password)
+
+ request.user.userprofile.cimar_sid = sid
+ request.user.userprofile.save()
+ except InvalidCredentials:
+ return HttpResponse("Invalid login details")
+
+
+ return HttpResponse("Logged in")
+
+ else:
+ try:
+ login_status = CimarAPI(request.user.userprofile.cimar_sid).check_login_status()
+ except NoSession:
+ login_status = False
+ return render(request, "cimar_login.html", {"login_status": login_status, "cimar_sid": request.user.userprofile.cimar_sid})
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index 4dfe748a..7f8b367c 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -53,4 +53,6 @@ django_svelte_jsoneditor
psutil
django-psutil-dash
django-template-partials
-easy_thumbnails
\ No newline at end of file
+easy_thumbnails
+requests
+pygments
\ No newline at end of file
diff --git a/templates/cimar_embed.html b/templates/cimar_embed.html
new file mode 100644
index 00000000..f54e6fa6
--- /dev/null
+++ b/templates/cimar_embed.html
@@ -0,0 +1,39 @@
+{% extends 'base.html' %}
+{% load partials %}
+
+{% partialdef viewer %}
+
+
+
+
+
+
+
+
+
+{% endpartialdef %}
+
+{% partialdef embed %}
+Close
+
+{% partial viewer %}
+
+{% endpartialdef %}
+
+
+{% block content %}
+{% partial viewer %}
+
+{% endblock content %}
+
+
+{% block css %}
+
+{% endblock css %}
+
\ No newline at end of file
diff --git a/templates/cimar_login.html b/templates/cimar_login.html
new file mode 100644
index 00000000..fb4dabd0
--- /dev/null
+++ b/templates/cimar_login.html
@@ -0,0 +1,23 @@
+{% extends 'base.html' %}
+
+{% block content %}
+
+
+
+
CIMAR login
+Login status: {{ login_status }} ({{cimar_sid}})
+
+ Logging in here allows your account to be linked with CIMAR. You may periodically need to re-login to keep the link active.
+
+
+
+
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/templates/cimar_series.html b/templates/cimar_series.html
new file mode 100644
index 00000000..2c327487
--- /dev/null
+++ b/templates/cimar_series.html
@@ -0,0 +1,15 @@
+Series
+ Series:
+ {% for series in series_data %}
+
+
+ Series {{ forloop.counter }}:
+
+ {{series|get_item:"series_description"}}
+
+ Images: {{series|get_item:"image_count"}}
+
+
+
+ {% endfor %}
+
\ No newline at end of file