diff --git a/generic/widgets.py b/generic/widgets.py
index a9a5d458..cb4015fc 100644
--- a/generic/widgets.py
+++ b/generic/widgets.py
@@ -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''
+ pk = getattr(ex, "pk", "")
+ options_html += f''
+ # badges for metadata where available
+ archive_badge = 'Archived' if getattr(ex, 'archive', False) else ''
+ open_badge = 'Open' if getattr(ex, 'open_access', False) else ''
+ mode_badge = 'Exam mode' if getattr(ex, 'exam_mode', False) else ''
+ cand_badge = 'Candidates only' if getattr(ex, 'candidates_only', False) else ''
selected_html += (
- f'
'
- f'{label}'
- f''
+ f''
+ f'{label}{archive_badge}{open_badge}{mode_badge}{cand_badge}'
+ f''
f''
)
except Exception:
@@ -349,8 +366,6 @@ class ExamSearchSelectMultipleWidget:
{f'{self.exam_model._meta.verbose_name.title()}
' if self.exam_model is not None else ''}
-
-
Advanced search tips
@@ -389,6 +404,8 @@ class ExamSearchSelectMultipleWidget:
Wildcard: use * to broaden results; wildcards may return more results and are rate-limited.
+
+
@@ -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):