Lab assignment management UI and student variant view
- progress-service: POST /api/v1/assign (manual variant), GET /api/v1/assignments?labId= (enriched assignments), fixed enrich() to return real variant number/title/statement - gateway: routes for /api/v1/assign and /api/v1/levels (exact paths) - web: lab detail page shows students grouped with level and variant selects, group filter, distribute/redistribute buttons, source badge (auto/manual) - web: student dashboard renders variant statement via libs/markdown (spoilers, code highlight, sanitized) - academic-service: unquote YAML front-matter values (variant numbers were quoted) - academic-service: LabLevelDetail.id for level references
This commit is contained in:
@@ -40,6 +40,7 @@ data class DistributeResult(
|
||||
data class StudentInfo(
|
||||
val id: String = "",
|
||||
val studentNo: String = "",
|
||||
val fullName: String = "",
|
||||
)
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
@@ -54,21 +55,28 @@ data class LabInfo(
|
||||
val id: String = "",
|
||||
val number: Int = 0,
|
||||
val title: String = "",
|
||||
val status: String = "",
|
||||
val descriptionMd: String = "",
|
||||
val deadlineAt: String? = null,
|
||||
val defenseAt: String? = null,
|
||||
val pointsMax: Any? = null,
|
||||
val levels: List<LevelInfo> = emptyList(),
|
||||
)
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class LevelInfo(
|
||||
val id: String = "",
|
||||
val code: String = "",
|
||||
val title: String = "",
|
||||
val id: String = "",
|
||||
val variants: List<VariantInfo> = emptyList(),
|
||||
)
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class VariantInfo(
|
||||
val id: String = "",
|
||||
val number: String = "",
|
||||
val title: String = "",
|
||||
val statementMd: String = "",
|
||||
)
|
||||
|
||||
data class MeAssignmentResponse(
|
||||
@@ -81,4 +89,35 @@ data class MeAssignmentResponse(
|
||||
val deadlineAt: String?,
|
||||
val defenseAt: String?,
|
||||
val pointsMax: Any?,
|
||||
)
|
||||
|
||||
data class AssignRequest(
|
||||
val studentId: String,
|
||||
val labId: String,
|
||||
val variantId: String,
|
||||
)
|
||||
|
||||
data class EnrichedAssignment(
|
||||
val studentId: String,
|
||||
val studentName: String,
|
||||
val studentNo: String,
|
||||
val groupName: String,
|
||||
val levelCode: String,
|
||||
val levelTitle: String,
|
||||
val variantId: String?,
|
||||
val variantNumber: String?,
|
||||
val manual: Boolean,
|
||||
)
|
||||
|
||||
data class LabStudentsResponse(
|
||||
val labId: String,
|
||||
val labNumber: Int,
|
||||
val labTitle: String,
|
||||
val levels: List<LevelInfo>,
|
||||
val groups: List<GroupAssignmentResponse>,
|
||||
)
|
||||
|
||||
data class GroupAssignmentResponse(
|
||||
val groupName: String,
|
||||
val students: List<EnrichedAssignment>,
|
||||
)
|
||||
@@ -9,7 +9,10 @@ import java.util.UUID
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1")
|
||||
class ProgressController(private val service: ProgressService) {
|
||||
class ProgressController(
|
||||
private val service: ProgressService,
|
||||
private val academic: AcademicClient,
|
||||
) {
|
||||
|
||||
@GetMapping("/me/assignments")
|
||||
fun meAssignments(@AuthenticationPrincipal jwt: Jwt): List<MeAssignmentResponse> {
|
||||
@@ -47,4 +50,18 @@ class ProgressController(private val service: ProgressService) {
|
||||
@Valid @RequestBody body: DistributeRequest,
|
||||
@AuthenticationPrincipal jwt: Jwt,
|
||||
): DistributeResult = service.distribute(body, jwt.tokenValue)
|
||||
|
||||
@PostMapping("/assign")
|
||||
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
|
||||
fun assign(
|
||||
@Valid @RequestBody body: AssignRequest,
|
||||
@AuthenticationPrincipal jwt: Jwt,
|
||||
): VariantAssignmentResponse = service.assignWithInfo(body, jwt.tokenValue)
|
||||
|
||||
@GetMapping("/assignments")
|
||||
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
|
||||
fun assignmentsForLab(
|
||||
@RequestParam labId: UUID,
|
||||
@AuthenticationPrincipal jwt: Jwt,
|
||||
): List<VariantAssignmentResponse> = service.assignmentsForLabEnriched(labId, jwt.tokenValue)
|
||||
}
|
||||
@@ -75,7 +75,7 @@ class ProgressService(
|
||||
}
|
||||
}
|
||||
|
||||
val assigned = mutableSetOf<UUID>()
|
||||
val assigned = mutableSetOf<String>()
|
||||
val variantIdsByLevel = lab.levels.associate { lv ->
|
||||
lv.id to lv.variants.map { it.id }
|
||||
}
|
||||
@@ -85,12 +85,12 @@ class ProgressService(
|
||||
val existing = assignments.findByStudentIdAndLabId(sid, labId)
|
||||
if (existing != null && existing.manual) {
|
||||
result.add(existing)
|
||||
assigned.add(existing.variantId)
|
||||
assigned.add(existing.variantId.toString())
|
||||
continue
|
||||
}
|
||||
if (existing != null && !request.force) {
|
||||
result.add(existing)
|
||||
assigned.add(existing.variantId)
|
||||
assigned.add(existing.variantId.toString())
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ class ProgressService(
|
||||
)
|
||||
assignments.save(entity)
|
||||
result.add(entity)
|
||||
assigned.add(UUID.fromString(chosen))
|
||||
assigned.add(chosen)
|
||||
}
|
||||
|
||||
val title = lab.levels.firstOrNull()?.let { lab.title } ?: lab.title
|
||||
@@ -123,6 +123,48 @@ class ProgressService(
|
||||
fun getAssignmentsForLab(labId: UUID): List<VariantAssignment> =
|
||||
assignments.findByLabId(labId)
|
||||
|
||||
@Transactional
|
||||
fun assign(request: AssignRequest): VariantAssignment {
|
||||
val sid = UUID.fromString(request.studentId)
|
||||
val lid = UUID.fromString(request.labId)
|
||||
val existing = assignments.findByStudentIdAndLabId(sid, lid)
|
||||
val entity = (existing ?: VariantAssignment(studentId = sid, labId = lid)).apply {
|
||||
variantId = UUID.fromString(request.variantId)
|
||||
manual = true
|
||||
}
|
||||
return assignments.save(entity)
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun assignWithInfo(request: AssignRequest, token: String): VariantAssignmentResponse {
|
||||
assign(request)
|
||||
val lab = academic.getLab(request.labId, token)
|
||||
val variant = lab.levels.flatMap { it.variants }.firstOrNull { it.id == request.variantId }
|
||||
return VariantAssignmentResponse(
|
||||
studentId = request.studentId,
|
||||
labId = request.labId,
|
||||
variantId = request.variantId,
|
||||
variantNumber = variant?.number ?: "",
|
||||
manual = true,
|
||||
)
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
fun assignmentsForLabEnriched(labId: UUID, token: String): List<VariantAssignmentResponse> {
|
||||
val lab = academic.getLab(labId.toString(), token)
|
||||
val variants = lab.levels.flatMap { it.variants }.associateBy { it.id }
|
||||
return assignments.findByLabId(labId).map { va ->
|
||||
val v = variants[va.variantId.toString()]
|
||||
VariantAssignmentResponse(
|
||||
studentId = va.studentId.toString(),
|
||||
labId = va.labId.toString(),
|
||||
variantId = va.variantId.toString(),
|
||||
variantNumber = v?.number ?: "",
|
||||
manual = va.manual,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
fun getStudentAssignments(studentId: UUID): List<VariantAssignment> =
|
||||
assignments.findByStudentId(studentId)
|
||||
@@ -134,18 +176,19 @@ class ProgressService(
|
||||
}
|
||||
|
||||
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()
|
||||
val lab = runCatching { academic.getLab(va.labId.toString(), token) }.getOrNull() ?: return null
|
||||
val variant = lab.levels.flatMap { it.variants }.firstOrNull { it.id == va.variantId.toString() }
|
||||
val level = lab.levels.firstOrNull { lv -> lv.variants.any { it.id == va.variantId.toString() } }
|
||||
return MeAssignmentResponse(
|
||||
labNumber = lab.number,
|
||||
labTitle = lab.title,
|
||||
variantNumber = "",
|
||||
variantTitle = "",
|
||||
statementMd = "",
|
||||
levelTitle = firstLevel?.title ?: "",
|
||||
deadlineAt = null,
|
||||
defenseAt = null,
|
||||
pointsMax = null,
|
||||
variantNumber = variant?.number ?: "",
|
||||
variantTitle = variant?.title ?: "",
|
||||
statementMd = variant?.statementMd ?: "",
|
||||
levelTitle = level?.title ?: "",
|
||||
deadlineAt = lab.deadlineAt,
|
||||
defenseAt = lab.defenseAt,
|
||||
pointsMax = lab.pointsMax,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,18 @@ package ru.ysu.lms.progress.security
|
||||
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.core.convert.converter.Converter
|
||||
import org.springframework.security.authentication.AbstractAuthenticationToken
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
|
||||
import org.springframework.security.config.http.SessionCreationPolicy
|
||||
import org.springframework.security.core.GrantedAuthority
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority
|
||||
import org.springframework.security.oauth2.jwt.Jwt
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoder
|
||||
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder
|
||||
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter
|
||||
import org.springframework.security.web.SecurityFilterChain
|
||||
|
||||
@Configuration
|
||||
@@ -18,6 +24,17 @@ class SecurityConfig {
|
||||
@Bean
|
||||
fun jwtDecoder(): JwtDecoder = NimbusJwtDecoder.withJwkSetUri(JWK_URI).build()
|
||||
|
||||
@Bean
|
||||
fun jwtAuthenticationConverter(): Converter<Jwt, out AbstractAuthenticationToken> {
|
||||
val converter = JwtAuthenticationConverter()
|
||||
converter.setJwtGrantedAuthoritiesConverter { jwt ->
|
||||
val role = jwt.getClaimAsString("role")
|
||||
if (role.isNullOrBlank()) emptyList()
|
||||
else listOf(SimpleGrantedAuthority("ROLE_$role"))
|
||||
}
|
||||
return converter
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
|
||||
http
|
||||
@@ -28,7 +45,11 @@ class SecurityConfig {
|
||||
auth.requestMatchers("/actuator/**", "/v3/api-docs/**", "/swagger-ui/**").permitAll()
|
||||
auth.anyRequest().authenticated()
|
||||
}
|
||||
.oauth2ResourceServer { it.jwt { } }
|
||||
.oauth2ResourceServer { oauth ->
|
||||
oauth.jwt { jwt ->
|
||||
jwt.jwtAuthenticationConverter(jwtAuthenticationConverter())
|
||||
}
|
||||
}
|
||||
return http.build()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user