Refactor: split ContentService and ProgressService; defaults tightened
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
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.CommentTemplate
|
||||
import ru.ysu.lms.progress.domain.GradeComment
|
||||
import ru.ysu.lms.progress.domain.LabGrade
|
||||
import ru.ysu.lms.progress.domain.LabGradeKey
|
||||
import ru.ysu.lms.progress.repo.CommentTemplateRepository
|
||||
import ru.ysu.lms.progress.repo.GradeCommentRepository
|
||||
import ru.ysu.lms.progress.repo.LabGradeRepository
|
||||
import java.math.BigDecimal
|
||||
import java.util.UUID
|
||||
|
||||
@Service
|
||||
class GradeService(
|
||||
private val grades: LabGradeRepository,
|
||||
private val templates: CommentTemplateRepository,
|
||||
private val gradeComments: GradeCommentRepository,
|
||||
) {
|
||||
@Transactional
|
||||
fun setGrade(request: GradeSetRequest): GradeResponse {
|
||||
if (request.score < BigDecimal.ZERO || request.score > BigDecimal("5")) {
|
||||
throw BadRequestException("Оценка должна быть в диапазоне 0–5")
|
||||
}
|
||||
val sid = UUID.fromString(request.studentId)
|
||||
val lid = UUID.fromString(request.labId)
|
||||
val key = LabGradeKey(sid, lid)
|
||||
val grade = grades.findById(key).orElseGet { LabGrade(studentId = key.studentId, labId = key.labId) }
|
||||
grade.score = request.score
|
||||
grades.save(grade)
|
||||
replaceComments(sid, lid, request.comments)
|
||||
return GradeResponse(
|
||||
request.studentId,
|
||||
request.labId,
|
||||
request.score,
|
||||
commentsOf(sid, lid).map { CommentOut(it.text, it.tone) },
|
||||
)
|
||||
}
|
||||
|
||||
private fun replaceComments(
|
||||
sid: UUID,
|
||||
lid: UUID,
|
||||
inputs: List<CommentInput>,
|
||||
) {
|
||||
val existing = gradeComments.findByStudentIdAndLabId(sid, lid).map { it.text + "|" + it.tone }.toSet()
|
||||
val incoming = inputs.filter { it.text.isNotBlank() }.map { it.text.trim() to normalizeTone(it.tone) }
|
||||
if (existing.toSet() == incoming.toSet()) return
|
||||
gradeComments.deleteByStudentIdAndLabId(sid, lid)
|
||||
for ((text, tone) in incoming) {
|
||||
gradeComments.save(GradeComment(studentId = sid, labId = lid, text = text, tone = tone))
|
||||
val t = templates.findByText(text)
|
||||
if (t == null) {
|
||||
templates.save(CommentTemplate(text = text, tone = tone, useCount = 1))
|
||||
} else {
|
||||
t.useCount += 1
|
||||
t.tone = tone
|
||||
templates.save(t)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun normalizeTone(tone: String): String =
|
||||
when (tone.lowercase()) {
|
||||
"positive", "положительный", "позитивный", "pos" -> "positive"
|
||||
"negative", "негативный", "отрицательный", "neg" -> "negative"
|
||||
else -> "neutral"
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
fun commentsOf(
|
||||
sid: UUID,
|
||||
lid: UUID,
|
||||
): List<GradeComment> = gradeComments.findByStudentIdAndLabId(sid, lid)
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
fun suggestComments(query: String): List<CommentOut> =
|
||||
templates
|
||||
.findTop30ByTextContainingIgnoreCaseOrderByUseCountDesc(query.trim())
|
||||
.map { CommentOut(it.text, it.tone) }
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
fun myComments(studentId: UUID): List<GradeComment> = gradeComments.findByStudentId(studentId)
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
fun listGrades(): List<GradeResponse> {
|
||||
val byGrade =
|
||||
gradeComments
|
||||
.findAll()
|
||||
.groupBy { it.studentId to it.labId }
|
||||
return grades.findAllBy().map {
|
||||
GradeResponse(
|
||||
studentId = it.studentId.toString(),
|
||||
labId = it.labId.toString(),
|
||||
score = it.score,
|
||||
comments = (byGrade[it.studentId to it.labId] ?: emptyList()).map { c -> CommentOut(c.text, c.tone) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,31 +1,23 @@
|
||||
package ru.ysu.lms.progress
|
||||
|
||||
import jakarta.validation.Valid
|
||||
import org.springframework.http.HttpHeaders
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.http.ResponseEntity
|
||||
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.GetMapping
|
||||
import org.springframework.web.bind.annotation.PathVariable
|
||||
import org.springframework.web.bind.annotation.PostMapping
|
||||
import org.springframework.web.bind.annotation.RequestBody
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RequestParam
|
||||
import org.springframework.web.bind.annotation.RequestPart
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import org.springframework.web.multipart.MultipartFile
|
||||
import java.net.URLEncoder
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.UUID
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1")
|
||||
class ProgressController(
|
||||
private val service: ProgressService,
|
||||
private val grades: GradeService,
|
||||
private val publications: GradePublicationService,
|
||||
private val submissions: SubmissionService,
|
||||
) {
|
||||
@GetMapping("/me/assignments")
|
||||
fun meAssignments(
|
||||
@@ -86,11 +78,11 @@ class ProgressController(
|
||||
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
|
||||
fun setGrade(
|
||||
@Valid @RequestBody body: GradeSetRequest,
|
||||
): GradeResponse = service.setGrade(body)
|
||||
): GradeResponse = grades.setGrade(body)
|
||||
|
||||
@GetMapping("/grades")
|
||||
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
|
||||
fun listGrades(): List<GradeResponse> = service.listGrades()
|
||||
fun listGrades(): List<GradeResponse> = grades.listGrades()
|
||||
|
||||
@PostMapping("/grades/publish")
|
||||
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
|
||||
@@ -120,7 +112,7 @@ class ProgressController(
|
||||
@AuthenticationPrincipal jwt: Jwt,
|
||||
@RequestParam(required = false) groupId: java.util.UUID?,
|
||||
): List<GradeResponse> {
|
||||
val all = service.listGrades().filter { it.studentId == jwt.subject }
|
||||
val all = grades.listGrades().filter { it.studentId == jwt.subject }
|
||||
if (groupId == null) return emptyList()
|
||||
return all.filter { publications.isPublished(groupId, java.util.UUID.fromString(it.labId)) }
|
||||
}
|
||||
@@ -129,60 +121,5 @@ class ProgressController(
|
||||
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
|
||||
fun suggestComments(
|
||||
@RequestParam q: String,
|
||||
): List<CommentOut> = service.suggestComments(q)
|
||||
|
||||
@PostMapping("/me/submissions/{labId}")
|
||||
fun submit(
|
||||
@PathVariable labId: UUID,
|
||||
@RequestPart("files") files: List<MultipartFile>,
|
||||
@AuthenticationPrincipal jwt: Jwt,
|
||||
): List<SubmissionDto> {
|
||||
val sid = UUID.fromString(jwt.subject)
|
||||
val all = submissions.submit(sid, labId, files, jwt.tokenValue)
|
||||
return submissions.mySubmissions(sid, labId)
|
||||
}
|
||||
|
||||
@GetMapping("/me/submissions")
|
||||
fun mySubmissions(
|
||||
@RequestParam labId: UUID,
|
||||
@AuthenticationPrincipal jwt: Jwt,
|
||||
): List<SubmissionDto> = submissions.mySubmissions(UUID.fromString(jwt.subject), labId)
|
||||
|
||||
@GetMapping("/me/submissions/{id}/download")
|
||||
fun downloadMine(
|
||||
@PathVariable id: UUID,
|
||||
@AuthenticationPrincipal jwt: Jwt,
|
||||
): ResponseEntity<ByteArray> {
|
||||
val sid = UUID.fromString(jwt.subject)
|
||||
val (bytes, name) = submissions.download(sid, id, sid, isAdmin = false)
|
||||
return attachment(bytes, name)
|
||||
}
|
||||
|
||||
@GetMapping("/submissions")
|
||||
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
|
||||
fun labSubmissions(
|
||||
@RequestParam labId: UUID,
|
||||
): List<SubmissionDto> = submissions.latestPerStudent(labId)
|
||||
|
||||
@GetMapping("/submissions/{id}/download")
|
||||
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
|
||||
fun downloadForAdmin(
|
||||
@PathVariable id: UUID,
|
||||
@AuthenticationPrincipal jwt: Jwt,
|
||||
): ResponseEntity<ByteArray> {
|
||||
val (bytes, name) = submissions.download(UUID.fromString(jwt.subject), id, null, isAdmin = true)
|
||||
return attachment(bytes, name)
|
||||
}
|
||||
|
||||
private fun attachment(
|
||||
bytes: ByteArray,
|
||||
name: String,
|
||||
): ResponseEntity<ByteArray> =
|
||||
ResponseEntity
|
||||
.ok()
|
||||
.header(
|
||||
HttpHeaders.CONTENT_DISPOSITION,
|
||||
"attachment; filename*=UTF-8''${URLEncoder.encode(name, StandardCharsets.UTF_8)}",
|
||||
).contentType(MediaType.APPLICATION_OCTET_STREAM)
|
||||
.body(bytes)
|
||||
): List<CommentOut> = grades.suggestComments(q)
|
||||
}
|
||||
|
||||
@@ -3,28 +3,17 @@ 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.CommentTemplate
|
||||
import ru.ysu.lms.progress.domain.GradeComment
|
||||
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.StudentLabLevelKey
|
||||
import ru.ysu.lms.progress.domain.VariantAssignment
|
||||
import ru.ysu.lms.progress.repo.CommentTemplateRepository
|
||||
import ru.ysu.lms.progress.repo.GradeCommentRepository
|
||||
import ru.ysu.lms.progress.repo.LabGradeRepository
|
||||
import ru.ysu.lms.progress.repo.StudentLabLevelRepository
|
||||
import ru.ysu.lms.progress.repo.VariantAssignmentRepository
|
||||
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 templates: CommentTemplateRepository,
|
||||
private val gradeComments: GradeCommentRepository,
|
||||
private val academic: AcademicClient,
|
||||
private val identity: IdentityClient,
|
||||
) {
|
||||
@@ -270,86 +259,6 @@ class ProgressService(
|
||||
@Transactional(readOnly = true)
|
||||
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 sid = UUID.fromString(request.studentId)
|
||||
val lid = UUID.fromString(request.labId)
|
||||
val key = LabGradeKey(sid, lid)
|
||||
val grade = grades.findById(key).orElseGet { LabGrade(studentId = key.studentId, labId = key.labId) }
|
||||
grade.score = request.score
|
||||
grades.save(grade)
|
||||
replaceComments(sid, lid, request.comments)
|
||||
return GradeResponse(
|
||||
request.studentId,
|
||||
request.labId,
|
||||
request.score,
|
||||
commentsOf(sid, lid).map { CommentOut(it.text, it.tone) },
|
||||
)
|
||||
}
|
||||
|
||||
private fun replaceComments(
|
||||
sid: UUID,
|
||||
lid: UUID,
|
||||
inputs: List<CommentInput>,
|
||||
) {
|
||||
val existing = gradeComments.findByStudentIdAndLabId(sid, lid).map { it.text + "|" + it.tone }.toSet()
|
||||
val incoming = inputs.filter { it.text.isNotBlank() }.map { it.text.trim() to normalizeTone(it.tone) }
|
||||
if (existing.toSet() == incoming.toSet()) return
|
||||
gradeComments.deleteByStudentIdAndLabId(sid, lid)
|
||||
for ((text, tone) in incoming) {
|
||||
gradeComments.save(GradeComment(studentId = sid, labId = lid, text = text, tone = tone))
|
||||
val t = templates.findByText(text)
|
||||
if (t == null) {
|
||||
templates.save(CommentTemplate(text = text, tone = tone, useCount = 1))
|
||||
} else {
|
||||
t.useCount += 1
|
||||
t.tone = tone
|
||||
templates.save(t)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun normalizeTone(tone: String): String =
|
||||
when (tone.lowercase()) {
|
||||
"positive", "положительный", "позитивный", "pos" -> "positive"
|
||||
"negative", "негативный", "отрицательный", "neg" -> "negative"
|
||||
else -> "neutral"
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
fun commentsOf(
|
||||
sid: UUID,
|
||||
lid: UUID,
|
||||
): List<GradeComment> = gradeComments.findByStudentIdAndLabId(sid, lid)
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
fun suggestComments(query: String): List<CommentOut> =
|
||||
templates
|
||||
.findTop30ByTextContainingIgnoreCaseOrderByUseCountDesc(query.trim())
|
||||
.map { CommentOut(it.text, it.tone) }
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
fun myComments(studentId: UUID): List<GradeComment> = gradeComments.findByStudentId(studentId)
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
fun listGrades(): List<GradeResponse> {
|
||||
val byGrade =
|
||||
gradeComments
|
||||
.findAll()
|
||||
.groupBy { it.studentId to it.labId }
|
||||
return grades.findAllBy().map {
|
||||
GradeResponse(
|
||||
studentId = it.studentId.toString(),
|
||||
labId = it.labId.toString(),
|
||||
score = it.score,
|
||||
comments = (byGrade[it.studentId to it.labId] ?: emptyList()).map { c -> CommentOut(c.text, c.tone) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
fun getStudentAssignment(
|
||||
studentId: UUID,
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
package ru.ysu.lms.progress
|
||||
|
||||
import org.springframework.http.HttpHeaders
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.http.ResponseEntity
|
||||
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.GetMapping
|
||||
import org.springframework.web.bind.annotation.PathVariable
|
||||
import org.springframework.web.bind.annotation.PostMapping
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RequestParam
|
||||
import org.springframework.web.bind.annotation.RequestPart
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import org.springframework.web.multipart.MultipartFile
|
||||
import java.net.URLEncoder
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.UUID
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1")
|
||||
class SubmissionController(
|
||||
private val submissions: SubmissionService,
|
||||
) {
|
||||
@PostMapping("/me/submissions/{labId}")
|
||||
fun submit(
|
||||
@PathVariable labId: UUID,
|
||||
@RequestPart("files") files: List<MultipartFile>,
|
||||
@AuthenticationPrincipal jwt: Jwt,
|
||||
): List<SubmissionDto> {
|
||||
val sid = UUID.fromString(jwt.subject)
|
||||
submissions.submit(sid, labId, files, jwt.tokenValue)
|
||||
return submissions.mySubmissions(sid, labId)
|
||||
}
|
||||
|
||||
@GetMapping("/me/submissions")
|
||||
fun mySubmissions(
|
||||
@RequestParam labId: UUID,
|
||||
@AuthenticationPrincipal jwt: Jwt,
|
||||
): List<SubmissionDto> = submissions.mySubmissions(UUID.fromString(jwt.subject), labId)
|
||||
|
||||
@GetMapping("/me/submissions/{id}/download")
|
||||
fun downloadMine(
|
||||
@PathVariable id: UUID,
|
||||
@AuthenticationPrincipal jwt: Jwt,
|
||||
): ResponseEntity<ByteArray> {
|
||||
val sid = UUID.fromString(jwt.subject)
|
||||
val (bytes, name) = submissions.download(sid, id, sid, isAdmin = false)
|
||||
return attachment(bytes, name)
|
||||
}
|
||||
|
||||
@GetMapping("/submissions")
|
||||
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
|
||||
fun labSubmissions(
|
||||
@RequestParam labId: UUID,
|
||||
): List<SubmissionDto> = submissions.latestPerStudent(labId)
|
||||
|
||||
@GetMapping("/submissions/{id}/download")
|
||||
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
|
||||
fun downloadForAdmin(
|
||||
@PathVariable id: UUID,
|
||||
@AuthenticationPrincipal jwt: Jwt,
|
||||
): ResponseEntity<ByteArray> {
|
||||
val (bytes, name) = submissions.download(UUID.fromString(jwt.subject), id, null, isAdmin = true)
|
||||
return attachment(bytes, name)
|
||||
}
|
||||
|
||||
private fun attachment(
|
||||
bytes: ByteArray,
|
||||
name: String,
|
||||
): ResponseEntity<ByteArray> =
|
||||
ResponseEntity
|
||||
.ok()
|
||||
.header(
|
||||
HttpHeaders.CONTENT_DISPOSITION,
|
||||
"attachment; filename*=UTF-8''${URLEncoder.encode(name, StandardCharsets.UTF_8)}",
|
||||
).contentType(MediaType.APPLICATION_OCTET_STREAM)
|
||||
.body(bytes)
|
||||
}
|
||||
Reference in New Issue
Block a user