feat: Enhance user uploads with import suggestions for already uploaded DICOM series

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
Ross
2026-04-30 22:20:09 +01:00
co-authored by Copilot
parent 771abf62e2
commit d12933e62a
2 changed files with 183 additions and 51 deletions
+118 -50
View File
@@ -27,6 +27,28 @@
<button type="button" class="btn btn-sm btn-outline-secondary deselect-all-btn" title="Deselect all in study">Deselect all</button>
</span>
</summary>
{% if study.import_suggestions %}
<div class="alert alert-info py-2 mt-2 mb-2">
<strong>Study already imported:</strong>
{% for suggestion in study.import_suggestions %}
<div class="d-flex flex-wrap align-items-center gap-2 mt-1">
<span>
Case:
<a href="{% url 'atlas:case_detail' suggestion.case.pk %}">{{ suggestion.case }}</a>
({{ suggestion.already_imported_count }} series already imported)
</span>
<button
type="button"
class="btn btn-sm btn-outline-primary import-missing-series-btn"
data-case-id="{{ suggestion.case.pk }}"
data-series-uids="{{ suggestion.missing_uids|join:',' }}"
>
Import remaining {{ suggestion.missing_uids|length }} series
</button>
</div>
{% endfor %}
</div>
{% endif %}
<ul class="study-series-list">
{% for series, n, tags, date in study.series %}
{% include 'atlas/partials/_series_item.html' with show_tags_link=True %}
@@ -191,68 +213,91 @@
}
});
function markSeriesImported(seriesId) {
const li = document.querySelector(`#series-list input[value="${seriesId}"]`)?.closest('li');
if (!li || li.classList.contains('imported')) return;
li.classList.add('imported');
if (!li.querySelector('.badge.bg-success')) {
const badge = document.createElement('span');
badge.className = 'badge bg-success ms-2';
badge.textContent = 'Imported';
li.appendChild(badge);
}
const checkbox = li.querySelector('input[name="selection"]');
if (checkbox) {
checkbox.disabled = true;
checkbox.checked = false;
}
const _sp = li.querySelector('span');
if (_sp) _sp.onclick = null;
li.style.pointerEvents = 'none';
li.style.opacity = '0.7';
}
async function importSeriesList(selectionList, importUrl, statusPrefix) {
const statusDiv = document.getElementById('import-status');
if (!statusDiv) return;
statusDiv.innerHTML = '';
const orderSeries = (document.querySelector('input[name="order-series"]:checked') || {}).value || '';
const csrfToken = '{{ csrf_token }}';
for (let i = 0; i < selectionList.length; i++) {
const seriesId = selectionList[i];
const statusId = `${statusPrefix}-${seriesId}`;
statusDiv.innerHTML += `<div id="${statusId}">Importing series ${seriesId}...</div>`;
try {
const response = await fetch(importUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-CSRFToken': csrfToken,
},
body: `selection=${encodeURIComponent(seriesId)}&order-series=${encodeURIComponent(orderSeries)}`,
credentials: 'same-origin',
});
const text = await response.text();
const statusEl = document.getElementById(statusId);
if (statusEl) {
statusEl.innerHTML = `Series ${seriesId}: ${text}`;
}
markSeriesImported(seriesId);
} catch (e) {
const statusEl = document.getElementById(statusId);
if (statusEl) {
statusEl.innerHTML = `Series ${seriesId}: <span style="color:red;">Failed</span>`;
}
}
}
statusDiv.innerHTML += '<div>All done.</div>';
updateSelectionCount();
}
const importButton = document.getElementById('import-dicoms-sequential-button');
if (importButton) {
importButton.addEventListener('click', async function() {
const checkboxes = Array.from(document.querySelectorAll('input[name="selection"]:checked'));
const allCheckboxes = Array.from(document.querySelectorAll('input[name="selection"]'));
// Only include checkboxes that are not disabled (i.e., not already imported)
const selectableCheckboxes = allCheckboxes.filter(cb => !cb.disabled);
const toImport = checkboxes.length
? checkboxes.filter(cb => !cb.disabled)
: selectableCheckboxes;
if (toImport.length === 0) {
alert("Please select at least one series to import (not already imported).");
const selectableSeries = allCheckboxes.filter(cb => !cb.disabled).map(cb => cb.value);
const selectedSeries = checkboxes.filter(cb => !cb.disabled).map(cb => cb.value);
const toImport = selectedSeries.length ? selectedSeries : selectableSeries;
if (!toImport.length) {
alert('Please select at least one series to import (not already imported).');
return;
}
const statusDiv = document.getElementById('import-status');
if (statusDiv) statusDiv.innerHTML = '';
const orderSeries = (document.querySelector('input[name="order-series"]:checked') || {}).value || '';
const csrfToken = '{{ csrf_token }}';
{% if case %}
const importUrl = "{% url 'api-1:import_dicoms_case' case.id %}";
{% else %}
const importUrl = "{% url 'api-1:import_dicoms' %}";
{% endif %}
for (let i = 0; i < toImport.length; i++) {
const seriesId = toImport[i].value;
statusDiv.innerHTML += `<div id="import-status-${seriesId}">Importing series ${seriesId}...</div>`;
try {
const response = await fetch(importUrl, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"X-CSRFToken": csrfToken,
},
body: `selection=${encodeURIComponent(seriesId)}&order-series=${encodeURIComponent(orderSeries)}`
});
const text = await response.text();
document.getElementById(`import-status-${seriesId}`).innerHTML = `Series ${seriesId}: ${text}`;
// Mark as imported in the UI and make unselectable
const li = document.querySelector(`#series-list input[value="${seriesId}"]`)?.closest('li');
if (li && !li.classList.contains('imported')) {
li.classList.add('imported');
if (!li.querySelector('.badge.bg-success')) {
const badge = document.createElement('span');
badge.className = 'badge bg-success ms-2';
badge.textContent = 'Imported';
li.appendChild(badge);
}
const checkbox = li.querySelector('input[name="selection"]');
if (checkbox) {
checkbox.disabled = true;
checkbox.checked = false;
}
const _sp = li.querySelector('span');
if (_sp) _sp.onclick = null;
li.style.pointerEvents = "none";
li.style.opacity = "0.7";
}
} catch (e) {
document.getElementById(`import-status-${seriesId}`).innerHTML = `Series ${seriesId}: <span style="color:red;">Failed</span>`;
}
}
statusDiv.innerHTML += "<div>All done.</div>";
await importSeriesList(toImport, importUrl, 'import-status');
});
}
@@ -346,6 +391,29 @@
}
});
document.body.addEventListener('click', async function (e) {
const trigger = e.target.closest('.import-missing-series-btn');
if (!trigger) return;
const casePk = trigger.dataset.caseId;
const seriesUids = (trigger.dataset.seriesUids || '')
.split(',')
.map(v => v.trim())
.filter(Boolean);
if (!casePk || !seriesUids.length) {
alert('No remaining series found for this study.');
return;
}
const confirmMsg = `Import ${seriesUids.length} remaining series into case ${casePk}?`;
if (!window.confirm(confirmMsg)) return;
const importTemplate = "{% url 'api-1:import_dicoms_case' 0 %}";
const importUrl = importTemplate.replace(/0\/?$/, casePk);
await importSeriesList(seriesUids, importUrl, `import-missing-${casePk}`);
});
// Row click toggles selection when clicking any non-interactive part of the series card
document.body.addEventListener('click', function (e) {
const item = e.target.closest('.series-item');