Lab editing and group grades

- academic-service: PUT /labs/{id} (title, deadline, defense, points, status,
  description), POST/PUT/DELETE variants endpoints
- progress-service: lab_grades table, POST/GET /api/v1/grades, GET /me/grades
- gateway: routes for /api/v1/assign and /api/v1/grades
- web: lab edit page (metadata + per-variant forms + add/delete variant)
- web: group page shows grades matrix per lab with editable score inputs
  and computed average column
- web: student dashboard renders statement via markdown, shows deadline
This commit is contained in:
2026-09-11 00:03:24 +03:00
parent feed600d43
commit cca20b5416
18 changed files with 567 additions and 12 deletions

View File

@@ -1,6 +1,7 @@
package ru.ysu.lms.progress
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import java.math.BigDecimal
data class LevelAssignmentRequest(
val studentId: String,
@@ -120,4 +121,16 @@ data class LabStudentsResponse(
data class GroupAssignmentResponse(
val groupName: String,
val students: List<EnrichedAssignment>,
)
data class GradeSetRequest(
val studentId: String,
val labId: String,
val score: BigDecimal,
)
data class GradeResponse(
val studentId: String,
val labId: String,
val score: BigDecimal,
)

View File

@@ -64,4 +64,16 @@ class ProgressController(
@RequestParam labId: UUID,
@AuthenticationPrincipal jwt: Jwt,
): List<VariantAssignmentResponse> = service.assignmentsForLabEnriched(labId, jwt.tokenValue)
@PostMapping("/grades")
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
fun setGrade(@Valid @RequestBody body: GradeSetRequest): GradeResponse = service.setGrade(body)
@GetMapping("/grades")
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
fun listGrades(): List<GradeResponse> = service.listGrades()
@GetMapping("/me/grades")
fun myGrades(@AuthenticationPrincipal jwt: Jwt): List<GradeResponse> =
service.listGrades().filter { it.studentId == jwt.subject }
}

View File

@@ -2,17 +2,23 @@ package ru.ysu.lms.progress
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import ru.ysu.lms.common.BadRequestException
import ru.ysu.lms.progress.domain.LabGrade
import ru.ysu.lms.progress.domain.LabGradeKey
import ru.ysu.lms.progress.domain.StudentLabLevel
import ru.ysu.lms.progress.domain.VariantAssignment
import ru.ysu.lms.progress.repo.LabGradeRepository
import ru.ysu.lms.progress.repo.StudentLabLevelRepository
import ru.ysu.lms.progress.repo.VariantAssignmentRepository
import ru.ysu.lms.progress.domain.StudentLabLevelKey
import java.math.BigDecimal
import java.util.UUID
@Service
class ProgressService(
private val levels: StudentLabLevelRepository,
private val assignments: VariantAssignmentRepository,
private val grades: LabGradeRepository,
private val academic: AcademicClient,
private val identity: IdentityClient,
) {
@@ -169,6 +175,22 @@ class ProgressService(
fun getStudentAssignments(studentId: UUID): List<VariantAssignment> =
assignments.findByStudentId(studentId)
@Transactional
fun setGrade(request: GradeSetRequest): GradeResponse {
if (request.score < BigDecimal.ZERO || request.score > BigDecimal("5")) {
throw BadRequestException("Оценка должна быть в диапазоне 0–5")
}
val key = LabGradeKey(UUID.fromString(request.studentId), UUID.fromString(request.labId))
val grade = grades.findById(key).orElseGet { LabGrade(studentId = key.studentId, labId = key.labId) }
grade.score = request.score
grades.save(grade)
return GradeResponse(request.studentId, request.labId, request.score)
}
@Transactional(readOnly = true)
fun listGrades(): List<GradeResponse> =
grades.findAllBy().map { GradeResponse(it.studentId.toString(), it.labId.toString(), it.score) }
@Transactional(readOnly = true)
fun getStudentAssignment(studentId: UUID, labId: UUID, token: String): MeAssignmentResponse? {
val va = assignments.findByStudentIdAndLabId(studentId, labId) ?: return null

View File

@@ -0,0 +1,26 @@
package ru.ysu.lms.progress.domain
import jakarta.persistence.Column
import jakarta.persistence.Entity
import jakarta.persistence.Id
import jakarta.persistence.IdClass
import jakarta.persistence.Table
import java.io.Serializable
import java.math.BigDecimal
import java.time.Instant
import java.util.UUID
@Entity
@Table(name = "lab_grades")
@IdClass(LabGradeKey::class)
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(nullable = false) var updatedAt: Instant = Instant.now(),
)
data class LabGradeKey(
val studentId: UUID = UUID(0, 0),
val labId: UUID = UUID(0, 0),
) : Serializable

View File

@@ -0,0 +1,12 @@
package ru.ysu.lms.progress.repo
import org.springframework.data.jpa.repository.JpaRepository
import ru.ysu.lms.progress.domain.LabGrade
import ru.ysu.lms.progress.domain.LabGradeKey
import java.util.UUID
interface LabGradeRepository : JpaRepository<LabGrade, LabGradeKey> {
fun findAllBy(): List<LabGrade>
fun findByStudentId(studentId: UUID): List<LabGrade>
fun findByStudentIdAndLabId(studentId: UUID, labId: UUID): LabGrade?
}

View File

@@ -0,0 +1,9 @@
-- Lab grades: one grade per student per lab.
CREATE TABLE lab_grades (
student_id uuid NOT NULL,
lab_id uuid NOT NULL,
score numeric(4,1) NOT NULL,
updated_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (student_id, lab_id)
);