Files
penracourses/generic/templates/generic/exam_scores_base.html
T
Ross fa98541e31 .
2026-07-06 22:06:58 +01:00

214 lines
9.4 KiB
HTML

{% extends exam.app_name|add:'/exams.html' %}
{% block content %}
<div class="">
<h2>{{ exam }}</h2>
{% if missing_cids or missing_users %}
<details>
<summary>This exam has candidates / users with unsubmitted results</summary>
{% if missing_cids %}
<div class="alert alert-warning" role="alert">
The following CIDs results have not been submitted:
{% for cid in missing_cids %}
{{cid}},
{% endfor %}
</div>
{% endif %}
{% if missing_users %}
<div class="alert alert-warning" role="alert">
The following users results have not been submitted:
{% for user in missing_users %}
{{user}} ({{user.id}}),
{% endfor %}
</div>
{% endif %}
</details>
{% endif %}
{% if unmarked %}
<div class="alert alert-warning" role="alert">
The following questions need marking
{% for exam_index in unmarked %}
<a href="{% url exam.app_name|add:':mark' exam.pk exam_index %}">{{ exam_index|add:1 }}</a>
{% endfor %}
</div>
{% endif %}
</div>
<!-- Stats Container Lazy-Loader -->
<div id="stats-container"
hx-get="{% url exam.app_name|add:':exam_scores_all' exam.pk %}?stats_only=true"
hx-trigger="load"
hx-swap="outerHTML">
<div class="card mb-3 bg-dark text-light border-secondary shadow-sm">
<div class="card-body text-center py-4">
<div class="spinner-border text-info" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<div class="text-muted mt-2">Loading statistics...</div>
</div>
</div>
</div>
<!-- Candidate Search/Filter Input -->
<div class="row mb-3">
<div class="col-md-6 col-lg-4">
<input type="text" id="candidate-filter" class="form-control form-control-sm bg-dark text-light border-secondary" placeholder="Filter by candidate name or ID..." oninput="filterCandidates(this.value)">
</div>
</div>
<div class="mb-3">
<div class="table-responsive">
<table class="table table-dark table-striped table-hover table-sm cid-score-table sortable small" id="candidate-scores-list-table">
<tr>
<th title="click to sort by candidate / user id">Candidate ID</th>
<th title="click to sort by score">Score</th>
{% if exam.app_name == "longs" or exam.app_name == "rapids" %}
<th>Normalised Score</th>
{% endif %}
</tr>
{% for cid in cids %}
<tr class="candidate-row" data-answer-count={{user_answer_count|get_item:cid}} data-cid-name="{{ cids_user_id_map|get_item:cid|lower }}" data-cid-val="{{ cid|lower }}">
{% if cid|slice:":1" == "u" %}
<td class="cid" >{{cids_user_id_map|get_item:cid}}</td>
{% else %}
<td class="cid" data-cid="{{cid|slice:'2:'}}"><a href="{% url 'cid_scores_admin' cid|slice:'2:' %}">{{cid}}</a>
<span title="Click to show candidate details"
hx-get="{% url 'generic:cid_details' cid|slice:'2:' %}"
hx-target="this"
>
<i class="bi bi-binoculars search-cid" ></i>
</span>
</td>
{% endif %}
<td>{{user_scores|get_item:cid}}</td>
{% if exam.app_name == "longs" or exam.app_name == "rapids" %}
<td>{{user_scores_normalised|get_item:cid}}</td>
{% endif %}
</tr>
{% endfor %}
</table>
</div>
</div>
<div class="mb-3">
<div class="d-flex justify-content-between align-items-center mb-1">
<h5 class="mb-0">Answers as a table</h5>
<div>
<button class="btn btn-sm btn-outline-secondary" data-bs-toggle="collapse" data-bs-target="#answers-table-container" aria-expanded="true">Toggle answers</button>
</div>
</div>
<div class="collapse show" id="answers-table-container">
<div hx-get="{% url exam.app_name|add:':exam_scores_all' exam.pk %}?table_only=true"
hx-trigger="load"
hx-swap="outerHTML">
<div class="text-center py-4">
<div class="spinner-border text-info" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<div class="text-muted mt-2">Loading answers table...</div>
</div>
</div>
</div>
{% block breakdown %}{% endblock breakdown %}
<details
hx-get="{% url exam.app_name|add:':exam_report_email_status' exam_id=exam.pk %}"
hx-trigger="revealed"
hx-target="#user-details">
<summary>Email results</summary>
User results emailed: {{exam.exam_results_emailed|default:"Never"}}<br/>
<button id="email-results-button"
title="Email results to users and their supervisor"
hx-get="{% url exam.app_name|add:':exam_report_email' exam_id=exam.pk %}"
hx-target="#user-details"
hx-confirm="This will email results, please make sure scores have been refreshed before continuing"
hx-prompt="Please enter an additional email to send to (user and supervisor will automatically be used if available) if required"
>Email user results</button>
<button id="email-unsent-results-button" title="Email results to users and their supervisor"
hx-get="{% url exam.app_name|add:':exam_report_email_unsent' exam_id=exam.pk %}"
hx-target="#user-details"
hx-confirm="This will email results, please make sure scores have been refreshed before continuing"
hx-prompt="Please enter an additional email to send to (user and supervisor will automatically be used if available) if required"
>Email unsent user results</button>
<button id="email-results-check-button" title="Check the status of emailed results"
hx-get="{% url exam.app_name|add:':exam_report_email_status' exam_id=exam.pk %}"
hx-trigger="click"
hx-target="#user-details">Refresh email status</button>
<p>Note: currently only works with registered users</p>
<div id="user-details"></div>
</details>
</div>
{% endblock %}
{% block js %}
<script>
function filterCandidates(query) {
const q = query.toLowerCase().trim();
// Filter candidate list rows
document.querySelectorAll('.candidate-row').forEach(row => {
const name = row.getAttribute('data-cid-name') || '';
const val = row.getAttribute('data-cid-val') || '';
if (name.includes(q) || val.includes(q)) {
row.classList.remove('d-none');
} else {
row.classList.add('d-none');
}
});
// Filter columns in answers table
document.querySelectorAll('.col-cid-cell').forEach(cell => {
const name = cell.getAttribute('data-cid-name') || '';
const val = cell.getAttribute('data-cid-val') || '';
if (name.includes(q) || val.includes(q)) {
cell.classList.remove('d-none');
} else {
cell.classList.add('d-none');
}
});
}
$(document).ready(() => {
const question_number = {{ question_number }};
$(".candidate-row").each((index, el) => {
if (el.dataset.answerCount != question_number) {
$(el).addClass("missing-answers")
}
});
});
// Re-run filter after HTMX content is swapped in
document.addEventListener('htmx:afterSwap', function(evt) {
const filterInput = document.getElementById('candidate-filter');
if (filterInput && filterInput.value) {
filterCandidates(filterInput.value);
}
});
</script>
<style>
.missing-answers td:first-child:before {
content: "*";
color: red;
}
.search-cid {
display: none;
}
.cid:hover .search-cid {
display: inline-block;
color: darkblue;
}
/* Reduce vertical spacing for dense score tables */
.cid-score-table td, .cid-score-table th, table.table-sm td, table.table-sm th {
padding: .25rem .4rem;
line-height: 1;
}
#stats-block { font-size: .9rem; }
</style>
{% endblock %}