feat: add course attestations and average grade column
- store attestation columns and per-student results in progress-service - add teacher UI for intermediate/pass/exam columns with semester and missed hours - show average score per student on the group page - show attestation results on the student dashboard
This commit is contained in:
@@ -17,6 +17,7 @@
|
||||
</div>
|
||||
<div class="actions">
|
||||
<a class="btn btn-secondary" th:href="@{/admin/groups/{id}/credentials(id=${group.id}, format='xlsx')}">Скачать пароли</a>
|
||||
<button class="btn btn-secondary" type="button" data-modal="modal-add-attestation">+ Аттестация</button>
|
||||
<button class="btn" type="button" data-modal="modal-add-student">+ Студент</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -30,10 +31,10 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div th:if="${#lists.isEmpty(labs)}" class="card card-body mb-6 text-muted">Опубликованных лабораторных пока нет.</div>
|
||||
<div th:if="${#lists.isEmpty(labs) and #lists.isEmpty(attestations)}" class="card card-body mb-6 text-muted">Опубликованных лабораторных пока нет.</div>
|
||||
<input type="hidden" id="pub-url" th:value="@{/admin/groups/{id}/publish-student(id=${group.id})}">
|
||||
<div id="defense-meta" hidden th:data-labs="${#strings.listJoin(labs.![id], ',')}"></div>
|
||||
<div class="card card-flush" th:if="${!#lists.isEmpty(labs)}">
|
||||
<div class="card card-flush" th:if="${!#lists.isEmpty(labs) or !#lists.isEmpty(attestations)}">
|
||||
<div class="scroll-x">
|
||||
<table class="matrix tablekit" id="group-table">
|
||||
<thead>
|
||||
@@ -70,6 +71,23 @@
|
||||
</div>
|
||||
</div>
|
||||
</th>
|
||||
<th class="text-center" title="Средний балл по выставленным оценкам">Средний балл</th>
|
||||
<th th:each="att : ${attestations}" class="text-center attestation-col" data-nosort>
|
||||
<div class="attestation-head">
|
||||
<div class="lab-head-actions">
|
||||
<form class="inline-form"
|
||||
th:action="@{/admin/groups/{gid}/attestations/{aid}/delete(gid=${group.id}, aid=${att.id})}"
|
||||
method="post"
|
||||
data-confirm="Удалить столбец аттестации вместе со всеми результатами?">
|
||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||
<button class="btn btn-danger btn-sm p-2" type="submit"
|
||||
title="Удалить аттестацию" aria-label="Удалить аттестацию">✕</button>
|
||||
</form>
|
||||
</div>
|
||||
<span class="attestation-kind" th:text="${att.kindLabel}">Аттестация</span>
|
||||
<span class="lab-col-title" th:text="${att.semester}">—</span>
|
||||
</div>
|
||||
</th>
|
||||
<th data-nosort>Пароль</th>
|
||||
<th class="text-right" data-nosort>Действия</th>
|
||||
</tr>
|
||||
@@ -120,6 +138,19 @@
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-center font-mono average-cell"
|
||||
th:text="${row.averageScore != null ? row.averageScore : '—'}">—</td>
|
||||
<td th:each="att : ${attestations}" class="text-center attestation-cell">
|
||||
<button class="score-link" type="button"
|
||||
th:classappend="${row.attestations[att.id] == null} ? 'none' : ''"
|
||||
th:attr="data-student=${row.studentId},data-student-name=${row.fullName},data-attestation=${att.id},data-attestation-kind=${att.kind},data-attestation-value=${row.attestations[att.id]?.value},data-missed-hours=${row.attestations[att.id]?.missedHours},data-attestation-label=${att.kindLabel},data-semester=${att.semester}"
|
||||
data-action="attestation"
|
||||
th:title="${att.kindLabel} + ' — ' + ${att.semester}">
|
||||
<span th:text="${row.attestations[att.id] != null ? row.attestations[att.id].label : '—'}">—</span>
|
||||
<span th:if="${row.attestations[att.id]?.missedHours != null}"
|
||||
class="cell-sub" th:text="' (' + ${row.attestations[att.id].missedHours} + ' ч.)'"></span>
|
||||
</button>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span th:if="${row.mustChangePassword}" class="badge badge-warn">временный</span>
|
||||
<span th:unless="${row.mustChangePassword}" class="badge badge-ok">изменён</span>
|
||||
@@ -139,7 +170,7 @@
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr th:if="${#lists.isEmpty(studentGrades)}"><td class="empty" th:attr="colspan=${#lists.size(labs) + 3}">В группе нет студентов.</td></tr>
|
||||
<tr th:if="${#lists.isEmpty(studentGrades)}"><td class="empty" th:attr="colspan=${#lists.size(labs) + #lists.size(attestations) + 4}">В группе нет студентов.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -173,6 +204,64 @@
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<!-- Диалог: добавление столбца аттестации -->
|
||||
<dialog id="modal-add-attestation" class="dlg">
|
||||
<div class="dlg-header">
|
||||
<h2 class="dlg-title">Новая аттестация</h2>
|
||||
<button class="dlg-close" type="button" aria-label="Закрыть">✕</button>
|
||||
</div>
|
||||
<div class="dlg-body">
|
||||
<form class="form-stack" method="post" th:action="@{/admin/groups/{id}/attestations(id=${group.id})}">
|
||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||
<div><label class="label" for="attestation-kind">Форма аттестации</label>
|
||||
<select class="input" id="attestation-kind" name="kind" required>
|
||||
<option value="intermediate">Промежуточная аттестация</option>
|
||||
<option value="pass">Зачёт</option>
|
||||
<option value="exam">Экзамен</option>
|
||||
</select></div>
|
||||
<div><label class="label" for="attestation-semester">Семестр</label>
|
||||
<input class="input" id="attestation-semester" name="semester" list="attestation-semesters"
|
||||
placeholder="осень 2026" maxlength="100" data-error-required="Укажите семестр" required>
|
||||
<datalist id="attestation-semesters">
|
||||
<option th:each="s : ${attestationSemesters}" th:value="${s}"></option>
|
||||
</datalist>
|
||||
<p class="hint">Например: «осень 2026» или «весна 2027». Семестр будет указан в ведомости и кабинете студента.</p>
|
||||
</div>
|
||||
<button class="btn btn-primary" type="submit">Добавить столбец</button>
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<!-- Диалог: результат аттестации студента -->
|
||||
<dialog id="modal-attestation" class="dlg">
|
||||
<div class="dlg-header">
|
||||
<h2 class="dlg-title" id="attestation-title">Результат аттестации</h2>
|
||||
<button class="dlg-close" type="button" aria-label="Закрыть">✕</button>
|
||||
</div>
|
||||
<div class="dlg-body">
|
||||
<form class="form-stack" id="attestation-form" method="post"
|
||||
th:action="@{/admin/groups/{id}/attestation-results(id=${group.id})}">
|
||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||
<input type="hidden" name="attestationId" id="attestation-id">
|
||||
<input type="hidden" name="studentId" id="attestation-student">
|
||||
<div><label class="label">Студент</label>
|
||||
<input class="input" id="attestation-who" readonly></div>
|
||||
<div><label class="label" for="attestation-value">Результат</label>
|
||||
<select class="input" id="attestation-value" name="value">
|
||||
<option value="">— не выставлено —</option>
|
||||
</select>
|
||||
<p class="hint">Оставьте пустым, чтобы убрать результат.</p>
|
||||
</div>
|
||||
<div id="attestation-hours-wrap"><label class="label" for="attestation-hours">Пропущено академических часов</label>
|
||||
<input class="input" id="attestation-hours" name="missedHours" type="number" min="0" step="1"
|
||||
inputmode="numeric" placeholder="например, 24">
|
||||
<p class="hint">Укажите для оценки «не атт», если известны пропуски по любой причине.</p>
|
||||
</div>
|
||||
<button class="btn btn-primary" type="submit">Сохранить</button>
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<!-- Диалог: индивидуальное задание (защита) -->
|
||||
<!-- Диалог: назначение варианта -->
|
||||
<dialog id="modal-variant" class="dlg dlg-lg">
|
||||
@@ -347,7 +436,7 @@
|
||||
<div class="dlg-body" id="assign-modal-body"></div>
|
||||
</dialog>
|
||||
|
||||
<script th:src="@{/js/admin-group.js(v='39')}" defer></script>
|
||||
<script th:src="@{/js/admin-group.js(v='40')}" defer></script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<!-- v=37: сброс кэша стилей после изменения геометрии; при изменении css поднимать версию -->
|
||||
<link rel="stylesheet" th:href="@{/css/bootstrap.min.css}">
|
||||
<link rel="stylesheet" th:href="@{/css/bootstrap-icons.min.css}">
|
||||
<link rel="stylesheet" th:href="@{/css/app.css(v='37')}">
|
||||
<link rel="stylesheet" th:href="@{/css/app.css(v='38')}">
|
||||
<link id="hljs-light" rel="stylesheet" th:href="@{/css/hljs-github.css(v='36')}">
|
||||
<link id="hljs-dark" rel="stylesheet" th:href="@{/css/hljs-github-dark.css(v='36')}" disabled>
|
||||
<script th:src="@{/js/highlight.min.js(v='36')}" defer></script>
|
||||
@@ -25,7 +25,7 @@
|
||||
<script th:src="@{/js/tablekit.js(v='38')}" defer></script>
|
||||
<script th:src="@{/js/bootstrap.bundle.min.js}" defer></script>
|
||||
<script th:src="@{/js/theme-init.js(v='36')}"></script>
|
||||
<script th:src="@{/js/app.js(v='38')}" defer></script>
|
||||
<script th:src="@{/js/app.js(v='39')}" defer></script>
|
||||
<script th:src="@{/js/ui-state.js(v='36')}" defer></script>
|
||||
<script th:src="@{/js/form-feedback.js(v='39')}" defer></script>
|
||||
<script th:src="@{/js/modal-ajax.js(v='1')}" defer></script>
|
||||
|
||||
@@ -24,6 +24,30 @@
|
||||
<a class="btn btn-secondary" th:href="@{/learn}">Учебные материалы</a>
|
||||
</div>
|
||||
|
||||
<h2 class="section-title mt-8">Аттестации</h2>
|
||||
<div th:if="${!#lists.isEmpty(attestations)}" class="card card-flush mb-6">
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Форма аттестации</th><th>Семестр</th><th>Результат</th><th>Пропущено</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr th:each="a : ${attestations}">
|
||||
<td th:text="${a.kindLabel}">—</td>
|
||||
<td th:text="${a.semester}">—</td>
|
||||
<td>
|
||||
<span class="badge"
|
||||
th:classappend="${a.positive == true} ? 'badge-ok' : (${a.positive == false} ? 'badge-warn' : '')"
|
||||
th:text="${a.valueLabel}">—</span>
|
||||
</td>
|
||||
<td class="text-muted" th:text="${a.missedHours != null ? a.missedHours + ' ч.' : '—'}">—</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div th:if="${#lists.isEmpty(attestations)}" class="card card-body text-muted mb-6">
|
||||
Аттестаций пока нет. Преподаватель добавит результаты после их выставления.
|
||||
</div>
|
||||
|
||||
<h2 class="section-title mt-8">Мои лабораторные</h2>
|
||||
<div class="row row-cols-1 row-cols-lg-2 g-4">
|
||||
<div th:each="c : ${cards}" class="col"><div class="card h-100">
|
||||
|
||||
Reference in New Issue
Block a user