Refactor exam management: update exam edit form and add exam links partial
This commit is contained in:
@@ -1,10 +1,11 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
<div id="exam-list">
|
<div id="exam-list">
|
||||||
<form hx-post="{% url 'anatomy:question_add_exam' question_id=question.pk %}" hx-target="#exam-list" hx-swap="innerHTML">
|
<form id="exam-edit-form">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
{{ form.media }}
|
||||||
<div class="input-group mb-2">
|
<div class="input-group mb-2">
|
||||||
{{ form.exam }}
|
{{ form.exam }}
|
||||||
<button class="btn btn-sm btn-primary" type="submit">Save</button>
|
<button class="btn btn-sm btn-primary" type="button" hx-post="{% url 'anatomy:question_add_exam' question_id=question.pk %}" hx-include="#id_exam" hx-target="#exam-list" hx-swap="innerHTML">Save</button>
|
||||||
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="document.getElementById('exam-list').innerHTML='';">Close</button>
|
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="document.getElementById('exam-list').innerHTML='';">Close</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<div id="exam-links" class="small" hx-swap-oob="outerHTML">
|
||||||
|
{% if question.exams.all %}
|
||||||
|
{% for exam in question.exams.all %}
|
||||||
|
<a href="{% url 'anatomy:exam_overview' pk=exam.pk %}" class="me-2">{{ exam }}</a>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
No exams
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
@@ -156,13 +156,7 @@
|
|||||||
|
|
||||||
<div class="mt-3">
|
<div class="mt-3">
|
||||||
<h6>Exams</h6>
|
<h6>Exams</h6>
|
||||||
<div class="small">
|
{% include 'anatomy/partials/exam_links.html' %}
|
||||||
{% for exam in question.exams.all %}
|
|
||||||
<a href="{% url 'anatomy:exam_overview' pk=exam.pk %}" class="me-2">{{ exam }}</a>
|
|
||||||
{% empty %}
|
|
||||||
No exams
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% if can_edit %}
|
{% if can_edit %}
|
||||||
<div class="mt-2"><button class="btn btn-sm btn-outline-secondary" hx-get="{% url 'anatomy:question_add_exam' question_id=question.pk %}" hx-target="#exam-list" hx-swap="innerHTML">Edit exam(s)</button>
|
<div class="mt-2"><button class="btn btn-sm btn-outline-secondary" hx-get="{% url 'anatomy:question_add_exam' question_id=question.pk %}" hx-target="#exam-list" hx-swap="innerHTML">Edit exam(s)</button>
|
||||||
<span id="exam-list"></span>
|
<span id="exam-list"></span>
|
||||||
|
|||||||
+34
-6
@@ -30,6 +30,7 @@ from dal import autocomplete
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.utils.html import format_html_join
|
from django.utils.html import format_html_join
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
from django.template.loader import render_to_string
|
||||||
|
|
||||||
|
|
||||||
from .forms import (
|
from .forms import (
|
||||||
@@ -1653,11 +1654,37 @@ def question_add_exam(request, question_id: int):
|
|||||||
else:
|
else:
|
||||||
question.exams.add(exam)
|
question.exams.add(exam)
|
||||||
|
|
||||||
# return updated fragment
|
# return out-of-band swap: update exam links and remove the editor
|
||||||
form = AddExamForm(user=request.user)
|
links_html = render_to_string("anatomy/partials/exam_links.html", {"question": question}, request=request)
|
||||||
return render(request, "anatomy/partials/exam_edit_fragment.html", {"form": form, "question": question})
|
oob_remove = '<div id="exam-list" hx-swap-oob="outerHTML"></div>'
|
||||||
|
return HttpResponse(mark_safe(links_html + oob_remove))
|
||||||
|
|
||||||
# else try to process from the search form
|
# else try to process from the search form or widget-provided values
|
||||||
|
# The ExamSearchWidget may submit a list; accept first value if present.
|
||||||
|
exam_vals = request.POST.getlist('exam') if hasattr(request.POST, 'getlist') else []
|
||||||
|
if not exam_vals:
|
||||||
|
# also accept single 'exam' param
|
||||||
|
single = request.POST.get('exam')
|
||||||
|
if single:
|
||||||
|
exam_vals = [single]
|
||||||
|
|
||||||
|
if exam_vals:
|
||||||
|
try:
|
||||||
|
exam = get_object_or_404(Exam, pk=int(exam_vals[0]))
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
exam = None
|
||||||
|
|
||||||
|
if exam:
|
||||||
|
if request.POST.get("remove", False) == "true":
|
||||||
|
question.exams.remove(exam)
|
||||||
|
else:
|
||||||
|
question.exams.add(exam)
|
||||||
|
|
||||||
|
links_html = render_to_string("anatomy/partials/exam_links.html", {"question": question}, request=request)
|
||||||
|
oob_remove = '<div id="exam-list" hx-swap-oob="outerHTML"></div>'
|
||||||
|
return HttpResponse(mark_safe(links_html + oob_remove))
|
||||||
|
|
||||||
|
# else try to process via bound form (fallback)
|
||||||
form = AddExamForm(request.POST, user=request.user)
|
form = AddExamForm(request.POST, user=request.user)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
exam = form.cleaned_data.get("exam")
|
exam = form.cleaned_data.get("exam")
|
||||||
@@ -1666,8 +1693,9 @@ def question_add_exam(request, question_id: int):
|
|||||||
else:
|
else:
|
||||||
question.exams.add(exam)
|
question.exams.add(exam)
|
||||||
|
|
||||||
form = AddExamForm(user=request.user)
|
links_html = render_to_string("anatomy/partials/exam_links.html", {"question": question}, request=request)
|
||||||
return render(request, "anatomy/partials/exam_edit_fragment.html", {"form": form, "question": question})
|
oob_remove = '<div id="exam-list" hx-swap-oob="outerHTML"></div>'
|
||||||
|
return HttpResponse(mark_safe(links_html + oob_remove))
|
||||||
else:
|
else:
|
||||||
# render form with errors
|
# render form with errors
|
||||||
return render(request, "anatomy/partials/exam_edit_fragment.html", {"form": form, "question": question})
|
return render(request, "anatomy/partials/exam_edit_fragment.html", {"form": form, "question": question})
|
||||||
|
|||||||
+25
-83
@@ -6675,30 +6675,27 @@ def exam_search_widget(request):
|
|||||||
if v is None:
|
if v is None:
|
||||||
return None
|
return None
|
||||||
return str(v).lower() in ("1", "true", "yes", "on")
|
return str(v).lower() in ("1", "true", "yes", "on")
|
||||||
|
# Render combined results, but restrict to exams the user can access
|
||||||
|
def user_can_view_exam(exam, user):
|
||||||
|
if user.is_superuser:
|
||||||
|
return True
|
||||||
|
if exam.open_access:
|
||||||
|
return True
|
||||||
|
authors = exam.get_author_objects()
|
||||||
|
if user in authors:
|
||||||
|
return True
|
||||||
|
if hasattr(exam, "markers") and user in exam.markers.all():
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
# Build queryset
|
# Build queryset
|
||||||
qs = None
|
qs = None
|
||||||
if model is not None:
|
if model is not None:
|
||||||
# Try common name-like fields for exams
|
field_lookups = ["name__icontains"]
|
||||||
from django.db.models import Q
|
|
||||||
|
|
||||||
field_lookups = ["name__icontains", "title__icontains", "examination__icontains", "modality__icontains"]
|
|
||||||
for lookup in field_lookups:
|
for lookup in field_lookups:
|
||||||
try:
|
qs = model.objects.filter(**{lookup: q})
|
||||||
qs = model.objects.filter(**{lookup: q})
|
if qs.exists():
|
||||||
if qs.exists():
|
break
|
||||||
break
|
|
||||||
except Exception:
|
|
||||||
qs = None
|
|
||||||
# fallback: try icontains on str() via name if present
|
|
||||||
if qs is None or not qs.exists():
|
|
||||||
try:
|
|
||||||
qs = model.objects.filter(Q(name__icontains=q) | Q(title__icontains=q))
|
|
||||||
except Exception:
|
|
||||||
try:
|
|
||||||
qs = model.objects.all()
|
|
||||||
except Exception:
|
|
||||||
qs = model.objects.none()
|
|
||||||
else:
|
else:
|
||||||
# If no model specified, try to search across several known exam models
|
# If no model specified, try to search across several known exam models
|
||||||
possible_models = [
|
possible_models = [
|
||||||
@@ -6738,30 +6735,6 @@ def exam_search_widget(request):
|
|||||||
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
|
|
||||||
def user_can_view_exam(exam, user):
|
|
||||||
try:
|
|
||||||
if user.is_superuser:
|
|
||||||
return True
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
try:
|
|
||||||
if exam.open_access:
|
|
||||||
return True
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
try:
|
|
||||||
authors = exam.get_author_objects()
|
|
||||||
if user in authors:
|
|
||||||
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 = []
|
rendered_results = []
|
||||||
for o in results[:50]:
|
for o in results[:50]:
|
||||||
@@ -6776,42 +6749,12 @@ def exam_search_widget(request):
|
|||||||
})
|
})
|
||||||
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})
|
||||||
|
|
||||||
|
logger.error(qs)
|
||||||
|
|
||||||
# Limit results
|
# Limit results
|
||||||
limit = 50 if q in ("*", "%") else 10
|
limit = 50 if q in ("*", "%") else 10
|
||||||
try:
|
qs = qs.order_by("name")[:limit]
|
||||||
qs = qs.order_by("name")[:limit]
|
|
||||||
except Exception:
|
|
||||||
qs = qs[:limit]
|
|
||||||
|
|
||||||
# Restrict results to exams the requesting 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
|
|
||||||
|
|
||||||
# Apply optional filters from GET params to the per-model queryset
|
# Apply optional filters from GET params to the per-model queryset
|
||||||
extra_filters = {}
|
extra_filters = {}
|
||||||
@@ -6821,17 +6764,14 @@ def exam_search_widget(request):
|
|||||||
extra_filters[pname] = val
|
extra_filters[pname] = val
|
||||||
|
|
||||||
results = []
|
results = []
|
||||||
|
logger.error(f"Exam search widget: model={model}, q='{q}', extra_filters={extra_filters}, initial_count={qs.count() if qs is not None else 'N/A'}")
|
||||||
for e in qs:
|
for e in qs:
|
||||||
# apply any extra_filters defensively (models may not have fields)
|
# apply any extra_filters defensively (models may not have fields)
|
||||||
skip = False
|
skip = False
|
||||||
for k, v in extra_filters.items():
|
for k, v in extra_filters.items():
|
||||||
try:
|
if getattr(e, k, None) != v:
|
||||||
if getattr(e, k, None) != v:
|
skip = True
|
||||||
skip = True
|
break
|
||||||
break
|
|
||||||
except Exception:
|
|
||||||
# if attribute access fails, don't skip
|
|
||||||
continue
|
|
||||||
if skip:
|
if skip:
|
||||||
continue
|
continue
|
||||||
if user_can_view_exam(e, request.user):
|
if user_can_view_exam(e, request.user):
|
||||||
@@ -6844,6 +6784,8 @@ def exam_search_widget(request):
|
|||||||
"candidates_only": getattr(e, "candidates_only", False),
|
"candidates_only": getattr(e, "candidates_only", False),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
logger.error(f"Exam search widget: returning {len(results)} results after filtering")
|
||||||
|
|
||||||
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})
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user