@@ -14,22 +14,18 @@
< button class = "btn btn-secondary" type = "button"
th:disabled = "${#lists.isEmpty(candidates)}"
onclick = "openModal('modal-enroll-student')" > Добавить существующего< / button >
< button class = "btn btn-secondary" type = "button" onclick = "openDefense(this)" > Назначить задание< / button >
< button class = "btn btn-secondary" type = "button" onclick = "openModal('modal-defense-import')" > Импорт CSV< / button >
< a class = "btn btn-secondary" th:href = "'/admin/groups/' + ${group.id} + '/submissions.zip'" > Скачать все работы< / a >
< a class = "btn btn-secondary" th:href = "@{/admin/groups/{id}/credentials(id=${group.id}, format='xlsx')}" > Логины XLSX< / a >
< a class = "btn btn-secondary" th:href = "@{/admin/groups/{id}/credentials(id=${group.id}, format='csv')}" > CSV< / a >
< / div >
< / div >
< div th:if = "${notice}" class = "alert alert-success" th:text = "${notice}" > < / div >
< div th:if = "${error}" class = "alert alert-error" th:text = "${error}" > < / div >
< div th:if = "${resetPassword}" class = "alert alert-warning" >
Новый пароль: < code class = "code font-mono" th:text = "${resetPassword}" > password< / code >
< / div >
< div class = "flex flex-wrap items-center justify-between gap-2 mb-2" >
< h2 class = "section-title m-0" > Результа ты по лабораторным < / h2 >
< h2 class = "section-title m-0" > Студен ты и лабораторные < / h2 >
< div class = "flex flex-wrap items-center gap-2" th:if = "${!#lists.isEmpty(labs)}" >
< input class = "input input-sm table-search" data-target = "matrix -table"
< input class = "input input-sm table-search" data-target = "group -table"
placeholder = "Поиск по студенту…" aria-label = "Поиск по студенту" >
< label class = "label m-0" for = "lab-focus" > Показать:< / label >
< select class = "input input-sm" id = "lab-focus" onchange = "focusLab()" >
@@ -39,14 +35,14 @@
< / div >
< / div >
< p class = "text-sm text-muted mb-4" >
Значки в я чейке : «№» — вариант, «🛡» — защита, «↓» — сдача (скачать) , «✎» — оценка.
Наведите на значок — появится пояснение; клик по «↓» скачивает последнее решение,
клик по «✎» открывает оценку и комментарии. Раздача вариантов — на странице лабораторной.
Я чейка : «№» — вариант, «🛡» — защита (клик — задание и код) , «↓» — сдача, «✎» — оценка.
Раздача вариантов — на странице лабораторной.
< / p >
< div th:if = "${#lists.isEmpty(labs)}" class = "card card-body mb-6 text-muted" > Опубликованных лабораторных пока нет.< / div >
< div class = "card card-flush mb-6 " th:if = "${!#lists.isEmpty(labs)}" >
< div class = "card card-flush" th:if = "${!#lists.isEmpty(labs)}" >
< div class = "scroll-x" >
< table class = "matrix tablekit" id = "matrix -table" >
< table class = "matrix tablekit" id = "group -table" >
< thead >
< tr >
< th class = "matrix-sticky th-sortable is-asc" title = "ФИО — по возрастанию" > Студент< / th >
@@ -67,6 +63,10 @@
< / div >
< / div >
< / th >
< th data-filter > Основа< / th >
< th > Варианты< / th >
< th data-filter > Пароль< / th >
< th class = "text-right" data-nosort > Действия< / th >
< / tr >
< / thead >
< tbody >
@@ -83,11 +83,12 @@
№< span class = "font-mono" th:classappend = "${cell.variantManual} ? 'text-strong' : ''"
th:text = "${cell.variantNumber} ?: '—'" > 12< / span >
< / span >
< span th:if = "${cell.defenseStatus == null}" class = "cell-chip chip-dim " titl e = "Защита не назначена" > 🛡—< / span >
< span th:if = "${cell.defenseStatus == 'waiting' and cell.defenseCode == null}" class = "cell-chip chip-warn" title = "Задание для защиты выдано, ещё без кода" > 🛡⌛< / span >
< span th:if = "${cell.defenseStatus == 'waiting' and cell.defenseCode != null}" class = "cell-chip chip-warn" title = "Защита: код студенту выдан, ждёт входа" > 🛡🔑< / span >
< span th:if = "${cell.defenseStatus == 'started'}" class = "cell-chip chip-warn" title = "Защита: студент пишет работу" > 🛡⏳< / span >
< span th:if = "${cell.defenseStatus == 'ready'}" class = "cell-chip chip-ok" title = "Защита: работа сохранена, можно принимать" > 🛡✓< / span >
< button class = "cell-chip" typ e = "button"
th:classappend = "${cell.defenseStatus == null} ? 'chip-dim' : (${cell.defenseStatus == 'ready'} ? 'chip-ok' : 'chip-warn')"
th:attr = "data-student=${row.studentId},data-student-name=${row.fullName},data-lab=${cell.labId},data-lab-name='ЛР ' + ${cell.labNumber},data-md=${cell.defenseAssignmentMd},data-session=${cell.defenseSessionId},data-status=${cell.defenseStatus}"
onclick = "openDefense(this)"
th:title = "${cell.defenseStatus == null} ? 'Назначить задание на защиту' : 'Защита: задание и действия'"
th:text = "${cell.defenseStatus == null} ? '🛡—' : (${cell.defenseStatus == 'waiting'} ? (${cell.defenseCode != null} ? '🛡🔑' : '🛡⌛') : (${cell.defenseStatus == 'started'} ? '🛡⏳' : '🛡✓'))" > 🛡—< / button >
< a th:if = "${cell.submissionId}" class = "cell-chip chip-link"
th:href = "@{/admin/submissions/{id}/download(id=${cell.submissionId}, group=${group.name}, no=${row.studentNo}, name=${row.fullName}, attempt=${cell.submissionSeq})}"
title = "Скачать последнее решение студента" > ↓< span class = "font-mono" th:text = "${cell.submissionSeq}" > 1< / span > < / a >
@@ -99,114 +100,34 @@
> ✎< span class = "font-mono" th:if = "${cell.score}" th:text = "' ' + ${cell.score}" > < / span > < / button >
< / div >
< / td >
< td class = "text-muted" th:text = "${row.basis}" > —< / td >
< td class = "text-sm" >
< span th:if = "${!#strings.isEmpty(row.variantsSummary)}" th:text = "${row.variantsSummary}" > —< / span >
< span th:if = "${#strings.isEmpty(row.variantsSummary)}" class = "text-muted" > не распределены< / span >
< / td >
< td > < 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" >
< form class = "inline-form" th:action = "@{/admin/groups/{gid}/students/{sid}/reset-password(gid=${group.id}, sid=${row.studentId})}" method = "post" >
< input type = "hidden" name = "_csrf" th:value = "${csrfToken}" >
< button class = "btn btn-secondary btn-sm" type = "submit"
title = "Сгенерировать новый пароль (покажется один раз, потребуется смена при входе)" > Сбросить пароль< / button >
< / form >
< form class = "inline-form"
th:action = "@{/admin/groups/{gid}/students/{sid}/unenroll(gid=${group.id}, sid=${row.studentId})}"
method = "post"
onsubmit = "return confirm('Отчислить этого студента из группы? Аккаунт, сдача и оценки сохранятся; студент сможет войти и будет без группы.')" >
< input type = "hidden" name = "_csrf" th:value = "${csrfToken}" >
< button class = "btn btn-danger btn-sm" type = "submit" > Отчислить из группы< / button >
< / form >
< / td >
< / tr >
< tr th:if = "${#lists.isEmpty(studentGrades)}" > < td class = "empty" th:attr = "colspan=${#lists.size(labs) + 1 }" > В группе нет студентов.< / td > < / tr >
< tr th:if = "${#lists.isEmpty(studentGrades)}" > < td class = "empty" th:attr = "colspan=${#lists.size(labs) + 5 }" > В группе нет студентов.< / td > < / tr >
< / tbody >
< / table >
< / div >
< / div >
< div class = "flex flex-wrap items-center justify-between gap-2 mb-2" >
< h2 class = "section-title m-0" > Индивидуальные задания (защита)< / h2 >
< div class = "flex flex-wrap items-center gap-2" th:if = "${!#lists.isEmpty(labs)}" >
< label class = "label m-0" for = "defense-lab-filter" > Лабораторная:< / label >
< select class = "input input-sm" id = "defense-lab-filter" onchange = "filterDefense()" >
< option value = "" > В с е < / option >
< option th:each = "lab : ${labs}" th:value = "${lab.id}" th:text = "|ЛР ${lab.number}|" > —< / option >
< / select >
< button class = "btn btn-primary btn-sm" type = "button" onclick = "openDefenseForm(this)" > Назначить задание< / button >
< button class = "btn btn-secondary btn-sm" type = "button" onclick = "openModal('modal-defense-import')" > Импорт CSV< / button >
< input class = "input input-sm table-search" data-target = "defense-table"
placeholder = "Поиск по студенту…" aria-label = "Поиск по заданиям защиты" >
< / div >
< / div >
< div class = "card card-flush mb-8" >
< table id = "defense-table" class = "tablekit" >
< thead > < tr > < th class = "th-sortable is-asc" title = "ФИО — по возрастанию" > Студент< / th > < th data-filter > ЛР< / th > < th > Задание< / th > < th data-filter > Статус< / th > < th class = "text-right" data-nosort > Действия< / th > < / tr > < / thead >
< tbody >
< tr th:each = "d : ${defenseRows}" th:attr = "data-lab=${d.labId}" >
< td >
< div th:text = "${d.fullName}" > —< / div >
< div class = "text-sm text-muted font-mono" th:text = "${d.studentNo}" > —< / div >
< / td >
< td class = "font-mono" th:text = "|ЛР ${d.labNumber}|" > —< / td >
< td class = "text-sm text-muted" th:text = "${#strings.abbreviate(d.assignmentMd, 60)}" > —< / td >
< td th:attr = "data-filter-value=${d.status == 'waiting' ? (d.code != null ? 'код выдан' : 'ждёт кода') : (d.status == 'started' ? 'пишет' : (d.status == 'ready' ? 'готов' : d.status))}" >
< span class = "badge badge-warn" th:if = "${d.status == 'waiting' and d.code == null}" > ждёт кода< / span >
< span class = "badge badge-warn" th:if = "${d.status == 'waiting' and d.code != null}" > код выдан< / span >
< span class = "badge badge-warn" th:if = "${d.status == 'started'}" > пишет (< span th:text = "${d.secondsLeft}" > < / span > c)< / span >
< span class = "badge badge-ok" th:if = "${d.status == 'ready'}" > готов< / span >
< / td >
< td class = "text-right defense-cell-actions" >
< form class = "inline-form" th:if = "${d.sessionId != null}"
th:action = "'/admin/defenses/' + ${d.sessionId} + '/issue-code'" method = "post" >
< input type = "hidden" name = "_csrf" th:value = "${csrfToken}" >
< input type = "hidden" name = "labId" th:value = "${d.labId}" >
< input type = "hidden" name = "groupId" th:value = "${group.id}" >
< button class = "btn btn-secondary btn-sm" type = "submit"
th:text = "${d.status == 'waiting' and d.code != null ? 'новый код' : 'выдать код'}" > выдать код< / button >
< / form >
< button class = "btn btn-secondary btn-sm" type = "button"
th:attr = "data-student=${d.studentId},data-student-name=${d.fullName},data-lab=${d.labId},data-lab-name='ЛР ' + ${d.labNumber},data-md=${d.assignmentMd}"
onclick = "openDefenseForm(this)" > изменить< / button >
< button class = "btn btn-secondary btn-sm" type = "button"
th:attr = "data-student-name=${d.fullName},data-lab-name='ЛР ' + ${d.labNumber},data-md=${d.assignmentMd}"
onclick = "showDefenseText(this)" > текст< / button >
< form class = "inline-form" th:if = "${d.status == 'ready'}"
th:action = "'/admin/defenses/' + ${d.sessionId} + '/reset'" method = "post"
onsubmit = "return confirm('Открыть защиту заново для нового (дополнительного) задания?')" >
< input type = "hidden" name = "_csrf" th:value = "${csrfToken}" >
< input type = "hidden" name = "labId" th:value = "${d.labId}" >
< input type = "hidden" name = "groupId" th:value = "${group.id}" >
< button class = "btn btn-danger btn-sm" type = "submit" > доп. задание: начать заново< / button >
< / form >
< / td >
< / tr >
< tr th:if = "${#lists.isEmpty(defenseRows)}" > < td colspan = "5" class = "empty" > Заданий для защиты пока нет.< / td > < / tr >
< / tbody >
< / table >
< / div >
< div class = "flex flex-wrap items-center justify-between gap-2 mb-2" >
< h2 class = "section-title m-0" > Студенты< / h2 >
< input class = "input input-sm table-search" data-target = "students-table"
placeholder = "Поиск по студенту…" aria-label = "Поиск по студентам" >
< / div >
< div class = "card card-flush" >
< table id = "students-table" class = "tablekit" >
< thead > < tr > < th data-nosort > №< / th > < th class = "th-sortable is-asc" title = "ФИО — по возрастанию" > ФИО< / th > < th > Логин< / th > < th data-filter > Основа< / th > < th > Варианты< / th > < th data-filter > Пароль< / th > < th class = "text-right" data-nosort > Действия< / th > < / tr > < / thead >
< tbody >
< tr th:each = "row, iter : ${studentGrades}" >
< td class = "text-muted" th:text = "${iter.count}" > 1< / td >
< td th:text = "${row.fullName}" > —< / td >
< td class = "font-mono" th:text = "${row.studentNo}" > —< / td >
< td class = "text-muted" th:text = "${row.basis}" > —< / td >
< td class = "text-sm" >
< span th:if = "${!#strings.isEmpty(row.variantsSummary)}" th:text = "${row.variantsSummary}" > —< / span >
< span th:if = "${#strings.isEmpty(row.variantsSummary)}" class = "text-muted" > не распределены< / span >
< / td >
< td > < 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" >
< form class = "inline-form" th:action = "@{/admin/groups/{gid}/students/{sid}/reset-password(gid=${group.id}, sid=${row.studentId})}" method = "post" >
< input type = "hidden" name = "_csrf" th:value = "${csrfToken}" >
< button class = "btn btn-secondary btn-sm" type = "submit"
title = "Сгенерировать новый пароль (покажется один раз, потребуется смена при входе)" > Сбросить пароль< / button >
< / form >
< form class = "inline-form"
th:action = "@{/admin/groups/{gid}/students/{sid}/unenroll(gid=${group.id}, sid=${row.studentId})}"
method = "post"
onsubmit = "return confirm('Отчислить этого студента из группы? Аккаунт, сдача и оценки сохранятся; студент сможет войти и будет без группы.')" >
< input type = "hidden" name = "_csrf" th:value = "${csrfToken}" >
< button class = "btn btn-danger btn-sm" type = "submit" > Отчислить из группы< / button >
< / form >
< / td >
< / tr >
< tr th:if = "${#lists.isEmpty(group.students)}" > < td colspan = "7" class = "empty" > В группе пока нет студентов.< / td > < / tr >
< / tbody >
< / table >
< / div >
<!-- Диалог: оценка + комментарии -->
< dialog id = "modal-grade" class = "modal" >
< div class = "modal-header" >
@@ -231,7 +152,7 @@
< / div >
< / dialog >
<!-- Диалог: назначение индивидуальног о задания -->
<!-- Диалог: индивидуальное задание (защита) -->
< dialog id = "modal-defense" class = "modal" >
< div class = "modal-header" >
< h2 class = "modal-title" > Индивидуальное задание< / h2 >
@@ -256,6 +177,24 @@
< textarea class = "input" id = "defense-md" name = "assignmentMd" rows = "10" required > < / textarea > < / div >
< button class = "btn btn-primary" type = "submit" > Сохранить задание< / button >
< / form >
< div id = "defense-actions" class = "mt-3" hidden >
< div class = "flex flex-wrap gap-2" >
< form class = "inline-form" id = "defense-issue-form" method = "post" >
< input type = "hidden" name = "_csrf" th:value = "${csrfToken}" >
< input type = "hidden" name = "labId" id = "defense-action-lab" >
< input type = "hidden" name = "groupId" th:value = "${group.id}" >
< button class = "btn btn-secondary btn-sm" type = "submit" > выдать код< / button >
< / form >
< button class = "btn btn-secondary btn-sm" type = "button" onclick = "showDefenseFromModal()" > текст задания< / button >
< form class = "inline-form" id = "defense-reset-form" method = "post"
onsubmit = "return confirm('Открыть защиту заново для нового (дополнительного) задания?')" >
< input type = "hidden" name = "_csrf" th:value = "${csrfToken}" >
< input type = "hidden" name = "labId" id = "defense-reset-lab" >
< input type = "hidden" name = "groupId" th:value = "${group.id}" >
< button class = "btn btn-danger btn-sm" type = "submit" > начать заново< / button >
< / form >
< / div >
< / div >
< / div >
< / dialog >
@@ -359,7 +298,6 @@
< / div >
< / div >
< script src = "/js/admin-group.js" defer > < / script >
< / div >
< / body >