diff --git a/generic/widgets.py b/generic/widgets.py
index 8fa170fb..ca86d03c 100644
--- a/generic/widgets.py
+++ b/generic/widgets.py
@@ -347,8 +347,28 @@ class ExamSearchSelectMultipleWidget:
{f'
{self.exam_model._meta.verbose_name.title()}
' if self.exam_model is not None else ''}
+
+
+
+
Advanced search tips
+
+ - Basic: type any part of an exam's name, code or identifier (case-insensitive substring match).
+ - Field-specific searches: prefix with
field:term to restrict the search. Supported fields:
+
+ name: — matches the exam name.
+ code: — matches an exam code or short identifier.
+ id: or pk: — match by numeric id.
+ date: — match by date (YYYY-MM-DD) where available.
+ type: — narrow to an exam type (e.g. type:anatomy).
+
+
+ - Type filter: use the dropdown to restrict results to a specific exam type in addition to your query.
+ - To add exams, click the Add button beside a result. Use Add all results to import visible matches.
+ - Wildcard: use
* to broaden results; wildcards may return more results and are rate-limited.
+
+
@@ -364,6 +384,11 @@ class ExamSearchSelectMultipleWidget:
const results = document.getElementById('{results_id}');
const select = document.getElementById('{field_id}');
const selectedList = document.getElementById('{selected_list_id}');
+ const helpBtn = document.getElementById('exam_search_help_btn_{self.name}');
+ 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 widgetFieldName = '{self.name}';
let timeout = null;
function fetchResults(q) {{
@@ -371,7 +396,8 @@ class ExamSearchSelectMultipleWidget:
results.innerHTML = '';
return;
}}
- const url = '{url}?field=' + encodeURIComponent('{self.name}') + '&q=' + encodeURIComponent(q) + ("&model=" + encodeURIComponent('{self.exam_model._meta.label}') || '');
+ const modelParam = defaultModel ? '&model=' + encodeURIComponent(defaultModel) : '';
+ const url = '{url}?field=' + encodeURIComponent(widgetFieldName) + '&q=' + encodeURIComponent(q) + modelParam;
fetch(url)
.then(r => r.text())
.then(html => {{ results.innerHTML = html; }});
@@ -382,14 +408,34 @@ class ExamSearchSelectMultipleWidget:
timeout = setTimeout(() => fetchResults(search.value), 300);
}});
+ // Toggle help panel visibility
+ if (helpBtn && helpPanel) {{
+ helpBtn.addEventListener('click', function(e) {{
+ e.preventDefault();
+ helpPanel.classList.toggle('d-none');
+ const expanded = !helpPanel.classList.contains('d-none');
+ helpBtn.setAttribute('aria-expanded', expanded ? 'true' : 'false');
+ }});
+ }}
+
+ // Delegate add clicks from results
results.addEventListener('click', function(e) {{
const btn = e.target.closest('.exam-add-btn');
if (!btn) return;
const id = btn.getAttribute('data-exam-id');
const text = btn.getAttribute('data-exam-text') || btn.textContent.trim();
- addExamToField('{self.name}', id, text);
+ addExamToField(widgetFieldName, id, text);
}});
+ // Wire up "Add all results" button if present
+ const addAllBtn = results.parentElement.querySelector('.exam-add-all-btn');
+ if (addAllBtn) {{
+ addAllBtn.addEventListener('click', function(e) {{
+ e.preventDefault();
+ addAllExamsFromResults(widgetFieldName);
+ }});
+ }}
+
window.addExamToField = function(fieldName, id, text) {{
const sel = document.getElementById('id_' + fieldName);
if (!sel) return;
@@ -414,6 +460,17 @@ class ExamSearchSelectMultipleWidget:
const li = document.getElementById('selected_exam_' + fieldName + '_' + id);
if (li && li.parentNode) li.parentNode.removeChild(li);
}}
+
+ window.addAllExamsFromResults = function(fieldName) {{
+ const list = document.getElementById('exam_search_results_list_' + fieldName);
+ if (!list) return;
+ const items = list.querySelectorAll('li[data-exam-id]');
+ items.forEach(function(it) {{
+ const id = it.getAttribute('data-exam-id');
+ const text = it.getAttribute('data-exam-text') || it.textContent.trim();
+ addExamToField(fieldName, id, text);
+ }});
+ }}
}})();