Phase 4: grade comments with frequency-ranked suggestions
- progress comment_templates (tone + use_count) and grade_comments tables; setGrade now accepts comments list; templates created/bumped automatically; GET /api/v1/comment-templates?q= returns suggestions ranked by frequency - admin group page: per-grade ✎ dialog (score + editable comment rows with tone, datalist suggestions), chips under each score, data-comments JSON attr - student lab page: score + colored tone chips - fixed damaged button/attributes from earlier manual template surgery
This commit is contained in:
@@ -128,12 +128,17 @@ data class GradeSetRequest(
|
||||
val studentId: String,
|
||||
val labId: String,
|
||||
val score: BigDecimal,
|
||||
val comments: List<CommentInput> = emptyList(),
|
||||
)
|
||||
|
||||
data class CommentInput(val text: String, val tone: String = "neutral")
|
||||
data class CommentOut(val text: String, val tone: String)
|
||||
|
||||
data class GradeResponse(
|
||||
val studentId: String,
|
||||
val labId: String,
|
||||
val score: BigDecimal,
|
||||
val comments: List<CommentOut> = emptyList(),
|
||||
)
|
||||
|
||||
data class SubmissionDto(
|
||||
|
||||
@@ -84,6 +84,10 @@ class ProgressController(
|
||||
fun myGrades(@AuthenticationPrincipal jwt: Jwt): List<GradeResponse> =
|
||||
service.listGrades().filter { it.studentId == jwt.subject }
|
||||
|
||||
@GetMapping("/comment-templates")
|
||||
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
|
||||
fun suggestComments(@RequestParam q: String): List<CommentOut> = service.suggestComments(q)
|
||||
|
||||
@PostMapping("/me/submissions/{labId}")
|
||||
fun submit(
|
||||
@PathVariable labId: UUID,
|
||||
|
||||
@@ -3,10 +3,14 @@ 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.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
|
||||
@@ -19,6 +23,8 @@ 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,
|
||||
) {
|
||||
@@ -180,16 +186,60 @@ class ProgressService(
|
||||
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 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)
|
||||
return GradeResponse(request.studentId, request.labId, request.score)
|
||||
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 listGrades(): List<GradeResponse> =
|
||||
grades.findAllBy().map { GradeResponse(it.studentId.toString(), it.labId.toString(), it.score) }
|
||||
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, labId: UUID, token: String): MeAssignmentResponse? {
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package ru.ysu.lms.progress.domain
|
||||
|
||||
import jakarta.persistence.Column
|
||||
import jakarta.persistence.Entity
|
||||
import jakarta.persistence.Id
|
||||
import jakarta.persistence.Table
|
||||
import ru.ysu.lms.common.Ids
|
||||
import java.time.Instant
|
||||
import java.util.UUID
|
||||
|
||||
@Entity
|
||||
@Table(name = "comment_templates")
|
||||
class CommentTemplate(
|
||||
@Id var id: UUID = Ids.v7(),
|
||||
@Column(nullable = false, unique = true) var text: String = "",
|
||||
@Column(nullable = false, length = 8) var tone: String = "neutral",
|
||||
@Column(name = "use_count", nullable = false) var useCount: Int = 1,
|
||||
)
|
||||
|
||||
@Entity
|
||||
@Table(name = "grade_comments")
|
||||
class GradeComment(
|
||||
@Id var id: UUID = Ids.v7(),
|
||||
@Column(name = "student_id", nullable = false) var studentId: UUID = UUID(0, 0),
|
||||
@Column(name = "lab_id", nullable = false) var labId: UUID = UUID(0, 0),
|
||||
@Column(nullable = false) var text: String = "",
|
||||
@Column(nullable = false, length = 8) var tone: String = "neutral",
|
||||
@Column(name = "created_at", nullable = false) var createdAt: Instant = Instant.now(),
|
||||
)
|
||||
@@ -0,0 +1,17 @@
|
||||
package ru.ysu.lms.progress.repo
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import ru.ysu.lms.progress.domain.CommentTemplate
|
||||
import ru.ysu.lms.progress.domain.GradeComment
|
||||
import java.util.UUID
|
||||
|
||||
interface CommentTemplateRepository : JpaRepository<CommentTemplate, UUID> {
|
||||
fun findByText(text: String): CommentTemplate?
|
||||
fun findTop30ByTextContainingIgnoreCaseOrderByUseCountDesc(text: String): List<CommentTemplate>
|
||||
}
|
||||
|
||||
interface GradeCommentRepository : JpaRepository<GradeComment, UUID> {
|
||||
fun findByStudentIdAndLabId(studentId: UUID, labId: UUID): List<GradeComment>
|
||||
fun deleteByStudentIdAndLabId(studentId: UUID, labId: UUID)
|
||||
fun findByStudentId(studentId: UUID): List<GradeComment>
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
-- Grade comments: reusable templates (frequency-ranked) + per-grade comments.
|
||||
|
||||
CREATE TABLE comment_templates (
|
||||
id uuid PRIMARY KEY DEFAULT uuidv7(),
|
||||
text text NOT NULL UNIQUE,
|
||||
tone varchar(8) NOT NULL DEFAULT 'neutral', -- positive | neutral | negative
|
||||
use_count integer NOT NULL DEFAULT 1
|
||||
);
|
||||
|
||||
CREATE TABLE grade_comments (
|
||||
id uuid PRIMARY KEY DEFAULT uuidv7(),
|
||||
student_id uuid NOT NULL,
|
||||
lab_id uuid NOT NULL,
|
||||
text text NOT NULL,
|
||||
tone varchar(8) NOT NULL DEFAULT 'neutral',
|
||||
created_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX grade_comments_grade_idx ON grade_comments (student_id, lab_id);
|
||||
Reference in New Issue
Block a user