Add pretty JSON filter and enhance JSON display in viewer state and annotations
This commit is contained in:
@@ -55,12 +55,10 @@
|
|||||||
<details>
|
<details>
|
||||||
<summary class="small text-muted" style="cursor:pointer;">Show advanced (viewer state & annotations)</summary>
|
<summary class="small text-muted" style="cursor:pointer;">Show advanced (viewer state & annotations)</summary>
|
||||||
<div>
|
<div>
|
||||||
<strong>Viewer State:</strong>
|
{% include 'atlas/partials/json_pretty.html' with json_value=ds.viewerstate title='Viewer State' %}
|
||||||
<pre class="small">{{ ds.viewerstate|default:"{}" }}</pre>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<strong>Annotations:</strong>
|
{% include 'atlas/partials/json_pretty.html' with json_value=ds.annotations title='Annotations' %}
|
||||||
<pre class="small">{{ ds.annotations|default:"{}" }}</pre>
|
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
<div class="json-block mb-2">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-1">
|
||||||
|
<strong>{{ title }}</strong>
|
||||||
|
<div>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-secondary btn-copy-json" title="Copy JSON">Copy</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-secondary btn-toggle-json" title="Toggle JSON">Hide</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="json-container small" style="max-height:360px; overflow:auto; font-family: monospace; background: #f8f9fa; padding: .5rem; border-radius: .25rem;"></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function(){
|
||||||
|
// Parse template-provided JSON value into a JS object
|
||||||
|
var obj;
|
||||||
|
try{
|
||||||
|
obj = {{ json_value|default:"{}"|safe }};
|
||||||
|
}catch(e){
|
||||||
|
try{ obj = JSON.parse(String({{ json_value|default:"{}"|safe }})); }catch(e){ obj = {}; }
|
||||||
|
}
|
||||||
|
|
||||||
|
var container = document.currentScript.previousElementSibling;
|
||||||
|
var btnCopy = document.currentScript.parentNode.querySelector('.btn-copy-json');
|
||||||
|
var btnToggle = document.currentScript.parentNode.querySelector('.btn-toggle-json');
|
||||||
|
|
||||||
|
function renderWithFormatter(depth){
|
||||||
|
container.innerHTML = '';
|
||||||
|
if (window.JSONFormatter){
|
||||||
|
try{
|
||||||
|
var fmt = new JSONFormatter(obj, depth || 1, {hoverPreviewEnabled: false, animateOpen: false});
|
||||||
|
container.appendChild(fmt.render());
|
||||||
|
return fmt;
|
||||||
|
}catch(e){
|
||||||
|
container.textContent = JSON.stringify(obj, null, 2);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
container.textContent = JSON.stringify(obj, null, 2);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dynamic load of json-formatter-js if not already present
|
||||||
|
function ensureFormatter(cb){
|
||||||
|
if (window.JSONFormatter) return cb();
|
||||||
|
var s = document.createElement('script');
|
||||||
|
s.src = 'https://unpkg.com/json-formatter-js@2.3.3/dist/json-formatter.umd.js';
|
||||||
|
s.onload = function(){ setTimeout(cb, 0); };
|
||||||
|
s.onerror = function(){ cb(); };
|
||||||
|
document.head.appendChild(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep current formatter instance so we can re-render collapsed/expanded
|
||||||
|
var currentFormatter = null;
|
||||||
|
ensureFormatter(function(){ currentFormatter = renderWithFormatter(1); });
|
||||||
|
|
||||||
|
btnToggle.addEventListener('click', function(){
|
||||||
|
// Toggle between collapsed (depth=1) and expanded (depth=0 => fully expanded)
|
||||||
|
if (btnToggle.dataset.state === 'expanded'){
|
||||||
|
currentFormatter = renderWithFormatter(1);
|
||||||
|
btnToggle.dataset.state = 'collapsed';
|
||||||
|
btnToggle.textContent = 'Show';
|
||||||
|
} else {
|
||||||
|
currentFormatter = renderWithFormatter(0);
|
||||||
|
btnToggle.dataset.state = 'expanded';
|
||||||
|
btnToggle.textContent = 'Hide';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
btnCopy.addEventListener('click', function(){
|
||||||
|
var text = JSON.stringify(obj, null, 2);
|
||||||
|
if (navigator.clipboard){ navigator.clipboard.writeText(text).catch(function(){}); }
|
||||||
|
else { var ta = document.createElement('textarea'); ta.value = text; document.body.appendChild(ta); ta.select(); try{document.execCommand('copy');}catch(e){} document.body.removeChild(ta); }
|
||||||
|
});
|
||||||
|
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import json
|
||||||
|
from django import template
|
||||||
|
from django.utils.safestring import mark_safe
|
||||||
|
from django.utils.html import escape
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
|
||||||
|
@register.filter(name="pretty_json")
|
||||||
|
def pretty_json(value):
|
||||||
|
"""Pretty-print a JSON string or Python object for display inside <pre>.
|
||||||
|
|
||||||
|
Returns an HTML-escaped, pretty-formatted JSON string (marked safe).
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if isinstance(value, str):
|
||||||
|
obj = json.loads(value)
|
||||||
|
else:
|
||||||
|
obj = value
|
||||||
|
pretty = json.dumps(obj, indent=2, default=str, sort_keys=True)
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
pretty = str(value) if value is not None else "{}"
|
||||||
|
except Exception:
|
||||||
|
pretty = "{}"
|
||||||
|
|
||||||
|
return mark_safe(escape(pretty))
|
||||||
@@ -3395,6 +3395,19 @@ def case_download(request, pk):
|
|||||||
"id": case.pk,
|
"id": case.pk,
|
||||||
"title": case.title,
|
"title": case.title,
|
||||||
"authors": [str(a) for a in case.author.all()],
|
"authors": [str(a) for a in case.author.all()],
|
||||||
|
# Non-DICOM metadata
|
||||||
|
"description": case.description,
|
||||||
|
"history": case.history,
|
||||||
|
"discussion": case.discussion,
|
||||||
|
"report": case.report,
|
||||||
|
"created_date": getattr(case, "created_date", None),
|
||||||
|
"modified_date": getattr(case, "modified_date", None),
|
||||||
|
"presentation": [str(p) for p in case.presentation.all()],
|
||||||
|
"subspecialty": [str(s) for s in case.subspecialty.all()],
|
||||||
|
"pathological_process": [str(p) for p in case.pathological_process.all()],
|
||||||
|
"procedures": [str(p) for p in case.procedures.all()],
|
||||||
|
"conditions": [str(c) for c in case.condition.all()],
|
||||||
|
"case_size_bytes": case.get_total_series_images_size() if hasattr(case, "get_total_series_images_size") else None,
|
||||||
},
|
},
|
||||||
"series": [],
|
"series": [],
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user