Add new endpoints and templates for displaying marked answers in the improved marking UI
This commit is contained in:
@@ -23,7 +23,7 @@
|
||||
</div>
|
||||
<div class="text-end">
|
||||
{% if question.answer_help %}
|
||||
<button class="btn btn-sm btn-outline-info" data-bs-toggle="collapse" data-bs-target="#marking-help">Help</button>
|
||||
<button class="btn btn-sm btn-outline-info" data-bs-toggle="collapse" data-bs-target="#marking-help">Answer Help</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -39,7 +39,7 @@
|
||||
<form method="POST" class="post-form">{% csrf_token %}
|
||||
<p class="small text-muted">Click each answer to toggle through marks awarded (as per colour). This UI writes marks immediately.</p>
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<div class="answer-list key small">Key: <span class="correct">2 Marks</span>, <span class="half-correct">1 Mark</span>, <span class="incorrect">0 Marks</span></div>
|
||||
<div></div>
|
||||
<div>
|
||||
{% if question_details.current > 1 %}
|
||||
<a class="btn btn-outline-secondary btn-sm me-1" href="{% url 'anatomy:mark2' exam.id question_number|add:-1 %}">Previous</a>
|
||||
@@ -51,26 +51,28 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<button class="btn btn-sm btn-outline-info me-1" hx-get="{% url 'anatomy:mark2_exam_marked' exam.id question_number %}" hx-target="#exam-marked-container" hx-swap="innerHTML">Show exam-submitted answers</button>
|
||||
<button class="btn btn-sm btn-outline-info" hx-get="{% url 'anatomy:mark2_question_marked' question.pk %}" hx-target="#question-marked-container" hx-swap="innerHTML">Show all stored answers for question</button>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h6>Unmarked</h6>
|
||||
<ul id="unmarked-list" class="list-group answer-list mb-3">
|
||||
{% include 'anatomy/partials/mark2_unmarked_list_fragment.html' %}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h6>Marked</h6>
|
||||
<ul id="marked-list" class="list-group answer-list mb-3">
|
||||
{% include 'anatomy/partials/mark2_marked_list_fragment.html' %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<h6>Answers</h6>
|
||||
<ul id="answer-list" class="list-group answer-list mb-3">
|
||||
{# Render currently-stored marked answers first, then unmarked submissions #}
|
||||
{% include 'anatomy/partials/mark2_marked_list_fragment.html' %}
|
||||
{% include 'anatomy/partials/mark2_unmarked_list_fragment.html' %}
|
||||
</ul>
|
||||
<div id="exam-marked-container" class="mt-2"></div>
|
||||
<div id="question-marked-container" class="mt-2"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<div class="small text-muted key">Key: <span class="key-item correct">2 Marks</span>, <span class="key-item half-correct">1 Mark</span>, <span class="key-item incorrect">0 Marks</span></div>
|
||||
</div>
|
||||
|
||||
<span class="visually-hidden">{{ form.as_p }}</span>
|
||||
</form>
|
||||
</div>
|
||||
@@ -88,13 +90,25 @@
|
||||
<style>
|
||||
/* Ensure answer states are visible and clickable in the new layout */
|
||||
.answer { cursor: pointer; }
|
||||
.answer.correct { color: #155724; background-color: #d4edda; }
|
||||
.answer.half-correct { color: #856404; background-color: #fff3cd; }
|
||||
.answer.incorrect { color: #721c24; background-color: #f8d7da; }
|
||||
/* Use border to indicate mark state instead of background */
|
||||
.answer.correct { color: #155724; background-color: transparent; }
|
||||
.answer.half-correct { color: #856404; background-color: transparent; }
|
||||
.answer.incorrect { color: #721c24; background-color: transparent; }
|
||||
.answer.not-marked { color: inherit; background-color: transparent; }
|
||||
.answer-list .list-group-item pre { margin: 0; font-family: monospace; white-space: pre-wrap; }
|
||||
.mark-controls { display:flex; gap:0.25rem; }
|
||||
.mark-btn { padding: 0.15rem 0.4rem; font-size: 0.8rem; }
|
||||
/* Per-state border indicators on the list item */
|
||||
.marked-item { }
|
||||
.marked-correct { border-left: 4px solid #2a9d3f; }
|
||||
.marked-half-correct { border-left: 4px solid #ffb020; }
|
||||
.marked-incorrect { border-left: 4px solid #d3413a; }
|
||||
|
||||
/* Key items use a small left-border swatch to match the list markers */
|
||||
.key .key-item { display:inline-block; padding-left:8px; margin-left:6px; }
|
||||
.key .key-item.correct { border-left: 8px solid #2a9d3f; }
|
||||
.key .key-item.half-correct { border-left: 8px solid #ffb020; }
|
||||
.key .key-item.incorrect { border-left: 8px solid #d3413a; }
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
@@ -138,13 +152,19 @@
|
||||
// set class preserving 'answer' base
|
||||
el.className = 'answer ' + newState;
|
||||
|
||||
// move DOM node to the appropriate list for immediate feedback
|
||||
// mark/unmark list item visually (no moving between lists in single-column mode)
|
||||
const li = el.closest('li');
|
||||
if (li) {
|
||||
// clear any previous per-state classes
|
||||
li.classList.remove('marked-correct','marked-half-correct','marked-incorrect','marked-item');
|
||||
if (newState === 'not-marked') {
|
||||
document.getElementById('unmarked-list').appendChild(li);
|
||||
} else {
|
||||
document.getElementById('marked-list').appendChild(li);
|
||||
// nothing
|
||||
} else if (newState === 'correct') {
|
||||
li.classList.add('marked-item','marked-correct');
|
||||
} else if (newState === 'half-correct') {
|
||||
li.classList.add('marked-item','marked-half-correct');
|
||||
} else if (newState === 'incorrect') {
|
||||
li.classList.add('marked-item','marked-incorrect');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,7 +175,7 @@
|
||||
|
||||
function updateMarkedAnswersField() {
|
||||
const marked = { 'correct': [], 'half-correct': [], 'incorrect': [] };
|
||||
document.querySelectorAll('#marked-list span.answer').forEach(function(el) {
|
||||
document.querySelectorAll('#answer-list span.answer').forEach(function(el) {
|
||||
if (el.classList.contains('correct')) marked.correct.push((el.title || el.textContent).trim());
|
||||
else if (el.classList.contains('half-correct')) marked['half-correct'].push((el.title || el.textContent).trim());
|
||||
else if (el.classList.contains('incorrect')) marked.incorrect.push((el.title || el.textContent).trim());
|
||||
@@ -211,7 +231,7 @@
|
||||
}
|
||||
|
||||
function setMarkForSpan(el, numeric) {
|
||||
// numeric: 2 -> correct, 1 -> half-correct, 0 -> incorrect, null -> not-marked
|
||||
// numeric: 2 -> correct, 1 -> half-correct, 0 -> incorrect, null -> not-marked
|
||||
const li = el.closest('li');
|
||||
let state = 'not-marked';
|
||||
if (numeric === 2) state = 'correct';
|
||||
@@ -219,8 +239,17 @@
|
||||
else if (numeric === 0) state = 'incorrect';
|
||||
el.className = 'answer ' + state;
|
||||
if (li) {
|
||||
if (state === 'not-marked') document.getElementById('unmarked-list').appendChild(li);
|
||||
else document.getElementById('marked-list').appendChild(li);
|
||||
// clear previous per-state classes
|
||||
li.classList.remove('marked-correct','marked-half-correct','marked-incorrect','marked-item');
|
||||
if (state === 'not-marked') {
|
||||
// nothing
|
||||
} else if (state === 'correct') {
|
||||
li.classList.add('marked-item','marked-correct');
|
||||
} else if (state === 'half-correct') {
|
||||
li.classList.add('marked-item','marked-half-correct');
|
||||
} else if (state === 'incorrect') {
|
||||
li.classList.add('marked-item','marked-incorrect');
|
||||
}
|
||||
}
|
||||
updateMarkedAnswersField();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
{% comment %}List of distinct answers submitted within this exam for the question, with counts and mark badges{% endcomment %}
|
||||
<div class="card mt-2">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<h6 class="card-title mb-0">Answers submitted in this exam</h6>
|
||||
<div>
|
||||
<div class="btn-group btn-group-sm" role="group" aria-label="Order">
|
||||
<button class="btn btn-sm btn-outline-primary{% if request.GET.order == 'count' or not request.GET.order %} active{% endif %}" hx-get="{{ request.path }}?order=count" hx-swap="outerHTML" hx-target="closest .card">By count</button>
|
||||
<button class="btn btn-sm btn-outline-primary{% if request.GET.order == 'mark' %} active{% endif %}" hx-get="{{ request.path }}?order=mark" hx-swap="outerHTML" hx-target="closest .card">By score</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if not items %}
|
||||
<div class="small text-muted">No answers submitted in this exam.</div>
|
||||
{% else %}
|
||||
<ul class="list-group">
|
||||
{% for it in items %}
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<div><pre class="mb-0">{{ it.answer }}</pre></div>
|
||||
<div class="text-end">
|
||||
{% if it.mark == 'correct' %}
|
||||
<span class="badge bg-success me-2">2</span>
|
||||
{% elif it.mark == 'half-correct' %}
|
||||
<span class="badge bg-warning text-dark me-2">1</span>
|
||||
{% elif it.mark == 'incorrect' %}
|
||||
<span class="badge bg-danger me-2">0</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary me-2">-</span>
|
||||
{% endif %}
|
||||
<span class="badge bg-light text-dark me-2">{{ it.count }}</span>
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,38 @@
|
||||
{% comment %}All stored Answer choices for this question grouped by status{% endcomment %}
|
||||
<div class="card mt-2">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title">All stored answers for this question</h6>
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<h6>2 marks</h6>
|
||||
<ul class="list-group mb-2">
|
||||
{% for answer in correct_answers %}
|
||||
<li class="list-group-item"><pre class="mb-0">{{ answer.answer }}</pre></li>
|
||||
{% empty %}
|
||||
<li class="list-group-item small text-muted">None</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<h6>1 mark</h6>
|
||||
<ul class="list-group mb-2">
|
||||
{% for answer in half_mark_answers %}
|
||||
<li class="list-group-item"><pre class="mb-0">{{ answer.answer }}</pre></li>
|
||||
{% empty %}
|
||||
<li class="list-group-item small text-muted">None</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<h6>0 marks</h6>
|
||||
<ul class="list-group mb-2">
|
||||
{% for answer in incorrect_answers %}
|
||||
<li class="list-group-item"><pre class="mb-0">{{ answer.answer }}</pre></li>
|
||||
{% empty %}
|
||||
<li class="list-group-item small text-muted">None</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -105,6 +105,8 @@ urlpatterns = [
|
||||
# New improved marking UI (mark2) and small toggle endpoint used by JS/HTMX
|
||||
path("exam/<int:exam_pk>/<int:sk>/mark2", views.mark2, name="mark2"),
|
||||
path("exam/mark2/toggle", views.mark2_toggle_answer, name="mark2_toggle_answer"),
|
||||
path("exam/<int:exam_pk>/<int:sk>/mark2/exam_marked", views.mark2_exam_marked, name="mark2_exam_marked"),
|
||||
path("question/<int:pk>/mark2/question_marked", views.mark2_question_marked, name="mark2_question_marked"),
|
||||
]
|
||||
|
||||
urlpatterns.extend(generic_view_urls(views.GenericViews))
|
||||
|
||||
@@ -836,6 +836,70 @@ def mark2_toggle_answer(request):
|
||||
return JsonResponse({"status": "ok"})
|
||||
|
||||
|
||||
@login_required
|
||||
def mark2_exam_marked(request, exam_pk, sk):
|
||||
"""Return a partial listing of answers submitted for this question within the given exam.
|
||||
|
||||
Shows distinct submitted answers with counts and any existing mark/status.
|
||||
"""
|
||||
exam = get_object_or_404(Exam, pk=exam_pk)
|
||||
questions = exam.get_questions()
|
||||
try:
|
||||
question = questions[sk]
|
||||
except Exception:
|
||||
raise Http404("Question not found in exam")
|
||||
|
||||
ua_qs = question.cid_user_answers.filter(exam=exam)
|
||||
|
||||
# Allow client to request ordering by 'count' (default) or by 'mark' (badge)
|
||||
order = request.GET.get('order', 'count')
|
||||
|
||||
# Group distinct submitted answers and count occurrences
|
||||
grouped = ua_qs.values('answer_compare').annotate(count=Count('id'))
|
||||
|
||||
items = []
|
||||
for row in grouped:
|
||||
ans = row['answer_compare']
|
||||
cnt = row['count']
|
||||
a = question.answers.filter(answer_compare__iexact=ans).first()
|
||||
mark = None
|
||||
if a is not None:
|
||||
if a.status == Answer.MarkOptions.CORRECT:
|
||||
mark = 'correct'
|
||||
elif a.status == Answer.MarkOptions.HALF_MARK:
|
||||
mark = 'half-correct'
|
||||
elif a.status == Answer.MarkOptions.INCORRECT:
|
||||
mark = 'incorrect'
|
||||
items.append({'answer': ans, 'count': cnt, 'mark': mark})
|
||||
|
||||
# Sort items according to requested ordering
|
||||
if order == 'mark':
|
||||
weight = {'correct': 3, 'half-correct': 2, 'incorrect': 1, None: 0}
|
||||
items.sort(key=lambda x: (weight.get(x.get('mark')), x.get('count', 0)), reverse=True)
|
||||
else:
|
||||
items.sort(key=lambda x: x.get('count', 0), reverse=True)
|
||||
|
||||
context = {'exam': exam, 'question': question, 'items': items}
|
||||
return render(request, 'anatomy/partials/mark2_exam_marked_fragment.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
def mark2_question_marked(request, pk):
|
||||
"""Return a partial listing of all stored Answer choices for this question (grouped by status)."""
|
||||
question = get_object_or_404(AnatomyQuestion, pk=pk)
|
||||
correct_answers = question.answers.filter(status=Answer.MarkOptions.CORRECT)
|
||||
half_mark_answers = question.answers.filter(status=Answer.MarkOptions.HALF_MARK)
|
||||
incorrect_answers = question.answers.filter(status=Answer.MarkOptions.INCORRECT)
|
||||
|
||||
context = {
|
||||
'question': question,
|
||||
'correct_answers': correct_answers,
|
||||
'half_mark_answers': half_mark_answers,
|
||||
'incorrect_answers': incorrect_answers,
|
||||
}
|
||||
return render(request, 'anatomy/partials/mark2_question_marked_fragment.html', context)
|
||||
|
||||
|
||||
|
||||
#@login_required
|
||||
#def exam_scores_refresh(request, pk):
|
||||
|
||||
Reference in New Issue
Block a user