Compare commits
10 Commits
26573f2210
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 920d96a0f5 | |||
| 2beffd84f1 | |||
| d8e28e88fc | |||
| 44edb5ebeb | |||
| e2e3ed1eaf | |||
| 7fb3e13548 | |||
| 9ddf2c6ce9 | |||
| a336612ad5 | |||
| c8581c620a | |||
| 54e5721267 |
@@ -13,10 +13,10 @@ object AttestationPresentation {
|
|||||||
mapOf(
|
mapOf(
|
||||||
"intermediate" to
|
"intermediate" to
|
||||||
mapOf(
|
mapOf(
|
||||||
"excellent" to "отл",
|
"excellent" to "отлично",
|
||||||
"satisfactory" to "атт",
|
"satisfactory" to "аттестован",
|
||||||
"unsatisfactory" to "неуд",
|
"unsatisfactory" to "неудовлетворительно",
|
||||||
"not_attested" to "не атт",
|
"not_attested" to "не аттестован",
|
||||||
),
|
),
|
||||||
"pass" to
|
"pass" to
|
||||||
mapOf(
|
mapOf(
|
||||||
@@ -43,6 +43,15 @@ object AttestationPresentation {
|
|||||||
|
|
||||||
fun kindLabel(kind: String): String = KIND_LABELS[kind] ?: kind
|
fun kindLabel(kind: String): String = KIND_LABELS[kind] ?: kind
|
||||||
|
|
||||||
|
/** Короткая подпись для шапки узкого столбца. */
|
||||||
|
fun shortKindLabel(kind: String): String =
|
||||||
|
when (kind) {
|
||||||
|
"intermediate" -> "Пр. атт."
|
||||||
|
"pass" -> "Зачёт"
|
||||||
|
"exam" -> "Экзамен"
|
||||||
|
else -> kind
|
||||||
|
}
|
||||||
|
|
||||||
/** Понимает и «2», и старые значения вроде «2 семестр»; вне 1–8 — 1. */
|
/** Понимает и «2», и старые значения вроде «2 семестр»; вне 1–8 — 1. */
|
||||||
fun semesterNumber(raw: String): Int {
|
fun semesterNumber(raw: String): Int {
|
||||||
val digits = raw.filter { it.isDigit() }
|
val digits = raw.filter { it.isDigit() }
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ data class GroupAttestation(
|
|||||||
val id: String = "",
|
val id: String = "",
|
||||||
val kind: String = "",
|
val kind: String = "",
|
||||||
val kindLabel: String = "",
|
val kindLabel: String = "",
|
||||||
|
val shortLabel: String = "",
|
||||||
val semester: Int = 1,
|
val semester: Int = 1,
|
||||||
val published: Boolean = false,
|
val published: Boolean = false,
|
||||||
val options: List<AttestationOption> = emptyList(),
|
val options: List<AttestationOption> = emptyList(),
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ object GroupAttestationViews {
|
|||||||
id = column.id,
|
id = column.id,
|
||||||
kind = column.kind,
|
kind = column.kind,
|
||||||
kindLabel = AttestationPresentation.kindLabel(column.kind),
|
kindLabel = AttestationPresentation.kindLabel(column.kind),
|
||||||
|
shortLabel = AttestationPresentation.shortKindLabel(column.kind),
|
||||||
semester = AttestationPresentation.semesterNumber(column.semester),
|
semester = AttestationPresentation.semesterNumber(column.semester),
|
||||||
published = column.published,
|
published = column.published,
|
||||||
options =
|
options =
|
||||||
|
|||||||
@@ -467,7 +467,7 @@ code.code { padding: 0.125rem 0.5rem; border-radius: var(--radius-xs); font-fami
|
|||||||
.variant-btn.manual .cell-none { font-weight: 600; }
|
.variant-btn.manual .cell-none { font-weight: 600; }
|
||||||
.score-link {
|
.score-link {
|
||||||
display: inline-block; padding: 0; border: none; background: none;
|
display: inline-block; padding: 0; border: none; background: none;
|
||||||
font-size: 0.8125rem; font-weight: 600; font-variant-numeric: tabular-nums;
|
font-size: 0.84375rem; font-weight: 600; font-variant-numeric: tabular-nums;
|
||||||
color: var(--c-text); cursor: pointer;
|
color: var(--c-text); cursor: pointer;
|
||||||
}
|
}
|
||||||
.score-link:hover { color: var(--c-accent); text-decoration: underline; }
|
.score-link:hover { color: var(--c-accent); text-decoration: underline; }
|
||||||
@@ -495,7 +495,12 @@ code.code { padding: 0.125rem 0.5rem; border-radius: var(--radius-xs); font-fami
|
|||||||
.cell-btn.st.dim::before { background: var(--c-border-strong); }
|
.cell-btn.st.dim::before { background: var(--c-border-strong); }
|
||||||
.mini-count { font-size: 0.72rem; margin-left: 0.15rem; color: var(--c-text-muted); }
|
.mini-count { font-size: 0.72rem; margin-left: 0.15rem; color: var(--c-text-muted); }
|
||||||
.defense-cell-timer { font-variant-numeric: tabular-nums; }
|
.defense-cell-timer { font-variant-numeric: tabular-nums; }
|
||||||
.lab-head-actions { display: flex; gap: 0.2rem; }
|
.lab-head-actions { display: flex; gap: 0.2rem; align-items: center; }
|
||||||
|
.lab-head-actions .btn {
|
||||||
|
height: 2.25rem; min-height: 2.25rem; padding: 0 0.5rem;
|
||||||
|
display: inline-flex; align-items: center; justify-content: center; box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.lab-head-actions .ico { font-size: 0.95rem; line-height: 1; }
|
||||||
.btn btn-secondary btn-sm p-2 {
|
.btn btn-secondary btn-sm p-2 {
|
||||||
display: inline-flex; align-items: center; justify-content: center;
|
display: inline-flex; align-items: center; justify-content: center;
|
||||||
min-width: 1.5rem; height: 1.5rem; padding: 0 0.3rem; cursor: pointer;
|
min-width: 1.5rem; height: 1.5rem; padding: 0 0.3rem; cursor: pointer;
|
||||||
@@ -546,30 +551,55 @@ dialog.dlg.dlg-lg .dlg-body { max-height: 88vh; min-height: 62vh; }
|
|||||||
.scroll-x { overflow-x: auto; }
|
.scroll-x { overflow-x: auto; }
|
||||||
.matrix { border-collapse: separate; border-spacing: 0; }
|
.matrix { border-collapse: separate; border-spacing: 0; }
|
||||||
.matrix th {
|
.matrix th {
|
||||||
vertical-align: top; padding: 0.75rem 1rem; text-align: left;
|
vertical-align: middle; padding: 0.75rem 1rem; text-align: left;
|
||||||
font-size: 0.75rem; font-weight: 500; color: var(--c-text-muted);
|
font-size: 0.75rem; font-weight: 500; color: var(--c-text-muted);
|
||||||
background: var(--c-surface); white-space: nowrap;
|
background: var(--c-surface); white-space: nowrap;
|
||||||
}
|
}
|
||||||
.matrix td { vertical-align: top; padding: 1rem; }
|
.matrix td { vertical-align: middle; padding: 1rem; font-size: 0.84375rem; }
|
||||||
|
#group-table td .student-name,
|
||||||
|
#group-table td .score-link,
|
||||||
|
#group-table td .attestation-select,
|
||||||
|
#group-table td .cell-sub,
|
||||||
|
#group-table td .sub-link,
|
||||||
|
#group-table td .cell-btn.st,
|
||||||
|
#group-table td .font-mono,
|
||||||
|
#group-table td .mini-count,
|
||||||
|
#group-table td .badge,
|
||||||
|
#group-table td .btn { font-size: 0.84375rem; }
|
||||||
/* Непрозрачный фон обязателен: закреплённый столбец не должен просвечивать при прокрутке. */
|
/* Непрозрачный фон обязателен: закреплённый столбец не должен просвечивать при прокрутке. */
|
||||||
.matrix tbody tr:hover td { background: var(--c-row-hover); }
|
.matrix tbody tr:hover td { background: var(--c-row-hover); }
|
||||||
.matrix tbody tr:hover td.matrix-sticky { background: var(--c-row-hover); }
|
.matrix tbody tr:hover td.matrix-sticky { background: var(--c-row-hover); }
|
||||||
.matrix tbody tr:last-child td { border-bottom: none; }
|
.matrix tbody tr:last-child td { border-bottom: none; }
|
||||||
.matrix th.lab-col { min-width: 9.5rem; border-left: 1px solid var(--c-border); }
|
.matrix th.lab-col { min-width: 9.5rem; border-left: 1px solid var(--c-border); }
|
||||||
.matrix td.cell-lab { min-width: 9.5rem; border-left: 1px solid var(--c-border); }
|
.matrix td.cell-lab { min-width: 9.5rem; border-left: 1px solid var(--c-border); }
|
||||||
.matrix th.attestation-col { width: 11rem; min-width: 10rem; max-width: 11rem; border-left: 1px solid var(--c-border); border-right: 1px solid var(--c-border); }
|
#group-table th.average-col,
|
||||||
.matrix td.attestation-cell { width: 11rem; min-width: 10rem; max-width: 11rem; border-left: 1px solid var(--c-border); border-right: 1px solid var(--c-border); }
|
#group-table th.actions-head { color: var(--c-text); font-weight: 400; }
|
||||||
.matrix th.average-col { min-width: 7rem; border-left: 1px solid var(--c-border); border-right: 1px solid var(--c-border); }
|
#group-table th.actions-head,
|
||||||
.matrix td.average-cell { min-width: 7rem; border-left: 1px solid var(--c-border); border-right: 1px solid var(--c-border); font-weight: 600; }
|
#group-table td.actions-cell { border-left: 1px solid var(--c-border); }
|
||||||
.attestation-title { display: flex; flex-wrap: wrap; align-items: center; gap: 0.35rem; width: 100%; max-width: 9rem; }
|
.matrix th.attestation-col {
|
||||||
.attestation-kind { font-weight: 600; color: var(--c-text); white-space: normal; }
|
width: 12rem; min-width: 11.5rem; max-width: 12rem;
|
||||||
|
border-left: 1px solid var(--c-border);
|
||||||
|
}
|
||||||
|
.matrix td.attestation-cell {
|
||||||
|
width: 12rem; min-width: 11.5rem; max-width: 12rem;
|
||||||
|
padding-left: 0.35rem; padding-right: 0.35rem;
|
||||||
|
border-left: 1px solid var(--c-border);
|
||||||
|
}
|
||||||
|
.matrix th.average-col,
|
||||||
|
.matrix td.average-cell {
|
||||||
|
min-width: 7rem; border-left: 1px solid var(--c-border); text-align: left;
|
||||||
|
}
|
||||||
|
.matrix td.average-cell { font-weight: 600; text-align: center; }
|
||||||
|
.attestation-title { display: flex; flex-wrap: wrap; align-items: center; gap: 0.35rem; width: 100%; max-width: 9rem; text-align: left; }
|
||||||
|
.attestation-kind { font-weight: 400; color: var(--c-text); white-space: normal; text-align: left; }
|
||||||
|
.attestation-col .lab-col-head, .attestation-col .lab-col-title { text-align: left; }
|
||||||
.js-attestation-form { display: flex; justify-content: center; width: 100%; }
|
.js-attestation-form { display: flex; justify-content: center; width: 100%; }
|
||||||
.attestation-select {
|
.attestation-select {
|
||||||
appearance: none; -webkit-appearance: none;
|
appearance: none; -webkit-appearance: none;
|
||||||
width: 9rem; max-width: 100%; min-width: 0; padding: 0.15rem 1.15rem 0.15rem 0.35rem;
|
width: 100%; max-width: 100%; min-width: 0; padding: 0.15rem 0.95rem 0.15rem 0.15rem;
|
||||||
border: 1px solid transparent; border-radius: var(--radius-sm);
|
border: 1px solid transparent; border-radius: var(--radius-sm);
|
||||||
background-color: transparent; color: var(--c-text);
|
background-color: transparent; color: var(--c-text);
|
||||||
font-size: 0.8125rem; font-weight: 600; font-variant-numeric: tabular-nums;
|
font-size: 0.84375rem; font-weight: 600; font-variant-numeric: tabular-nums;
|
||||||
text-align: center; text-align-last: center; cursor: pointer;
|
text-align: center; text-align-last: center; cursor: pointer;
|
||||||
background-image:
|
background-image:
|
||||||
linear-gradient(45deg, transparent 50%, currentColor 50%),
|
linear-gradient(45deg, transparent 50%, currentColor 50%),
|
||||||
@@ -584,6 +614,9 @@ dialog.dlg.dlg-lg .dlg-body { max-height: 88vh; min-height: 62vh; }
|
|||||||
.attestation-select:disabled { opacity: 0.6; cursor: wait; }
|
.attestation-select:disabled { opacity: 0.6; cursor: wait; }
|
||||||
.attestation-select option { text-align: left; text-align-last: left; background: var(--c-surface); color: var(--c-text); }
|
.attestation-select option { text-align: left; text-align-last: left; background: var(--c-surface); color: var(--c-text); }
|
||||||
.matrix td.student-cell { min-width: 12rem; }
|
.matrix td.student-cell { min-width: 12rem; }
|
||||||
|
.student-no-line { display: flex; align-items: center; gap: 0.35rem; }
|
||||||
|
.password-dot { width: 6px; height: 6px; flex: 0 0 auto; background: var(--c-tone-pos); }
|
||||||
|
.password-dot.warn { background: #b45309; }
|
||||||
.student-name { font-weight: 600; font-size: 0.84375rem; }
|
.student-name { font-weight: 600; font-size: 0.84375rem; }
|
||||||
.student-cell .font-mono { margin-top: 0.125rem; }
|
.student-cell .font-mono { margin-top: 0.125rem; }
|
||||||
.lab-col-head { display: flex; flex-direction: column; align-items: flex-start; gap: 0.375rem; }
|
.lab-col-head { display: flex; flex-direction: column; align-items: flex-start; gap: 0.375rem; }
|
||||||
|
|||||||
@@ -770,9 +770,6 @@ document.addEventListener('submit', function (ev) {
|
|||||||
? 'Аттестация видна студентам — скрыть'
|
? 'Аттестация видна студентам — скрыть'
|
||||||
: 'Аттестация скрыта — показать студентам';
|
: 'Аттестация скрыта — показать студентам';
|
||||||
}
|
}
|
||||||
var head = form.closest('th');
|
|
||||||
var badge = head ? head.querySelector('.js-att-hidden') : null;
|
|
||||||
if (badge) badge.hidden = published;
|
|
||||||
showToast(published ? 'Аттестация открыта студентам' : 'Аттестация скрыта от студентов', 'success');
|
showToast(published ? 'Аттестация открыта студентам' : 'Аттестация скрыта от студентов', 'success');
|
||||||
})
|
})
|
||||||
.catch(function (error) {
|
.catch(function (error) {
|
||||||
|
|||||||
@@ -16,23 +16,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row g-4 mt-4">
|
<div class="row g-4 mt-4">
|
||||||
<div class="col-lg-6">
|
|
||||||
<div class="flex items-center justify-between mb-4">
|
|
||||||
<h2 class="section-title m-0">Лабораторные</h2>
|
|
||||||
<a th:href="@{/admin/labs}" class="text-sm">Все лабораторные →</a>
|
|
||||||
</div>
|
|
||||||
<div class="card card-flush">
|
|
||||||
<table>
|
|
||||||
<tbody>
|
|
||||||
<tr th:each="lab : ${labs}">
|
|
||||||
<td><a th:href="@{/admin/labs/{id}(id=${lab.id})}" th:text="|ЛР ${lab.number}. ${lab.title}|">—</a></td>
|
|
||||||
<td class="text-muted" th:text="${lab.variantCount} + ' вар.'">—</td>
|
|
||||||
</tr>
|
|
||||||
<tr th:if="${#lists.isEmpty(labs)}"><td class="empty">Лабораторных нет.</td></tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-lg-6">
|
<div class="col-lg-6">
|
||||||
<div class="flex items-center justify-between mb-4">
|
<div class="flex items-center justify-between mb-4">
|
||||||
<h2 class="section-title m-0">Группы</h2>
|
<h2 class="section-title m-0">Группы</h2>
|
||||||
@@ -50,6 +33,23 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<h2 class="section-title m-0">Лабораторные</h2>
|
||||||
|
<a th:href="@{/admin/labs}" class="text-sm">Все лабораторные →</a>
|
||||||
|
</div>
|
||||||
|
<div class="card card-flush">
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
<tr th:each="lab : ${labs}">
|
||||||
|
<td><a th:href="@{/admin/labs/{id}(id=${lab.id})}" th:text="|ЛР ${lab.number}. ${lab.title}|">—</a></td>
|
||||||
|
<td class="text-muted" th:text="${lab.variantCount} + ' вар.'">—</td>
|
||||||
|
</tr>
|
||||||
|
<tr th:if="${#lists.isEmpty(labs)}"><td class="empty">Лабораторных нет.</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -73,12 +73,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
<th class="text-center average-col" title="Средний балл по выставленным оценкам">Средний балл</th>
|
<th class="average-col" title="Средний балл по выставленным оценкам">Средний балл</th>
|
||||||
<th th:each="att : ${attestations}" class="text-center attestation-col" data-nosort>
|
<th th:each="att : ${attestations}" class="text-center attestation-col" data-nosort>
|
||||||
<div class="lab-col-head">
|
<div class="lab-col-head">
|
||||||
<div class="attestation-title">
|
<div class="attestation-title">
|
||||||
<span class="attestation-kind" th:text="${att.kindLabel}">Аттестация</span>
|
<span class="attestation-kind" th:text="${att.shortLabel}">Пр. атт.</span>
|
||||||
<span class="badge badge-warn js-att-hidden" th:hidden="${att.published}">скрыто</span>
|
|
||||||
</div>
|
</div>
|
||||||
<span class="lab-col-title" th:text="'Семестр ' + ${att.semester}">Семестр 1</span>
|
<span class="lab-col-title" th:text="'Семестр ' + ${att.semester}">Семестр 1</span>
|
||||||
<div class="lab-head-actions">
|
<div class="lab-head-actions">
|
||||||
@@ -106,15 +105,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
<th data-nosort>Пароль</th>
|
<th class="text-right actions-head" data-nosort>Действия</th>
|
||||||
<th class="text-right" data-nosort>Действия</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr th:each="row : ${studentGrades}">
|
<tr th:each="row : ${studentGrades}">
|
||||||
<td class="student-cell matrix-sticky">
|
<td class="student-cell matrix-sticky">
|
||||||
<div class="student-name" th:text="${row.fullName}">—</div>
|
<div class="student-name" th:text="${row.fullName}">—</div>
|
||||||
<div class="text-sm text-muted font-mono" th:text="${row.studentNo}">—</div>
|
<div class="student-no-line">
|
||||||
|
<span class="password-dot" th:classappend="${row.mustChangePassword} ? 'warn' : 'ok'"
|
||||||
|
th:title="${row.mustChangePassword} ? 'Системный пароль — требуется смена' : 'Пароль изменён'"
|
||||||
|
aria-label="Статус пароля"></span>
|
||||||
|
<div class="text-sm text-muted font-mono" th:text="${row.studentNo}">—</div>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td th:each="cell, iterStat : ${row.cells}" class="text-center cell-lab"
|
<td th:each="cell, iterStat : ${row.cells}" class="text-center cell-lab"
|
||||||
th:attr="data-lab=${cell.labId},data-sort=${cell.variantNumber},data-filter-label=${cell.variantNumber != null ? 'вариант ' + cell.variantNumber : 'без варианта'}">
|
th:attr="data-lab=${cell.labId},data-sort=${cell.variantNumber},data-filter-label=${cell.variantNumber != null ? 'вариант ' + cell.variantNumber : 'без варианта'}">
|
||||||
@@ -156,9 +159,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-center average-cell"
|
<td class="average-cell"
|
||||||
th:text="${row.averageScore != null ? row.averageScore : '—'}">—</td>
|
th:text="${row.averageScore != null ? row.averageScore : '—'}">—</td>
|
||||||
<td th:each="att : ${attestations}" class="text-center attestation-cell">
|
<td th:each="att : ${attestations}" class="attestation-cell">
|
||||||
<form class="inline-form js-attestation-form" method="post"
|
<form class="inline-form js-attestation-form" method="post"
|
||||||
th:action="@{/admin/groups/{id}/attestation-results(id=${group.id})}">
|
th:action="@{/admin/groups/{id}/attestation-results(id=${group.id})}">
|
||||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||||
@@ -173,10 +176,6 @@
|
|||||||
</select>
|
</select>
|
||||||
</form>
|
</form>
|
||||||
</td>
|
</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>
|
|
||||||
</td>
|
|
||||||
<td class="text-right actions-cell">
|
<td class="text-right actions-cell">
|
||||||
<div class="actions-col">
|
<div class="actions-col">
|
||||||
<form th:action="@{/admin/groups/{gid}/students/{sid}/reset-password(gid=${group.id}, sid=${row.studentId})}" method="post" data-confirm="Сбросить пароль студенту? Текущий пароль перестанет работать, новый покажется один раз.">
|
<form th:action="@{/admin/groups/{gid}/students/{sid}/reset-password(gid=${group.id}, sid=${row.studentId})}" method="post" data-confirm="Сбросить пароль студенту? Текущий пароль перестанет работать, новый покажется один раз.">
|
||||||
@@ -192,7 +191,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr th:if="${#lists.isEmpty(studentGrades)}"><td class="empty" th:attr="colspan=${#lists.size(labs) + #lists.size(attestations) + 4}">В группе нет студентов.</td></tr>
|
<tr th:if="${#lists.isEmpty(studentGrades)}"><td class="empty" th:attr="colspan=${#lists.size(labs) + #lists.size(attestations) + 3}">В группе нет студентов.</td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@@ -434,7 +433,7 @@
|
|||||||
<div class="dlg-body" id="assign-modal-body"></div>
|
<div class="dlg-body" id="assign-modal-body"></div>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
<script th:src="@{/js/admin-group.js(v='42')}" defer></script>
|
<script th:src="@{/js/admin-group.js(v='43')}" defer></script>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
<!-- v=37: сброс кэша стилей после изменения геометрии; при изменении css поднимать версию -->
|
<!-- v=37: сброс кэша стилей после изменения геометрии; при изменении css поднимать версию -->
|
||||||
<link rel="stylesheet" th:href="@{/css/bootstrap.min.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/bootstrap-icons.min.css}">
|
||||||
<link rel="stylesheet" th:href="@{/css/app.css(v='41')}">
|
<link rel="stylesheet" th:href="@{/css/app.css(v='52')}">
|
||||||
<link id="hljs-light" rel="stylesheet" th:href="@{/css/hljs-github.css(v='36')}">
|
<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>
|
<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>
|
<script th:src="@{/js/highlight.min.js(v='36')}" defer></script>
|
||||||
|
|||||||
@@ -44,7 +44,7 @@
|
|||||||
|
|
||||||
<div class="card card-flush">
|
<div class="card card-flush">
|
||||||
<table>
|
<table>
|
||||||
<thead><tr><th>Статья</th><th class="th-w-md" data-nosort>Действия</th></tr></thead>
|
<thead><tr><th>Статья</th><th class="th-w-md" data-nosort th:if="${currentUser != null and currentUser.admin}">Действия</th></tr></thead>
|
||||||
<tbody data-article-list>
|
<tbody data-article-list>
|
||||||
<tr th:each="article : ${section.articles}" th:attr="data-article-id=${article.id}">
|
<tr th:each="article : ${section.articles}" th:attr="data-article-id=${article.id}">
|
||||||
<td>
|
<td>
|
||||||
@@ -55,7 +55,7 @@
|
|||||||
th:text="${article.title}">—</a>
|
th:text="${article.title}">—</a>
|
||||||
<span th:if="${!article.published}" class="badge badge-warn ml-1">скрыто</span>
|
<span th:if="${!article.published}" class="badge badge-warn ml-1">скрыто</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-right actions-cell">
|
<td class="text-right actions-cell" th:if="${currentUser != null and currentUser.admin}">
|
||||||
<a class="btn btn-secondary btn-sm p-2" title="Править статью" aria-label="Править статью"
|
<a class="btn btn-secondary btn-sm p-2" title="Править статью" aria-label="Править статью"
|
||||||
th:href="@{/learn/{c}/{s}/{a}(c=${chapter.slug}, s=${section.slug}, a=${article.slug}, edit=1)}"><i class="ico bi bi-pencil" aria-hidden="true"></i></a>
|
th:href="@{/learn/{c}/{s}/{a}(c=${chapter.slug}, s=${section.slug}, a=${article.slug}, edit=1)}"><i class="ico bi bi-pencil" aria-hidden="true"></i></a>
|
||||||
<form class="inline-form" th:action="@{/admin/content/articles/toggle-published}" method="post">
|
<form class="inline-form" th:action="@{/admin/content/articles/toggle-published}" method="post">
|
||||||
@@ -73,7 +73,7 @@
|
|||||||
</form>
|
</form>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr th:if="${#lists.isEmpty(section.articles)}"><td colspan="2" class="empty">Статей пока нет.</td></tr>
|
<tr th:if="${#lists.isEmpty(section.articles)}"><td th:colspan="${currentUser != null and currentUser.admin} ? 2 : 1" class="empty">Статей пока нет.</td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@@ -118,7 +118,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
<dialog id="modal-new-article" class="dlg">
|
<dialog id="modal-new-article" class="dlg" th:if="${currentUser != null and currentUser.admin}">
|
||||||
<div class="dlg-header">
|
<div class="dlg-header">
|
||||||
<h2 class="dlg-title">Новая статья в разделе</h2>
|
<h2 class="dlg-title">Новая статья в разделе</h2>
|
||||||
<button class="dlg-close" type="button" aria-label="Закрыть">✕</button>
|
<button class="dlg-close" type="button" aria-label="Закрыть">✕</button>
|
||||||
@@ -143,7 +143,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
<script th:src="@{/js/content-sort.js(v='36')}" defer></script>
|
<th:block th:if="${currentUser != null and currentUser.admin}">
|
||||||
|
<script th:src="@{/js/content-sort.js(v='36')}" defer></script>
|
||||||
|
</th:block>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
package ru.ysu.lms.web.learn
|
||||||
|
|
||||||
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import org.springframework.mock.web.MockHttpServletRequest
|
||||||
|
import org.springframework.mock.web.MockHttpServletResponse
|
||||||
|
import org.springframework.mock.web.MockServletContext
|
||||||
|
import org.thymeleaf.context.WebContext
|
||||||
|
import org.thymeleaf.spring6.SpringTemplateEngine
|
||||||
|
import org.thymeleaf.templatemode.TemplateMode
|
||||||
|
import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver
|
||||||
|
import org.thymeleaf.web.servlet.JakartaServletWebApplication
|
||||||
|
import ru.ysu.lms.web.session.SessionUser
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
class LearnSectionTemplateTest {
|
||||||
|
private val engine =
|
||||||
|
SpringTemplateEngine().apply {
|
||||||
|
setTemplateResolver(
|
||||||
|
ClassLoaderTemplateResolver().apply {
|
||||||
|
prefix = "templates/"
|
||||||
|
suffix = ".html"
|
||||||
|
characterEncoding = "UTF-8"
|
||||||
|
templateMode = TemplateMode.HTML
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `student does not see article admin actions`() {
|
||||||
|
val html = render("STUDENT")
|
||||||
|
|
||||||
|
assertThat(html).contains("Статья 1")
|
||||||
|
assertThat(html).doesNotContain("Удалить статью")
|
||||||
|
assertThat(html).doesNotContain("Править статью")
|
||||||
|
assertThat(html).doesNotContain("Скрыть статью")
|
||||||
|
assertThat(html).doesNotContain("modal-new-article")
|
||||||
|
assertThat(html).doesNotContain(">Действия</th>")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `admin sees article actions`() {
|
||||||
|
val html = render("ADMIN")
|
||||||
|
|
||||||
|
assertThat(html).contains("Статья 1")
|
||||||
|
assertThat(html).contains("Удалить статью")
|
||||||
|
assertThat(html).contains("Править статью")
|
||||||
|
assertThat(html).contains("Скрыть статью")
|
||||||
|
assertThat(html).contains("modal-new-article")
|
||||||
|
assertThat(html).contains(">Действия</th>")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun render(role: String): String {
|
||||||
|
val servletContext = MockServletContext()
|
||||||
|
val application = JakartaServletWebApplication.buildApplication(servletContext)
|
||||||
|
val exchange =
|
||||||
|
application.buildExchange(
|
||||||
|
MockHttpServletRequest(servletContext),
|
||||||
|
MockHttpServletResponse(),
|
||||||
|
)
|
||||||
|
val context = WebContext(exchange, Locale.forLanguageTag("ru"))
|
||||||
|
context.setVariable("csrfToken", "csrf")
|
||||||
|
context.setVariable(
|
||||||
|
"currentUser",
|
||||||
|
SessionUser(
|
||||||
|
id = "user-1",
|
||||||
|
login = role.lowercase(),
|
||||||
|
role = role,
|
||||||
|
name = "Пользователь",
|
||||||
|
mustChangePassword = false,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
context.setVariable(
|
||||||
|
"chapter",
|
||||||
|
mapOf(
|
||||||
|
"slug" to "chapter-1",
|
||||||
|
"title" to "Глава",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
context.setVariable(
|
||||||
|
"section",
|
||||||
|
mapOf(
|
||||||
|
"id" to "section-1",
|
||||||
|
"slug" to "section-1",
|
||||||
|
"title" to "Раздел",
|
||||||
|
"published" to true,
|
||||||
|
"articles" to
|
||||||
|
listOf(
|
||||||
|
mapOf(
|
||||||
|
"id" to "article-1",
|
||||||
|
"title" to "Статья 1",
|
||||||
|
"slug" to "article-1",
|
||||||
|
"published" to true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
context.setVariable("back", "/learn/chapter-1")
|
||||||
|
context.setVariable("backParent", "/learn")
|
||||||
|
context.setVariable(
|
||||||
|
"sectionChoices",
|
||||||
|
listOf(mapOf("id" to "section-2", "chapterId" to "chapter-1", "label" to "Глава / Раздел 2")),
|
||||||
|
)
|
||||||
|
return engine.process("learn/section", context)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user