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:
@@ -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(
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
|
||||
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user