Refactor: bind admin forms via @ModelAttribute DTOs; detekt fully default

This commit is contained in:
2026-09-27 23:13:29 +03:00
parent 42664297de
commit d3f70b0a25
11 changed files with 255 additions and 185 deletions

View File

@@ -9,21 +9,16 @@ config:
warningsAsErrors: false warningsAsErrors: false
complexity: complexity:
# Все правила сложности — на значениях по умолчанию: крупные агрегаты раздроблены # Все правила сложности — строго на значениях detekt по умолчанию. Крупные агрегаты
# (web ApiClient — по доменам, AdminController — AdminSupport + 5 контроллеров, # раздроблены: ApiClient — по доменам, AdminController — AdminSupport + 5 контроллеров,
# импорт — ContentImporter, авторинг/порядок и оценки — отдельные сервисы, # импорт — ContentImporter, авторинг/порядок и оценки — отдельные сервисы, submissions —
# submissions — отдельный контроллер). Единственное ослабление — LongParameterList # отдельный контроллер; MVC-формы связаны через @ModelAttribute-DTO.
# для MVC-форм (см. ниже): у обработчиков формы @RequestParam естественно много.
LongMethod: LongMethod:
threshold: 60 threshold: 60
LongParameterList: LongParameterList:
# MVC-обработчики форм принимают набор @RequestParam — параметров объективно много. functionThreshold: 6
functionThreshold: 8 constructorThreshold: 7
constructorThreshold: 8
ignoreDefaultParameters: true ignoreDefaultParameters: true
ignoreAnnotated:
- 'PostMapping'
- 'GetMapping'
TooManyFunctions: TooManyFunctions:
thresholdInFiles: 20 thresholdInFiles: 20
thresholdInClasses: 20 thresholdInClasses: 20

View File

@@ -12,6 +12,15 @@ import ru.ysu.lms.common.BadRequestException
import ru.ysu.lms.common.NotFoundException import ru.ysu.lms.common.NotFoundException
import java.util.UUID 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 @Service
class ContentAuthoringService( class ContentAuthoringService(
private val chapters: ContentChapterRepository, private val chapters: ContentChapterRepository,
@@ -141,53 +150,46 @@ class ContentAuthoringService(
/** Создаёт статью; если раздела нет — создаёт главу/раздел по названиям. */ /** Создаёт статью; если раздела нет — создаёт главу/раздел по названиям. */
@Transactional @Transactional
fun createArticlePlaced( fun createArticlePlaced(placement: ArticlePlacement): ContentArticleView {
title: String,
bodyMd: String,
chapterId: UUID?,
newChapterTitle: String?,
sectionId: UUID?,
newSectionTitle: String?,
): ContentArticleView {
val targetSection: ContentSection = val targetSection: ContentSection =
when { when {
sectionId != null && !newSectionTitle.isNullOrBlank() -> { placement.sectionId != null && !placement.newSectionTitle.isNullOrBlank() -> {
val anchor = val anchor =
sections sections
.findById(sectionId) .findById(placement.sectionId)
.orElseThrow { NotFoundException("Раздел не найден") } .orElseThrow { NotFoundException("Раздел не найден") }
val node = createSection(anchor.chapterId, newSectionTitle, null) val node = createSection(anchor.chapterId, placement.newSectionTitle, null)
sections sections
.findById(UUID.fromString(node.id)) .findById(UUID.fromString(node.id))
.orElseThrow { NotFoundException("Раздел не найден") } .orElseThrow { NotFoundException("Раздел не найден") }
} }
sectionId != null -> placement.sectionId != null ->
sections sections
.findById(sectionId) .findById(placement.sectionId)
.orElseThrow { NotFoundException("Раздел не найден") } .orElseThrow { NotFoundException("Раздел не найден") }
!newSectionTitle.isNullOrBlank() -> { !placement.newSectionTitle.isNullOrBlank() -> {
val chapter: ContentChapter = val chapter: ContentChapter =
when { when {
chapterId != null -> placement.chapterId != null ->
chapters chapters
.findById(chapterId) .findById(placement.chapterId)
.orElseThrow { NotFoundException("Глава не найдена") } .orElseThrow { NotFoundException("Глава не найдена") }
!newChapterTitle.isNullOrBlank() -> { !placement.newChapterTitle.isNullOrBlank() -> {
val node = createChapter(newChapterTitle, null) val node = createChapter(placement.newChapterTitle, null)
chapters chapters
.findById(UUID.fromString(node.id)) .findById(UUID.fromString(node.id))
.orElseThrow { NotFoundException("Глава не найдена") } .orElseThrow { NotFoundException("Глава не найдена") }
} }
else -> throw BadRequestException("Укажите главу") else -> throw BadRequestException("Укажите главу")
} }
val node = createSection(chapter.id, newSectionTitle, null) val node = createSection(chapter.id, placement.newSectionTitle, null)
sections sections
.findById(UUID.fromString(node.id)) .findById(UUID.fromString(node.id))
.orElseThrow { NotFoundException("Раздел не найден") } .orElseThrow { NotFoundException("Раздел не найден") }
} }
else -> throw BadRequestException("Укажите раздел") else -> throw BadRequestException("Укажите раздел")
} }
return createArticle(targetSection.id, title, bodyMd) return createArticle(targetSection.id, placement.title, placement.bodyMd)
} }
@Transactional @Transactional

View File

@@ -12,6 +12,7 @@ import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController import org.springframework.web.bind.annotation.RestController
import org.springframework.web.multipart.MultipartFile 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.ContentAuthoringService
import ru.ysu.lms.academic.content.ContentImportResult import ru.ysu.lms.academic.content.ContentImportResult
import ru.ysu.lms.academic.content.ContentService import ru.ysu.lms.academic.content.ContentService
@@ -170,12 +171,14 @@ class ContentAdminController(
fun createArticle( fun createArticle(
@RequestBody body: ArticleCreatePayload, @RequestBody body: ArticleCreatePayload,
) = authoring.createArticlePlaced( ) = authoring.createArticlePlaced(
title = body.title, ArticlePlacement(
bodyMd = body.bodyMd, title = body.title,
chapterId = body.chapterId?.let { UUID.fromString(it) }, bodyMd = body.bodyMd,
newChapterTitle = body.newChapterTitle, chapterId = body.chapterId?.let { UUID.fromString(it) },
sectionId = body.sectionId?.let { UUID.fromString(it) }, newChapterTitle = body.newChapterTitle,
newSectionTitle = body.newSectionTitle, sectionId = body.sectionId?.let { UUID.fromString(it) },
newSectionTitle = body.newSectionTitle,
),
) )
@PutMapping("/articles/{id}") @PutMapping("/articles/{id}")

View File

@@ -2,6 +2,7 @@ package ru.ysu.lms.web.admin
import jakarta.servlet.http.HttpSession import jakarta.servlet.http.HttpSession
import org.springframework.stereotype.Controller 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.PathVariable
import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestMapping
@@ -26,38 +27,34 @@ class AdminDefenseController(
@PostMapping("/labs/{labId}/defense/assign") @PostMapping("/labs/{labId}/defense/assign")
fun assignDefense( fun assignDefense(
@PathVariable labId: String, @PathVariable labId: String,
@RequestParam studentId: String, @ModelAttribute form: DefenseForm,
@RequestParam assignmentMd: String,
@RequestParam(required = false) groupId: String?,
session: HttpSession, session: HttpSession,
redirect: RedirectAttributes, redirect: RedirectAttributes,
): String = ): String =
try { try {
api.assignDefense(token(session), studentId, labId, assignmentMd) api.assignDefense(token(session), form.studentId, labId, form.assignmentMd)
redirect.addFlashAttribute("notice", "Индивидуальное задание сохранено") redirect.addFlashAttribute("notice", "Индивидуальное задание сохранено")
redirectToGroup(groupId, labId) redirectToGroup(form.groupId, labId)
} catch (ex: ApiException) { } catch (ex: ApiException) {
redirect.addFlashAttribute("error", ex.message) redirect.addFlashAttribute("error", ex.message)
redirectToGroup(groupId, labId) redirectToGroup(form.groupId, labId)
} }
/** Назначение/правка индивидуального задания со страницы группы. */ /** Назначение/правка индивидуального задания со страницы группы. */
@PostMapping("/defenses/save") @PostMapping("/defenses/save")
fun saveDefense( fun saveDefense(
@RequestParam studentId: String, @ModelAttribute form: DefenseForm,
@RequestParam labId: String, @RequestParam labId: String,
@RequestParam assignmentMd: String,
@RequestParam(required = false) groupId: String?,
session: HttpSession, session: HttpSession,
redirect: RedirectAttributes, redirect: RedirectAttributes,
): String = ): String =
try { try {
api.assignDefense(token(session), studentId, labId, assignmentMd) api.assignDefense(token(session), form.studentId, labId, form.assignmentMd)
redirect.addFlashAttribute("notice", "Индивидуальное задание сохранено") redirect.addFlashAttribute("notice", "Индивидуальное задание сохранено")
redirectToGroup(groupId, labId) redirectToGroup(form.groupId, labId)
} catch (ex: ApiException) { } catch (ex: ApiException) {
redirect.addFlashAttribute("error", ex.message) redirect.addFlashAttribute("error", ex.message)
redirectToGroup(groupId, labId) redirectToGroup(form.groupId, labId)
} }
@PostMapping("/labs/{labId}/defense/import") @PostMapping("/labs/{labId}/defense/import")

View File

@@ -2,6 +2,7 @@ package ru.ysu.lms.web.admin
import jakarta.servlet.http.HttpSession import jakarta.servlet.http.HttpSession
import org.springframework.stereotype.Controller 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.PathVariable
import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestMapping
@@ -29,14 +30,12 @@ class AdminDistributeController(
@PostMapping("/groups/{gid}/set-level") @PostMapping("/groups/{gid}/set-level")
fun setStudentLevel( fun setStudentLevel(
@PathVariable gid: String, @PathVariable gid: String,
@RequestParam studentId: String, @ModelAttribute form: StudentLevelForm,
@RequestParam labId: String,
@RequestParam labLevelId: String,
session: HttpSession, session: HttpSession,
redirect: RedirectAttributes, redirect: RedirectAttributes,
): String = ): String =
try { try {
api.setLevel(token(session), LevelAssignPayload(studentId, labId, labLevelId)) api.setLevel(token(session), LevelAssignPayload(form.studentId, form.labId, form.labLevelId))
redirect.addFlashAttribute("notice", "Уровень обновлён — вариант подобран автоматически") redirect.addFlashAttribute("notice", "Уровень обновлён — вариант подобран автоматически")
"redirect:/admin/groups/$gid" "redirect:/admin/groups/$gid"
} catch (ex: ApiException) { } catch (ex: ApiException) {
@@ -47,14 +46,12 @@ class AdminDistributeController(
@PostMapping("/groups/{gid}/pin-variant") @PostMapping("/groups/{gid}/pin-variant")
fun pinStudentVariant( fun pinStudentVariant(
@PathVariable gid: String, @PathVariable gid: String,
@RequestParam studentId: String, @ModelAttribute form: PinVariantForm,
@RequestParam labId: String,
@RequestParam variantId: String,
session: HttpSession, session: HttpSession,
redirect: RedirectAttributes, redirect: RedirectAttributes,
): String = ): String =
try { 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.addFlashAttribute("notice", "Закреплён вариант ${result.variantNumber}")
"redirect:/admin/groups/$gid" "redirect:/admin/groups/$gid"
} catch (ex: ApiException) { } catch (ex: ApiException) {

View File

@@ -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<String>? = null,
val levelTitles: List<String>? = null,
val variantIds: List<String>? = null,
val variantLevelIds: List<String>? = null,
val variantNumbers: List<String>? = null,
val variantTitles: List<String>? = null,
val variantStatements: List<String>? = 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,
)

View File

@@ -7,6 +7,7 @@ import org.springframework.http.ResponseEntity
import org.springframework.stereotype.Controller import org.springframework.stereotype.Controller
import org.springframework.ui.Model import org.springframework.ui.Model
import org.springframework.web.bind.annotation.GetMapping 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.PathVariable
import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestMapping
@@ -188,7 +189,7 @@ class AdminGroupController(
buildDefenseRows(t, group, labs) buildDefenseRows(t, group, labs)
val candidates = runCatching { api.groupCandidates(t, id) }.getOrDefault(emptyList()) 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" return "admin/group"
} }
@@ -223,36 +224,37 @@ class AdminGroupController(
} }
}.sortedWith(compareBy({ it.fullName.lowercase() }, { it.labNumber })) }.sortedWith(compareBy({ it.fullName.lowercase() }, { it.labNumber }))
private data class GroupViewModel(
val group: GroupDetailDto,
val labs: List<LabSummaryDto>,
val candidates: List<StudentCandidateDto>,
val rows: List<GroupStudentRow>,
val defenseRows: List<GroupDefenseRow>,
val publishedLabs: Set<String>,
)
private fun fillGroupModel( private fun fillGroupModel(
model: Model, model: Model,
group: GroupDetailDto, view: GroupViewModel,
labs: List<LabSummaryDto>,
candidates: List<StudentCandidateDto>,
rows: List<GroupStudentRow>,
defenseRows: List<GroupDefenseRow>,
publishedLabs: Set<String>,
) { ) {
model.addAttribute("group", group) model.addAttribute("group", view.group)
model.addAttribute("labs", labs) model.addAttribute("labs", view.labs)
model.addAttribute("candidates", candidates) model.addAttribute("candidates", view.candidates)
model.addAttribute("studentGrades", rows) model.addAttribute("studentGrades", view.rows)
model.addAttribute("defenseRows", defenseRows) model.addAttribute("defenseRows", view.defenseRows)
model.addAttribute("publishedLabIds", publishedLabs) model.addAttribute("publishedLabIds", view.publishedLabs)
} }
@PostMapping("/groups/{id}/grades") @PostMapping("/groups/{id}/grades")
fun setGrade( fun setGrade(
@PathVariable id: String, @PathVariable id: String,
@RequestParam studentId: String, @ModelAttribute form: SetGradeForm,
@RequestParam labId: String,
@RequestParam score: String,
@RequestParam(required = false) commentsJson: String?,
session: HttpSession, session: HttpSession,
redirect: RedirectAttributes, redirect: RedirectAttributes,
): String = ): String =
try { try {
val comments = parseComments(commentsJson ?: "[]") val comments = parseComments(form.commentsJson ?: "[]")
api.setGrade(token(session), GradeSetPayload(studentId, labId, score, comments)) api.setGrade(token(session), GradeSetPayload(form.studentId, form.labId, form.score, comments))
redirect.addFlashAttribute("notice", "Оценка сохранена") redirect.addFlashAttribute("notice", "Оценка сохранена")
"redirect:/admin/groups/$id" "redirect:/admin/groups/$id"
} catch (ex: ApiException) { } catch (ex: ApiException) {
@@ -294,14 +296,11 @@ class AdminGroupController(
@GetMapping("/submissions/{id}/download") @GetMapping("/submissions/{id}/download")
fun downloadSubmissionAsAdmin( fun downloadSubmissionAsAdmin(
@PathVariable id: String, @PathVariable id: String,
@RequestParam(required = false) group: String?, @ModelAttribute form: SubmissionDownloadForm,
@RequestParam(required = false) no: String?,
@RequestParam(required = false) name: String?,
@RequestParam(required = false) attempt: String?,
session: HttpSession, session: HttpSession,
): ResponseEntity<ByteArray> { ): ResponseEntity<ByteArray> {
val file = api.downloadSubmissionAsAdmin(token(session), id) 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) val encoded = java.net.URLEncoder.encode(filename, java.nio.charset.StandardCharsets.UTF_8)
return ResponseEntity return ResponseEntity
.ok() .ok()

View File

@@ -4,6 +4,7 @@ import jakarta.servlet.http.HttpSession
import org.springframework.stereotype.Controller import org.springframework.stereotype.Controller
import org.springframework.ui.Model import org.springframework.ui.Model
import org.springframework.web.bind.annotation.GetMapping 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.PathVariable
import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestMapping
@@ -141,7 +142,10 @@ class AdminLabController(
val defaultLevel = lab.levels.firstOrNull() val defaultLevel = lab.levels.firstOrNull()
val assignmentGroups = val assignmentGroups =
buildAssignmentGroups(t, lab, levelByStudent, assignmentByStudent, submissionsByStudent, defaultLevel) buildAssignmentGroups(
t,
LabAssignmentData(lab, levelByStudent, assignmentByStudent, submissionsByStudent, defaultLevel),
)
model.addAttribute("lab", lab) model.addAttribute("lab", lab)
model.addAttribute("descriptionHtml", markdown.render(lab.descriptionMd)) model.addAttribute("descriptionHtml", markdown.render(lab.descriptionMd))
@@ -176,14 +180,18 @@ class AdminLabController(
return "admin/lab" return "admin/lab"
} }
private data class LabAssignmentData(
val lab: LabDetailDto,
val levelByStudent: Map<String, LevelAssignmentResultDto>,
val assignmentByStudent: Map<String, VariantAssignmentResultDto>,
val submissionsByStudent: Map<String, SubmissionDto>,
val defaultLevel: LabLevelDetailDto?,
)
/** Строки «группа → студенты» со их уровнем, вариантом и последней сдачей. */ /** Строки «группа → студенты» со их уровнем, вариантом и последней сдачей. */
private fun buildAssignmentGroups( private fun buildAssignmentGroups(
token: String, token: String,
lab: LabDetailDto, data: LabAssignmentData,
levelByStudent: Map<String, LevelAssignmentResultDto>,
assignmentByStudent: Map<String, VariantAssignmentResultDto>,
submissionsByStudent: Map<String, SubmissionDto>,
defaultLevel: LabLevelDetailDto?,
): List<AdminLabGroup> = ): List<AdminLabGroup> =
api.listGroups(token).map { g -> api.listGroups(token).map { g ->
val detail = api.groupDetail(token, g.id) val detail = api.groupDetail(token, g.id)
@@ -191,9 +199,9 @@ class AdminLabController(
groupName = g.name, groupName = g.name,
students = students =
detail.students.map { s -> detail.students.map { s ->
val levelState = levelByStudent[s.id] val levelState = data.levelByStudent[s.id]
val level = lab.levels.firstOrNull { it.id == levelState?.labLevelId } ?: defaultLevel val level = data.lab.levels.firstOrNull { it.id == levelState?.labLevelId } ?: data.defaultLevel
val va = assignmentByStudent[s.id] val va = data.assignmentByStudent[s.id]
AdminLabStudent( AdminLabStudent(
studentId = s.id, studentId = s.id,
fullName = s.fullName, fullName = s.fullName,
@@ -204,9 +212,9 @@ class AdminLabController(
variantId = va?.variantId, variantId = va?.variantId,
variantNumber = va?.variantNumber, variantNumber = va?.variantNumber,
source = if (va?.manual == true) "вручную" else "авто", source = if (va?.manual == true) "вручную" else "авто",
submissionId = submissionsByStudent[s.id]?.id, submissionId = data.submissionsByStudent[s.id]?.id,
submissionSeq = submissionsByStudent[s.id]?.seq, submissionSeq = data.submissionsByStudent[s.id]?.seq,
submissionFiles = submissionsByStudent[s.id]?.files ?: emptyList(), submissionFiles = data.submissionsByStudent[s.id]?.files ?: emptyList(),
) )
}, },
) )
@@ -231,21 +239,14 @@ class AdminLabController(
@PostMapping("/labs/create") @PostMapping("/labs/create")
fun createLab( fun createLab(
@RequestParam number: Int, @ModelAttribute form: CreateLabForm,
@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,
session: HttpSession, session: HttpSession,
redirect: RedirectAttributes, redirect: RedirectAttributes,
): String = ): String =
try { try {
// levels: строки "код;название" по одной на строку // levels: строки "код;название" по одной на строку
val levelList = val levelList =
levels form.levels
.lines() .lines()
.map { it.trim() } .map { it.trim() }
.filter { it.isNotBlank() } .filter { it.isNotBlank() }
@@ -261,17 +262,17 @@ class AdminLabController(
api.createLab( api.createLab(
token(session), token(session),
LabCreatePayload( LabCreatePayload(
number = number, number = form.number,
title = title.trim(), title = form.title.trim(),
deadlineAt = deadlineAt =
ru.ysu.lms.web.DateUtils ru.ysu.lms.web.DateUtils
.toUtcIso(deadlineAt), .toUtcIso(form.deadlineAt),
defenseAt = defenseAt =
ru.ysu.lms.web.DateUtils ru.ysu.lms.web.DateUtils
.toUtcIso(defenseAt), .toUtcIso(form.defenseAt),
pointsMax = java.math.BigDecimal(pointsMax), pointsMax = java.math.BigDecimal(form.pointsMax),
status = status, status = form.status,
descriptionMd = descriptionMd, descriptionMd = form.descriptionMd,
levels = levelList, levels = levelList,
), ),
) )
@@ -285,18 +286,16 @@ class AdminLabController(
@PostMapping("/labs/{labId}/levels/save") @PostMapping("/labs/{labId}/levels/save")
fun saveLevel( fun saveLevel(
@PathVariable labId: String, @PathVariable labId: String,
@RequestParam(required = false) levelId: String?, @ModelAttribute form: SaveLevelForm,
@RequestParam code: String,
@RequestParam title: String,
session: HttpSession, session: HttpSession,
redirect: RedirectAttributes, redirect: RedirectAttributes,
): String = ): String =
try { try {
if (levelId.isNullOrBlank()) { if (form.levelId.isNullOrBlank()) {
api.addLevel(token(session), labId, LevelSavePayload(code, title)) api.addLevel(token(session), labId, LevelSavePayload(form.code, form.title))
redirect.addFlashAttribute("notice", "Уровень добавлен") redirect.addFlashAttribute("notice", "Уровень добавлен")
} else { } else {
api.renameLevel(token(session), labId, levelId, title) api.renameLevel(token(session), labId, form.levelId, form.title)
redirect.addFlashAttribute("notice", "Уровень обновлён") redirect.addFlashAttribute("notice", "Уровень обновлён")
} }
"redirect:/admin/labs/$labId" "redirect:/admin/labs/$labId"
@@ -418,64 +417,45 @@ class AdminLabController(
@PostMapping("/labs/{id}/edit") @PostMapping("/labs/{id}/edit")
fun editLab( fun editLab(
@PathVariable id: String, @PathVariable id: String,
@RequestParam title: String, @ModelAttribute form: EditLabForm,
@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<String>?,
@RequestParam(required = false) levelTitles: List<String>?,
@RequestParam(required = false) variantIds: List<String>?,
@RequestParam(required = false) variantLevelIds: List<String>?,
@RequestParam(required = false) variantNumbers: List<String>?,
@RequestParam(required = false) variantTitles: List<String>?,
@RequestParam(required = false) variantStatements: List<String>?,
@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?,
session: HttpSession, session: HttpSession,
redirect: RedirectAttributes, redirect: RedirectAttributes,
): String { ): String {
val t = token(session) val t = token(session)
return try { return try {
handleEditDeleteAction(t, id, action, redirect)?.let { return it } handleEditDeleteAction(t, id, form.action, redirect)?.let { return it }
api.updateLab( api.updateLab(
t, t,
id, id,
LabUpdatePayload( LabUpdatePayload(
title, form.title,
ru.ysu.lms.web.DateUtils ru.ysu.lms.web.DateUtils
.toUtcIso(deadlineAt), .toUtcIso(form.deadlineAt),
ru.ysu.lms.web.DateUtils ru.ysu.lms.web.DateUtils
.toUtcIso(defenseAt), .toUtcIso(form.defenseAt),
pointsMax, form.pointsMax,
status, form.status,
descriptionMd, form.descriptionMd,
), ),
) )
applyLevelRenames(t, id, levelIds, levelTitles) applyLevelRenames(t, id, form.levelIds, form.levelTitles)
applyVariantSaves(t, id, variantIds, variantNumbers, variantLevelIds, variantTitles, variantStatements) applyVariantSaves(t, id, form.variantIds, form.variantNumbers, form.variantLevelIds, form.variantTitles, form.variantStatements)
when { when {
action == "add-level" && !newLevelCode.isNullOrBlank() && !newLevelTitle.isNullOrBlank() -> { form.action == "add-level" && !form.newLevelCode.isNullOrBlank() && !form.newLevelTitle.isNullOrBlank() -> {
api.addLevel(t, id, LevelSavePayload(newLevelCode.trim(), newLevelTitle.trim())) api.addLevel(t, id, LevelSavePayload(form.newLevelCode.trim(), form.newLevelTitle.trim()))
redirect.addFlashAttribute("notice", "Сохранено; уровень добавлен") redirect.addFlashAttribute("notice", "Сохранено; уровень добавлен")
} }
action == "add-variant" && !addLevelId.isNullOrBlank() && !addNumber.isNullOrBlank() -> { form.action == "add-variant" && !form.addLevelId.isNullOrBlank() && !form.addNumber.isNullOrBlank() -> {
api.saveVariant( api.saveVariant(
t, t,
id, id,
null, null,
VariantSavePayload( VariantSavePayload(
addLevelId, form.addLevelId,
addNumber.trim(), form.addNumber.trim(),
addTitle.orEmpty().trim(), form.addTitle.orEmpty().trim(),
addStatementMd.orEmpty(), form.addStatementMd.orEmpty(),
), ),
) )
redirect.addFlashAttribute("notice", "Сохранено; вариант добавлен") redirect.addFlashAttribute("notice", "Сохранено; вариант добавлен")
@@ -492,16 +472,17 @@ class AdminLabController(
@PostMapping("/labs/{labId}/variants/save") @PostMapping("/labs/{labId}/variants/save")
fun saveVariant( fun saveVariant(
@PathVariable labId: String, @PathVariable labId: String,
@RequestParam(required = false) variantId: String?, @ModelAttribute form: SaveVariantForm,
@RequestParam labLevelId: String,
@RequestParam number: String,
@RequestParam title: String,
@RequestParam statementMd: String,
session: HttpSession, session: HttpSession,
redirect: RedirectAttributes, redirect: RedirectAttributes,
): String = ): String =
try { 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.addFlashAttribute("notice", "Вариант сохранён")
"redirect:/admin/labs/$labId/edit" "redirect:/admin/labs/$labId/edit"
} catch (ex: ApiException) { } catch (ex: ApiException) {

View File

@@ -6,6 +6,7 @@ import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity import org.springframework.http.ResponseEntity
import org.springframework.stereotype.Controller import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.GetMapping 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.PathVariable
import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestMapping
@@ -66,11 +67,7 @@ class AdminStudentController(
@PostMapping("/groups/{groupId}/students/create") @PostMapping("/groups/{groupId}/students/create")
fun createStudent( fun createStudent(
@PathVariable groupId: String, @PathVariable groupId: String,
@RequestParam studentNo: String, @ModelAttribute form: CreateStudentForm,
@RequestParam lastName: String,
@RequestParam(required = false) firstName: String?,
@RequestParam(required = false) middleName: String?,
@RequestParam(required = false) basis: String?,
session: HttpSession, session: HttpSession,
redirect: RedirectAttributes, redirect: RedirectAttributes,
): String = ): String =
@@ -80,11 +77,11 @@ class AdminStudentController(
token(session), token(session),
groupId, groupId,
CreateStudentRequestDto( CreateStudentRequestDto(
studentNo = studentNo.trim(), studentNo = form.studentNo.trim(),
lastName = lastName.trim(), lastName = form.lastName.trim(),
firstName = firstName.orEmpty().trim(), firstName = form.firstName.orEmpty().trim(),
middleName = middleName.orEmpty().trim(), middleName = form.middleName.orEmpty().trim(),
basis = basis.orEmpty().trim(), basis = form.basis.orEmpty().trim(),
), ),
) )
redirect.addFlashAttribute("createdStudent", result) redirect.addFlashAttribute("createdStudent", result)

View File

@@ -16,6 +16,7 @@ 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.ArticlesReorderDto import ru.ysu.lms.web.api.ArticlesReorderDto
import ru.ysu.lms.web.api.ContentReorderDto 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.SectionsReorderDto
import ru.ysu.lms.web.api.createArticle import ru.ysu.lms.web.api.createArticle
import ru.ysu.lms.web.api.createChapter import ru.ysu.lms.web.api.createChapter
@@ -269,12 +270,14 @@ class ContentAdminController(
try { try {
api.createArticle( api.createArticle(
token(session), token(session),
form.chapterId, CreateArticlePayload(
form.sectionId, chapterId = form.chapterId,
form.newChapterTitle?.takeIf { it.isNotBlank() }, sectionId = form.sectionId,
form.newSectionTitle?.takeIf { it.isNotBlank() }, newChapterTitle = form.newChapterTitle?.takeIf { it.isNotBlank() },
form.title.trim(), newSectionTitle = form.newSectionTitle?.takeIf { it.isNotBlank() },
form.bodyMd, title = form.title.trim(),
bodyMd = form.bodyMd,
),
) )
redirect.addFlashAttribute("notice", "Статья «${form.title.trim()}» создана") redirect.addFlashAttribute("notice", "Статья «${form.title.trim()}» создана")
redirectBack(form.back, "redirect:/admin/content") redirectBack(form.back, "redirect:/admin/content")

View File

@@ -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( fun ApiClient.createArticle(
token: String, token: String,
chapterId: String?, payload: CreateArticlePayload,
sectionId: String?,
newChapterTitle: String?,
newSectionTitle: String?,
title: String,
bodyMd: String,
): ContentArticleDto = ): ContentArticleDto =
call { call {
client client
@@ -248,12 +252,12 @@ fun ApiClient.createArticle(
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.body( .body(
mapOf( mapOf(
"chapterId" to chapterId, "chapterId" to payload.chapterId,
"sectionId" to sectionId, "sectionId" to payload.sectionId,
"newChapterTitle" to newChapterTitle, "newChapterTitle" to payload.newChapterTitle,
"newSectionTitle" to newSectionTitle, "newSectionTitle" to payload.newSectionTitle,
"title" to title, "title" to payload.title,
"bodyMd" to bodyMd, "bodyMd" to payload.bodyMd,
), ),
).retrieve() ).retrieve()
.body(ContentArticleDto::class.java) .body(ContentArticleDto::class.java)