feat: general lab comment and strict square UI
Add a non-required general comment for a student's lab grade (progress V9, API, admin modal, student card). Empty string is the default. Switch the UI to square corners and bump static asset versions.
This commit is contained in:
@@ -125,9 +125,9 @@ cp .env.prod.example .env.prod # заполнить LMS_DB_PASSWORD
|
||||
оценка и комментарии отдельным блоком, защита по одноразовому коду (30 мин).
|
||||
- **Публикация оценок**: в группе по каждой лабе — кнопка «опубликовать»; студент
|
||||
видит оценку и комментарии только после публикации (публикуется вся группа).
|
||||
- **Комментарии к оценкам**: тональность (positive/negative/neutral), на странице
|
||||
студента — ненумерованными списками в трёх колонках, хранятся в БД,
|
||||
автоподсказки при вводе ранжируются по частоте использования.
|
||||
- **Комментарии к оценкам**: общий (итоговый) комментарий по лабораторной + комментарии
|
||||
с тональностью (positive/negative/neutral); на странице студента — ненумерованными списками
|
||||
в трёх колонках, хранятся в БД, автоподсказки при вводе ранжируются по частоте использования.
|
||||
- **Архив сдач группы**: одна кнопка «Скачать все работы» — ZIP со всеми
|
||||
последними решениями группы, разложенными по лабам и студентам.
|
||||
- **Материалы** `/learn` доступны без входа; аккордеон глав: свёрнуты все,
|
||||
|
||||
@@ -101,7 +101,7 @@ sequenceDiagram
|
||||
DC->>DC: docker build (5 стадий)
|
||||
DC->>PG: старт postgres (volume lms_pgdata)
|
||||
DC->>DC: старт identity → academic → progress → gateway → web
|
||||
Note over DC: Flyway применяет миграции автоматически<br/>при старте сервисов (identity V1..V3, academic V1..V5, progress V1..V8; у web миграций нет)
|
||||
Note over DC: Flyway применяет миграции автоматически<br/>при старте сервисов (identity V1..V3, academic V1..V5, progress V1..V9; у web миграций нет)
|
||||
```
|
||||
|
||||
- Локально (`docker-compose.yml`) поднимается только `postgres`; прод-стек — `docker-compose.prod.yml`.
|
||||
|
||||
@@ -269,6 +269,7 @@ classDiagram
|
||||
+UUID studentId
|
||||
+UUID labId
|
||||
+BigDecimal score
|
||||
+String generalComment
|
||||
+Instant updatedAt
|
||||
}
|
||||
class GradePublication {
|
||||
@@ -635,6 +636,7 @@ erDiagram
|
||||
uuid student_id PK
|
||||
uuid lab_id PK
|
||||
numeric score
|
||||
text general_comment
|
||||
timestamptz updated_at
|
||||
}
|
||||
GRADE_PUBLICATIONS {
|
||||
|
||||
@@ -134,6 +134,8 @@ data class GradeSetRequest(
|
||||
val labId: String,
|
||||
val score: BigDecimal,
|
||||
val comments: List<CommentInput> = emptyList(),
|
||||
/** Итоговый комментарий по работе в целом; может быть пустым. */
|
||||
val generalComment: String = "",
|
||||
)
|
||||
|
||||
data class CommentInput(
|
||||
@@ -158,6 +160,7 @@ data class GradeResponse(
|
||||
val labId: String,
|
||||
val score: BigDecimal,
|
||||
val comments: List<CommentOut> = emptyList(),
|
||||
val generalComment: String = "",
|
||||
)
|
||||
|
||||
data class SubmissionDto(
|
||||
|
||||
@@ -29,6 +29,8 @@ class GradeService(
|
||||
val key = LabGradeKey(sid, lid)
|
||||
val grade = grades.findById(key).orElseGet { LabGrade(studentId = key.studentId, labId = key.labId) }
|
||||
grade.score = request.score
|
||||
grade.generalComment = request.generalComment.trim()
|
||||
grade.updatedAt = java.time.Instant.now()
|
||||
grades.save(grade)
|
||||
replaceComments(sid, lid, request.comments)
|
||||
return GradeResponse(
|
||||
@@ -36,6 +38,7 @@ class GradeService(
|
||||
request.labId,
|
||||
request.score,
|
||||
commentsOf(sid, lid).map { CommentOut(it.text, it.tone) },
|
||||
grade.generalComment,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -122,6 +125,7 @@ class GradeService(
|
||||
labId = it.labId.toString(),
|
||||
score = it.score,
|
||||
comments = (byGrade[it.studentId to it.labId] ?: emptyList()).map { c -> CommentOut(c.text, c.tone) },
|
||||
generalComment = it.generalComment,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ class LabGrade(
|
||||
@Id @Column(nullable = false) var studentId: UUID = UUID(0, 0),
|
||||
@Id @Column(nullable = false) var labId: UUID = UUID(0, 0),
|
||||
@Column(nullable = false) var score: BigDecimal = BigDecimal.ZERO,
|
||||
@Column(name = "general_comment", nullable = false) var generalComment: String = "",
|
||||
@Column(nullable = false) var updatedAt: Instant = Instant.now(),
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
-- Общий (итоговый) комментарий преподавателя по лабораторной для конкретного студента.
|
||||
ALTER TABLE lab_grades ADD COLUMN general_comment text NOT NULL DEFAULT '';
|
||||
@@ -82,6 +82,7 @@ data class SetGradeForm(
|
||||
val labId: String = "",
|
||||
val score: String = "",
|
||||
val commentsJson: String? = null,
|
||||
val generalComment: String = "",
|
||||
)
|
||||
|
||||
data class SubmissionDownloadForm(
|
||||
|
||||
@@ -144,6 +144,7 @@ class AdminGroupController(
|
||||
val (group, labs) = loadGroupAndLabs(t, id)
|
||||
val grades = api.listGrades(t)
|
||||
val gradeBy = grades.associate { "${it.studentId}|${it.labId}" to it.score }
|
||||
val generalCommentBy = grades.associate { "${it.studentId}|${it.labId}" to it.generalComment }
|
||||
val commentsBy = grades.associate { "${it.studentId}|${it.labId}" to it.comments.map { ct -> ct.text to ct.tone } }
|
||||
val memberIds = group.students.map { it.id }
|
||||
val memberIdSet = memberIds.toSet()
|
||||
@@ -168,7 +169,18 @@ class AdminGroupController(
|
||||
val submissionsBy = labs.associateWith { lab -> dataByLab.getValue(lab.id).submissions.associateBy { it.studentId } }
|
||||
val defensesBy = labs.associateWith { lab -> dataByLab.getValue(lab.id).defenses.associateBy { it.studentId } }
|
||||
|
||||
val rows = buildRows(group.students, labs, gradeBy, commentsBy, publishedByLab, assignmentsBy, submissionsBy, defensesBy)
|
||||
val rows =
|
||||
buildRows(
|
||||
group.students,
|
||||
labs,
|
||||
gradeBy,
|
||||
generalCommentBy,
|
||||
commentsBy,
|
||||
publishedByLab,
|
||||
assignmentsBy,
|
||||
submissionsBy,
|
||||
defensesBy,
|
||||
)
|
||||
|
||||
fillGroupModel(model, groupViewModel(t, id, GroupRenderData(group, labs, rows, labPublication, submissionsBy)))
|
||||
return "admin/group"
|
||||
@@ -179,6 +191,7 @@ class AdminGroupController(
|
||||
students: List<StudentRowDto>,
|
||||
labs: List<LabSummaryDto>,
|
||||
gradeBy: Map<String, String?>,
|
||||
generalCommentBy: Map<String, String>,
|
||||
commentsBy: Map<String, List<Pair<String, String>>>,
|
||||
publishedByLab: Map<String, Set<String>>,
|
||||
assignmentsBy: Map<LabSummaryDto, Map<String, VariantAssignmentResultDto>>,
|
||||
@@ -200,6 +213,7 @@ class AdminGroupController(
|
||||
variantId = va?.variantId,
|
||||
variantManual = va?.manual == true,
|
||||
score = gradeBy["${s.id}|${lab.id}"],
|
||||
generalComment = generalCommentBy["${s.id}|${lab.id}"].orEmpty(),
|
||||
published = publishedByLab.getValue(lab.id).contains(s.id),
|
||||
submissionSeq = sub?.seq,
|
||||
submissionId = sub?.id,
|
||||
@@ -344,7 +358,16 @@ class AdminGroupController(
|
||||
): String =
|
||||
try {
|
||||
val comments = parseComments(form.commentsJson ?: "[]")
|
||||
api.setGrade(token(session), GradeSetPayload(form.studentId, form.labId, form.score, comments))
|
||||
api.setGrade(
|
||||
token(session),
|
||||
GradeSetPayload(
|
||||
studentId = form.studentId,
|
||||
labId = form.labId,
|
||||
score = form.score,
|
||||
comments = comments,
|
||||
generalComment = form.generalComment,
|
||||
),
|
||||
)
|
||||
redirect.addFlashAttribute("notice", "Оценка сохранена")
|
||||
"redirect:/admin/groups/$id"
|
||||
} catch (ex: ApiException) {
|
||||
|
||||
@@ -26,6 +26,7 @@ data class GroupLabCell(
|
||||
val variantId: String? = null,
|
||||
val variantManual: Boolean = false,
|
||||
val score: String? = null,
|
||||
val generalComment: String = "",
|
||||
val published: Boolean = false,
|
||||
val submissionSeq: Int? = null,
|
||||
val submissionId: String? = null,
|
||||
|
||||
@@ -261,6 +261,7 @@ data class GradeDto(
|
||||
val labId: String = "",
|
||||
val score: String = "",
|
||||
val comments: List<CommentDto> = emptyList(),
|
||||
val generalComment: String = "",
|
||||
)
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@@ -291,6 +292,7 @@ data class GradeSetPayload(
|
||||
val labId: String,
|
||||
val score: String,
|
||||
val comments: List<CommentPayload> = emptyList(),
|
||||
val generalComment: String = "",
|
||||
)
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
|
||||
@@ -108,6 +108,7 @@ class StudentController(
|
||||
val myGrade = if (gid != null) api.myGrades(t, gid).firstOrNull { it.labId == id } else null
|
||||
model.addAttribute("canSeeGrade", myGrade != null)
|
||||
model.addAttribute("grade", myGrade?.score)
|
||||
model.addAttribute("generalComment", myGrade?.generalComment.orEmpty())
|
||||
model.addAttribute(
|
||||
"commentsPositive",
|
||||
myGrade?.comments?.filter { it.tone == "positive" } ?: emptyList<ru.ysu.lms.web.api.CommentDto>(),
|
||||
|
||||
@@ -17,6 +17,7 @@ body {
|
||||
винный — деструктивные действия и негативная тональность. */
|
||||
/* muted-текст Bootstrap (.text-muted/placeholder) идёт через --bs-secondary-color с !important,
|
||||
поэтому переопределяем её переменные под нашу палитру в обеих темах */
|
||||
/* Строгая геометрия: скругления отключены намеренно — каркас, поля и кнопки прямоугольные. */
|
||||
:root {
|
||||
--bs-secondary-color-rgb: 116, 116, 124;
|
||||
--bs-secondary-color: rgba(116, 116, 124, 0.9);
|
||||
@@ -24,8 +25,12 @@ body {
|
||||
--bs-body-bg: #fafafa;
|
||||
--bs-body-color: #131316;
|
||||
--bs-border-color: #e6e6e9;
|
||||
--bs-border-radius: 0.5rem;
|
||||
--bs-border-radius-lg: 0.625rem;
|
||||
--bs-border-radius: 0;
|
||||
--bs-border-radius-sm: 0;
|
||||
--bs-border-radius-lg: 0;
|
||||
--bs-border-radius-xl: 0;
|
||||
--bs-border-radius-xxl: 0;
|
||||
--bs-border-radius-pill: 0;
|
||||
--bs-link-color: #7a2333;
|
||||
--bs-link-hover-color: #9d2b3c;
|
||||
--c-bg: #fafafa;
|
||||
@@ -48,10 +53,10 @@ body {
|
||||
--c-tone-pos: #2f6b3a;
|
||||
--c-tone-neg: #9d2b3c;
|
||||
--c-tone-neu: #74747c;
|
||||
--radius-xs: 0.125rem;
|
||||
--radius-sm: 0.25rem;
|
||||
--radius: 0.5rem;
|
||||
--radius-lg: 0.625rem;
|
||||
--radius-xs: 0;
|
||||
--radius-sm: 0;
|
||||
--radius: 0;
|
||||
--radius-lg: 0;
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
@@ -159,7 +164,7 @@ code { --bs-code-color: var(--c-text); color: var(--c-text); }
|
||||
}
|
||||
.nav-user { font-weight: 500; color: var(--c-accent); text-decoration: none; }
|
||||
.nav-user:hover { color: var(--c-accent-hover); }
|
||||
.nav-user.defense-active::before { content: ""; display: inline-block; width: 0.5rem; height: 0.5rem; border-radius: 50%; background: var(--c-accent); margin-right: 0.4rem; vertical-align: middle; }
|
||||
.nav-user.defense-active::before { content: ""; display: inline-block; width: 0.5rem; height: 0.5rem; border-radius: 0; background: var(--c-accent); margin-right: 0.4rem; vertical-align: middle; }
|
||||
.page-main { padding: 2.5rem 1.75rem 3.5rem; }
|
||||
@media (max-width: 720px) { .page-main { padding: 1.5rem 0.875rem 2.5rem; } }
|
||||
|
||||
@@ -189,7 +194,7 @@ a { --bs-link-color: var(--c-accent); --bs-link-hover-color: var(--c-accent-hove
|
||||
* { scrollbar-width: thin; scrollbar-color: var(--c-border-strong) var(--c-bg); }
|
||||
::-webkit-scrollbar { width: 10px; height: 10px; }
|
||||
::-webkit-scrollbar-track { background: var(--c-bg); }
|
||||
::-webkit-scrollbar-thumb { background: var(--c-border-strong); border: 2px solid var(--c-bg); border-radius: 8px; }
|
||||
::-webkit-scrollbar-thumb { background: var(--c-border-strong); border: 2px solid var(--c-bg); border-radius: 0; }
|
||||
::-webkit-scrollbar-thumb:hover { background: var(--c-accent); }
|
||||
::-webkit-scrollbar-corner { background: var(--c-bg); }
|
||||
|
||||
@@ -427,7 +432,7 @@ code.code { padding: 0.125rem 0.5rem; border-radius: var(--radius-xs); font-fami
|
||||
.pub-toggle {
|
||||
appearance: none; -webkit-appearance: none;
|
||||
width: 1rem; height: 1rem; margin: 0; padding: 0; cursor: pointer;
|
||||
border-radius: 50%; border: 1.5px solid var(--c-border-strong);
|
||||
border-radius: 0; border: 1.5px solid var(--c-border-strong);
|
||||
background: transparent; vertical-align: middle;
|
||||
transition: border-color 0.15s, box-shadow 0.15s;
|
||||
}
|
||||
@@ -466,7 +471,7 @@ code.code { padding: 0.125rem 0.5rem; border-radius: var(--radius-xs); font-fami
|
||||
color: var(--c-text-muted); font-size: 0.75rem; text-decoration: none; cursor: pointer;
|
||||
}
|
||||
.cell-btn.st:hover { background: none; color: var(--c-text); }
|
||||
.cell-btn.st::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: var(--c-border-strong); }
|
||||
.cell-btn.st::before { content: ""; width: 6px; height: 6px; border-radius: 0; background: var(--c-border-strong); }
|
||||
.cell-btn.st.ok { color: var(--c-tone-pos); }
|
||||
.cell-btn.st.ok::before { background: var(--c-tone-pos); }
|
||||
.cell-btn.st.warn { color: #b45309; }
|
||||
@@ -557,6 +562,11 @@ dialog.dlg.dlg-lg .dlg-body { max-height: 88vh; min-height: 62vh; }
|
||||
.md-preview { max-width: 22rem; }
|
||||
|
||||
/* ---- Комментарии к оценке (ЛК студента) ---- */
|
||||
.general-comment {
|
||||
margin-bottom: 1rem; padding: 0.35rem 0 0.35rem 0.9rem;
|
||||
border-left: 3px solid var(--c-accent);
|
||||
}
|
||||
.general-comment .group-title { margin-bottom: 0.35rem; }
|
||||
.review-cols { display: grid; grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr)); gap: 1rem; }
|
||||
.review-col h3 { margin-bottom: 0.35rem; }
|
||||
.review-list { margin: 0; padding-left: 1.15rem; }
|
||||
|
||||
@@ -20,6 +20,8 @@ function openGradeEditor(btn) {
|
||||
var title = $id('grade-title');
|
||||
if (title) title.textContent = (btn.getAttribute('data-lab-name') || '') + ' — ' + (btn.getAttribute('data-student-name') || '');
|
||||
$id('grade-score').value = btn.getAttribute('data-score') || '';
|
||||
var general = $id('grade-general');
|
||||
if (general) general.value = btn.getAttribute('data-general-comment') || '';
|
||||
|
||||
var box = $id('cs-rows');
|
||||
box.innerHTML = '';
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
</button>
|
||||
<button class="score-link" type="button"
|
||||
th:classappend="${cell.score == null} ? 'none' : ''"
|
||||
th:attr="data-student=${row.studentId},data-student-name=${row.fullName},data-lab=${cell.labId},data-lab-name='ЛР ' + ${cell.labNumber},data-score=${cell.score},data-comments=${row.commentsJson},data-ci=${iterStat.index}" data-action="grade"
|
||||
th:attr="data-student=${row.studentId},data-student-name=${row.fullName},data-lab=${cell.labId},data-lab-name='ЛР ' + ${cell.labNumber},data-score=${cell.score},data-general-comment=${cell.generalComment},data-comments=${row.commentsJson},data-ci=${iterStat.index}" data-action="grade"
|
||||
th:title="${cell.score} ? 'Оценка ' + ${cell.score} + ' — изменить' : 'Оценки нет — выставить балл и комментарии'">
|
||||
<span th:text="${cell.score} ?: '—'">—</span>
|
||||
</button>
|
||||
@@ -160,6 +160,9 @@
|
||||
<div><label class="label">Студент</label><input class="input" id="grade-who" readonly></div>
|
||||
<div><label class="label" for="grade-score">Балл (0–5)</label>
|
||||
<input class="input" id="grade-score" name="score" type="number" min="0" max="5" step="0.1"></div>
|
||||
<div><label class="label" for="grade-general">Общий комментарий по лабораторной</label>
|
||||
<textarea class="input" id="grade-general" name="generalComment" rows="4"
|
||||
placeholder="Итоговый комментарий по работе в целом…"></textarea></div>
|
||||
<div id="cs-rows"></div>
|
||||
<div class="flex gap-2">
|
||||
<button class="btn btn-secondary" type="button" data-action="comment-add">+ комментарий</button>
|
||||
@@ -342,7 +345,7 @@
|
||||
<div class="dlg-body" id="assign-modal-body"></div>
|
||||
</dialog>
|
||||
|
||||
<script th:src="@{/js/admin-group.js(v='34')}" defer></script>
|
||||
<script th:src="@{/js/admin-group.js(v='35')}" defer></script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -196,7 +196,7 @@
|
||||
</dialog>
|
||||
</th:block>
|
||||
</div>
|
||||
<script th:src="@{/js/admin-lab.js(v='34')}" defer></script>
|
||||
<script th:src="@{/js/admin-lab.js(v='35')}" defer></script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -13,22 +13,22 @@
|
||||
<noscript>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
</noscript>
|
||||
<link rel="icon" type="image/svg+xml" th:href="@{/favicon.svg(v='34')}">
|
||||
<!-- v=4: сброс кэша стилей после редизайна; при изменении css поднимать версию -->
|
||||
<link rel="icon" type="image/svg+xml" th:href="@{/favicon.svg(v='35')}">
|
||||
<!-- v=35: сброс кэша стилей после изменения геометрии; при изменении 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='34')}">
|
||||
<link id="hljs-light" rel="stylesheet" th:href="@{/css/hljs-github.css(v='34')}">
|
||||
<link id="hljs-dark" rel="stylesheet" th:href="@{/css/hljs-github-dark.css(v='34')}" disabled>
|
||||
<script th:src="@{/js/highlight.min.js(v='34')}" defer></script>
|
||||
<script th:src="@{/js/table-scroll.js(v='34')}" defer></script>
|
||||
<script th:src="@{/js/tablekit.js(v='34')}" defer></script>
|
||||
<link rel="stylesheet" th:href="@{/css/app.css(v='35')}">
|
||||
<link id="hljs-light" rel="stylesheet" th:href="@{/css/hljs-github.css(v='35')}">
|
||||
<link id="hljs-dark" rel="stylesheet" th:href="@{/css/hljs-github-dark.css(v='35')}" disabled>
|
||||
<script th:src="@{/js/highlight.min.js(v='35')}" defer></script>
|
||||
<script th:src="@{/js/table-scroll.js(v='35')}" defer></script>
|
||||
<script th:src="@{/js/tablekit.js(v='35')}" defer></script>
|
||||
<script th:src="@{/js/bootstrap.bundle.min.js}" defer></script>
|
||||
<script th:src="@{/js/theme-init.js(v='34')}"></script>
|
||||
<script th:src="@{/js/app.js(v='34')}" defer></script>
|
||||
<script th:src="@{/js/ui-state.js(v='34')}" defer></script>
|
||||
<script th:src="@{/js/md-content.js(v='34')}" defer></script>
|
||||
<script th:src="@{/js/submit-guard.js(v='34')}" defer></script>
|
||||
<script th:src="@{/js/theme-init.js(v='35')}"></script>
|
||||
<script th:src="@{/js/app.js(v='35')}" defer></script>
|
||||
<script th:src="@{/js/ui-state.js(v='35')}" defer></script>
|
||||
<script th:src="@{/js/md-content.js(v='35')}" defer></script>
|
||||
<script th:src="@{/js/submit-guard.js(v='35')}" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="page d-flex flex-column min-vh-100">
|
||||
|
||||
@@ -138,7 +138,7 @@
|
||||
</div>
|
||||
|
||||
|
||||
<script th:src="@{/js/mdeditor.js(v='34')}" defer></script>
|
||||
<script th:src="@{/js/mdeditor.js(v='35')}" defer></script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -183,7 +183,7 @@
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<script th:src="@{/js/content-sort.js(v='34')}" defer></script>
|
||||
<script th:src="@{/js/content-sort.js(v='35')}" defer></script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -236,8 +236,8 @@
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<script th:src="@{/js/mdeditor.js(v='34')}" defer></script>
|
||||
<script th:src="@{/js/content-sort.js(v='34')}" defer></script>
|
||||
<script th:src="@{/js/mdeditor.js(v='35')}" defer></script>
|
||||
<script th:src="@{/js/content-sort.js(v='35')}" defer></script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -143,7 +143,7 @@
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<script th:src="@{/js/content-sort.js(v='34')}" defer></script>
|
||||
<script th:src="@{/js/content-sort.js(v='35')}" defer></script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -57,6 +57,10 @@
|
||||
<h2 class="section-title">Оценка и комментарии</h2>
|
||||
<div class="card card-body mb-6">
|
||||
<p class="mb-4 mt-1">Ваша оценка за лабораторную: <span class="score-big" th:text="${grade}"></span></p>
|
||||
<div th:if="${!#strings.isEmpty(generalComment)}" class="general-comment">
|
||||
<h3 class="group-title">Общий комментарий по лабораторной</h3>
|
||||
<p class="pre-wrap mb-4" th:text="${generalComment}">—</p>
|
||||
</div>
|
||||
<div class="review-cols">
|
||||
<div class="review-col" th:if="${!#lists.isEmpty(commentsPositive)}">
|
||||
<h3 class="group-title tone-positive-text">Позитивный</h3>
|
||||
@@ -107,7 +111,7 @@
|
||||
<p class="text-sm text-muted">Ожидайте преподавателя с проверкой.</p>
|
||||
</div>
|
||||
</div>
|
||||
<script th:src="@{/js/defense-timer.js(v='34')}" defer></script>
|
||||
<script th:src="@{/js/defense-timer.js(v='35')}" defer></script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user