start integrating cimar
This commit is contained in:
@@ -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 = {
|
||||
@@ -418,6 +423,18 @@ class CaseForm(ModelForm):
|
||||
"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
|
||||
instance = ModelForm.save(self, False)
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -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("<a href='{}'>{}</a>", 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())
|
||||
|
||||
@@ -509,6 +521,8 @@ class Case(models.Model, AuthorMixin, QuestionMixin):
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def extract_image_dicom_json_from_ds(ds, url, image_index):
|
||||
to_keep = [
|
||||
"Columns",
|
||||
|
||||
@@ -19,7 +19,65 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div><a href="/ohif/viewer/dicomjson?url=https://www.penracourses.org.uk{% url 'atlas:case_dicom_json' case.pk %}">View case in OHIF</a> <a target="_blank" href="/ohif/viewer/dicomjson?url=https://www.penracourses.org.uk{% url 'atlas:case_dicom_json' case.pk %}">(new tab)</a>
|
||||
<div>
|
||||
|
||||
{% if case.cimar_uuid %}
|
||||
|
||||
<details><summary>Details</summary>
|
||||
<br/>
|
||||
Case uuid: {{case.cimar_uuid}}
|
||||
<br/>
|
||||
{{case.get_cimar_case.viewer_link}}?{{cimar_sid}}
|
||||
|
||||
<button
|
||||
hx-get='{% url "cimar_study_viewer_embed" case.cimar_uuid %}'
|
||||
hx-target="#modals-here"
|
||||
hx-trigger="click"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#modals-here"
|
||||
class="btn primary">Open Modal</button>
|
||||
|
||||
<div id="modals-here"
|
||||
class="modal modal-blur fade"
|
||||
style="display: none"
|
||||
aria-hidden="false"
|
||||
tabindex="-1">
|
||||
<div class="modal-dialog modal-lg modal-dialog-centered" role="document">
|
||||
<div class="modal-content"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a href='{% url "cimar_study_viewer" case.cimar_uuid %}'>Viewer</a>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>Study details</summary>
|
||||
{{ case.get_cimar_case.get_pretty_study_details }}
|
||||
</details>
|
||||
<details>
|
||||
<summary>Study schema</summary>
|
||||
{{ case.get_cimar_case.get_pretty_study_schema }}
|
||||
</details>
|
||||
<details>
|
||||
<summary>Study series</summary>
|
||||
<button
|
||||
id='load-series'
|
||||
hx-get='{% url "cimar_study_series_block" case.cimar_uuid %}'
|
||||
hx-trigger='load'
|
||||
hx-swap='outerHTML'>
|
||||
Load Series
|
||||
</button>
|
||||
</details>
|
||||
<a href='{% url "generic:cimar_case_refresh" case.cimar_uuid %}'>Refresh</a>
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
|
||||
{% else %}
|
||||
<a href="/ohif/viewer/dicomjson?url=https://www.penracourses.org.uk{% url 'atlas:case_dicom_json' case.pk %}">View case in OHIF</a> <a target="_blank" href="/ohif/viewer/dicomjson?url=https://www.penracourses.org.uk{% url 'atlas:case_dicom_json' case.pk %}">(new tab)</a>
|
||||
{% endif %}
|
||||
|
||||
|
||||
<div class="date">
|
||||
{{ case.created_date|date:"d/m/Y" }}
|
||||
|
||||
@@ -378,6 +378,7 @@ urlpatterns = [
|
||||
# path("all_questions/", views.all_questions, name="all_questions"),
|
||||
path("case/<int:pk>/scrap", views.atlas_scrap, name="case_scrap"),
|
||||
path("case/<int:pk>/delete", views.CaseDelete.as_view(), name="case_delete"),
|
||||
path("case/<int:case_id>/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/<int:pk>/create",
|
||||
|
||||
+37
-2
@@ -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")
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -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)),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
+118
-1
@@ -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 = "<style>" + formatter.get_style_defs() + "</style><br>"
|
||||
|
||||
# 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)
|
||||
|
||||
@@ -1827,3 +1859,88 @@ class ExamCollection(models.Model, AuthorMixin):
|
||||
# blank=True,
|
||||
# help_text="Author/Manager(s) of the exam collection",
|
||||
# )
|
||||
|
||||
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"
|
||||
|
||||
@@ -243,6 +243,13 @@ urlpatterns = [
|
||||
views.toggle_share_with_supervisor,
|
||||
name="toggle_share_with_supervisor",
|
||||
),
|
||||
path(
|
||||
"cimar_case/<str:uuid>/refresh",
|
||||
views.cimar_case_refresh,
|
||||
name="cimar_case_refresh"
|
||||
),
|
||||
|
||||
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -84,6 +84,7 @@ from .models import (
|
||||
CidUser,
|
||||
CidUserExam,
|
||||
CidUserGroup,
|
||||
CimarCase,
|
||||
ExamBase,
|
||||
ExamCollection,
|
||||
ExamUserStatus,
|
||||
@@ -4085,3 +4086,16 @@ 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")
|
||||
|
||||
|
||||
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")
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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:
|
||||
|
||||
+22
@@ -158,6 +158,28 @@ urlpatterns = [
|
||||
path("ac/", autocomplete_urls),
|
||||
path('psutil/', psutil_urlpatterns(), name='psutil'),
|
||||
path("cimar/", views.cimar, name="cimar"),
|
||||
path(
|
||||
"cimar/study/<str:uuid>/dicom_json",
|
||||
views.cimar_study_dicom_json,
|
||||
name="cimar_study_dicom_json",
|
||||
),
|
||||
path(
|
||||
"cimar/study/<str:uuid>/viewer",
|
||||
views.cimar_study_viewer,
|
||||
name="cimar_study_viewer",
|
||||
),
|
||||
path(
|
||||
"cimar/study/<str:uuid>/viewer/embed",
|
||||
views.cimar_study_viewer_embed,
|
||||
name="cimar_study_viewer_embed",
|
||||
),
|
||||
path(
|
||||
"cimar/study/<str:uuid>/series_block",
|
||||
views.cimar_study_series_block,
|
||||
name="cimar_study_series_block",
|
||||
),
|
||||
|
||||
path("cimar/login/", views.cimar_login, name="cimar_login"),
|
||||
|
||||
]
|
||||
|
||||
|
||||
+64
-1
@@ -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 <a href='mailto:{settings.CONTACT_EMAIL}'>{settings.CONTACT_EMAIL}</a>"
|
||||
)
|
||||
|
||||
|
||||
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})
|
||||
@@ -54,3 +54,5 @@ psutil
|
||||
django-psutil-dash
|
||||
django-template-partials
|
||||
easy_thumbnails
|
||||
requests
|
||||
pygments
|
||||
@@ -0,0 +1,39 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load partials %}
|
||||
|
||||
{% partialdef viewer %}
|
||||
<div style="height: 100vh; width: 95%, mangin: 0; padding: 0; overflow: hidden;">
|
||||
|
||||
|
||||
<iframe class="embedded-viewer" src="{{viewer_link}}" width="100%" height="95%" style="border:none;"></iframe>
|
||||
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.embedded-viewer {
|
||||
border: none;
|
||||
pointer-events: auto;
|
||||
margin: 0; height: 100%; overflow: hidden
|
||||
}
|
||||
</style>
|
||||
|
||||
{% endpartialdef %}
|
||||
|
||||
{% partialdef embed %}
|
||||
Close
|
||||
|
||||
{% partial viewer %}
|
||||
|
||||
{% endpartialdef %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
{% partial viewer %}
|
||||
|
||||
{% endblock content %}
|
||||
|
||||
|
||||
{% block css %}
|
||||
|
||||
{% endblock css %}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="anatomy">
|
||||
|
||||
|
||||
<h2>CIMAR login</h2>
|
||||
Login status: {{ login_status }} ({{cimar_sid}})<br/>
|
||||
|
||||
Logging in here allows your account to be linked with CIMAR. You may periodically need to re-login to keep the link active.
|
||||
<form method="POST" class="post-form">
|
||||
Username: <input type="text" id="username" name="username" value=""/> <br/>
|
||||
Password: <input type="password" id="password" name="password" value=""/> <br/>
|
||||
<button hx-post='{% url "cimar_login" %}'
|
||||
hx-target="#results">Login</button>
|
||||
</form>
|
||||
|
||||
<div id="results"></div>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,15 @@
|
||||
<h3>Series</h3>
|
||||
<div class="pre-whitespace multi-image-block"><b>Series:</b>
|
||||
{% for series in series_data %}
|
||||
<span class="series-block">
|
||||
<span>
|
||||
<span class="series-block-series-number">Series {{ forloop.counter }}:</span><br>
|
||||
<span>
|
||||
<span class='series-block-examination'>{{series|get_item:"series_description"}}</span><br/>
|
||||
<span class='series-block-thumbnail'><img src='{{series|get_item:"thumbnail"}}?{{cimar_sid}}' /></span><br/>
|
||||
<span class='series-block-image-number'>Images: <span class='series-block-image-number-count'>{{series|get_item:"image_count"}}</span></span>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
Reference in New Issue
Block a user