Add patient grouping and selection functionality to user uploads

This commit is contained in:
Ross
2026-06-15 16:48:15 +01:00
parent b5d844e3aa
commit 8003dcd714
4 changed files with 206 additions and 17 deletions
+125 -3
View File
@@ -17,7 +17,97 @@
{% if series_list %}
<div id="series-list">
{% if grouped_series %}
{% if grouped_patients %}
{% for patient in grouped_patients %}
<div class="patient-block mb-4 p-3 border rounded" style="background: rgba(255,255,255,0.015); border-color: rgba(255,255,255,0.08) !important;">
<div class="patient-header d-flex flex-wrap align-items-center justify-content-between pb-2 mb-3 border-bottom border-secondary">
<span>
<i class="bi bi-person-badge me-2 text-info"></i>
<strong>Patient ID:</strong> <span class="badge bg-secondary font-monospace">{{ patient.patient_id }}</span>
</span>
<div class="btn-group">
<button type="button" class="btn btn-sm btn-outline-primary select-all-patient-btn" title="Select all series for this patient">Select Patient Series</button>
<button type="button" class="btn btn-sm btn-outline-secondary deselect-all-patient-btn" title="Deselect all series for this patient">Deselect all</button>
</div>
</div>
{% for study_uid, study in patient.studies %}
<details class="study-block {% if study.partial_imported_uids %}study-block--partial-import{% endif %}" open>
<summary>
<strong>{{ study.description|default:"(no description)" }}</strong>
<small class="text-muted"> ({{ study_uid }}) — {{ study.series|length }} series</small>
<span class="ms-2">
<button type="button" class="btn btn-sm btn-outline-secondary select-all-btn" title="Select all in study">Select all</button>
<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.partial_imported_uids %}
<div class="alert alert-warning py-2 mt-2 mb-2 d-flex flex-wrap align-items-center justify-content-between gap-2">
<div>
<strong>Partial import detected:</strong>
{{ study.partial_imported_count }} series in this study already exist.
The highlighted rows can finish importing the remaining images.
{% if study.matched_existing_series %}
<div class="small mt-1">
Existing series in Atlas:
{% for existing_series in study.matched_existing_series %}
<a href="{% url 'atlas:series_detail' existing_series.pk %}">{{ existing_series }}</a>{% if not forloop.last %}, {% endif %}
{% endfor %}
</div>
{% endif %}
</div>
<button
type="button"
class="btn btn-sm btn-warning import-partial-series-btn"
data-series-uids="{{ study.partial_imported_uids|join:',' }}"
{% if study.preferred_case_id %}data-case-id="{{ study.preferred_case_id }}"{% endif %}
>Finish these imports</button>
</div>
{% endif %}
{% 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 %}
{% if series in study.partial_imported_uids %}
{% if case %}
{% include 'atlas/partials/_series_item.html' with show_tags_link=True series_is_partial=True series_item_classes='series-item--partial-import' import_case_id=case.pk %}
{% else %}
{% include 'atlas/partials/_series_item.html' with show_tags_link=True series_is_partial=True series_item_classes='series-item--partial-import' import_case_id=study.preferred_case_id %}
{% endif %}
{% else %}
{% if case %}
{% include 'atlas/partials/_series_item.html' with show_tags_link=True series_is_partial=False series_item_classes='' import_case_id=case.pk %}
{% else %}
{% include 'atlas/partials/_series_item.html' with show_tags_link=True series_is_partial=False series_item_classes='' %}
{% endif %}
{% endif %}
{% endfor %}
</ul>
</details>
{% endfor %}
</div>
{% endfor %}
{% elif grouped_series %}
{% for study_uid, study in grouped_series %}
<details class="study-block {% if study.partial_imported_uids %}study-block--partial-import{% endif %}" open>
<summary>
@@ -233,7 +323,7 @@
</div>
</div>
<div class="modal fade" id="caseSeriesModal" tabindex="-1" aria-hidden="true">
<div class="modal fade" id="caseSeriesModal" aria-hidden="true">
<div class="modal-dialog modal-xl modal-dialog-centered modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
@@ -531,7 +621,7 @@
});
}
// Select / Deselect all series within a study block
// Select / Deselect all series within a study block or patient block
document.addEventListener('click', function (e) {
const target = e.target;
if (target.matches('.select-all-btn')) {
@@ -562,6 +652,34 @@
});
if (typeof updateSelectionCount === 'function') updateSelectionCount();
}
if (target.matches('.select-all-patient-btn')) {
const patientBlock = target.closest('.patient-block');
if (!patientBlock) return;
const inputs = patientBlock.querySelectorAll('input[name="selection"]');
inputs.forEach(cb => {
if (!cb.disabled) {
cb.checked = true;
const li = cb.closest('.series-item');
if (li) li.classList.add('selected');
cb.dispatchEvent(new Event('change', { bubbles: true }));
}
});
if (typeof updateSelectionCount === 'function') updateSelectionCount();
}
if (target.matches('.deselect-all-patient-btn')) {
const patientBlock = target.closest('.patient-block');
if (!patientBlock) return;
const inputs = patientBlock.querySelectorAll('input[name="selection"]');
inputs.forEach(cb => {
if (!cb.disabled) {
cb.checked = false;
const li = cb.closest('.series-item');
if (li) li.classList.remove('selected');
cb.dispatchEvent(new Event('change', { bubbles: true }));
}
});
if (typeof updateSelectionCount === 'function') updateSelectionCount();
}
});
// Listen for `case:selected` events dispatched by the case-search widget and perform the import
@@ -884,6 +1002,10 @@
color: #ffffff;
}
.select2-container {
width: 100% !important;
}
#series-list li {
}
#series-list .selected {