fix: polish attestation columns and inline grading

- use numeric semesters 1–8 and add show/hide publication per attestation
- edit attestation results directly in the group table without a modal
- hide unpublished attestations from students
- align header separators, average column and action buttons with the existing UI
- remove missed-hours fields until a separate attendance model is ready
This commit is contained in:
2026-10-26 17:15:16 +03:00
parent f7a1830548
commit fb9511280a
20 changed files with 290 additions and 176 deletions

View File

@@ -39,6 +39,13 @@ class AttestationController(
return mapOf("ok" to true)
}
@PostMapping("/attestations/{id}/publication")
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
fun setPublished(
@PathVariable id: UUID,
@RequestBody body: AttestationPublicationRequest,
): AttestationResponse = service.setPublished(id, body.published)
@PostMapping("/attestations/{id}/results")
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
fun setResult(

View File

@@ -30,27 +30,41 @@ class AttestationService(
groupId: UUID,
studentId: UUID,
): List<AttestationResponse> =
list(groupId).map { column ->
column.copy(results = column.results.filter { it.studentId == studentId.toString() })
}
list(groupId)
.filter { it.published }
.map { column ->
column.copy(results = column.results.filter { it.studentId == studentId.toString() })
}
@Transactional
fun create(request: AttestationCreateRequest): AttestationResponse {
val kind = normalizeKind(request.kind)
val semester = request.semester.trim()
if (semester.isEmpty()) throw BadRequestException("Укажите семестр")
if (semester.length > MAX_SEMESTER_LENGTH) throw BadRequestException("Слишком длинное название семестра")
if (request.semester !in 1..8) throw BadRequestException("Укажите семестр от 1 до 8")
val column =
attestations.save(
Attestation(
groupId = UUID.fromString(request.groupId),
kind = kind,
semester = semester,
semester = request.semester,
),
)
return toResponse(column, emptyList())
}
@Transactional
fun setPublished(
attestationId: UUID,
published: Boolean,
): AttestationResponse {
val column =
attestations
.findById(attestationId)
.orElseThrow { NotFoundException("Аттестация не найдена") }
column.published = published
attestations.save(column)
return toResponse(column, results.findByAttestationId(attestationId))
}
@Transactional
fun delete(attestationId: UUID) {
val column =
@@ -81,15 +95,10 @@ class AttestationService(
val allowed = VALUES[column.kind] ?: emptySet()
if (value !in allowed) throw BadRequestException("Недопустимая оценка для этой формы аттестации")
val missedHours = if (column.kind == KIND_INTERMEDIATE) request.missedHours else null
if (missedHours != null && missedHours < 0) {
throw BadRequestException("Число пропущенных часов не может быть отрицательным")
}
val entity =
(existing ?: AttestationResult(attestationId = attestationId, studentId = studentId)).apply {
this.value = value
this.missedHours = missedHours
this.updatedAt = Instant.now()
}
results.save(entity)
@@ -111,17 +120,17 @@ class AttestationService(
groupId = column.groupId.toString(),
kind = column.kind,
semester = column.semester,
published = column.published,
results =
rows
.sortedBy { it.studentId }
.map { AttestationResultResponse(it.studentId.toString(), it.value, it.missedHours) },
.map { AttestationResultResponse(it.studentId.toString(), it.value) },
)
companion object {
const val KIND_INTERMEDIATE = "intermediate"
const val KIND_PASS = "pass"
const val KIND_EXAM = "exam"
private const val MAX_SEMESTER_LENGTH = 100
private val KINDS = setOf(KIND_INTERMEDIATE, KIND_PASS, KIND_EXAM)
private val VALUES =

View File

@@ -167,27 +167,31 @@ data class AttestationCreateRequest(
val groupId: String,
/** intermediate — промежуточная аттестация, pass — зачёт, exam — экзамен. */
val kind: String,
val semester: String,
/** Номер семестра: 1–8. */
val semester: Int,
)
data class AttestationResultRequest(
val studentId: String,
/** Пустое значение очищает результат. */
val value: String = "",
val missedHours: Int? = null,
)
data class AttestationResultResponse(
val studentId: String,
val value: String,
val missedHours: Int? = null,
)
data class AttestationPublicationRequest(
val published: Boolean,
)
data class AttestationResponse(
val id: String,
val groupId: String,
val kind: String,
val semester: String,
val semester: Int,
val published: Boolean = false,
val results: List<AttestationResultResponse> = emptyList(),
)

View File

@@ -16,7 +16,8 @@ class Attestation(
@Id var id: UUID = Ids.v7(),
@Column(name = "group_id", nullable = false) var groupId: UUID = UUID(0, 0),
@Column(nullable = false, length = 16) var kind: String = "intermediate",
@Column(nullable = false, length = 100) var semester: String = "",
@Column(nullable = false) var semester: Int = 1,
@Column(nullable = false) var published: Boolean = false,
@Column(name = "created_at", nullable = false) var createdAt: Instant = Instant.now(),
)
@@ -27,7 +28,6 @@ class AttestationResult(
@Id @Column(name = "attestation_id", nullable = false) var attestationId: UUID = UUID(0, 0),
@Id @Column(name = "student_id", nullable = false) var studentId: UUID = UUID(0, 0),
@Column(nullable = false, length = 32) var value: String = "",
@Column(name = "missed_hours") var missedHours: Int? = null,
@Column(name = "updated_at", nullable = false) var updatedAt: Instant = Instant.now(),
)

View File

@@ -0,0 +1,16 @@
-- Семестр аттестации — номер 1–8; пропуски пока не учитываем.
ALTER TABLE attestations ADD COLUMN semester_no integer;
UPDATE attestations
SET semester_no = NULLIF(regexp_replace(semester, '[^0-9]', '', 'g'), '')::integer
WHERE regexp_replace(semester, '[^0-9]', '', 'g') ~ '^[1-8]$';
UPDATE attestations SET semester_no = 1 WHERE semester_no IS NULL;
ALTER TABLE attestations ALTER COLUMN semester_no SET NOT NULL;
ALTER TABLE attestations DROP COLUMN semester;
ALTER TABLE attestations RENAME COLUMN semester_no TO semester;
ALTER TABLE attestations ADD CONSTRAINT attestations_semester_check CHECK (semester BETWEEN 1 AND 8);
ALTER TABLE attestations ADD COLUMN published boolean NOT NULL DEFAULT false;
ALTER TABLE attestation_results DROP COLUMN missed_hours;