diff --git a/config/detekt/detekt.yml b/config/detekt/detekt.yml index 12f5d54..ccc1588 100644 --- a/config/detekt/detekt.yml +++ b/config/detekt/detekt.yml @@ -9,21 +9,16 @@ config: warningsAsErrors: false complexity: - # Все правила сложности — на значениях по умолчанию: крупные агрегаты раздроблены - # (web ApiClient — по доменам, AdminController — AdminSupport + 5 контроллеров, - # импорт — ContentImporter, авторинг/порядок и оценки — отдельные сервисы, - # submissions — отдельный контроллер). Единственное ослабление — LongParameterList - # для MVC-форм (см. ниже): у обработчиков формы @RequestParam естественно много. + # Все правила сложности — строго на значениях detekt по умолчанию. Крупные агрегаты + # раздроблены: ApiClient — по доменам, AdminController — AdminSupport + 5 контроллеров, + # импорт — ContentImporter, авторинг/порядок и оценки — отдельные сервисы, submissions — + # отдельный контроллер; MVC-формы связаны через @ModelAttribute-DTO. LongMethod: threshold: 60 LongParameterList: - # MVC-обработчики форм принимают набор @RequestParam — параметров объективно много. - functionThreshold: 8 - constructorThreshold: 8 + functionThreshold: 6 + constructorThreshold: 7 ignoreDefaultParameters: true - ignoreAnnotated: - - 'PostMapping' - - 'GetMapping' TooManyFunctions: thresholdInFiles: 20 thresholdInClasses: 20 diff --git a/services/academic-service/src/main/kotlin/ru/ysu/lms/academic/content/ContentAuthoringService.kt b/services/academic-service/src/main/kotlin/ru/ysu/lms/academic/content/ContentAuthoringService.kt index 8047ec0..08fee22 100644 --- a/services/academic-service/src/main/kotlin/ru/ysu/lms/academic/content/ContentAuthoringService.kt +++ b/services/academic-service/src/main/kotlin/ru/ysu/lms/academic/content/ContentAuthoringService.kt @@ -12,6 +12,15 @@ import ru.ysu.lms.common.BadRequestException import ru.ysu.lms.common.NotFoundException import java.util.UUID +data class ArticlePlacement( + val title: String, + val bodyMd: String, + val chapterId: UUID?, + val newChapterTitle: String?, + val sectionId: UUID?, + val newSectionTitle: String?, +) + @Service class ContentAuthoringService( private val chapters: ContentChapterRepository, @@ -141,53 +150,46 @@ class ContentAuthoringService( /** Создаёт статью; если раздела нет — создаёт главу/раздел по названиям. */ @Transactional - fun createArticlePlaced( - title: String, - bodyMd: String, - chapterId: UUID?, - newChapterTitle: String?, - sectionId: UUID?, - newSectionTitle: String?, - ): ContentArticleView { + fun createArticlePlaced(placement: ArticlePlacement): ContentArticleView { val targetSection: ContentSection = when { - sectionId != null && !newSectionTitle.isNullOrBlank() -> { + placement.sectionId != null && !placement.newSectionTitle.isNullOrBlank() -> { val anchor = sections - .findById(sectionId) + .findById(placement.sectionId) .orElseThrow { NotFoundException("Раздел не найден") } - val node = createSection(anchor.chapterId, newSectionTitle, null) + val node = createSection(anchor.chapterId, placement.newSectionTitle, null) sections .findById(UUID.fromString(node.id)) .orElseThrow { NotFoundException("Раздел не найден") } } - sectionId != null -> + placement.sectionId != null -> sections - .findById(sectionId) + .findById(placement.sectionId) .orElseThrow { NotFoundException("Раздел не найден") } - !newSectionTitle.isNullOrBlank() -> { + !placement.newSectionTitle.isNullOrBlank() -> { val chapter: ContentChapter = when { - chapterId != null -> + placement.chapterId != null -> chapters - .findById(chapterId) + .findById(placement.chapterId) .orElseThrow { NotFoundException("Глава не найдена") } - !newChapterTitle.isNullOrBlank() -> { - val node = createChapter(newChapterTitle, null) + !placement.newChapterTitle.isNullOrBlank() -> { + val node = createChapter(placement.newChapterTitle, null) chapters .findById(UUID.fromString(node.id)) .orElseThrow { NotFoundException("Глава не найдена") } } else -> throw BadRequestException("Укажите главу") } - val node = createSection(chapter.id, newSectionTitle, null) + val node = createSection(chapter.id, placement.newSectionTitle, null) sections .findById(UUID.fromString(node.id)) .orElseThrow { NotFoundException("Раздел не найден") } } else -> throw BadRequestException("Укажите раздел") } - return createArticle(targetSection.id, title, bodyMd) + return createArticle(targetSection.id, placement.title, placement.bodyMd) } @Transactional diff --git a/services/academic-service/src/main/kotlin/ru/ysu/lms/academic/controller/ContentAdminController.kt b/services/academic-service/src/main/kotlin/ru/ysu/lms/academic/controller/ContentAdminController.kt index 449a187..ea0dd2e 100644 --- a/services/academic-service/src/main/kotlin/ru/ysu/lms/academic/controller/ContentAdminController.kt +++ b/services/academic-service/src/main/kotlin/ru/ysu/lms/academic/controller/ContentAdminController.kt @@ -12,6 +12,7 @@ import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController import org.springframework.web.multipart.MultipartFile +import ru.ysu.lms.academic.content.ArticlePlacement import ru.ysu.lms.academic.content.ContentAuthoringService import ru.ysu.lms.academic.content.ContentImportResult import ru.ysu.lms.academic.content.ContentService @@ -170,12 +171,14 @@ class ContentAdminController( fun createArticle( @RequestBody body: ArticleCreatePayload, ) = authoring.createArticlePlaced( - title = body.title, - bodyMd = body.bodyMd, - chapterId = body.chapterId?.let { UUID.fromString(it) }, - newChapterTitle = body.newChapterTitle, - sectionId = body.sectionId?.let { UUID.fromString(it) }, - newSectionTitle = body.newSectionTitle, + ArticlePlacement( + title = body.title, + bodyMd = body.bodyMd, + chapterId = body.chapterId?.let { UUID.fromString(it) }, + newChapterTitle = body.newChapterTitle, + sectionId = body.sectionId?.let { UUID.fromString(it) }, + newSectionTitle = body.newSectionTitle, + ), ) @PutMapping("/articles/{id}") diff --git a/web/src/main/kotlin/ru/ysu/lms/web/admin/AdminDefenseController.kt b/web/src/main/kotlin/ru/ysu/lms/web/admin/AdminDefenseController.kt index 60883d2..d829877 100644 --- a/web/src/main/kotlin/ru/ysu/lms/web/admin/AdminDefenseController.kt +++ b/web/src/main/kotlin/ru/ysu/lms/web/admin/AdminDefenseController.kt @@ -2,6 +2,7 @@ package ru.ysu.lms.web.admin import jakarta.servlet.http.HttpSession import org.springframework.stereotype.Controller +import org.springframework.web.bind.annotation.ModelAttribute import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestMapping @@ -26,38 +27,34 @@ class AdminDefenseController( @PostMapping("/labs/{labId}/defense/assign") fun assignDefense( @PathVariable labId: String, - @RequestParam studentId: String, - @RequestParam assignmentMd: String, - @RequestParam(required = false) groupId: String?, + @ModelAttribute form: DefenseForm, session: HttpSession, redirect: RedirectAttributes, ): String = try { - api.assignDefense(token(session), studentId, labId, assignmentMd) + api.assignDefense(token(session), form.studentId, labId, form.assignmentMd) redirect.addFlashAttribute("notice", "Индивидуальное задание сохранено") - redirectToGroup(groupId, labId) + redirectToGroup(form.groupId, labId) } catch (ex: ApiException) { redirect.addFlashAttribute("error", ex.message) - redirectToGroup(groupId, labId) + redirectToGroup(form.groupId, labId) } /** Назначение/правка индивидуального задания со страницы группы. */ @PostMapping("/defenses/save") fun saveDefense( - @RequestParam studentId: String, + @ModelAttribute form: DefenseForm, @RequestParam labId: String, - @RequestParam assignmentMd: String, - @RequestParam(required = false) groupId: String?, session: HttpSession, redirect: RedirectAttributes, ): String = try { - api.assignDefense(token(session), studentId, labId, assignmentMd) + api.assignDefense(token(session), form.studentId, labId, form.assignmentMd) redirect.addFlashAttribute("notice", "Индивидуальное задание сохранено") - redirectToGroup(groupId, labId) + redirectToGroup(form.groupId, labId) } catch (ex: ApiException) { redirect.addFlashAttribute("error", ex.message) - redirectToGroup(groupId, labId) + redirectToGroup(form.groupId, labId) } @PostMapping("/labs/{labId}/defense/import") diff --git a/web/src/main/kotlin/ru/ysu/lms/web/admin/AdminDistributeController.kt b/web/src/main/kotlin/ru/ysu/lms/web/admin/AdminDistributeController.kt index 487c552..94cb0a7 100644 --- a/web/src/main/kotlin/ru/ysu/lms/web/admin/AdminDistributeController.kt +++ b/web/src/main/kotlin/ru/ysu/lms/web/admin/AdminDistributeController.kt @@ -2,6 +2,7 @@ package ru.ysu.lms.web.admin import jakarta.servlet.http.HttpSession import org.springframework.stereotype.Controller +import org.springframework.web.bind.annotation.ModelAttribute import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestMapping @@ -29,14 +30,12 @@ class AdminDistributeController( @PostMapping("/groups/{gid}/set-level") fun setStudentLevel( @PathVariable gid: String, - @RequestParam studentId: String, - @RequestParam labId: String, - @RequestParam labLevelId: String, + @ModelAttribute form: StudentLevelForm, session: HttpSession, redirect: RedirectAttributes, ): String = try { - api.setLevel(token(session), LevelAssignPayload(studentId, labId, labLevelId)) + api.setLevel(token(session), LevelAssignPayload(form.studentId, form.labId, form.labLevelId)) redirect.addFlashAttribute("notice", "Уровень обновлён — вариант подобран автоматически") "redirect:/admin/groups/$gid" } catch (ex: ApiException) { @@ -47,14 +46,12 @@ class AdminDistributeController( @PostMapping("/groups/{gid}/pin-variant") fun pinStudentVariant( @PathVariable gid: String, - @RequestParam studentId: String, - @RequestParam labId: String, - @RequestParam variantId: String, + @ModelAttribute form: PinVariantForm, session: HttpSession, redirect: RedirectAttributes, ): String = try { - val result = api.assignVariant(token(session), AssignVariantPayload(studentId, labId, variantId)) + val result = api.assignVariant(token(session), AssignVariantPayload(form.studentId, form.labId, form.variantId)) redirect.addFlashAttribute("notice", "Закреплён вариант ${result.variantNumber}") "redirect:/admin/groups/$gid" } catch (ex: ApiException) { diff --git a/web/src/main/kotlin/ru/ysu/lms/web/admin/AdminForms.kt b/web/src/main/kotlin/ru/ysu/lms/web/admin/AdminForms.kt new file mode 100644 index 0000000..36de6f0 --- /dev/null +++ b/web/src/main/kotlin/ru/ysu/lms/web/admin/AdminForms.kt @@ -0,0 +1,92 @@ +package ru.ysu.lms.web.admin + +// Формы админ-контроллеров: связываются через @ModelAttribute. +// Имена полей совпадают с именами полей в шаблонах (то же, что было в @RequestParam). + +data class CreateStudentForm( + val studentNo: String = "", + val lastName: String = "", + val firstName: String? = null, + val middleName: String? = null, + val basis: String? = null, +) + +data class StudentLevelForm( + val studentId: String = "", + val labId: String = "", + val labLevelId: String = "", +) + +data class PinVariantForm( + val studentId: String = "", + val labId: String = "", + val variantId: String = "", +) + +data class DefenseForm( + val studentId: String = "", + val assignmentMd: String = "", + val groupId: String? = null, +) + +data class CreateLabForm( + val number: Int = 0, + val title: String = "", + val deadlineAt: String? = null, + val defenseAt: String? = null, + val pointsMax: String = "", + val status: String = "", + val descriptionMd: String = "", + val levels: String = "", +) + +data class SaveLevelForm( + val levelId: String? = null, + val code: String = "", + val title: String = "", +) + +data class SaveVariantForm( + val variantId: String? = null, + val labLevelId: String = "", + val number: String = "", + val title: String = "", + val statementMd: String = "", +) + +data class EditLabForm( + val title: String = "", + val deadlineAt: String? = null, + val defenseAt: String? = null, + val pointsMax: String = "", + val status: String = "", + val descriptionMd: String = "", + val action: String = "save", + val levelIds: List? = null, + val levelTitles: List? = null, + val variantIds: List? = null, + val variantLevelIds: List? = null, + val variantNumbers: List? = null, + val variantTitles: List? = null, + val variantStatements: List? = null, + val newLevelCode: String? = null, + val newLevelTitle: String? = null, + val addLevelId: String? = null, + val addNumber: String? = null, + val addTitle: String? = null, + val addStatementMd: String? = null, +) + +data class SetGradeForm( + val studentId: String = "", + val labId: String = "", + val score: String = "", + val commentsJson: String? = null, +) + +data class SubmissionDownloadForm( + val group: String? = null, + val no: String? = null, + val name: String? = null, + val attempt: String? = null, +) diff --git a/web/src/main/kotlin/ru/ysu/lms/web/admin/AdminGroupController.kt b/web/src/main/kotlin/ru/ysu/lms/web/admin/AdminGroupController.kt index c015866..3d840e8 100644 --- a/web/src/main/kotlin/ru/ysu/lms/web/admin/AdminGroupController.kt +++ b/web/src/main/kotlin/ru/ysu/lms/web/admin/AdminGroupController.kt @@ -7,6 +7,7 @@ import org.springframework.http.ResponseEntity import org.springframework.stereotype.Controller import org.springframework.ui.Model import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.ModelAttribute import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestMapping @@ -188,7 +189,7 @@ class AdminGroupController( buildDefenseRows(t, group, labs) val candidates = runCatching { api.groupCandidates(t, id) }.getOrDefault(emptyList()) - fillGroupModel(model, group, labs, candidates, rows, defenseRows, publishedLabs) + fillGroupModel(model, GroupViewModel(group, labs, candidates, rows, defenseRows, publishedLabs)) return "admin/group" } @@ -223,36 +224,37 @@ class AdminGroupController( } }.sortedWith(compareBy({ it.fullName.lowercase() }, { it.labNumber })) + private data class GroupViewModel( + val group: GroupDetailDto, + val labs: List, + val candidates: List, + val rows: List, + val defenseRows: List, + val publishedLabs: Set, + ) + private fun fillGroupModel( model: Model, - group: GroupDetailDto, - labs: List, - candidates: List, - rows: List, - defenseRows: List, - publishedLabs: Set, + view: GroupViewModel, ) { - model.addAttribute("group", group) - model.addAttribute("labs", labs) - model.addAttribute("candidates", candidates) - model.addAttribute("studentGrades", rows) - model.addAttribute("defenseRows", defenseRows) - model.addAttribute("publishedLabIds", publishedLabs) + model.addAttribute("group", view.group) + model.addAttribute("labs", view.labs) + model.addAttribute("candidates", view.candidates) + model.addAttribute("studentGrades", view.rows) + model.addAttribute("defenseRows", view.defenseRows) + model.addAttribute("publishedLabIds", view.publishedLabs) } @PostMapping("/groups/{id}/grades") fun setGrade( @PathVariable id: String, - @RequestParam studentId: String, - @RequestParam labId: String, - @RequestParam score: String, - @RequestParam(required = false) commentsJson: String?, + @ModelAttribute form: SetGradeForm, session: HttpSession, redirect: RedirectAttributes, ): String = try { - val comments = parseComments(commentsJson ?: "[]") - api.setGrade(token(session), GradeSetPayload(studentId, labId, score, comments)) + val comments = parseComments(form.commentsJson ?: "[]") + api.setGrade(token(session), GradeSetPayload(form.studentId, form.labId, form.score, comments)) redirect.addFlashAttribute("notice", "Оценка сохранена") "redirect:/admin/groups/$id" } catch (ex: ApiException) { @@ -294,14 +296,11 @@ class AdminGroupController( @GetMapping("/submissions/{id}/download") fun downloadSubmissionAsAdmin( @PathVariable id: String, - @RequestParam(required = false) group: String?, - @RequestParam(required = false) no: String?, - @RequestParam(required = false) name: String?, - @RequestParam(required = false) attempt: String?, + @ModelAttribute form: SubmissionDownloadForm, session: HttpSession, ): ResponseEntity { val file = api.downloadSubmissionAsAdmin(token(session), id) - val filename = submissionFilename(group, no, name, attempt) ?: file.filename + val filename = submissionFilename(form.group, form.no, form.name, form.attempt) ?: file.filename val encoded = java.net.URLEncoder.encode(filename, java.nio.charset.StandardCharsets.UTF_8) return ResponseEntity .ok() diff --git a/web/src/main/kotlin/ru/ysu/lms/web/admin/AdminLabController.kt b/web/src/main/kotlin/ru/ysu/lms/web/admin/AdminLabController.kt index e34df82..cf08cb1 100644 --- a/web/src/main/kotlin/ru/ysu/lms/web/admin/AdminLabController.kt +++ b/web/src/main/kotlin/ru/ysu/lms/web/admin/AdminLabController.kt @@ -4,6 +4,7 @@ import jakarta.servlet.http.HttpSession import org.springframework.stereotype.Controller import org.springframework.ui.Model import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.ModelAttribute import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestMapping @@ -141,7 +142,10 @@ class AdminLabController( val defaultLevel = lab.levels.firstOrNull() val assignmentGroups = - buildAssignmentGroups(t, lab, levelByStudent, assignmentByStudent, submissionsByStudent, defaultLevel) + buildAssignmentGroups( + t, + LabAssignmentData(lab, levelByStudent, assignmentByStudent, submissionsByStudent, defaultLevel), + ) model.addAttribute("lab", lab) model.addAttribute("descriptionHtml", markdown.render(lab.descriptionMd)) @@ -176,14 +180,18 @@ class AdminLabController( return "admin/lab" } + private data class LabAssignmentData( + val lab: LabDetailDto, + val levelByStudent: Map, + val assignmentByStudent: Map, + val submissionsByStudent: Map, + val defaultLevel: LabLevelDetailDto?, + ) + /** Строки «группа → студенты» со их уровнем, вариантом и последней сдачей. */ private fun buildAssignmentGroups( token: String, - lab: LabDetailDto, - levelByStudent: Map, - assignmentByStudent: Map, - submissionsByStudent: Map, - defaultLevel: LabLevelDetailDto?, + data: LabAssignmentData, ): List = api.listGroups(token).map { g -> val detail = api.groupDetail(token, g.id) @@ -191,9 +199,9 @@ class AdminLabController( 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] + val levelState = data.levelByStudent[s.id] + val level = data.lab.levels.firstOrNull { it.id == levelState?.labLevelId } ?: data.defaultLevel + val va = data.assignmentByStudent[s.id] AdminLabStudent( studentId = s.id, fullName = s.fullName, @@ -204,9 +212,9 @@ class AdminLabController( 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(), + submissionId = data.submissionsByStudent[s.id]?.id, + submissionSeq = data.submissionsByStudent[s.id]?.seq, + submissionFiles = data.submissionsByStudent[s.id]?.files ?: emptyList(), ) }, ) @@ -231,21 +239,14 @@ class AdminLabController( @PostMapping("/labs/create") fun createLab( - @RequestParam number: Int, - @RequestParam title: String, - @RequestParam(required = false) deadlineAt: String?, - @RequestParam(required = false) defenseAt: String?, - @RequestParam pointsMax: String, - @RequestParam status: String, - @RequestParam descriptionMd: String, - @RequestParam levels: String, + @ModelAttribute form: CreateLabForm, session: HttpSession, redirect: RedirectAttributes, ): String = try { // levels: строки "код;название" по одной на строку val levelList = - levels + form.levels .lines() .map { it.trim() } .filter { it.isNotBlank() } @@ -261,17 +262,17 @@ class AdminLabController( api.createLab( token(session), LabCreatePayload( - number = number, - title = title.trim(), + number = form.number, + title = form.title.trim(), deadlineAt = ru.ysu.lms.web.DateUtils - .toUtcIso(deadlineAt), + .toUtcIso(form.deadlineAt), defenseAt = ru.ysu.lms.web.DateUtils - .toUtcIso(defenseAt), - pointsMax = java.math.BigDecimal(pointsMax), - status = status, - descriptionMd = descriptionMd, + .toUtcIso(form.defenseAt), + pointsMax = java.math.BigDecimal(form.pointsMax), + status = form.status, + descriptionMd = form.descriptionMd, levels = levelList, ), ) @@ -285,18 +286,16 @@ class AdminLabController( @PostMapping("/labs/{labId}/levels/save") fun saveLevel( @PathVariable labId: String, - @RequestParam(required = false) levelId: String?, - @RequestParam code: String, - @RequestParam title: String, + @ModelAttribute form: SaveLevelForm, session: HttpSession, redirect: RedirectAttributes, ): String = try { - if (levelId.isNullOrBlank()) { - api.addLevel(token(session), labId, LevelSavePayload(code, title)) + if (form.levelId.isNullOrBlank()) { + api.addLevel(token(session), labId, LevelSavePayload(form.code, form.title)) redirect.addFlashAttribute("notice", "Уровень добавлен") } else { - api.renameLevel(token(session), labId, levelId, title) + api.renameLevel(token(session), labId, form.levelId, form.title) redirect.addFlashAttribute("notice", "Уровень обновлён") } "redirect:/admin/labs/$labId" @@ -418,64 +417,45 @@ class AdminLabController( @PostMapping("/labs/{id}/edit") fun editLab( @PathVariable id: String, - @RequestParam title: String, - @RequestParam(required = false) deadlineAt: String?, - @RequestParam(required = false) defenseAt: String?, - @RequestParam pointsMax: String, - @RequestParam status: String, - @RequestParam descriptionMd: String, - @RequestParam(defaultValue = "save") action: String, - @RequestParam(required = false) levelIds: List?, - @RequestParam(required = false) levelTitles: List?, - @RequestParam(required = false) variantIds: List?, - @RequestParam(required = false) variantLevelIds: List?, - @RequestParam(required = false) variantNumbers: List?, - @RequestParam(required = false) variantTitles: List?, - @RequestParam(required = false) variantStatements: List?, - @RequestParam(required = false) newLevelCode: String?, - @RequestParam(required = false) newLevelTitle: String?, - @RequestParam(required = false) addLevelId: String?, - @RequestParam(required = false) addNumber: String?, - @RequestParam(required = false) addTitle: String?, - @RequestParam(required = false) addStatementMd: String?, + @ModelAttribute form: EditLabForm, session: HttpSession, redirect: RedirectAttributes, ): String { val t = token(session) return try { - handleEditDeleteAction(t, id, action, redirect)?.let { return it } + handleEditDeleteAction(t, id, form.action, redirect)?.let { return it } api.updateLab( t, id, LabUpdatePayload( - title, + form.title, ru.ysu.lms.web.DateUtils - .toUtcIso(deadlineAt), + .toUtcIso(form.deadlineAt), ru.ysu.lms.web.DateUtils - .toUtcIso(defenseAt), - pointsMax, - status, - descriptionMd, + .toUtcIso(form.defenseAt), + form.pointsMax, + form.status, + form.descriptionMd, ), ) - applyLevelRenames(t, id, levelIds, levelTitles) - applyVariantSaves(t, id, variantIds, variantNumbers, variantLevelIds, variantTitles, variantStatements) + applyLevelRenames(t, id, form.levelIds, form.levelTitles) + applyVariantSaves(t, id, form.variantIds, form.variantNumbers, form.variantLevelIds, form.variantTitles, form.variantStatements) when { - action == "add-level" && !newLevelCode.isNullOrBlank() && !newLevelTitle.isNullOrBlank() -> { - api.addLevel(t, id, LevelSavePayload(newLevelCode.trim(), newLevelTitle.trim())) + form.action == "add-level" && !form.newLevelCode.isNullOrBlank() && !form.newLevelTitle.isNullOrBlank() -> { + api.addLevel(t, id, LevelSavePayload(form.newLevelCode.trim(), form.newLevelTitle.trim())) redirect.addFlashAttribute("notice", "Сохранено; уровень добавлен") } - action == "add-variant" && !addLevelId.isNullOrBlank() && !addNumber.isNullOrBlank() -> { + form.action == "add-variant" && !form.addLevelId.isNullOrBlank() && !form.addNumber.isNullOrBlank() -> { api.saveVariant( t, id, null, VariantSavePayload( - addLevelId, - addNumber.trim(), - addTitle.orEmpty().trim(), - addStatementMd.orEmpty(), + form.addLevelId, + form.addNumber.trim(), + form.addTitle.orEmpty().trim(), + form.addStatementMd.orEmpty(), ), ) redirect.addFlashAttribute("notice", "Сохранено; вариант добавлен") @@ -492,16 +472,17 @@ class AdminLabController( @PostMapping("/labs/{labId}/variants/save") fun saveVariant( @PathVariable labId: String, - @RequestParam(required = false) variantId: String?, - @RequestParam labLevelId: String, - @RequestParam number: String, - @RequestParam title: String, - @RequestParam statementMd: String, + @ModelAttribute form: SaveVariantForm, session: HttpSession, redirect: RedirectAttributes, ): String = try { - api.saveVariant(token(session), labId, variantId, VariantSavePayload(labLevelId, number, title, statementMd)) + api.saveVariant( + token(session), + labId, + form.variantId, + VariantSavePayload(form.labLevelId, form.number, form.title, form.statementMd), + ) redirect.addFlashAttribute("notice", "Вариант сохранён") "redirect:/admin/labs/$labId/edit" } catch (ex: ApiException) { diff --git a/web/src/main/kotlin/ru/ysu/lms/web/admin/AdminStudentController.kt b/web/src/main/kotlin/ru/ysu/lms/web/admin/AdminStudentController.kt index b3252b7..f906c40 100644 --- a/web/src/main/kotlin/ru/ysu/lms/web/admin/AdminStudentController.kt +++ b/web/src/main/kotlin/ru/ysu/lms/web/admin/AdminStudentController.kt @@ -6,6 +6,7 @@ import org.springframework.http.MediaType import org.springframework.http.ResponseEntity import org.springframework.stereotype.Controller import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.ModelAttribute import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestMapping @@ -66,11 +67,7 @@ class AdminStudentController( @PostMapping("/groups/{groupId}/students/create") fun createStudent( @PathVariable groupId: String, - @RequestParam studentNo: String, - @RequestParam lastName: String, - @RequestParam(required = false) firstName: String?, - @RequestParam(required = false) middleName: String?, - @RequestParam(required = false) basis: String?, + @ModelAttribute form: CreateStudentForm, session: HttpSession, redirect: RedirectAttributes, ): String = @@ -80,11 +77,11 @@ class AdminStudentController( token(session), groupId, CreateStudentRequestDto( - studentNo = studentNo.trim(), - lastName = lastName.trim(), - firstName = firstName.orEmpty().trim(), - middleName = middleName.orEmpty().trim(), - basis = basis.orEmpty().trim(), + studentNo = form.studentNo.trim(), + lastName = form.lastName.trim(), + firstName = form.firstName.orEmpty().trim(), + middleName = form.middleName.orEmpty().trim(), + basis = form.basis.orEmpty().trim(), ), ) redirect.addFlashAttribute("createdStudent", result) diff --git a/web/src/main/kotlin/ru/ysu/lms/web/admin/ContentAdminController.kt b/web/src/main/kotlin/ru/ysu/lms/web/admin/ContentAdminController.kt index 470bd3a..c5cdffb 100644 --- a/web/src/main/kotlin/ru/ysu/lms/web/admin/ContentAdminController.kt +++ b/web/src/main/kotlin/ru/ysu/lms/web/admin/ContentAdminController.kt @@ -16,6 +16,7 @@ import ru.ysu.lms.web.api.ApiClient import ru.ysu.lms.web.api.ApiException import ru.ysu.lms.web.api.ArticlesReorderDto import ru.ysu.lms.web.api.ContentReorderDto +import ru.ysu.lms.web.api.CreateArticlePayload import ru.ysu.lms.web.api.SectionsReorderDto import ru.ysu.lms.web.api.createArticle import ru.ysu.lms.web.api.createChapter @@ -269,12 +270,14 @@ class ContentAdminController( try { api.createArticle( token(session), - form.chapterId, - form.sectionId, - form.newChapterTitle?.takeIf { it.isNotBlank() }, - form.newSectionTitle?.takeIf { it.isNotBlank() }, - form.title.trim(), - form.bodyMd, + CreateArticlePayload( + chapterId = form.chapterId, + sectionId = form.sectionId, + newChapterTitle = form.newChapterTitle?.takeIf { it.isNotBlank() }, + newSectionTitle = form.newSectionTitle?.takeIf { it.isNotBlank() }, + title = form.title.trim(), + bodyMd = form.bodyMd, + ), ) redirect.addFlashAttribute("notice", "Статья «${form.title.trim()}» создана") redirectBack(form.back, "redirect:/admin/content") diff --git a/web/src/main/kotlin/ru/ysu/lms/web/api/ApiClientContent.kt b/web/src/main/kotlin/ru/ysu/lms/web/api/ApiClientContent.kt index 6900b20..860f947 100644 --- a/web/src/main/kotlin/ru/ysu/lms/web/api/ApiClientContent.kt +++ b/web/src/main/kotlin/ru/ysu/lms/web/api/ApiClientContent.kt @@ -231,14 +231,18 @@ fun ApiClient.deleteSectionWithMove( } } +data class CreateArticlePayload( + val chapterId: String?, + val sectionId: String?, + val newChapterTitle: String?, + val newSectionTitle: String?, + val title: String, + val bodyMd: String, +) + fun ApiClient.createArticle( token: String, - chapterId: String?, - sectionId: String?, - newChapterTitle: String?, - newSectionTitle: String?, - title: String, - bodyMd: String, + payload: CreateArticlePayload, ): ContentArticleDto = call { client @@ -248,12 +252,12 @@ fun ApiClient.createArticle( .contentType(MediaType.APPLICATION_JSON) .body( mapOf( - "chapterId" to chapterId, - "sectionId" to sectionId, - "newChapterTitle" to newChapterTitle, - "newSectionTitle" to newSectionTitle, - "title" to title, - "bodyMd" to bodyMd, + "chapterId" to payload.chapterId, + "sectionId" to payload.sectionId, + "newChapterTitle" to payload.newChapterTitle, + "newSectionTitle" to payload.newSectionTitle, + "title" to payload.title, + "bodyMd" to payload.bodyMd, ), ).retrieve() .body(ContentArticleDto::class.java)