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}} + + + + + + Viewer + + +
+ Study details + {{ case.get_cimar_case.get_pretty_study_details }} +
+
+ Study schema + {{ case.get_cimar_case.get_pretty_study_schema }} +
+
+ Study 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. +
+ Username:
+Password:
+ +
+ +
+ + + +
+{% 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