Refactor: extract helpers from groupDetail/editLab; detekt thresholds to defaults
This commit is contained in:
@@ -9,15 +9,15 @@ config:
|
|||||||
warningsAsErrors: false
|
warningsAsErrors: false
|
||||||
|
|
||||||
complexity:
|
complexity:
|
||||||
# Крупные агрегаты уже раздроблены (web ApiClient — по доменам, AdminController —
|
# Все правила сложности — на значениях по умолчанию: крупные агрегаты раздроблены
|
||||||
# на AdminSupport + 5 контроллеров, импорт материалов — в ContentImporter).
|
# (web ApiClient — по доменам, AdminController — AdminSupport + 5 контроллеров,
|
||||||
# Оставшиеся послабления точечные и перечислены ниже; после дробления ContentService
|
# импорт — ContentImporter, авторинг/порядок и оценки — отдельные сервисы,
|
||||||
# и выноса хелперов из методов groupDetail/labDetail/editLab их можно снять.
|
# submissions — отдельный контроллер). Единственное ослабление — LongParameterList
|
||||||
|
# для MVC-форм (см. ниже): у обработчиков формы @RequestParam естественно много.
|
||||||
LongMethod:
|
LongMethod:
|
||||||
# groupDetail (82), labDetail (73), editLab (86) в админ-контроллерах.
|
threshold: 60
|
||||||
threshold: 90
|
|
||||||
LongParameterList:
|
LongParameterList:
|
||||||
# MVC-обработчики форм принимают набор @RequestParam — для них это нормально.
|
# MVC-обработчики форм принимают набор @RequestParam — параметров объективно много.
|
||||||
functionThreshold: 8
|
functionThreshold: 8
|
||||||
constructorThreshold: 8
|
constructorThreshold: 8
|
||||||
ignoreDefaultParameters: true
|
ignoreDefaultParameters: true
|
||||||
@@ -25,7 +25,6 @@ complexity:
|
|||||||
- 'PostMapping'
|
- 'PostMapping'
|
||||||
- 'GetMapping'
|
- 'GetMapping'
|
||||||
TooManyFunctions:
|
TooManyFunctions:
|
||||||
# ContentService (26) — следующий кандидат на разделение.
|
|
||||||
thresholdInFiles: 20
|
thresholdInFiles: 20
|
||||||
thresholdInClasses: 20
|
thresholdInClasses: 20
|
||||||
thresholdInInterfaces: 20
|
thresholdInInterfaces: 20
|
||||||
@@ -36,8 +35,7 @@ complexity:
|
|||||||
ComplexCondition:
|
ComplexCondition:
|
||||||
threshold: 4
|
threshold: 4
|
||||||
CyclomaticComplexMethod:
|
CyclomaticComplexMethod:
|
||||||
# editLab (16) в AdminLabController.
|
threshold: 15
|
||||||
threshold: 17
|
|
||||||
|
|
||||||
style:
|
style:
|
||||||
MagicNumber:
|
MagicNumber:
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ import ru.ysu.lms.markdown.MarkdownRenderer
|
|||||||
import ru.ysu.lms.web.api.ApiClient
|
import ru.ysu.lms.web.api.ApiClient
|
||||||
import ru.ysu.lms.web.api.ApiException
|
import ru.ysu.lms.web.api.ApiException
|
||||||
import ru.ysu.lms.web.api.GradeSetPayload
|
import ru.ysu.lms.web.api.GradeSetPayload
|
||||||
|
import ru.ysu.lms.web.api.GroupDetailDto
|
||||||
|
import ru.ysu.lms.web.api.LabSummaryDto
|
||||||
|
import ru.ysu.lms.web.api.StudentCandidateDto
|
||||||
import ru.ysu.lms.web.api.createGroup
|
import ru.ysu.lms.web.api.createGroup
|
||||||
import ru.ysu.lms.web.api.downloadSubmissionAsAdmin
|
import ru.ysu.lms.web.api.downloadSubmissionAsAdmin
|
||||||
import ru.ysu.lms.web.api.getLabAssignments
|
import ru.ysu.lms.web.api.getLabAssignments
|
||||||
@@ -128,8 +131,7 @@ class AdminGroupController(
|
|||||||
model: Model,
|
model: Model,
|
||||||
): String {
|
): String {
|
||||||
val t = token(session)
|
val t = token(session)
|
||||||
val group = api.groupDetail(t, id)
|
val (group, labs) = loadGroupAndLabs(t, id)
|
||||||
val labs = api.listLabs(t).filter { it.status == "published" }
|
|
||||||
val grades = api.listGrades(t)
|
val grades = api.listGrades(t)
|
||||||
val gradeBy = grades.associate { "${it.studentId}|${it.labId}" to it.score }
|
val gradeBy = grades.associate { "${it.studentId}|${it.labId}" to it.score }
|
||||||
val commentsBy = grades.associate { "${it.studentId}|${it.labId}" to it.comments.map { ct -> ct.text to ct.tone } }
|
val commentsBy = grades.associate { "${it.studentId}|${it.labId}" to it.comments.map { ct -> ct.text to ct.tone } }
|
||||||
@@ -183,33 +185,59 @@ class AdminGroupController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val defenseRows =
|
val defenseRows =
|
||||||
labs
|
buildDefenseRows(t, group, labs)
|
||||||
.flatMap { lab ->
|
|
||||||
val byStudent = api.labDefenses(t, lab.id).associateBy { it.studentId }
|
|
||||||
group.students.mapNotNull { s ->
|
|
||||||
val d = byStudent[s.id] ?: return@mapNotNull null
|
|
||||||
GroupDefenseRow(
|
|
||||||
sessionId = d.id,
|
|
||||||
studentId = s.id,
|
|
||||||
fullName = s.fullName,
|
|
||||||
studentNo = s.studentNo,
|
|
||||||
labId = lab.id,
|
|
||||||
labNumber = lab.number,
|
|
||||||
status = d.status,
|
|
||||||
code = d.code?.takeIf { d.status == "waiting" },
|
|
||||||
assignmentMd = d.assignmentMd,
|
|
||||||
secondsLeft = d.secondsLeft,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}.sortedWith(compareBy({ it.fullName.lowercase() }, { it.labNumber }))
|
|
||||||
|
|
||||||
|
val candidates = runCatching { api.groupCandidates(t, id) }.getOrDefault(emptyList())
|
||||||
|
fillGroupModel(model, group, labs, candidates, rows, defenseRows, publishedLabs)
|
||||||
|
return "admin/group"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadGroupAndLabs(
|
||||||
|
token: String,
|
||||||
|
id: String,
|
||||||
|
): Pair<GroupDetailDto, List<LabSummaryDto>> = api.groupDetail(token, id) to api.listLabs(token).filter { it.status == "published" }
|
||||||
|
|
||||||
|
/** Строки для блока «Защиты»: сессии защиты по всем лабораторным группы. */
|
||||||
|
private fun buildDefenseRows(
|
||||||
|
token: String,
|
||||||
|
group: GroupDetailDto,
|
||||||
|
labs: List<LabSummaryDto>,
|
||||||
|
): List<GroupDefenseRow> =
|
||||||
|
labs
|
||||||
|
.flatMap { lab ->
|
||||||
|
val byStudent = api.labDefenses(token, lab.id).associateBy { it.studentId }
|
||||||
|
group.students.mapNotNull { s ->
|
||||||
|
val d = byStudent[s.id] ?: return@mapNotNull null
|
||||||
|
GroupDefenseRow(
|
||||||
|
sessionId = d.id,
|
||||||
|
studentId = s.id,
|
||||||
|
fullName = s.fullName,
|
||||||
|
studentNo = s.studentNo,
|
||||||
|
labId = lab.id,
|
||||||
|
labNumber = lab.number,
|
||||||
|
status = d.status,
|
||||||
|
code = d.code?.takeIf { d.status == "waiting" },
|
||||||
|
assignmentMd = d.assignmentMd,
|
||||||
|
secondsLeft = d.secondsLeft,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}.sortedWith(compareBy({ it.fullName.lowercase() }, { it.labNumber }))
|
||||||
|
|
||||||
|
private fun fillGroupModel(
|
||||||
|
model: Model,
|
||||||
|
group: GroupDetailDto,
|
||||||
|
labs: List<LabSummaryDto>,
|
||||||
|
candidates: List<StudentCandidateDto>,
|
||||||
|
rows: List<GroupStudentRow>,
|
||||||
|
defenseRows: List<GroupDefenseRow>,
|
||||||
|
publishedLabs: Set<String>,
|
||||||
|
) {
|
||||||
model.addAttribute("group", group)
|
model.addAttribute("group", group)
|
||||||
model.addAttribute("labs", labs)
|
model.addAttribute("labs", labs)
|
||||||
model.addAttribute("candidates", runCatching { api.groupCandidates(t, id) }.getOrDefault(emptyList()))
|
model.addAttribute("candidates", candidates)
|
||||||
model.addAttribute("studentGrades", rows)
|
model.addAttribute("studentGrades", rows)
|
||||||
model.addAttribute("defenseRows", defenseRows)
|
model.addAttribute("defenseRows", defenseRows)
|
||||||
model.addAttribute("publishedLabIds", publishedLabs)
|
model.addAttribute("publishedLabIds", publishedLabs)
|
||||||
return "admin/group"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/groups/{id}/grades")
|
@PostMapping("/groups/{id}/grades")
|
||||||
|
|||||||
@@ -15,10 +15,15 @@ import ru.ysu.lms.web.api.ApiClient
|
|||||||
import ru.ysu.lms.web.api.ApiException
|
import ru.ysu.lms.web.api.ApiException
|
||||||
import ru.ysu.lms.web.api.AssignVariantPayload
|
import ru.ysu.lms.web.api.AssignVariantPayload
|
||||||
import ru.ysu.lms.web.api.LabCreatePayload
|
import ru.ysu.lms.web.api.LabCreatePayload
|
||||||
|
import ru.ysu.lms.web.api.LabDetailDto
|
||||||
|
import ru.ysu.lms.web.api.LabLevelDetailDto
|
||||||
import ru.ysu.lms.web.api.LabUpdatePayload
|
import ru.ysu.lms.web.api.LabUpdatePayload
|
||||||
import ru.ysu.lms.web.api.LevelAssignPayload
|
import ru.ysu.lms.web.api.LevelAssignPayload
|
||||||
|
import ru.ysu.lms.web.api.LevelAssignmentResultDto
|
||||||
import ru.ysu.lms.web.api.LevelPayload
|
import ru.ysu.lms.web.api.LevelPayload
|
||||||
import ru.ysu.lms.web.api.LevelSavePayload
|
import ru.ysu.lms.web.api.LevelSavePayload
|
||||||
|
import ru.ysu.lms.web.api.SubmissionDto
|
||||||
|
import ru.ysu.lms.web.api.VariantAssignmentResultDto
|
||||||
import ru.ysu.lms.web.api.VariantSavePayload
|
import ru.ysu.lms.web.api.VariantSavePayload
|
||||||
import ru.ysu.lms.web.api.addLevel
|
import ru.ysu.lms.web.api.addLevel
|
||||||
import ru.ysu.lms.web.api.assignVariant
|
import ru.ysu.lms.web.api.assignVariant
|
||||||
@@ -136,32 +141,7 @@ class AdminLabController(
|
|||||||
val defaultLevel = lab.levels.firstOrNull()
|
val defaultLevel = lab.levels.firstOrNull()
|
||||||
|
|
||||||
val assignmentGroups =
|
val assignmentGroups =
|
||||||
api.listGroups(t).map { g ->
|
buildAssignmentGroups(t, lab, levelByStudent, assignmentByStudent, submissionsByStudent, defaultLevel)
|
||||||
val detail = api.groupDetail(t, g.id)
|
|
||||||
AdminLabGroup(
|
|
||||||
groupName = g.name,
|
|
||||||
students =
|
|
||||||
detail.students.map { s ->
|
|
||||||
val levelState = levelByStudent[s.id]
|
|
||||||
val level = lab.levels.firstOrNull { it.id == levelState?.labLevelId } ?: defaultLevel
|
|
||||||
val va = assignmentByStudent[s.id]
|
|
||||||
AdminLabStudent(
|
|
||||||
studentId = s.id,
|
|
||||||
fullName = s.fullName,
|
|
||||||
studentNo = s.studentNo,
|
|
||||||
levelId = level?.id ?: "",
|
|
||||||
levelTitle = level?.title ?: "—",
|
|
||||||
levelChanged = levelState?.manual == true,
|
|
||||||
variantId = va?.variantId,
|
|
||||||
variantNumber = va?.variantNumber,
|
|
||||||
source = if (va?.manual == true) "вручную" else "авто",
|
|
||||||
submissionId = submissionsByStudent[s.id]?.id,
|
|
||||||
submissionSeq = submissionsByStudent[s.id]?.seq,
|
|
||||||
submissionFiles = submissionsByStudent[s.id]?.files ?: emptyList(),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
model.addAttribute("lab", lab)
|
model.addAttribute("lab", lab)
|
||||||
model.addAttribute("descriptionHtml", markdown.render(lab.descriptionMd))
|
model.addAttribute("descriptionHtml", markdown.render(lab.descriptionMd))
|
||||||
@@ -196,6 +176,42 @@ class AdminLabController(
|
|||||||
return "admin/lab"
|
return "admin/lab"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Строки «группа → студенты» со их уровнем, вариантом и последней сдачей. */
|
||||||
|
private fun buildAssignmentGroups(
|
||||||
|
token: String,
|
||||||
|
lab: LabDetailDto,
|
||||||
|
levelByStudent: Map<String, LevelAssignmentResultDto>,
|
||||||
|
assignmentByStudent: Map<String, VariantAssignmentResultDto>,
|
||||||
|
submissionsByStudent: Map<String, SubmissionDto>,
|
||||||
|
defaultLevel: LabLevelDetailDto?,
|
||||||
|
): List<AdminLabGroup> =
|
||||||
|
api.listGroups(token).map { g ->
|
||||||
|
val detail = api.groupDetail(token, g.id)
|
||||||
|
AdminLabGroup(
|
||||||
|
groupName = g.name,
|
||||||
|
students =
|
||||||
|
detail.students.map { s ->
|
||||||
|
val levelState = levelByStudent[s.id]
|
||||||
|
val level = lab.levels.firstOrNull { it.id == levelState?.labLevelId } ?: defaultLevel
|
||||||
|
val va = assignmentByStudent[s.id]
|
||||||
|
AdminLabStudent(
|
||||||
|
studentId = s.id,
|
||||||
|
fullName = s.fullName,
|
||||||
|
studentNo = s.studentNo,
|
||||||
|
levelId = level?.id ?: "",
|
||||||
|
levelTitle = level?.title ?: "—",
|
||||||
|
levelChanged = levelState?.manual == true,
|
||||||
|
variantId = va?.variantId,
|
||||||
|
variantNumber = va?.variantNumber,
|
||||||
|
source = if (va?.manual == true) "вручную" else "авто",
|
||||||
|
submissionId = submissionsByStudent[s.id]?.id,
|
||||||
|
submissionSeq = submissionsByStudent[s.id]?.seq,
|
||||||
|
submissionFiles = submissionsByStudent[s.id]?.files ?: emptyList(),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping("/labs/{labId}/assign")
|
@PostMapping("/labs/{labId}/assign")
|
||||||
fun assignVariant(
|
fun assignVariant(
|
||||||
@PathVariable labId: String,
|
@PathVariable labId: String,
|
||||||
@@ -306,6 +322,77 @@ class AdminLabController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/labs/{id}/edit")
|
@GetMapping("/labs/{id}/edit")
|
||||||
|
/** Быстрые действия «удалить уровень/вариант» на форме лабы; null — обычное сохранение. */
|
||||||
|
private fun handleEditDeleteAction(
|
||||||
|
token: String,
|
||||||
|
id: String,
|
||||||
|
action: String,
|
||||||
|
redirect: RedirectAttributes,
|
||||||
|
): String? =
|
||||||
|
when {
|
||||||
|
action.startsWith("delete-level-") -> {
|
||||||
|
api.deleteLevel(token, id, action.removePrefix("delete-level-"))
|
||||||
|
redirect.addFlashAttribute("notice", "Уровень удалён")
|
||||||
|
"redirect:/admin/labs/$id/edit"
|
||||||
|
}
|
||||||
|
action.startsWith("delete-variant-") -> {
|
||||||
|
api.deleteVariant(token, id, action.removePrefix("delete-variant-"))
|
||||||
|
redirect.addFlashAttribute("notice", "Вариант удалён")
|
||||||
|
"redirect:/admin/labs/$id/edit"
|
||||||
|
}
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun applyLevelRenames(
|
||||||
|
token: String,
|
||||||
|
id: String,
|
||||||
|
levelIds: List<String>?,
|
||||||
|
levelTitles: List<String>?,
|
||||||
|
) {
|
||||||
|
levelIds.orEmpty().zip(levelTitles.orEmpty()).forEach { (levelId, newTitle) ->
|
||||||
|
if (levelId.isNotBlank() && newTitle.isNotBlank()) {
|
||||||
|
api.renameLevel(token, id, levelId, newTitle.trim())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("LongParameterList")
|
||||||
|
private fun applyVariantSaves(
|
||||||
|
token: String,
|
||||||
|
id: String,
|
||||||
|
variantIds: List<String>?,
|
||||||
|
variantNumbers: List<String>?,
|
||||||
|
variantLevelIds: List<String>?,
|
||||||
|
variantTitles: List<String>?,
|
||||||
|
variantStatements: List<String>?,
|
||||||
|
) {
|
||||||
|
variantIds.orEmpty().forEachIndexed { idx, variantId ->
|
||||||
|
val number =
|
||||||
|
variantNumbers
|
||||||
|
.orEmpty()
|
||||||
|
.getOrNull(idx)
|
||||||
|
.orEmpty()
|
||||||
|
.trim()
|
||||||
|
if (variantId.isNotBlank() && number.isNotBlank()) {
|
||||||
|
api.saveVariant(
|
||||||
|
token,
|
||||||
|
id,
|
||||||
|
variantId,
|
||||||
|
VariantSavePayload(
|
||||||
|
variantLevelIds.orEmpty().getOrNull(idx).orEmpty(),
|
||||||
|
number,
|
||||||
|
variantTitles
|
||||||
|
.orEmpty()
|
||||||
|
.getOrNull(idx)
|
||||||
|
.orEmpty()
|
||||||
|
.trim(),
|
||||||
|
variantStatements.orEmpty().getOrNull(idx).orEmpty(),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun editLabForm(
|
fun editLabForm(
|
||||||
@PathVariable id: String,
|
@PathVariable id: String,
|
||||||
session: HttpSession,
|
session: HttpSession,
|
||||||
@@ -356,18 +443,7 @@ class AdminLabController(
|
|||||||
): String {
|
): String {
|
||||||
val t = token(session)
|
val t = token(session)
|
||||||
return try {
|
return try {
|
||||||
when {
|
handleEditDeleteAction(t, id, action, redirect)?.let { return it }
|
||||||
action.startsWith("delete-level-") -> {
|
|
||||||
api.deleteLevel(t, id, action.removePrefix("delete-level-"))
|
|
||||||
redirect.addFlashAttribute("notice", "Уровень удалён")
|
|
||||||
return "redirect:/admin/labs/$id/edit"
|
|
||||||
}
|
|
||||||
action.startsWith("delete-variant-") -> {
|
|
||||||
api.deleteVariant(t, id, action.removePrefix("delete-variant-"))
|
|
||||||
redirect.addFlashAttribute("notice", "Вариант удалён")
|
|
||||||
return "redirect:/admin/labs/$id/edit"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
api.updateLab(
|
api.updateLab(
|
||||||
t,
|
t,
|
||||||
@@ -383,36 +459,8 @@ class AdminLabController(
|
|||||||
descriptionMd,
|
descriptionMd,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
levelIds.orEmpty().zip(levelTitles.orEmpty()).forEach { (levelId, newTitle) ->
|
applyLevelRenames(t, id, levelIds, levelTitles)
|
||||||
if (levelId.isNotBlank() && newTitle.isNotBlank()) {
|
applyVariantSaves(t, id, variantIds, variantNumbers, variantLevelIds, variantTitles, variantStatements)
|
||||||
api.renameLevel(t, id, levelId, newTitle.trim())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
variantIds.orEmpty().forEachIndexed { idx, variantId ->
|
|
||||||
val number =
|
|
||||||
variantNumbers
|
|
||||||
.orEmpty()
|
|
||||||
.getOrNull(idx)
|
|
||||||
.orEmpty()
|
|
||||||
.trim()
|
|
||||||
if (variantId.isNotBlank() && number.isNotBlank()) {
|
|
||||||
api.saveVariant(
|
|
||||||
t,
|
|
||||||
id,
|
|
||||||
variantId,
|
|
||||||
VariantSavePayload(
|
|
||||||
variantLevelIds.orEmpty().getOrNull(idx).orEmpty(),
|
|
||||||
number,
|
|
||||||
variantTitles
|
|
||||||
.orEmpty()
|
|
||||||
.getOrNull(idx)
|
|
||||||
.orEmpty()
|
|
||||||
.trim(),
|
|
||||||
variantStatements.orEmpty().getOrNull(idx).orEmpty(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
when {
|
when {
|
||||||
action == "add-level" && !newLevelCode.isNullOrBlank() && !newLevelTitle.isNullOrBlank() -> {
|
action == "add-level" && !newLevelCode.isNullOrBlank() && !newLevelTitle.isNullOrBlank() -> {
|
||||||
api.addLevel(t, id, LevelSavePayload(newLevelCode.trim(), newLevelTitle.trim()))
|
api.addLevel(t, id, LevelSavePayload(newLevelCode.trim(), newLevelTitle.trim()))
|
||||||
|
|||||||
Reference in New Issue
Block a user