Refactor user uploads template to enhance selection functionality and update count display
This commit is contained in:
+10
-5
@@ -450,7 +450,7 @@ class NormalCaseFilter(django_filters.FilterSet):
|
||||
("months", "Months"),
|
||||
("years", "Years"),
|
||||
)
|
||||
age_unit = django_filters.ChoiceFilter(label="Unit", choices=AGE_UNIT_CHOICES)
|
||||
age_unit = django_filters.ChoiceFilter(label="Unit", choices=AGE_UNIT_CHOICES, method="filter_age_unit")
|
||||
|
||||
class Meta:
|
||||
model = NormalCase
|
||||
@@ -554,9 +554,9 @@ class NormalCaseFilter(django_filters.FilterSet):
|
||||
|
||||
def filter_min_value(self, queryset, name, value):
|
||||
try:
|
||||
unit = getattr(self.form, "cleaned_data", {}).get("age_unit", "years")
|
||||
unit = getattr(self.form, "cleaned_data", {}).get("age_unit", "days")
|
||||
except Exception:
|
||||
unit = "years"
|
||||
unit = "days"
|
||||
try:
|
||||
days = self._convert_to_days(value, unit)
|
||||
except Exception:
|
||||
@@ -565,15 +565,20 @@ class NormalCaseFilter(django_filters.FilterSet):
|
||||
|
||||
def filter_max_value(self, queryset, name, value):
|
||||
try:
|
||||
unit = getattr(self.form, "cleaned_data", {}).get("age_unit", "years")
|
||||
unit = getattr(self.form, "cleaned_data", {}).get("age_unit", "days")
|
||||
except Exception:
|
||||
unit = "years"
|
||||
unit = "days"
|
||||
try:
|
||||
days = self._convert_to_days(value, unit)
|
||||
except Exception:
|
||||
return queryset
|
||||
return queryset.filter(age_days__lte=days)
|
||||
|
||||
def filter_age_unit(self, queryset, name, value):
|
||||
# `age_unit` is a helper input used by min/max conversion and does not
|
||||
# map to a database field on NormalCase.
|
||||
return queryset
|
||||
|
||||
|
||||
class ResourceFilter(django_filters.FilterSet):
|
||||
name = django_filters.CharFilter(field_name='name', lookup_expr='icontains')
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
<ul class="study-series-list">
|
||||
{% for series, n, tags, date in study.series %}
|
||||
<li class="series-item">
|
||||
<div _="on click toggle .selected on me.closest('li') then set i to the first <input/> in me then set i.checked to not i.checked" class="series-left">
|
||||
<div class="series-left">
|
||||
<input class="hidden" type="checkbox" name="selection" value="{{ series }}">
|
||||
<h5 class="series-title">{{ tags.SeriesDescription|default:series }}</h5>
|
||||
<div class="series-meta small text-muted">
|
||||
@@ -65,7 +65,7 @@
|
||||
<ul>
|
||||
{% for series, n, tags, date in series_list %}
|
||||
<li class="series-item">
|
||||
<div _="on click toggle .selected on me.closest('li') then set i to the first <input/> in me then set i.checked to not i.checked" class="series-left">
|
||||
<div class="series-left">
|
||||
<input class="hidden" type="checkbox" name="selection" value="{{ series }}">
|
||||
<h5 class="series-title">{{ tags.SeriesDescription|default:series }}</h5>
|
||||
<div class="series-meta small text-muted">
|
||||
@@ -117,17 +117,20 @@
|
||||
</form>
|
||||
</details>
|
||||
|
||||
<button id="delete-uploads-button"
|
||||
hx-post="{% url 'api-1:clear_dicoms' %}"
|
||||
hx-include="[name='selection']"
|
||||
hx-confirm="This will clear selected uploads, Continue?"
|
||||
title="Deleted selected uploads"
|
||||
>Delete Uploads</button>
|
||||
<div id="selection-actions" class="d-flex align-items-center gap-2 mb-2">
|
||||
<div id="selection-count" class="small text-muted">Selected: <span id="selected-count">0</span></div>
|
||||
<button id="delete-uploads-button"
|
||||
hx-post="{% url 'api-1:clear_dicoms' %}"
|
||||
hx-include="[name='selection']"
|
||||
hx-confirm="This will clear selected uploads, Continue?"
|
||||
title="Deleted selected uploads"
|
||||
>Delete Uploads</button>
|
||||
|
||||
<button id="import-dicoms-sequential-button"
|
||||
type="button"
|
||||
class="btn btn-primary"
|
||||
>Import {% if case %}into case{% endif %}</button>
|
||||
<button id="import-dicoms-sequential-button"
|
||||
type="button"
|
||||
class="btn btn-primary"
|
||||
>Import {% if case %}into case{% endif %}</button>
|
||||
</div>
|
||||
|
||||
<div id="import-status"></div>
|
||||
|
||||
@@ -165,6 +168,34 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Update the selected count shown beside action buttons
|
||||
function updateSelectionCount() {
|
||||
const all = Array.from(document.querySelectorAll('input[name="selection"]'));
|
||||
const selected = all.filter(cb => cb.checked && !cb.disabled).length;
|
||||
const el = document.getElementById('selected-count');
|
||||
if (el) el.textContent = selected;
|
||||
// disable action buttons if none selected
|
||||
const deleteBtn = document.getElementById('delete-uploads-button');
|
||||
const importBtn = document.getElementById('import-dicoms-sequential-button');
|
||||
if (deleteBtn) deleteBtn.disabled = selected === 0;
|
||||
if (importBtn) importBtn.disabled = selected === 0;
|
||||
}
|
||||
|
||||
// Keep count up-to-date on checkbox changes and attribute changes
|
||||
document.body.addEventListener('change', function (e) {
|
||||
if (e.target && e.target.name === 'selection') updateSelectionCount();
|
||||
});
|
||||
|
||||
// Observe attribute changes (disabled) under the series list to update count
|
||||
(function () {
|
||||
const list = document.getElementById('series-list');
|
||||
if (!list) return;
|
||||
const mo = new MutationObserver(() => updateSelectionCount());
|
||||
mo.observe(list, { attributes: true, subtree: true, attributeFilter: ['disabled'] });
|
||||
})();
|
||||
|
||||
// Initialize count on page load
|
||||
document.addEventListener('DOMContentLoaded', updateSelectionCount);
|
||||
// Show modal when HTMX swaps content into the modal body
|
||||
document.body.addEventListener('htmx:afterSwap', function (e) {
|
||||
try {
|
||||
@@ -252,7 +283,7 @@
|
||||
</script>
|
||||
|
||||
<script>
|
||||
// Select / Deselect all series within a study block
|
||||
// Select / Deselect all series within a study block (JS-driven)
|
||||
document.addEventListener('click', function (e) {
|
||||
const target = e.target;
|
||||
if (target.matches('.select-all-btn')) {
|
||||
@@ -264,8 +295,10 @@
|
||||
cb.checked = true;
|
||||
const li = cb.closest('li');
|
||||
if (li) li.classList.add('selected');
|
||||
cb.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
});
|
||||
if (typeof updateSelectionCount === 'function') updateSelectionCount();
|
||||
}
|
||||
if (target.matches('.deselect-all-btn')) {
|
||||
const detail = target.closest('.study-block');
|
||||
@@ -276,10 +309,26 @@
|
||||
cb.checked = false;
|
||||
const li = cb.closest('li');
|
||||
if (li) li.classList.remove('selected');
|
||||
cb.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
});
|
||||
if (typeof updateSelectionCount === 'function') updateSelectionCount();
|
||||
}
|
||||
});
|
||||
|
||||
// Row click toggles selection (avoid when clicking interactive elements inside)
|
||||
document.body.addEventListener('click', function (e) {
|
||||
const el = e.target.closest('.series-left');
|
||||
if (!el) return;
|
||||
// ignore clicks on links, buttons or inputs inside the block
|
||||
if (e.target.closest('a,button,input')) return;
|
||||
const cb = el.querySelector('input[name="selection"]');
|
||||
if (!cb || cb.disabled) return;
|
||||
cb.checked = !cb.checked;
|
||||
const li = el.closest('li');
|
||||
if (li) li.classList.toggle('selected', cb.checked);
|
||||
cb.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
});
|
||||
</script>
|
||||
|
||||
<script>
|
||||
|
||||
Reference in New Issue
Block a user