Enhance ExamSearch widgets to support customizable default filters and improve metadata display
This commit is contained in:
+59
-13
@@ -1,6 +1,15 @@
|
||||
from django.contrib.auth.models import User
|
||||
from django.urls import reverse
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.conf import settings
|
||||
import json
|
||||
|
||||
# Default filters for exam search widget. Can be overridden in Django settings
|
||||
# as `GENERIC_EXAM_SEARCH_DEFAULT_FILTERS` mapping, e.g.:
|
||||
# GENERIC_EXAM_SEARCH_DEFAULT_FILTERS = { 'archive': False, 'exam_mode': True }
|
||||
DEFAULT_EXAM_SEARCH_FILTERS = getattr(
|
||||
settings, "GENERIC_EXAM_SEARCH_DEFAULT_FILTERS", {"archive": False, "exam_mode": True}
|
||||
)
|
||||
|
||||
|
||||
class UserSearchSelectMultipleWidget:
|
||||
@@ -282,11 +291,13 @@ class ExamSearchSelectMultipleWidget:
|
||||
- display_fields: optional list of extra fields to show in results
|
||||
"""
|
||||
|
||||
def __init__(self, name, value, exam_model=None, display_fields=None):
|
||||
def __init__(self, name, value, exam_model=None, display_fields=None, default_filters=None):
|
||||
self.name = name
|
||||
self.value = value or []
|
||||
self.exam_model = exam_model
|
||||
self.display_fields = display_fields or []
|
||||
# default filters for this widget instance; fall back to module defaults
|
||||
self.default_filters = default_filters if default_filters is not None else DEFAULT_EXAM_SEARCH_FILTERS
|
||||
|
||||
def render(self):
|
||||
field_id = f"id_{self.name}"
|
||||
@@ -323,11 +334,17 @@ class ExamSearchSelectMultipleWidget:
|
||||
|
||||
for ex in instances:
|
||||
label = str(ex)
|
||||
options_html += f'<option value="{getattr(ex, "pk", "")}" selected>{label}</option>'
|
||||
pk = getattr(ex, "pk", "")
|
||||
options_html += f'<option value="{pk}" selected>{label}</option>'
|
||||
# badges for metadata where available
|
||||
archive_badge = '<span class="badge bg-secondary ms-2">Archived</span>' if getattr(ex, 'archive', False) else ''
|
||||
open_badge = '<span class="badge bg-success ms-2">Open</span>' if getattr(ex, 'open_access', False) else ''
|
||||
mode_badge = '<span class="badge bg-info text-dark ms-2">Exam mode</span>' if getattr(ex, 'exam_mode', False) else ''
|
||||
cand_badge = '<span class="badge bg-warning text-dark ms-2">Candidates only</span>' if getattr(ex, 'candidates_only', False) else ''
|
||||
selected_html += (
|
||||
f'<li id="selected_exam_{self.name}_{getattr(ex, "pk", "")}" class="list-group-item d-flex justify-content-between align-items-center">'
|
||||
f'<span>{label}</span>'
|
||||
f'<button type="button" class="btn btn-sm btn-outline-danger ms-2" onclick="removeExamFromField(\'{self.name}\', {getattr(ex, "pk", "")})">Remove</button>'
|
||||
f'<li id="selected_exam_{self.name}_{pk}" class="list-group-item d-flex justify-content-between align-items-center">'
|
||||
f'<span>{label}{archive_badge}{open_badge}{mode_badge}{cand_badge}</span>'
|
||||
f'<button type="button" class="btn btn-sm btn-outline-danger ms-2" onclick="removeExamFromField(\'{self.name}\', {pk})">Remove</button>'
|
||||
f'</li>'
|
||||
)
|
||||
except Exception:
|
||||
@@ -349,8 +366,6 @@ class ExamSearchSelectMultipleWidget:
|
||||
{f'<div class="badge bg-secondary small">{self.exam_model._meta.verbose_name.title()}</div>' if self.exam_model is not None else ''}
|
||||
<button type="button" id="exam_search_help_btn_{self.name}" class="btn btn-sm btn-outline-secondary" aria-expanded="false" aria-controls="exam_search_help_panel_{self.name}" title="Advanced search help">?</button>
|
||||
</div>
|
||||
<div id="{results_id}" class="mt-2"></div>
|
||||
|
||||
<div id="exam_search_help_panel_{self.name}" class="card card-body mt-2 d-none" style="font-size:.9rem;">
|
||||
<strong>Advanced search tips</strong>
|
||||
<div class="mb-2 mt-2">
|
||||
@@ -389,6 +404,8 @@ class ExamSearchSelectMultipleWidget:
|
||||
<li>Wildcard: use <code>*</code> to broaden results; wildcards may return more results and are rate-limited.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div id="{results_id}" class="mt-2"></div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -408,6 +425,7 @@ class ExamSearchSelectMultipleWidget:
|
||||
const helpPanel = document.getElementById('exam_search_help_panel_{self.name}');
|
||||
|
||||
const defaultModel = '{self.exam_model._meta.label if self.exam_model is not None else ''}';
|
||||
const defaultFilters = {json.dumps(self.default_filters)};
|
||||
const widgetFieldName = '{self.name}';
|
||||
|
||||
let timeout = null;
|
||||
@@ -422,10 +440,30 @@ class ExamSearchSelectMultipleWidget:
|
||||
const f_exam_mode = document.getElementById('filter_exam_mode_{self.name}');
|
||||
const f_candidates = document.getElementById('filter_candidates_only_{self.name}');
|
||||
let extra = '';
|
||||
if (f_archive && f_archive.checked) extra += '&archive=1';
|
||||
if (f_open && f_open.checked) extra += '&open_access=1';
|
||||
if (f_exam_mode && f_exam_mode.checked) extra += '&exam_mode=1';
|
||||
if (f_candidates && f_candidates.checked) extra += '&candidates_only=1';
|
||||
|
||||
// Initialize checkbox states from defaults (if provided)
|
||||
try {{
|
||||
if (f_archive && defaultFilters.hasOwnProperty('archive')) f_archive.checked = !!defaultFilters.archive;
|
||||
if (f_open && defaultFilters.hasOwnProperty('open_access')) f_open.checked = !!defaultFilters.open_access;
|
||||
if (f_exam_mode && defaultFilters.hasOwnProperty('exam_mode')) f_exam_mode.checked = !!defaultFilters.exam_mode;
|
||||
if (f_candidates && defaultFilters.hasOwnProperty('candidates_only')) f_candidates.checked = !!defaultFilters.candidates_only;
|
||||
}} catch (e) {{ /* ignore */ }}
|
||||
|
||||
// For each supported filter, send the param if there's a default or the checkbox exists.
|
||||
function appendFilterParam(name, checkboxEl, defaultVal) {{
|
||||
if (typeof defaultVal !== 'undefined') {{
|
||||
const val = checkboxEl ? checkboxEl.checked : defaultVal;
|
||||
extra += '&' + encodeURIComponent(name) + '=' + (val ? '1' : '0');
|
||||
}} else if (checkboxEl && checkboxEl.checked) {{
|
||||
extra += '&' + encodeURIComponent(name) + '=1';
|
||||
}}
|
||||
}}
|
||||
|
||||
appendFilterParam('archive', f_archive, defaultFilters.archive);
|
||||
appendFilterParam('open_access', f_open, defaultFilters.open_access);
|
||||
appendFilterParam('exam_mode', f_exam_mode, defaultFilters.exam_mode);
|
||||
appendFilterParam('candidates_only', f_candidates, defaultFilters.candidates_only);
|
||||
|
||||
const url = '{url}?field=' + encodeURIComponent(widgetFieldName) + '&q=' + encodeURIComponent(q) + modelParam + extra;
|
||||
fetch(url)
|
||||
.then(r => r.text())
|
||||
@@ -518,16 +556,24 @@ class ExamSearchWidget:
|
||||
needs_multipart_form = False
|
||||
use_fieldset = False
|
||||
|
||||
def __init__(self, field=None, exam_model=None, display_fields=None):
|
||||
def __init__(self, field=None, exam_model=None, display_fields=None, default_filters=None):
|
||||
self.field = field
|
||||
self.attrs = {}
|
||||
self.exam_model = exam_model
|
||||
self.display_fields = display_fields or []
|
||||
# allow overriding defaults per widget instance
|
||||
self.default_filters = default_filters if default_filters is not None else DEFAULT_EXAM_SEARCH_FILTERS
|
||||
|
||||
def render(self, name, value, attrs=None, renderer=None):
|
||||
self.attrs = attrs or {}
|
||||
value = value or []
|
||||
widget = ExamSearchSelectMultipleWidget(name, value, exam_model=self.exam_model, display_fields=self.display_fields)
|
||||
widget = ExamSearchSelectMultipleWidget(
|
||||
name,
|
||||
value,
|
||||
exam_model=self.exam_model,
|
||||
display_fields=self.display_fields,
|
||||
default_filters=self.default_filters,
|
||||
)
|
||||
return widget.render()
|
||||
|
||||
def value_from_datadict(self, data, files, name):
|
||||
|
||||
Reference in New Issue
Block a user