Refactor: extract helpers to satisfy detekt (throws, returns, loops)
This commit is contained in:
@@ -29,18 +29,8 @@ class AssetStorage(
|
|||||||
): StoredAsset {
|
): StoredAsset {
|
||||||
val ext = originalName.substringAfterLast('.', "").lowercase()
|
val ext = originalName.substringAfterLast('.', "").lowercase()
|
||||||
val isVideo = ext in VIDEO_EXTENSIONS
|
val isVideo = ext in VIDEO_EXTENSIONS
|
||||||
val isImage = ext in IMAGE_EXTENSIONS
|
requireAllowedType(originalName, isVideo, isImage = ext in IMAGE_EXTENSIONS)
|
||||||
if (!isVideo && !isImage) {
|
requireSizeWithinLimit(originalName, bytes.size, isVideo)
|
||||||
throw BadRequestException(
|
|
||||||
"Недопустимый тип файла: $originalName " +
|
|
||||||
"(изображения: ${IMAGE_EXTENSIONS.joinToString(", ")}; " +
|
|
||||||
"видео: ${VIDEO_EXTENSIONS.joinToString(", ")})",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
val limit = if (isVideo) MAX_VIDEO_BYTES else MAX_IMAGE_BYTES
|
|
||||||
if (bytes.size > limit) {
|
|
||||||
throw BadRequestException("Файл $originalName слишком большой (> ${limit / 1024 / 1024} МБ)")
|
|
||||||
}
|
|
||||||
val publicName = "${UUID.randomUUID()}.$ext"
|
val publicName = "${UUID.randomUUID()}.$ext"
|
||||||
val target = rootDir.resolve(publicName).normalize()
|
val target = rootDir.resolve(publicName).normalize()
|
||||||
if (!target.startsWith(rootDir)) {
|
if (!target.startsWith(rootDir)) {
|
||||||
@@ -50,6 +40,31 @@ class AssetStorage(
|
|||||||
return StoredAsset(publicName, target, mimeFor(ext))
|
return StoredAsset(publicName, target, mimeFor(ext))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun requireAllowedType(
|
||||||
|
originalName: String,
|
||||||
|
isVideo: Boolean,
|
||||||
|
isImage: Boolean,
|
||||||
|
) {
|
||||||
|
if (!isVideo && !isImage) {
|
||||||
|
throw BadRequestException(
|
||||||
|
"Недопустимый тип файла: $originalName " +
|
||||||
|
"(изображения: ${IMAGE_EXTENSIONS.joinToString(", ")}; " +
|
||||||
|
"видео: ${VIDEO_EXTENSIONS.joinToString(", ")})",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun requireSizeWithinLimit(
|
||||||
|
originalName: String,
|
||||||
|
size: Int,
|
||||||
|
isVideo: Boolean,
|
||||||
|
) {
|
||||||
|
val limit = if (isVideo) MAX_VIDEO_BYTES else MAX_IMAGE_BYTES
|
||||||
|
if (size > limit) {
|
||||||
|
throw BadRequestException("Файл $originalName слишком большой (> ${limit / 1024 / 1024} МБ)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun read(publicName: String): Pair<Path, String>? {
|
fun read(publicName: String): Pair<Path, String>? {
|
||||||
val clean = publicName.substringAfterLast('/')
|
val clean = publicName.substringAfterLast('/')
|
||||||
val target = rootDir.resolve(clean).normalize()
|
val target = rootDir.resolve(clean).normalize()
|
||||||
|
|||||||
@@ -168,12 +168,12 @@ class ContentService(
|
|||||||
articleSlug: String,
|
articleSlug: String,
|
||||||
includeHidden: Boolean = false,
|
includeHidden: Boolean = false,
|
||||||
): ContentArticleView {
|
): ContentArticleView {
|
||||||
val ch = chapters.findBySlug(chapterSlug) ?: throw NotFoundException("Глава не найдена")
|
val ch = requireFound(chapters.findBySlug(chapterSlug), "Глава не найдена")
|
||||||
if (!includeHidden && !ch.published) throw NotFoundException("Глава скрыта")
|
requireVisible(ch.published, includeHidden, "Глава скрыта")
|
||||||
val sec = sections.findByChapterIdAndSlug(ch.id, sectionSlug) ?: throw NotFoundException("Раздел не найден")
|
val sec = requireFound(sections.findByChapterIdAndSlug(ch.id, sectionSlug), "Раздел не найден")
|
||||||
if (!includeHidden && !sec.published) throw NotFoundException("Раздел скрыт")
|
requireVisible(sec.published, includeHidden, "Раздел скрыт")
|
||||||
val a = articles.findBySectionIdAndSlug(sec.id, articleSlug) ?: throw NotFoundException("Статья не найдена")
|
val a = requireFound(articles.findBySectionIdAndSlug(sec.id, articleSlug), "Статья не найдена")
|
||||||
if (!includeHidden && !a.published) throw NotFoundException("Статья скрыта")
|
requireVisible(a.published, includeHidden, "Статья скрыта")
|
||||||
return ContentArticleView(
|
return ContentArticleView(
|
||||||
id = a.id.toString(),
|
id = a.id.toString(),
|
||||||
chapterSlug = ch.slug,
|
chapterSlug = ch.slug,
|
||||||
@@ -258,6 +258,19 @@ class ContentService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun <T> requireFound(
|
||||||
|
value: T?,
|
||||||
|
message: String,
|
||||||
|
): T = value ?: throw NotFoundException(message)
|
||||||
|
|
||||||
|
private fun requireVisible(
|
||||||
|
published: Boolean,
|
||||||
|
includeHidden: Boolean,
|
||||||
|
message: String,
|
||||||
|
) {
|
||||||
|
if (!includeHidden && !published) throw NotFoundException(message)
|
||||||
|
}
|
||||||
|
|
||||||
private fun uuidOrNull(value: String): UUID? = runCatching { UUID.fromString(value) }.getOrNull()
|
private fun uuidOrNull(value: String): UUID? = runCatching { UUID.fromString(value) }.getOrNull()
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
|
|||||||
@@ -81,20 +81,7 @@ class LabService(
|
|||||||
|
|
||||||
val levelByCode = levelEntities.associateBy { it.code }
|
val levelByCode = levelEntities.associateBy { it.code }
|
||||||
|
|
||||||
bundle.variants.forEach { vmeta ->
|
saveVariants(lab, levelByCode, bundle.variants)
|
||||||
val level =
|
|
||||||
levelByCode[vmeta.level]
|
|
||||||
?: throw BadRequestException("Неизвестный уровень ${vmeta.level} у варианта ${vmeta.number}")
|
|
||||||
variantsRepo.save(
|
|
||||||
Variant(
|
|
||||||
lab = lab,
|
|
||||||
labLevel = level,
|
|
||||||
number = vmeta.number,
|
|
||||||
title = vmeta.title,
|
|
||||||
statementMd = vmeta.statement,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return LabImportResult(
|
return LabImportResult(
|
||||||
labId = lab.id.toString(),
|
labId = lab.id.toString(),
|
||||||
@@ -248,10 +235,54 @@ class LabService(
|
|||||||
return detail(labId)
|
return detail(labId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
private fun validateCreate(request: LabCreateRequest) {
|
||||||
fun create(request: LabCreateRequest): LabDetail {
|
|
||||||
if (request.title.isNullOrBlank()) throw BadRequestException("Название не может быть пустым")
|
if (request.title.isNullOrBlank()) throw BadRequestException("Название не может быть пустым")
|
||||||
if ((request.number ?: 0) <= 0) throw BadRequestException("Номер лабораторной должен быть положительным")
|
if ((request.number ?: 0) <= 0) throw BadRequestException("Номер лабораторной должен быть положительным")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveLevels(
|
||||||
|
lab: Lab,
|
||||||
|
levels: List<LevelSaveRequest>,
|
||||||
|
) {
|
||||||
|
val levelMetas = levels.ifEmpty { listOf(LevelSaveRequest(code = "basic", title = "Основная группа")) }
|
||||||
|
levelMetas.forEachIndexed { index, meta ->
|
||||||
|
if (meta.code.isNullOrBlank()) throw BadRequestException("Код уровня не может быть пустым")
|
||||||
|
labLevels.save(
|
||||||
|
LabLevel(
|
||||||
|
lab = lab,
|
||||||
|
code = meta.code.trim(),
|
||||||
|
title = meta.title.orEmpty().ifBlank { meta.code },
|
||||||
|
description = meta.description.orEmpty(),
|
||||||
|
position = meta.position?.takeIf { it > 0 } ?: index + 1,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveVariants(
|
||||||
|
lab: Lab,
|
||||||
|
levelByCode: Map<String, LabLevel>,
|
||||||
|
variantMetas: List<VariantMeta>,
|
||||||
|
) {
|
||||||
|
variantMetas.forEach { vmeta ->
|
||||||
|
val level =
|
||||||
|
levelByCode[vmeta.level]
|
||||||
|
?: throw BadRequestException("Неизвестный уровень ${vmeta.level} у варианта ${vmeta.number}")
|
||||||
|
variantsRepo.save(
|
||||||
|
Variant(
|
||||||
|
lab = lab,
|
||||||
|
labLevel = level,
|
||||||
|
number = vmeta.number,
|
||||||
|
title = vmeta.title,
|
||||||
|
statementMd = vmeta.statement,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
fun create(request: LabCreateRequest): LabDetail {
|
||||||
|
validateCreate(request)
|
||||||
val semester = findOrCreateSemester(request.semesterTitle.orEmpty(), request.semesterYear ?: 0)
|
val semester = findOrCreateSemester(request.semesterTitle.orEmpty(), request.semesterYear ?: 0)
|
||||||
if (labs.existsByNumberAndSemesterId(request.number ?: 0, semester.id)) {
|
if (labs.existsByNumberAndSemesterId(request.number ?: 0, semester.id)) {
|
||||||
throw ConflictException("Лабораторная №${request.number} уже существует в этом семестре")
|
throw ConflictException("Лабораторная №${request.number} уже существует в этом семестре")
|
||||||
@@ -269,22 +300,7 @@ class LabService(
|
|||||||
status = request.status ?: "draft",
|
status = request.status ?: "draft",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
val levelMetas =
|
saveLevels(lab, request.levels)
|
||||||
request.levels.ifEmpty {
|
|
||||||
listOf(LevelSaveRequest(code = "basic", title = "Основная группа"))
|
|
||||||
}
|
|
||||||
levelMetas.forEachIndexed { index, meta ->
|
|
||||||
if (meta.code.isNullOrBlank()) throw BadRequestException("Код уровня не может быть пустым")
|
|
||||||
labLevels.save(
|
|
||||||
LabLevel(
|
|
||||||
lab = lab,
|
|
||||||
code = meta.code.trim(),
|
|
||||||
title = meta.title.orEmpty().ifBlank { meta.code },
|
|
||||||
description = meta.description.orEmpty(),
|
|
||||||
position = meta.position?.takeIf { it > 0 } ?: index + 1,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return detail(lab.id)
|
return detail(lab.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -89,12 +89,13 @@ class GroupService(
|
|||||||
groups.findById(groupId).orElseThrow { NotFoundException("Группа не найдена") }
|
groups.findById(groupId).orElseThrow { NotFoundException("Группа не найдена") }
|
||||||
val memberIds = enrollments.findAllByGroupId(groupId).mapNotNull { it.student?.accountId }.toSet()
|
val memberIds = enrollments.findAllByGroupId(groupId).mapNotNull { it.student?.accountId }.toSet()
|
||||||
val currentByStudent = HashMap<UUID, Pair<UUID, String>>()
|
val currentByStudent = HashMap<UUID, Pair<UUID, String>>()
|
||||||
for (e in enrollments.findAll()) {
|
enrollments
|
||||||
val sid = e.student?.accountId ?: continue
|
.findAll()
|
||||||
val gid = e.group?.id ?: continue
|
.mapNotNull { enrollment ->
|
||||||
val name = e.group?.name ?: continue
|
val sid = enrollment.student?.accountId ?: return@mapNotNull null
|
||||||
currentByStudent[sid] = gid to name
|
val group = enrollment.group ?: return@mapNotNull null
|
||||||
}
|
sid to (group.id to group.name)
|
||||||
|
}.forEach { (sid, group) -> currentByStudent[sid] = group }
|
||||||
return students
|
return students
|
||||||
.findAll()
|
.findAll()
|
||||||
.filter { it.accountId !in memberIds }
|
.filter { it.accountId !in memberIds }
|
||||||
|
|||||||
@@ -92,26 +92,43 @@ class StudentImportParser {
|
|||||||
filename: String,
|
filename: String,
|
||||||
rows: List<List<String>>,
|
rows: List<List<String>>,
|
||||||
): ParsedGroup {
|
): ParsedGroup {
|
||||||
|
val headerIndex = requireHeader(rows)
|
||||||
|
val columns = requireColumns(rows[headerIndex])
|
||||||
|
val students = rows.drop(headerIndex + 1).mapNotNull { parseStudent(it, columns) }
|
||||||
|
if (students.isEmpty()) {
|
||||||
|
throw BadRequestException("В таблице не найдено ни одного студента")
|
||||||
|
}
|
||||||
|
return ParsedGroup(groupNameFrom(filename, rows), students)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun requireHeader(rows: List<List<String>>): Int {
|
||||||
val headerIndex = rows.indexOfFirst { row -> row.any { isStudentHeader(it) } }
|
val headerIndex = rows.indexOfFirst { row -> row.any { isStudentHeader(it) } }
|
||||||
if (headerIndex < 0) {
|
if (headerIndex < 0) {
|
||||||
throw BadRequestException("Не найден столбец «Студенческий номер». Проверьте шапку таблицы.")
|
throw BadRequestException("Не найден столбец «Студенческий номер». Проверьте шапку таблицы.")
|
||||||
}
|
}
|
||||||
val columns = mapColumns(rows[headerIndex])
|
return headerIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun requireColumns(header: List<String>): Map<String, Int> {
|
||||||
|
val columns = mapColumns(header)
|
||||||
if (columns["studentNo"] == null) {
|
if (columns["studentNo"] == null) {
|
||||||
throw BadRequestException("Не найден столбец «Студенческий номер»")
|
throw BadRequestException("Не найден столбец «Студенческий номер»")
|
||||||
}
|
}
|
||||||
val students =
|
return columns
|
||||||
rows.drop(headerIndex + 1).mapNotNull { row ->
|
}
|
||||||
|
|
||||||
|
private fun parseStudent(
|
||||||
|
row: List<String>,
|
||||||
|
columns: Map<String, Int>,
|
||||||
|
): ParsedStudent? {
|
||||||
fun cell(key: String): String = columns[key]?.let { row.getOrNull(it) }.orEmpty().trim()
|
fun cell(key: String): String = columns[key]?.let { row.getOrNull(it) }.orEmpty().trim()
|
||||||
val studentNo = normalizeNumber(cell("studentNo"))
|
val studentNo = normalizeNumber(cell("studentNo"))
|
||||||
val lastName = cell("lastName")
|
val lastName = cell("lastName")
|
||||||
if (studentNo.isBlank() && lastName.isBlank()) {
|
if (studentNo.isBlank() && lastName.isBlank()) return null
|
||||||
return@mapNotNull null
|
|
||||||
}
|
|
||||||
if (studentNo.isBlank()) {
|
if (studentNo.isBlank()) {
|
||||||
throw BadRequestException("В одной из строк отсутствует студенческий номер")
|
throw BadRequestException("В одной из строк отсутствует студенческий номер")
|
||||||
}
|
}
|
||||||
ParsedStudent(
|
return ParsedStudent(
|
||||||
studentNo = studentNo,
|
studentNo = studentNo,
|
||||||
lastName = lastName,
|
lastName = lastName,
|
||||||
firstName = cell("firstName"),
|
firstName = cell("firstName"),
|
||||||
@@ -120,11 +137,6 @@ class StudentImportParser {
|
|||||||
note = cell("note"),
|
note = cell("note"),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (students.isEmpty()) {
|
|
||||||
throw BadRequestException("В таблице не найдено ни одного студента")
|
|
||||||
}
|
|
||||||
return ParsedGroup(groupNameFrom(filename, rows), students)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun isStudentHeader(value: String): Boolean {
|
private fun isStudentHeader(value: String): Boolean {
|
||||||
val normalized = value.lowercase().replace("\u00a0", " ").trim()
|
val normalized = value.lowercase().replace("\u00a0", " ").trim()
|
||||||
|
|||||||
@@ -114,16 +114,22 @@ class DefenseService(
|
|||||||
code: String,
|
code: String,
|
||||||
): DefenseDto {
|
): DefenseDto {
|
||||||
syncTimeouts()
|
syncTimeouts()
|
||||||
val s =
|
val s = requireWaitingSession(studentId, code)
|
||||||
sessions.findByCode(code.trim())
|
|
||||||
?: throw BadRequestException("Код не найден")
|
|
||||||
if (s.studentId != studentId) throw ForbiddenException("Этот код выдан другому студенту")
|
|
||||||
if (s.status != DefenseStatus.waiting.name) throw BadRequestException("Защита по этому коду уже начата")
|
|
||||||
s.status = DefenseStatus.started.name
|
s.status = DefenseStatus.started.name
|
||||||
s.startedAt = Instant.now()
|
s.startedAt = Instant.now()
|
||||||
return toDto(sessions.save(s))
|
return toDto(sessions.save(s))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun requireWaitingSession(
|
||||||
|
studentId: UUID,
|
||||||
|
code: String,
|
||||||
|
): DefenseSession {
|
||||||
|
val s = sessions.findByCode(code.trim()) ?: throw BadRequestException("Код не найден")
|
||||||
|
if (s.studentId != studentId) throw ForbiddenException("Этот код выдан другому студенту")
|
||||||
|
if (s.status != DefenseStatus.waiting.name) throw BadRequestException("Защита по этому коду уже начата")
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
fun finish(
|
fun finish(
|
||||||
studentId: UUID,
|
studentId: UUID,
|
||||||
|
|||||||
@@ -32,6 +32,21 @@ class LearnController(
|
|||||||
|
|
||||||
private fun isAdmin(session: HttpSession): Boolean = (session.getAttribute(SessionKeys.USER) as? SessionUser)?.isAdmin == true
|
private fun isAdmin(session: HttpSession): Boolean = (session.getAttribute(SessionKeys.USER) as? SessionUser)?.isAdmin == true
|
||||||
|
|
||||||
|
private fun treeContainsArticle(
|
||||||
|
tree: ContentTreeNodeDto,
|
||||||
|
chapter: String,
|
||||||
|
section: String,
|
||||||
|
article: String,
|
||||||
|
): Boolean {
|
||||||
|
val sec =
|
||||||
|
tree.chapters
|
||||||
|
.firstOrNull { it.slug == chapter }
|
||||||
|
?.sections
|
||||||
|
?.firstOrNull { it.slug == section }
|
||||||
|
?: return false
|
||||||
|
return sec.articles.any { it.slug == article }
|
||||||
|
}
|
||||||
|
|
||||||
private fun sectionChoices(tree: ContentTreeNodeDto): List<Map<String, String>> =
|
private fun sectionChoices(tree: ContentTreeNodeDto): List<Map<String, String>> =
|
||||||
tree.chapters.flatMap { chapter ->
|
tree.chapters.flatMap { chapter ->
|
||||||
chapter.sections.map { section ->
|
chapter.sections.map { section ->
|
||||||
@@ -103,9 +118,7 @@ class LearnController(
|
|||||||
): String {
|
): String {
|
||||||
val admin = isAdmin(session)
|
val admin = isAdmin(session)
|
||||||
val tree: ContentTreeNodeDto = api.contentTree(token(session), all = admin)
|
val tree: ContentTreeNodeDto = api.contentTree(token(session), all = admin)
|
||||||
val ch = tree.chapters.firstOrNull { it.slug == chapter } ?: return "redirect:/learn"
|
if (!treeContainsArticle(tree, chapter, section, article)) return "redirect:/learn"
|
||||||
val sec = ch.sections.firstOrNull { it.slug == section } ?: return "redirect:/learn"
|
|
||||||
if (sec.articles.none { it.slug == article }) return "redirect:/learn"
|
|
||||||
|
|
||||||
val a: ContentArticleDto =
|
val a: ContentArticleDto =
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user