Enhance exam review question fragment to display parts and responses with HTMX support for improved interactivity and user experience
This commit is contained in:
+35
-1
@@ -95,9 +95,43 @@ class AuthorOrCheckerRequiredMixin(object):
|
|||||||
|
|
||||||
@register.filter
|
@register.filter
|
||||||
def get_item(dictionary, key, default=None):
|
def get_item(dictionary, key, default=None):
|
||||||
|
"""Safe template filter to fetch an item by key/index from various containers.
|
||||||
|
|
||||||
|
Supports dict-like objects (uses .get), lists/tuples (indexing by integer
|
||||||
|
keys), and other indexable objects. Returns ``default`` when the lookup
|
||||||
|
fails or the input is None.
|
||||||
|
"""
|
||||||
if dictionary is None:
|
if dictionary is None:
|
||||||
return default
|
return default
|
||||||
return dictionary.get(key)
|
|
||||||
|
# If it's dict-like, prefer .get
|
||||||
|
try:
|
||||||
|
if hasattr(dictionary, "get") and callable(getattr(dictionary, "get")):
|
||||||
|
# Allow .get default behaviour
|
||||||
|
try:
|
||||||
|
return dictionary.get(key, default)
|
||||||
|
except Exception:
|
||||||
|
# Fall through to other strategies
|
||||||
|
pass
|
||||||
|
|
||||||
|
# If it's a list/tuple and key is integer-like, return by index
|
||||||
|
if isinstance(dictionary, (list, tuple)):
|
||||||
|
try:
|
||||||
|
idx = int(key)
|
||||||
|
except Exception:
|
||||||
|
return default
|
||||||
|
try:
|
||||||
|
return dictionary[idx]
|
||||||
|
except Exception:
|
||||||
|
return default
|
||||||
|
|
||||||
|
# Generic attempt: try __getitem__ with key (handles QueryDict, lists with int keys, etc.)
|
||||||
|
try:
|
||||||
|
return dictionary[key]
|
||||||
|
except Exception:
|
||||||
|
return default
|
||||||
|
except Exception:
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
def user_is_admin(user):
|
def user_is_admin(user):
|
||||||
|
|||||||
@@ -1,3 +1,101 @@
|
|||||||
|
<div class="card mb-3">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">Question</h5>
|
||||||
|
<p class="lead">{{ question|safe }}</p>
|
||||||
|
|
||||||
|
<div class="mt-3">
|
||||||
|
<h6>Parts</h6>
|
||||||
|
<ul class="list-group">
|
||||||
|
{% if question.get_questions %}
|
||||||
|
{% for part in question.get_questions %}
|
||||||
|
<li class="list-group-item d-flex justify-content-between align-items-start">
|
||||||
|
<div>
|
||||||
|
<strong>Part {{ forloop.counter }}.</strong>
|
||||||
|
<span class="ms-2">{{ part }}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{# If we can determine the correct answer for this part show a badge - best-effort #}
|
||||||
|
{% if question.get_answers %}
|
||||||
|
{% with ans=question.get_answers|get_item:forloop.counter0 %}
|
||||||
|
{% if ans %}
|
||||||
|
<span class="badge bg-primary">Answer: {{ ans }}</span>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<li class="list-group-item">No parts defined for this question.</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="mt-2">
|
||||||
|
<a class="btn btn-sm btn-outline-secondary" href="{% url app_name|add:':question_detail' question.pk %}" target="_blank">View full question details</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="mt-3">
|
||||||
|
<h6>Response summary</h6>
|
||||||
|
<div id="physics-response-summary"
|
||||||
|
hx-get="{% url 'physics:exam_review_question_summary' exam.pk q_index %}"
|
||||||
|
hx-trigger="load"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-indicator="#physics-summary-indicator-{{ question.pk }}">
|
||||||
|
<div id="physics-summary-indicator-{{ question.pk }}" style="display:none">
|
||||||
|
<span class="spinner-border spinner-border-sm text-primary" role="status" aria-hidden="true"></span>
|
||||||
|
<span class="visually-hidden">Loading summary</span>
|
||||||
|
</div>
|
||||||
|
<style> #physics-summary-indicator-{{ question.pk }}.htmx-request{display:inline-block !important;} </style>
|
||||||
|
<div class="small text-muted">Loading summary…</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<h6 class="mb-0">Responses in this exam</h6>
|
||||||
|
<div>
|
||||||
|
<button class="btn btn-sm btn-outline-primary"
|
||||||
|
hx-get="{% url 'physics:exam_review_question_responses' exam.pk q_index %}"
|
||||||
|
hx-target="#physics-responses-container"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-indicator="#physics-responses-indicator-{{ question.pk }}">Show responses</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2" id="physics-responses-container">
|
||||||
|
<div id="physics-responses-indicator-{{ question.pk }}" style="display:none">
|
||||||
|
<span class="spinner-border spinner-border-sm text-primary" role="status" aria-hidden="true"></span>
|
||||||
|
<span class="visually-hidden">Loading responses</span>
|
||||||
|
</div>
|
||||||
|
<style> #physics-responses-indicator-{{ question.pk }}.htmx-request{display:inline-block !important;} </style>
|
||||||
|
<div class="text-muted small">Responses are hidden — click “Show responses” to load.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between mt-3">
|
||||||
|
{% if prev_index is not None %}
|
||||||
|
<a class="btn btn-outline-primary"
|
||||||
|
hx-get="{% url app_name|add:':exam_review_question' exam.pk prev_index %}"
|
||||||
|
hx-target="#review-content"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-push-url="true">← Previous</a>
|
||||||
|
{% else %}
|
||||||
|
<span></span>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if next_index is not None %}
|
||||||
|
<a class="btn btn-primary"
|
||||||
|
hx-get="{% url app_name|add:':exam_review_question' exam.pk next_index %}"
|
||||||
|
hx-target="#review-content"
|
||||||
|
hx-swap="innerHTML"
|
||||||
|
hx-push-url="true">Next →</a>
|
||||||
|
{% else %}
|
||||||
|
<a class="btn btn-success" href="{% url app_name|add:':exam_overview' exam.pk %}">Complete review</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="card mb-3">
|
<div class="card mb-3">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="mb-2"><span class="badge bg-primary">Physics</span></div>
|
<div class="mb-2"><span class="badge bg-primary">Physics</span></div>
|
||||||
|
|||||||
Reference in New Issue
Block a user