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):