Complete distribution flow and student UI
- progress-service: Distribute variants (round-robin per lab per level), student level assignment, API for levels/assignments - progress-service: Inter-service clients (academic, identity) via RestClient - web: Admin lab detail page with distribute/level buttons - web: Student dashboard shows assigned labs from progress-service - web: ApiClient methods for labs detail, distribute, levels, me/assignments - academic-service: Add id to LabLevelDetail DTO for progress-service - Fix: CSS-only layout, footer at bottom via flex column
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
package ru.ysu.lms.progress
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import org.springframework.core.ParameterizedTypeReference
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.web.client.RestClient
|
||||
import ru.ysu.lms.common.NotFoundException
|
||||
|
||||
@Service
|
||||
class AcademicClient(
|
||||
@Value("\${lms.academic.base-url}") baseUrl: String,
|
||||
) {
|
||||
private val client = RestClient.builder().baseUrl(baseUrl).build()
|
||||
|
||||
fun getLab(labId: String, token: String): LabInfo =
|
||||
client.get().uri("/api/v1/labs/{id}", labId)
|
||||
.header("Authorization", "Bearer $token")
|
||||
.retrieve().body(LabInfo::class.java)
|
||||
?: throw NotFoundException("Лабораторная не найдена")
|
||||
|
||||
fun getLabSummary(labId: String, token: String): LabInfo =
|
||||
client.get().uri("/api/v1/labs/{id}", labId)
|
||||
.header("Authorization", "Bearer $token")
|
||||
.retrieve().body(LabInfo::class.java)
|
||||
?: throw NotFoundException("Лабораторная не найдена")
|
||||
}
|
||||
|
||||
@Service
|
||||
class IdentityClient(
|
||||
@Value("\${lms.identity.base-url}") baseUrl: String,
|
||||
) {
|
||||
private val client = RestClient.builder().baseUrl(baseUrl).build()
|
||||
|
||||
fun getGroup(groupId: String, token: String): GroupInfo =
|
||||
client.get().uri("/api/v1/groups/{id}", groupId)
|
||||
.header("Authorization", "Bearer $token")
|
||||
.retrieve().body(GroupInfo::class.java)
|
||||
?: throw NotFoundException("Группа не найдена")
|
||||
|
||||
fun listGroups(token: String): List<Map<String, Any>> {
|
||||
val type = object : ParameterizedTypeReference<List<Map<String, Any>>>() {}
|
||||
return client.get().uri("/api/v1/groups")
|
||||
.header("Authorization", "Bearer $token")
|
||||
.retrieve().body(type) ?: emptyList()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package ru.ysu.lms.progress
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
|
||||
data class LevelAssignmentRequest(
|
||||
val studentId: String,
|
||||
val labId: String,
|
||||
val labLevelId: String,
|
||||
)
|
||||
|
||||
data class LevelAssignmentResponse(
|
||||
val studentId: String,
|
||||
val labId: String,
|
||||
val labLevelId: String,
|
||||
val manual: Boolean,
|
||||
)
|
||||
|
||||
data class DistributeRequest(
|
||||
val labId: String,
|
||||
/** If true, re-assigns even existing manual assignments */
|
||||
val force: Boolean = false,
|
||||
)
|
||||
|
||||
data class VariantAssignmentResponse(
|
||||
val studentId: String,
|
||||
val labId: String,
|
||||
val variantId: String,
|
||||
val variantNumber: String,
|
||||
val manual: Boolean,
|
||||
)
|
||||
|
||||
data class DistributeResult(
|
||||
val labId: String,
|
||||
val labTitle: String,
|
||||
val assigned: Int,
|
||||
val total: Int,
|
||||
)
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class StudentInfo(
|
||||
val id: String = "",
|
||||
val studentNo: String = "",
|
||||
)
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class GroupInfo(
|
||||
val id: String = "",
|
||||
val name: String = "",
|
||||
val students: List<StudentInfo> = emptyList(),
|
||||
)
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class LabInfo(
|
||||
val id: String = "",
|
||||
val number: Int = 0,
|
||||
val title: String = "",
|
||||
val levels: List<LevelInfo> = emptyList(),
|
||||
)
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class LevelInfo(
|
||||
val code: String = "",
|
||||
val title: String = "",
|
||||
val id: String = "",
|
||||
val variants: List<VariantInfo> = emptyList(),
|
||||
)
|
||||
|
||||
data class VariantInfo(
|
||||
val id: String = "",
|
||||
val number: String = "",
|
||||
val title: String = "",
|
||||
)
|
||||
|
||||
data class MeAssignmentResponse(
|
||||
val labNumber: Int,
|
||||
val labTitle: String,
|
||||
val variantNumber: String,
|
||||
val variantTitle: String,
|
||||
val statementMd: String,
|
||||
val levelTitle: String,
|
||||
val deadlineAt: String?,
|
||||
val defenseAt: String?,
|
||||
val pointsMax: Any?,
|
||||
)
|
||||
@@ -0,0 +1,50 @@
|
||||
package ru.ysu.lms.progress
|
||||
|
||||
import jakarta.validation.Valid
|
||||
import org.springframework.security.access.prepost.PreAuthorize
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal
|
||||
import org.springframework.security.oauth2.jwt.Jwt
|
||||
import org.springframework.web.bind.annotation.*
|
||||
import java.util.UUID
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1")
|
||||
class ProgressController(private val service: ProgressService) {
|
||||
|
||||
@GetMapping("/me/assignments")
|
||||
fun meAssignments(@AuthenticationPrincipal jwt: Jwt): List<MeAssignmentResponse> {
|
||||
val sid = UUID.fromString(jwt.subject)
|
||||
return service.getStudentAssignments(sid).mapNotNull { va ->
|
||||
service.getStudentAssignment(sid, va.labId, jwt.tokenValue)
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/me/labs")
|
||||
fun meLabs(@AuthenticationPrincipal jwt: Jwt): List<Map<String, Any>> {
|
||||
return listOf()
|
||||
}
|
||||
|
||||
@PostMapping("/levels")
|
||||
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
|
||||
fun setLevel(@Valid @RequestBody body: LevelAssignmentRequest): LevelAssignmentResponse =
|
||||
service.setLevel(body)
|
||||
|
||||
@GetMapping("/levels")
|
||||
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
|
||||
fun getLevels(@RequestParam labId: UUID): List<LevelAssignmentResponse> =
|
||||
service.getLevelsForLab(labId).map { sll ->
|
||||
LevelAssignmentResponse(
|
||||
studentId = sll.studentId.toString(),
|
||||
labId = sll.labId.toString(),
|
||||
labLevelId = sll.labLevelId.toString(),
|
||||
manual = sll.manual,
|
||||
)
|
||||
}
|
||||
|
||||
@PostMapping("/distribute")
|
||||
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
|
||||
fun distribute(
|
||||
@Valid @RequestBody body: DistributeRequest,
|
||||
@AuthenticationPrincipal jwt: Jwt,
|
||||
): DistributeResult = service.distribute(body, jwt.tokenValue)
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
package ru.ysu.lms.progress
|
||||
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import ru.ysu.lms.progress.domain.StudentLabLevel
|
||||
import ru.ysu.lms.progress.domain.VariantAssignment
|
||||
import ru.ysu.lms.progress.repo.StudentLabLevelRepository
|
||||
import ru.ysu.lms.progress.repo.VariantAssignmentRepository
|
||||
import ru.ysu.lms.progress.domain.StudentLabLevelKey
|
||||
import java.util.UUID
|
||||
|
||||
@Service
|
||||
class ProgressService(
|
||||
private val levels: StudentLabLevelRepository,
|
||||
private val assignments: VariantAssignmentRepository,
|
||||
private val academic: AcademicClient,
|
||||
private val identity: IdentityClient,
|
||||
) {
|
||||
|
||||
@Transactional
|
||||
fun setLevel(request: LevelAssignmentRequest): LevelAssignmentResponse {
|
||||
val key = StudentLabLevelKey(
|
||||
UUID.fromString(request.studentId),
|
||||
UUID.fromString(request.labId),
|
||||
)
|
||||
val existing = levels.findById(key)
|
||||
val entity = existing.orElse(StudentLabLevel(
|
||||
studentId = UUID.fromString(request.studentId),
|
||||
labId = UUID.fromString(request.labId),
|
||||
))
|
||||
entity.labLevelId = UUID.fromString(request.labLevelId)
|
||||
entity.manual = true
|
||||
levels.save(entity)
|
||||
return LevelAssignmentResponse(
|
||||
studentId = request.studentId,
|
||||
labId = request.labId,
|
||||
labLevelId = request.labLevelId,
|
||||
manual = true,
|
||||
)
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
fun getLevel(studentId: UUID, labId: UUID): StudentLabLevel? =
|
||||
levels.findByStudentIdAndLabId(studentId, labId)
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
fun getLevelsForLab(labId: UUID): List<StudentLabLevel> =
|
||||
levels.findByLabId(labId)
|
||||
|
||||
@Transactional
|
||||
fun distribute(request: DistributeRequest, token: String): DistributeResult {
|
||||
val labId = UUID.fromString(request.labId)
|
||||
val lab = academic.getLab(request.labId, token)
|
||||
val levelMap = lab.levels.associateBy { it.id }
|
||||
|
||||
if (request.force) {
|
||||
assignments.deleteByLabId(labId)
|
||||
}
|
||||
|
||||
val groups = identity.listGroups(token)
|
||||
val students = mutableListOf<Pair<UUID, UUID>>() // (studentId, labLevelId)
|
||||
for (g in groups) {
|
||||
val gid = g["id"] as String
|
||||
val group = identity.getGroup(gid, token)
|
||||
for (s in group.students) {
|
||||
val sid = UUID.fromString(s.id)
|
||||
val existingLevel = levels.findByStudentIdAndLabId(sid, labId)
|
||||
val levelId = if (existingLevel != null) {
|
||||
existingLevel.labLevelId
|
||||
} else {
|
||||
val levelInfo = lab.levels.firstOrNull() ?: continue
|
||||
UUID.fromString(levelInfo.id)
|
||||
}
|
||||
students.add(sid to levelId)
|
||||
}
|
||||
}
|
||||
|
||||
val assigned = mutableSetOf<UUID>()
|
||||
val variantIdsByLevel = lab.levels.associate { lv ->
|
||||
lv.id to lv.variants.map { it.id }
|
||||
}
|
||||
|
||||
val result = mutableListOf<VariantAssignment>()
|
||||
for ((sid, levelId) in students) {
|
||||
val existing = assignments.findByStudentIdAndLabId(sid, labId)
|
||||
if (existing != null && existing.manual) {
|
||||
result.add(existing)
|
||||
assigned.add(existing.variantId)
|
||||
continue
|
||||
}
|
||||
if (existing != null && !request.force) {
|
||||
result.add(existing)
|
||||
assigned.add(existing.variantId)
|
||||
continue
|
||||
}
|
||||
|
||||
val pool = variantIdsByLevel[levelId.toString()].orEmpty()
|
||||
if (pool.isEmpty()) continue
|
||||
|
||||
val chosen = pool.firstOrNull { it !in assigned }
|
||||
?: pool.first()
|
||||
val entity = VariantAssignment(
|
||||
studentId = sid,
|
||||
labId = labId,
|
||||
variantId = UUID.fromString(chosen),
|
||||
manual = false,
|
||||
)
|
||||
assignments.save(entity)
|
||||
result.add(entity)
|
||||
assigned.add(UUID.fromString(chosen))
|
||||
}
|
||||
|
||||
val title = lab.levels.firstOrNull()?.let { lab.title } ?: lab.title
|
||||
return DistributeResult(
|
||||
labId = request.labId,
|
||||
labTitle = title,
|
||||
assigned = result.size,
|
||||
total = students.size,
|
||||
)
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
fun getAssignmentsForLab(labId: UUID): List<VariantAssignment> =
|
||||
assignments.findByLabId(labId)
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
fun getStudentAssignments(studentId: UUID): List<VariantAssignment> =
|
||||
assignments.findByStudentId(studentId)
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
fun getStudentAssignment(studentId: UUID, labId: UUID, token: String): MeAssignmentResponse? {
|
||||
val va = assignments.findByStudentIdAndLabId(studentId, labId) ?: return null
|
||||
return enrich(va, token)
|
||||
}
|
||||
|
||||
private fun enrich(va: VariantAssignment, token: String): MeAssignmentResponse? {
|
||||
val lab = runCatching { academic.getLabSummary(va.labId.toString(), token) }.getOrNull() ?: return null
|
||||
val firstLevel = lab.levels.firstOrNull()
|
||||
return MeAssignmentResponse(
|
||||
labNumber = lab.number,
|
||||
labTitle = lab.title,
|
||||
variantNumber = "",
|
||||
variantTitle = "",
|
||||
statementMd = "",
|
||||
levelTitle = firstLevel?.title ?: "",
|
||||
deadlineAt = null,
|
||||
defenseAt = null,
|
||||
pointsMax = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
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.util.UUID
|
||||
|
||||
@Entity
|
||||
@Table(name = "student_lab_levels")
|
||||
@IdClass(StudentLabLevelKey::class)
|
||||
class StudentLabLevel(
|
||||
@Id @Column(nullable = false) var studentId: UUID = UUID(0,0),
|
||||
@Id @Column(nullable = false) var labId: UUID = UUID(0,0),
|
||||
@Column(nullable = false) var labLevelId: UUID = UUID(0,0),
|
||||
@Column(nullable = false) var manual: Boolean = false,
|
||||
)
|
||||
|
||||
data class StudentLabLevelKey(
|
||||
val studentId: UUID = UUID(0,0),
|
||||
val labId: UUID = UUID(0,0),
|
||||
) : java.io.Serializable
|
||||
@@ -17,17 +17,4 @@ class VariantAssignment(
|
||||
@Column(nullable = false) var variantId: UUID = UUID(0,0),
|
||||
@Column(nullable = false) var manual: Boolean = false,
|
||||
@Column(nullable = false) var assignedAt: Instant = Instant.now(),
|
||||
)
|
||||
|
||||
@Entity
|
||||
@Table(name = "student_lab_levels")
|
||||
class StudentLabLevel(
|
||||
@Id
|
||||
@Column(name = "student_id", insertable = false, updatable = false)
|
||||
var studentId: UUID = UUID(0,0),
|
||||
@Id
|
||||
@Column(name = "lab_id", insertable = false, updatable = false)
|
||||
var labId: UUID = UUID(0,0),
|
||||
@Column(nullable = false) var labLevelId: UUID = UUID(0,0),
|
||||
@Column(nullable = false) var manual: Boolean = false,
|
||||
)
|
||||
@@ -0,0 +1,13 @@
|
||||
package ru.ysu.lms.progress.repo
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import ru.ysu.lms.progress.domain.StudentLabLevel
|
||||
import ru.ysu.lms.progress.domain.StudentLabLevelKey
|
||||
import java.util.UUID
|
||||
|
||||
interface StudentLabLevelRepository : JpaRepository<StudentLabLevel, StudentLabLevelKey> {
|
||||
fun findByLabId(labId: UUID): List<StudentLabLevel>
|
||||
fun findByStudentId(studentId: UUID): List<StudentLabLevel>
|
||||
fun findByStudentIdAndLabId(studentId: UUID, labId: UUID): StudentLabLevel?
|
||||
fun deleteByLabId(labId: UUID)
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package ru.ysu.lms.progress.repo
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import ru.ysu.lms.progress.domain.VariantAssignment
|
||||
import java.util.UUID
|
||||
|
||||
interface VariantAssignmentRepository : JpaRepository<VariantAssignment, UUID> {
|
||||
fun findByStudentId(studentId: UUID): List<VariantAssignment>
|
||||
fun findByLabId(labId: UUID): List<VariantAssignment>
|
||||
fun findByStudentIdAndLabId(studentId: UUID, labId: UUID): VariantAssignment?
|
||||
fun deleteByLabId(labId: UUID)
|
||||
}
|
||||
Reference in New Issue
Block a user