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"),
|
("months", "Months"),
|
||||||
("years", "Years"),
|
("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:
|
class Meta:
|
||||||
model = NormalCase
|
model = NormalCase
|
||||||
@@ -554,9 +554,9 @@ class NormalCaseFilter(django_filters.FilterSet):
|
|||||||
|
|
||||||
def filter_min_value(self, queryset, name, value):
|
def filter_min_value(self, queryset, name, value):
|
||||||
try:
|
try:
|
||||||
unit = getattr(self.form, "cleaned_data", {}).get("age_unit", "years")
|
unit = getattr(self.form, "cleaned_data", {}).get("age_unit", "days")
|
||||||
except Exception:
|
except Exception:
|
||||||
unit = "years"
|
unit = "days"
|
||||||
try:
|
try:
|
||||||
days = self._convert_to_days(value, unit)
|
days = self._convert_to_days(value, unit)
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -565,15 +565,20 @@ class NormalCaseFilter(django_filters.FilterSet):
|
|||||||
|
|
||||||
def filter_max_value(self, queryset, name, value):
|
def filter_max_value(self, queryset, name, value):
|
||||||
try:
|
try:
|
||||||
unit = getattr(self.form, "cleaned_data", {}).get("age_unit", "years")
|
unit = getattr(self.form, "cleaned_data", {}).get("age_unit", "days")
|
||||||
except Exception:
|
except Exception:
|
||||||
unit = "years"
|
unit = "days"
|
||||||
try:
|
try:
|
||||||
days = self._convert_to_days(value, unit)
|
days = self._convert_to_days(value, unit)
|
||||||
except Exception:
|
except Exception:
|
||||||
return queryset
|
return queryset
|
||||||
return queryset.filter(age_days__lte=days)
|
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):
|
class ResourceFilter(django_filters.FilterSet):
|
||||||
name = django_filters.CharFilter(field_name='name', lookup_expr='icontains')
|
name = django_filters.CharFilter(field_name='name', lookup_expr='icontains')
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
<ul class="study-series-list">
|
<ul class="study-series-list">
|
||||||
{% for series, n, tags, date in study.series %}
|
{% for series, n, tags, date in study.series %}
|
||||||
<li class="series-item">
|
<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 }}">
|
<input class="hidden" type="checkbox" name="selection" value="{{ series }}">
|
||||||
<h5 class="series-title">{{ tags.SeriesDescription|default:series }}</h5>
|
<h5 class="series-title">{{ tags.SeriesDescription|default:series }}</h5>
|
||||||
<div class="series-meta small text-muted">
|
<div class="series-meta small text-muted">
|
||||||
@@ -65,7 +65,7 @@
|
|||||||
<ul>
|
<ul>
|
||||||
{% for series, n, tags, date in series_list %}
|
{% for series, n, tags, date in series_list %}
|
||||||
<li class="series-item">
|
<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 }}">
|
<input class="hidden" type="checkbox" name="selection" value="{{ series }}">
|
||||||
<h5 class="series-title">{{ tags.SeriesDescription|default:series }}</h5>
|
<h5 class="series-title">{{ tags.SeriesDescription|default:series }}</h5>
|
||||||
<div class="series-meta small text-muted">
|
<div class="series-meta small text-muted">
|
||||||
@@ -117,6 +117,8 @@
|
|||||||
</form>
|
</form>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
<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"
|
<button id="delete-uploads-button"
|
||||||
hx-post="{% url 'api-1:clear_dicoms' %}"
|
hx-post="{% url 'api-1:clear_dicoms' %}"
|
||||||
hx-include="[name='selection']"
|
hx-include="[name='selection']"
|
||||||
@@ -128,6 +130,7 @@
|
|||||||
type="button"
|
type="button"
|
||||||
class="btn btn-primary"
|
class="btn btn-primary"
|
||||||
>Import {% if case %}into case{% endif %}</button>
|
>Import {% if case %}into case{% endif %}</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="import-status"></div>
|
<div id="import-status"></div>
|
||||||
|
|
||||||
@@ -165,6 +168,34 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<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
|
// Show modal when HTMX swaps content into the modal body
|
||||||
document.body.addEventListener('htmx:afterSwap', function (e) {
|
document.body.addEventListener('htmx:afterSwap', function (e) {
|
||||||
try {
|
try {
|
||||||
@@ -252,7 +283,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<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) {
|
document.addEventListener('click', function (e) {
|
||||||
const target = e.target;
|
const target = e.target;
|
||||||
if (target.matches('.select-all-btn')) {
|
if (target.matches('.select-all-btn')) {
|
||||||
@@ -264,8 +295,10 @@
|
|||||||
cb.checked = true;
|
cb.checked = true;
|
||||||
const li = cb.closest('li');
|
const li = cb.closest('li');
|
||||||
if (li) li.classList.add('selected');
|
if (li) li.classList.add('selected');
|
||||||
|
cb.dispatchEvent(new Event('change', { bubbles: true }));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
if (typeof updateSelectionCount === 'function') updateSelectionCount();
|
||||||
}
|
}
|
||||||
if (target.matches('.deselect-all-btn')) {
|
if (target.matches('.deselect-all-btn')) {
|
||||||
const detail = target.closest('.study-block');
|
const detail = target.closest('.study-block');
|
||||||
@@ -276,10 +309,26 @@
|
|||||||
cb.checked = false;
|
cb.checked = false;
|
||||||
const li = cb.closest('li');
|
const li = cb.closest('li');
|
||||||
if (li) li.classList.remove('selected');
|
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>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
Reference in New Issue
Block a user