Modal: multi-select projects and teams via Choices.js

Replaces the two single <select> elements in the report config modal
with <select multiple> enhanced by Choices.js (CDN 10.2.0, admin-only
gated, graceful fallback to native on CDN failure).

Removes the 'All Projects' / 'All Teams' placeholder option rows —
empty selection = all, matching Choices.js convention.

Persists selected values across submissions via two new context keys
(selected_project_ids, selected_team_ids) threaded through index() and
generate_report().

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Konrad du Plessis 2026-04-22 23:05:00 +02:00
parent 16d192d5fc
commit 748c7c79d7
2 changed files with 34 additions and 6 deletions

View File

@ -61,20 +61,18 @@
<!-- Project Filter (optional) -->
<div class="col-12">
<label class="form-label fw-semibold">Project <span class="text-muted fw-normal">(optional)</span></label>
<select name="project" class="form-select">
<option value="">All Projects</option>
<select name="project" class="form-select report-multi" multiple data-placeholder="All projects (leave empty for all)">
{% for p in projects %}
<option value="{{ p.id }}">{{ p.name }}</option>
<option value="{{ p.id }}"{% if p.id|stringformat:"s" in selected_project_ids %} selected{% endif %}>{{ p.name }}</option>
{% endfor %}
</select>
</div>
<!-- Team Filter (optional) -->
<div class="col-12">
<label class="form-label fw-semibold">Team <span class="text-muted fw-normal">(optional)</span></label>
<select name="team" class="form-select">
<option value="">All Teams</option>
<select name="team" class="form-select report-multi" multiple data-placeholder="All teams (leave empty for all)">
{% for t in teams %}
<option value="{{ t.id }}">{{ t.name }}</option>
<option value="{{ t.id }}"{% if t.id|stringformat:"s" in selected_team_ids %} selected{% endif %}>{{ t.name }}</option>
{% endfor %}
</select>
</div>
@ -132,3 +130,25 @@
if (modeCustom) modeCustom.addEventListener('change', toggleMode);
})();
</script>
{# === CHOICES.JS — multi-select enhancement (admin-only) === #}
{# Loaded CDN-only; falls back to native <select multiple> if the CDN fails. #}
{% if user.is_staff or user.is_superuser %}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/choices.js@10.2.0/public/assets/styles/choices.min.css">
<script src="https://cdn.jsdelivr.net/npm/choices.js@10.2.0/public/assets/scripts/choices.min.js" defer></script>
<script>
(function() {
document.addEventListener('DOMContentLoaded', function() {
if (typeof Choices === 'undefined') return; // graceful fallback
document.querySelectorAll('.report-multi').forEach(function(el) {
new Choices(el, {
removeItemButton: true,
shouldSort: false,
placeholder: true,
placeholderValue: el.getAttribute('data-placeholder') || '',
});
});
});
})();
</script>
{% endif %}

View File

@ -430,6 +430,9 @@ def index(request):
'certs_expired_count': certs_expired_count,
'certs_expiring_count': certs_expiring_count,
'certs_alert_total': certs_alert_total,
# Empty on the home dashboard — modal opens clean (no pre-selected filters)
'selected_project_ids': [],
'selected_team_ids': [],
}
return render(request, 'core/index.html', context)
@ -2383,6 +2386,11 @@ def generate_report(request):
# populate (same lists the Dashboard modal uses)
context['projects'] = Project.objects.all().order_by('name')
context['teams'] = Team.objects.all().order_by('name')
# For the modal's <select multiple> pre-selection: stringify the IDs so
# the template's `{% if p.id|stringformat:"s" in selected_project_ids %}`
# comparison works (Django templates compare strings to strings).
context['selected_project_ids'] = [str(p) for p in (project_ids or [])]
context['selected_team_ids'] = [str(t) for t in (team_ids or [])]
return render(request, 'core/report.html', context)