.
This commit is contained in:
@@ -1,9 +1,16 @@
|
|||||||
{% if results %}
|
{% if results %}
|
||||||
<ul class="list-unstyled mb-0" id="exam_search_results_list_{{ field }}">
|
<ul class="list-unstyled mb-0" id="exam_search_results_list_{{ field }}">
|
||||||
{% for item in results %}
|
{% for item in results %}
|
||||||
<li class="py-1 d-flex justify-content-between align-items-center" data-exam-id="{{ item.id }}" data-exam-text="{{ item.text|escapejs }}">
|
<li class="py-1 d-flex justify-content-between align-items-center"
|
||||||
|
data-exam-id="{{ item.id }}" data-exam-text="{{ item.text|escapejs }}"
|
||||||
|
data-exam-archive="{{ item.archive|yesno:'1,0' }}" data-exam-open-access="{{ item.open_access|yesno:'1,0' }}"
|
||||||
|
data-exam-exam-mode="{{ item.exam_mode|yesno:'1,0' }}" data-exam-candidates-only="{{ item.candidates_only|yesno:'1,0' }}">
|
||||||
<div class="flex-grow-1">
|
<div class="flex-grow-1">
|
||||||
{{ item.text|escape }}
|
{{ item.text|escape }}
|
||||||
|
{% if item.archive %}<span class="badge bg-secondary ms-2">Archived</span>{% endif %}
|
||||||
|
{% if item.open_access %}<span class="badge bg-success ms-2">Open</span>{% endif %}
|
||||||
|
{% if item.exam_mode %}<span class="badge bg-info text-dark ms-2">Exam mode</span>{% endif %}
|
||||||
|
{% if item.candidates_only %}<span class="badge bg-warning text-dark ms-2">Candidates only</span>{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<button type="button" class="btn btn-sm btn-primary exam-add-btn" data-exam-id="{{ item.id }}" data-exam-text="{{ item.text|escapejs }}">Add</button>
|
<button type="button" class="btn btn-sm btn-primary exam-add-btn" data-exam-id="{{ item.id }}" data-exam-text="{{ item.text|escapejs }}">Add</button>
|
||||||
|
|||||||
+137
-12
@@ -6084,6 +6084,13 @@ def exam_search_widget(request):
|
|||||||
except Exception:
|
except Exception:
|
||||||
model = None
|
model = None
|
||||||
|
|
||||||
|
# Helper to parse optional boolean GET params
|
||||||
|
def _get_bool_param(name):
|
||||||
|
v = request.GET.get(name)
|
||||||
|
if v is None:
|
||||||
|
return None
|
||||||
|
return str(v).lower() in ("1", "true", "yes", "on")
|
||||||
|
|
||||||
# Build queryset
|
# Build queryset
|
||||||
qs = None
|
qs = None
|
||||||
if model is not None:
|
if model is not None:
|
||||||
@@ -6118,20 +6125,34 @@ def exam_search_widget(request):
|
|||||||
"sbas.Exam",
|
"sbas.Exam",
|
||||||
]
|
]
|
||||||
results = []
|
results = []
|
||||||
|
|
||||||
|
# gather optional filters from GET params
|
||||||
|
extra_filters = {}
|
||||||
|
for pname in ("archive", "open_access", "exam_mode", "candidates_only"):
|
||||||
|
val = _get_bool_param(pname)
|
||||||
|
if val is not None:
|
||||||
|
extra_filters[pname] = val
|
||||||
|
|
||||||
for ml in possible_models:
|
for ml in possible_models:
|
||||||
try:
|
try:
|
||||||
m = apps.get_model(*ml.split("."))
|
m = apps.get_model(*ml.split("."))
|
||||||
except Exception:
|
except Exception:
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
rqs = m.objects.filter(name__icontains=q)[:5]
|
# try common name-like fields and apply extra_filters when possible
|
||||||
except Exception:
|
|
||||||
try:
|
try:
|
||||||
rqs = m.objects.filter(title__icontains=q)[:5]
|
rqs = m.objects.filter(name__icontains=q, **extra_filters)[:5]
|
||||||
except Exception:
|
except Exception:
|
||||||
rqs = m.objects.none()
|
try:
|
||||||
|
rqs = m.objects.filter(title__icontains=q, **extra_filters)[:5]
|
||||||
|
except Exception:
|
||||||
|
# fallback to unfiltered small slice
|
||||||
|
rqs = m.objects.filter(name__icontains=q)[:5]
|
||||||
|
except Exception:
|
||||||
|
rqs = m.objects.none()
|
||||||
for o in rqs:
|
for o in rqs:
|
||||||
results.append(o)
|
results.append(o)
|
||||||
|
|
||||||
# Render combined results, but restrict to exams the user can access
|
# Render combined results, but restrict to exams the user can access
|
||||||
def user_can_view_exam(exam, user):
|
def user_can_view_exam(exam, user):
|
||||||
try:
|
try:
|
||||||
@@ -6160,7 +6181,14 @@ def exam_search_widget(request):
|
|||||||
rendered_results = []
|
rendered_results = []
|
||||||
for o in results[:50]:
|
for o in results[:50]:
|
||||||
if user_can_view_exam(o, request.user):
|
if user_can_view_exam(o, request.user):
|
||||||
rendered_results.append({"id": getattr(o, "pk", None), "text": str(o)})
|
rendered_results.append({
|
||||||
|
"id": getattr(o, "pk", None),
|
||||||
|
"text": str(o),
|
||||||
|
"archive": getattr(o, "archive", False),
|
||||||
|
"open_access": getattr(o, "open_access", False),
|
||||||
|
"exam_mode": getattr(o, "exam_mode", False),
|
||||||
|
"candidates_only": getattr(o, "candidates_only", False),
|
||||||
|
})
|
||||||
return render(request, "generic/partials/exam_search_widget_results.html", {"results": rendered_results, "field": field, "q": q_raw})
|
return render(request, "generic/partials/exam_search_widget_results.html", {"results": rendered_results, "field": field, "q": q_raw})
|
||||||
|
|
||||||
# Limit results
|
# Limit results
|
||||||
@@ -6200,10 +6228,36 @@ def exam_search_widget(request):
|
|||||||
pass
|
pass
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# Apply optional filters from GET params to the per-model queryset
|
||||||
|
extra_filters = {}
|
||||||
|
for pname in ("archive", "open_access", "exam_mode", "candidates_only"):
|
||||||
|
val = _get_bool_param(pname)
|
||||||
|
if val is not None:
|
||||||
|
extra_filters[pname] = val
|
||||||
|
|
||||||
results = []
|
results = []
|
||||||
for e in qs:
|
for e in qs:
|
||||||
|
# apply any extra_filters defensively (models may not have fields)
|
||||||
|
skip = False
|
||||||
|
for k, v in extra_filters.items():
|
||||||
|
try:
|
||||||
|
if getattr(e, k, None) != v:
|
||||||
|
skip = True
|
||||||
|
break
|
||||||
|
except Exception:
|
||||||
|
# if attribute access fails, don't skip
|
||||||
|
continue
|
||||||
|
if skip:
|
||||||
|
continue
|
||||||
if user_can_view_exam(e, request.user):
|
if user_can_view_exam(e, request.user):
|
||||||
results.append({"id": e.pk, "text": str(e)})
|
results.append({
|
||||||
|
"id": e.pk,
|
||||||
|
"text": str(e),
|
||||||
|
"archive": getattr(e, "archive", False),
|
||||||
|
"open_access": getattr(e, "open_access", False),
|
||||||
|
"exam_mode": getattr(e, "exam_mode", False),
|
||||||
|
"candidates_only": getattr(e, "candidates_only", False),
|
||||||
|
})
|
||||||
|
|
||||||
return render(request, "generic/partials/exam_search_widget_results.html", {"results": results, "field": field, "q": q_raw})
|
return render(request, "generic/partials/exam_search_widget_results.html", {"results": results, "field": field, "q": q_raw})
|
||||||
|
|
||||||
@@ -6223,14 +6277,85 @@ def supervisor_search(request):
|
|||||||
q = (request.GET.get("q") or "").strip()
|
q = (request.GET.get("q") or "").strip()
|
||||||
row = request.GET.get("row")
|
row = request.GET.get("row")
|
||||||
if not q:
|
if not q:
|
||||||
return HttpResponse("")
|
else:
|
||||||
|
# If no model specified, try to search across several known exam models
|
||||||
|
possible_models = [
|
||||||
|
"anatomy.Exam",
|
||||||
|
"longs.Exam",
|
||||||
|
"rapids.Exam",
|
||||||
|
"shorts.Exam",
|
||||||
|
"physics.Exam",
|
||||||
|
"sbas.Exam",
|
||||||
|
]
|
||||||
|
results = []
|
||||||
|
|
||||||
supervisors_qs = Supervisor.objects.filter(
|
# gather optional filters from GET params
|
||||||
Q(name__icontains=q) | Q(email__icontains=q)
|
extra_filters = {}
|
||||||
).order_by("name")[:10]
|
for pname in ("archive", "open_access", "exam_mode", "candidates_only"):
|
||||||
|
val = _get_bool_param(pname)
|
||||||
|
if val is not None:
|
||||||
|
extra_filters[pname] = val
|
||||||
|
|
||||||
results = [{"id": s.pk, "text": f"{s.name} - {s.email or ''}"} for s in supervisors_qs]
|
for ml in possible_models:
|
||||||
|
try:
|
||||||
|
m = apps.get_model(*ml.split("."))
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
# try common name-like fields and apply extra_filters when possible
|
||||||
|
try:
|
||||||
|
rqs = m.objects.filter(name__icontains=q, **extra_filters)[:5]
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
rqs = m.objects.filter(title__icontains=q, **extra_filters)[:5]
|
||||||
|
except Exception:
|
||||||
|
# fallback to unfiltered small slice
|
||||||
|
rqs = m.objects.filter(name__icontains=q)[:5]
|
||||||
|
except Exception:
|
||||||
|
rqs = m.objects.none()
|
||||||
|
for o in rqs:
|
||||||
|
results.append(o)
|
||||||
|
|
||||||
return render(request, "generic/partials/supervisor_search_results.html", {"results": results, "row": row, "q": q})
|
# Render combined results, but restrict to exams the user can access
|
||||||
|
def user_can_view_exam(exam, user):
|
||||||
|
try:
|
||||||
|
if user.is_superuser:
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
if getattr(exam, "open_access", False) and getattr(exam, "active", False):
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
authors = exam.get_author_objects()
|
||||||
|
if user in authors:
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
if hasattr(exam, "cid_user_exam") and exam.cid_user_exam.filter(user_user=user).exists():
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
if hasattr(exam, "markers") and user in exam.markers.all():
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return False
|
||||||
|
|
||||||
|
rendered_results = []
|
||||||
|
for o in results[:50]:
|
||||||
|
if user_can_view_exam(o, request.user):
|
||||||
|
rendered_results.append({
|
||||||
|
"id": getattr(o, "pk", None),
|
||||||
|
"text": str(o),
|
||||||
|
"archive": getattr(o, "archive", False),
|
||||||
|
"open_access": getattr(o, "open_access", False),
|
||||||
|
"exam_mode": getattr(o, "exam_mode", False),
|
||||||
|
"candidates_only": getattr(o, "candidates_only", False),
|
||||||
|
})
|
||||||
|
return render(request, "generic/partials/exam_search_widget_results.html", {"results": rendered_results, "field": field, "q": q_raw})
|
||||||
|
|
||||||
+32
-3
@@ -353,6 +353,27 @@ class ExamSearchSelectMultipleWidget:
|
|||||||
|
|
||||||
<div id="exam_search_help_panel_{self.name}" class="card card-body mt-2 d-none" style="font-size:.9rem;">
|
<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>
|
<strong>Advanced search tips</strong>
|
||||||
|
<div class="mb-2 mt-2">
|
||||||
|
<strong class="small">Filters</strong>
|
||||||
|
<div class="d-flex gap-2 mt-1">
|
||||||
|
<div class="form-check form-check-inline">
|
||||||
|
<input class="form-check-input" type="checkbox" id="filter_archive_{self.name}" />
|
||||||
|
<label class="form-check-label small" for="filter_archive_{self.name}">Archived only</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-check-inline">
|
||||||
|
<input class="form-check-input" type="checkbox" id="filter_open_access_{self.name}" />
|
||||||
|
<label class="form-check-label small" for="filter_open_access_{self.name}">Open access only</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-check-inline">
|
||||||
|
<input class="form-check-input" type="checkbox" id="filter_exam_mode_{self.name}" />
|
||||||
|
<label class="form-check-label small" for="filter_exam_mode_{self.name}">Exam mode only</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check form-check-inline">
|
||||||
|
<input class="form-check-input" type="checkbox" id="filter_candidates_only_{self.name}" />
|
||||||
|
<label class="form-check-label small" for="filter_candidates_only_{self.name}">Candidates only</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<ul class="mb-0 mt-1">
|
<ul class="mb-0 mt-1">
|
||||||
<li>Basic: type any part of an exam's name, code or identifier (case-insensitive substring match).</li>
|
<li>Basic: type any part of an exam's name, code or identifier (case-insensitive substring match).</li>
|
||||||
<li>Field-specific searches: prefix with <code>field:term</code> to restrict the search. Supported fields:
|
<li>Field-specific searches: prefix with <code>field:term</code> to restrict the search. Supported fields:
|
||||||
@@ -364,7 +385,6 @@ class ExamSearchSelectMultipleWidget:
|
|||||||
<li><code>type:</code> — narrow to an exam type (e.g. <code>type:anatomy</code>).</li>
|
<li><code>type:</code> — narrow to an exam type (e.g. <code>type:anatomy</code>).</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li>Type filter: use the dropdown to restrict results to a specific exam type in addition to your query.</li>
|
|
||||||
<li>To add exams, click the <em>Add</em> button beside a result. Use <em>Add all results</em> to import visible matches.</li>
|
<li>To add exams, click the <em>Add</em> button beside a result. Use <em>Add all results</em> to import visible matches.</li>
|
||||||
<li>Wildcard: use <code>*</code> to broaden results; wildcards may return more results and are rate-limited.</li>
|
<li>Wildcard: use <code>*</code> to broaden results; wildcards may return more results and are rate-limited.</li>
|
||||||
</ul>
|
</ul>
|
||||||
@@ -396,8 +416,17 @@ class ExamSearchSelectMultipleWidget:
|
|||||||
results.innerHTML = '';
|
results.innerHTML = '';
|
||||||
return;
|
return;
|
||||||
}}
|
}}
|
||||||
const modelParam = defaultModel ? '&model=' + encodeURIComponent(defaultModel) : '';
|
const modelParam = defaultModel ? '&model=' + encodeURIComponent(defaultModel) : '';
|
||||||
const url = '{url}?field=' + encodeURIComponent(widgetFieldName) + '&q=' + encodeURIComponent(q) + modelParam;
|
const f_archive = document.getElementById('filter_archive_{self.name}');
|
||||||
|
const f_open = document.getElementById('filter_open_access_{self.name}');
|
||||||
|
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';
|
||||||
|
const url = '{url}?field=' + encodeURIComponent(widgetFieldName) + '&q=' + encodeURIComponent(q) + modelParam + extra;
|
||||||
fetch(url)
|
fetch(url)
|
||||||
.then(r => r.text())
|
.then(r => r.text())
|
||||||
.then(html => {{ results.innerHTML = html; }});
|
.then(html => {{ results.innerHTML = html; }});
|
||||||
|
|||||||
Reference in New Issue
Block a user