Refactor: split ContentService and ProgressService; defaults tightened
This commit is contained in:
@@ -0,0 +1,244 @@
|
||||
package ru.ysu.lms.academic.content
|
||||
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import ru.ysu.lms.academic.domain.ContentArticle
|
||||
import ru.ysu.lms.academic.domain.ContentChapter
|
||||
import ru.ysu.lms.academic.domain.ContentSection
|
||||
import ru.ysu.lms.academic.repo.ContentArticleRepository
|
||||
import ru.ysu.lms.academic.repo.ContentChapterRepository
|
||||
import ru.ysu.lms.academic.repo.ContentSectionRepository
|
||||
import ru.ysu.lms.common.BadRequestException
|
||||
import ru.ysu.lms.common.NotFoundException
|
||||
import java.util.UUID
|
||||
|
||||
@Service
|
||||
class ContentAuthoringService(
|
||||
private val chapters: ContentChapterRepository,
|
||||
private val sections: ContentSectionRepository,
|
||||
private val articles: ContentArticleRepository,
|
||||
) {
|
||||
@Transactional
|
||||
fun updateArticle(
|
||||
articleId: UUID,
|
||||
title: String,
|
||||
bodyMd: String,
|
||||
) {
|
||||
val a = articles.findById(articleId).orElseThrow { NotFoundException("Статья не найдена") }
|
||||
if (title.isBlank()) throw BadRequestException("Заголовок не может быть пустым")
|
||||
a.title = title.trim()
|
||||
a.bodyMd = bodyMd
|
||||
articles.save(a)
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun deleteArticle(articleId: UUID) {
|
||||
if (!articles.existsById(articleId)) throw NotFoundException("Статья не найдена")
|
||||
articles.deleteById(articleId)
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun createChapter(
|
||||
title: String,
|
||||
slug: String?,
|
||||
): ChapterTreeNode {
|
||||
if (title.isBlank()) throw BadRequestException("Название главы не может быть пустым")
|
||||
val cleanSlug =
|
||||
slug?.takeIf { it.isNotBlank() }?.let { Slugs.slugify(it) }
|
||||
?: Slugs.uniqueSlug(title, chapters.findAllByOrderByPositionAscIdAsc().map { it.slug }.toSet())
|
||||
val maxPos = chapters.findAllByOrderByPositionAscIdAsc().maxOfOrNull { it.position } ?: 0
|
||||
val ch = chapters.save(ContentChapter(title = title.trim(), slug = cleanSlug, position = maxPos + 1))
|
||||
return ChapterTreeNode(
|
||||
id = ch.id.toString(),
|
||||
title = ch.title,
|
||||
slug = ch.slug,
|
||||
position = ch.position,
|
||||
published = ch.published,
|
||||
sections = emptyList(),
|
||||
)
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun renameChapter(
|
||||
chapterId: UUID,
|
||||
title: String,
|
||||
) {
|
||||
val ch = chapters.findById(chapterId).orElseThrow { NotFoundException("Глава не найдена") }
|
||||
if (title.isBlank()) throw BadRequestException("Название не может быть пустым")
|
||||
ch.title = title.trim()
|
||||
chapters.save(ch)
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun deleteChapter(chapterId: UUID) {
|
||||
if (!chapters.existsById(chapterId)) throw NotFoundException("Глава не найдена")
|
||||
chapters.deleteById(chapterId)
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun createSection(
|
||||
chapterId: UUID,
|
||||
title: String,
|
||||
slug: String?,
|
||||
): SectionTreeNode {
|
||||
val ch = chapters.findById(chapterId).orElseThrow { NotFoundException("Глава не найдена") }
|
||||
if (title.isBlank()) throw BadRequestException("Название раздела не может быть пустым")
|
||||
val siblings = sections.findByChapterIdOrderByPositionAscIdAsc(ch.id)
|
||||
val cleanSlug =
|
||||
slug?.takeIf { it.isNotBlank() }?.let { Slugs.slugify(it) }
|
||||
?: Slugs.uniqueSlug(title, siblings.map { it.slug }.toSet())
|
||||
val maxPos = siblings.maxOfOrNull { it.position } ?: 0
|
||||
val sec =
|
||||
sections.save(
|
||||
ContentSection(chapterId = ch.id, title = title.trim(), slug = cleanSlug, position = maxPos + 1),
|
||||
)
|
||||
return SectionTreeNode(
|
||||
id = sec.id.toString(),
|
||||
title = sec.title,
|
||||
slug = sec.slug,
|
||||
position = sec.position,
|
||||
published = sec.published,
|
||||
articles = emptyList(),
|
||||
)
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun renameSection(
|
||||
sectionId: UUID,
|
||||
title: String,
|
||||
) {
|
||||
val sec = sections.findById(sectionId).orElseThrow { NotFoundException("Раздел не найден") }
|
||||
if (title.isBlank()) throw BadRequestException("Название не может быть пустым")
|
||||
sec.title = title.trim()
|
||||
sections.save(sec)
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun deleteSection(sectionId: UUID) {
|
||||
if (!sections.existsById(sectionId)) throw NotFoundException("Раздел не найден")
|
||||
sections.deleteById(sectionId)
|
||||
}
|
||||
|
||||
/** Переносит все статьи раздела в другой и удаляет раздел. */
|
||||
@Transactional
|
||||
fun moveArticlesAndDeleteSection(
|
||||
sectionId: UUID,
|
||||
targetSectionId: UUID,
|
||||
) {
|
||||
val src = sections.findById(sectionId).orElseThrow { NotFoundException("Раздел не найден") }
|
||||
val dst = sections.findById(targetSectionId).orElseThrow { NotFoundException("Раздел назначения не найден") }
|
||||
if (src.id == dst.id) throw BadRequestException("Раздел назначения совпадает с исходным")
|
||||
val moving = articles.findBySectionIdOrderByPositionAscIdAsc(src.id)
|
||||
var pos = articles.findBySectionIdOrderByPositionAscIdAsc(dst.id).maxOfOrNull { it.position } ?: 0
|
||||
for (a in moving) {
|
||||
pos += 1
|
||||
a.sectionId = dst.id
|
||||
a.position = pos
|
||||
articles.save(a)
|
||||
}
|
||||
sections.delete(src)
|
||||
}
|
||||
|
||||
/** Создаёт статью; если раздела нет — создаёт главу/раздел по названиям. */
|
||||
@Transactional
|
||||
fun createArticlePlaced(
|
||||
title: String,
|
||||
bodyMd: String,
|
||||
chapterId: UUID?,
|
||||
newChapterTitle: String?,
|
||||
sectionId: UUID?,
|
||||
newSectionTitle: String?,
|
||||
): ContentArticleView {
|
||||
val targetSection: ContentSection =
|
||||
when {
|
||||
sectionId != null && !newSectionTitle.isNullOrBlank() -> {
|
||||
val anchor =
|
||||
sections
|
||||
.findById(sectionId)
|
||||
.orElseThrow { NotFoundException("Раздел не найден") }
|
||||
val node = createSection(anchor.chapterId, newSectionTitle, null)
|
||||
sections
|
||||
.findById(UUID.fromString(node.id))
|
||||
.orElseThrow { NotFoundException("Раздел не найден") }
|
||||
}
|
||||
sectionId != null ->
|
||||
sections
|
||||
.findById(sectionId)
|
||||
.orElseThrow { NotFoundException("Раздел не найден") }
|
||||
!newSectionTitle.isNullOrBlank() -> {
|
||||
val chapter: ContentChapter =
|
||||
when {
|
||||
chapterId != null ->
|
||||
chapters
|
||||
.findById(chapterId)
|
||||
.orElseThrow { NotFoundException("Глава не найдена") }
|
||||
!newChapterTitle.isNullOrBlank() -> {
|
||||
val node = createChapter(newChapterTitle, null)
|
||||
chapters
|
||||
.findById(UUID.fromString(node.id))
|
||||
.orElseThrow { NotFoundException("Глава не найдена") }
|
||||
}
|
||||
else -> throw BadRequestException("Укажите главу")
|
||||
}
|
||||
val node = createSection(chapter.id, newSectionTitle, null)
|
||||
sections
|
||||
.findById(UUID.fromString(node.id))
|
||||
.orElseThrow { NotFoundException("Раздел не найден") }
|
||||
}
|
||||
else -> throw BadRequestException("Укажите раздел")
|
||||
}
|
||||
return createArticle(targetSection.id, title, bodyMd)
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun createArticle(
|
||||
sectionId: UUID,
|
||||
title: String,
|
||||
bodyMd: String,
|
||||
): ContentArticleView {
|
||||
val sec = sections.findById(sectionId).orElseThrow { NotFoundException("Раздел не найден") }
|
||||
val ch = chapters.findById(sec.chapterId).orElseThrow { NotFoundException("Глава не найдена") }
|
||||
if (title.isBlank()) throw BadRequestException("Заголовок не может быть пустым")
|
||||
val siblings = articles.findBySectionIdOrderByPositionAscIdAsc(sec.id)
|
||||
val slug = Slugs.uniqueSlug(title, siblings.map { it.slug }.toSet())
|
||||
val maxPos = siblings.maxOfOrNull { it.position } ?: 0
|
||||
val a =
|
||||
articles.save(
|
||||
ContentArticle(sectionId = sec.id, title = title.trim(), slug = slug, bodyMd = bodyMd, position = maxPos + 1),
|
||||
)
|
||||
return ContentArticleView(
|
||||
id = a.id.toString(),
|
||||
chapterSlug = ch.slug,
|
||||
chapterTitle = ch.title,
|
||||
sectionSlug = sec.slug,
|
||||
sectionTitle = sec.title,
|
||||
slug = a.slug,
|
||||
title = a.title,
|
||||
bodyMd = a.bodyMd,
|
||||
)
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun moveArticle(
|
||||
articleId: UUID,
|
||||
newSectionId: UUID,
|
||||
): ContentArticleView {
|
||||
val a = articles.findById(articleId).orElseThrow { NotFoundException("Статья не найдена") }
|
||||
val sec = sections.findById(newSectionId).orElseThrow { NotFoundException("Раздел не найден") }
|
||||
val ch = chapters.findById(sec.chapterId).orElseThrow { NotFoundException("Глава не найдена") }
|
||||
val siblings = articles.findBySectionIdOrderByPositionAscIdAsc(sec.id)
|
||||
a.sectionId = sec.id
|
||||
a.position = (siblings.maxOfOrNull { it.position } ?: 0) + 1
|
||||
articles.save(a)
|
||||
return ContentArticleView(
|
||||
id = a.id.toString(),
|
||||
chapterSlug = ch.slug,
|
||||
chapterTitle = ch.title,
|
||||
sectionSlug = sec.slug,
|
||||
sectionTitle = sec.title,
|
||||
slug = a.slug,
|
||||
title = a.title,
|
||||
bodyMd = a.bodyMd,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package ru.ysu.lms.academic.content
|
||||
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import ru.ysu.lms.academic.repo.ContentArticleRepository
|
||||
import ru.ysu.lms.academic.repo.ContentChapterRepository
|
||||
import ru.ysu.lms.academic.repo.ContentSectionRepository
|
||||
import java.util.UUID
|
||||
|
||||
@Service
|
||||
class ContentOrderService(
|
||||
private val chapters: ContentChapterRepository,
|
||||
private val sections: ContentSectionRepository,
|
||||
private val articles: ContentArticleRepository,
|
||||
) {
|
||||
/** Применяет новое расположение глав, разделов и статей (drag & drop в админке). */
|
||||
@Transactional
|
||||
fun reorder(tree: ReorderTree) {
|
||||
tree.chapters.forEachIndexed { chapterIndex, chapterDto ->
|
||||
val chapter =
|
||||
chapters.findById(uuidOrNull(chapterDto.id) ?: return@forEachIndexed).orElse(null)
|
||||
?: return@forEachIndexed
|
||||
chapter.position = chapterIndex + 1
|
||||
chapters.save(chapter)
|
||||
|
||||
chapterDto.sections.forEachIndexed { sectionIndex, sectionDto ->
|
||||
val section =
|
||||
sections.findById(uuidOrNull(sectionDto.id) ?: return@forEachIndexed).orElse(null)
|
||||
?: return@forEachIndexed
|
||||
section.chapterId = chapter.id
|
||||
section.position = sectionIndex + 1
|
||||
sections.save(section)
|
||||
|
||||
sectionDto.articles.forEachIndexed { articleIndex, articleDto ->
|
||||
val article =
|
||||
articles.findById(uuidOrNull(articleDto.id) ?: return@forEachIndexed).orElse(null)
|
||||
?: return@forEachIndexed
|
||||
article.sectionId = section.id
|
||||
article.position = articleIndex + 1
|
||||
articles.save(article)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Порядок разделов внутри главы и статей внутри них (drag & drop на странице главы). */
|
||||
@Transactional
|
||||
fun reorderSections(
|
||||
chapterId: UUID,
|
||||
tree: List<ReorderSection>,
|
||||
) {
|
||||
tree.forEachIndexed { sectionIndex, sectionDto ->
|
||||
val section =
|
||||
sections.findById(uuidOrNull(sectionDto.id) ?: return@forEachIndexed).orElse(null)
|
||||
?: return@forEachIndexed
|
||||
if (section.chapterId != chapterId) return@forEachIndexed
|
||||
section.position = sectionIndex + 1
|
||||
sections.save(section)
|
||||
|
||||
sectionDto.articles.forEachIndexed { articleIndex, articleDto ->
|
||||
val article =
|
||||
articles.findById(uuidOrNull(articleDto.id) ?: return@forEachIndexed).orElse(null)
|
||||
?: return@forEachIndexed
|
||||
article.sectionId = section.id
|
||||
article.position = articleIndex + 1
|
||||
articles.save(article)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Порядок статей внутри одного раздела (drag & drop на странице раздела). */
|
||||
@Transactional
|
||||
fun reorderArticles(
|
||||
sectionId: UUID,
|
||||
tree: List<ReorderArticle>,
|
||||
) {
|
||||
tree.forEachIndexed { articleIndex, articleDto ->
|
||||
val article =
|
||||
articles.findById(uuidOrNull(articleDto.id) ?: return@forEachIndexed).orElse(null)
|
||||
?: return@forEachIndexed
|
||||
if (article.sectionId != sectionId) return@forEachIndexed
|
||||
article.position = articleIndex + 1
|
||||
articles.save(article)
|
||||
}
|
||||
}
|
||||
|
||||
private fun uuidOrNull(value: String): UUID? = runCatching { UUID.fromString(value) }.getOrNull()
|
||||
}
|
||||
@@ -2,13 +2,9 @@ package ru.ysu.lms.academic.content
|
||||
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import ru.ysu.lms.academic.domain.ContentArticle
|
||||
import ru.ysu.lms.academic.domain.ContentChapter
|
||||
import ru.ysu.lms.academic.domain.ContentSection
|
||||
import ru.ysu.lms.academic.repo.ContentArticleRepository
|
||||
import ru.ysu.lms.academic.repo.ContentChapterRepository
|
||||
import ru.ysu.lms.academic.repo.ContentSectionRepository
|
||||
import ru.ysu.lms.common.BadRequestException
|
||||
import ru.ysu.lms.common.NotFoundException
|
||||
import java.util.UUID
|
||||
|
||||
@@ -173,77 +169,6 @@ class ContentService(
|
||||
)
|
||||
}
|
||||
|
||||
/** Применяет новое расположение глав, разделов и статей (drag & drop в админке). */
|
||||
@Transactional
|
||||
fun reorder(tree: ReorderTree) {
|
||||
tree.chapters.forEachIndexed { chapterIndex, chapterDto ->
|
||||
val chapter =
|
||||
chapters.findById(uuidOrNull(chapterDto.id) ?: return@forEachIndexed).orElse(null)
|
||||
?: return@forEachIndexed
|
||||
chapter.position = chapterIndex + 1
|
||||
chapters.save(chapter)
|
||||
|
||||
chapterDto.sections.forEachIndexed { sectionIndex, sectionDto ->
|
||||
val section =
|
||||
sections.findById(uuidOrNull(sectionDto.id) ?: return@forEachIndexed).orElse(null)
|
||||
?: return@forEachIndexed
|
||||
section.chapterId = chapter.id
|
||||
section.position = sectionIndex + 1
|
||||
sections.save(section)
|
||||
|
||||
sectionDto.articles.forEachIndexed { articleIndex, articleDto ->
|
||||
val article =
|
||||
articles.findById(uuidOrNull(articleDto.id) ?: return@forEachIndexed).orElse(null)
|
||||
?: return@forEachIndexed
|
||||
article.sectionId = section.id
|
||||
article.position = articleIndex + 1
|
||||
articles.save(article)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Порядок разделов внутри главы и статей внутри них (drag & drop на странице главы). */
|
||||
@Transactional
|
||||
fun reorderSections(
|
||||
chapterId: UUID,
|
||||
tree: List<ReorderSection>,
|
||||
) {
|
||||
tree.forEachIndexed { sectionIndex, sectionDto ->
|
||||
val section =
|
||||
sections.findById(uuidOrNull(sectionDto.id) ?: return@forEachIndexed).orElse(null)
|
||||
?: return@forEachIndexed
|
||||
if (section.chapterId != chapterId) return@forEachIndexed
|
||||
section.position = sectionIndex + 1
|
||||
sections.save(section)
|
||||
|
||||
sectionDto.articles.forEachIndexed { articleIndex, articleDto ->
|
||||
val article =
|
||||
articles.findById(uuidOrNull(articleDto.id) ?: return@forEachIndexed).orElse(null)
|
||||
?: return@forEachIndexed
|
||||
article.sectionId = section.id
|
||||
article.position = articleIndex + 1
|
||||
articles.save(article)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Порядок статей внутри одного раздела (drag & drop на странице раздела). */
|
||||
@Transactional
|
||||
fun reorderArticles(
|
||||
sectionId: UUID,
|
||||
tree: List<ReorderArticle>,
|
||||
) {
|
||||
tree.forEachIndexed { articleIndex, articleDto ->
|
||||
val article =
|
||||
articles.findById(uuidOrNull(articleDto.id) ?: return@forEachIndexed).orElse(null)
|
||||
?: return@forEachIndexed
|
||||
if (article.sectionId != sectionId) return@forEachIndexed
|
||||
article.position = articleIndex + 1
|
||||
articles.save(article)
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T> requireFound(
|
||||
value: T?,
|
||||
message: String,
|
||||
@@ -257,8 +182,6 @@ class ContentService(
|
||||
if (!includeHidden && !published) throw NotFoundException(message)
|
||||
}
|
||||
|
||||
private fun uuidOrNull(value: String): UUID? = runCatching { UUID.fromString(value) }.getOrNull()
|
||||
|
||||
@Transactional
|
||||
fun setChapterPublished(
|
||||
chapterId: UUID,
|
||||
@@ -309,230 +232,6 @@ class ContentService(
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun updateArticle(
|
||||
articleId: UUID,
|
||||
title: String,
|
||||
bodyMd: String,
|
||||
) {
|
||||
val a = articles.findById(articleId).orElseThrow { NotFoundException("Статья не найдена") }
|
||||
if (title.isBlank()) throw BadRequestException("Заголовок не может быть пустым")
|
||||
a.title = title.trim()
|
||||
a.bodyMd = bodyMd
|
||||
articles.save(a)
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun deleteArticle(articleId: UUID) {
|
||||
if (!articles.existsById(articleId)) throw NotFoundException("Статья не найдена")
|
||||
articles.deleteById(articleId)
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun createChapter(
|
||||
title: String,
|
||||
slug: String?,
|
||||
): ChapterTreeNode {
|
||||
if (title.isBlank()) throw BadRequestException("Название главы не может быть пустым")
|
||||
val cleanSlug =
|
||||
slug?.takeIf { it.isNotBlank() }?.let { Slugs.slugify(it) }
|
||||
?: Slugs.uniqueSlug(title, chapters.findAllByOrderByPositionAscIdAsc().map { it.slug }.toSet())
|
||||
val maxPos = chapters.findAllByOrderByPositionAscIdAsc().maxOfOrNull { it.position } ?: 0
|
||||
val ch = chapters.save(ContentChapter(title = title.trim(), slug = cleanSlug, position = maxPos + 1))
|
||||
return ChapterTreeNode(
|
||||
id = ch.id.toString(),
|
||||
title = ch.title,
|
||||
slug = ch.slug,
|
||||
position = ch.position,
|
||||
published = ch.published,
|
||||
sections = emptyList(),
|
||||
)
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun renameChapter(
|
||||
chapterId: UUID,
|
||||
title: String,
|
||||
) {
|
||||
val ch = chapters.findById(chapterId).orElseThrow { NotFoundException("Глава не найдена") }
|
||||
if (title.isBlank()) throw BadRequestException("Название не может быть пустым")
|
||||
ch.title = title.trim()
|
||||
chapters.save(ch)
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun deleteChapter(chapterId: UUID) {
|
||||
if (!chapters.existsById(chapterId)) throw NotFoundException("Глава не найдена")
|
||||
chapters.deleteById(chapterId)
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun createSection(
|
||||
chapterId: UUID,
|
||||
title: String,
|
||||
slug: String?,
|
||||
): SectionTreeNode {
|
||||
val ch = chapters.findById(chapterId).orElseThrow { NotFoundException("Глава не найдена") }
|
||||
if (title.isBlank()) throw BadRequestException("Название раздела не может быть пустым")
|
||||
val siblings = sections.findByChapterIdOrderByPositionAscIdAsc(ch.id)
|
||||
val cleanSlug =
|
||||
slug?.takeIf { it.isNotBlank() }?.let { Slugs.slugify(it) }
|
||||
?: Slugs.uniqueSlug(title, siblings.map { it.slug }.toSet())
|
||||
val maxPos = siblings.maxOfOrNull { it.position } ?: 0
|
||||
val sec =
|
||||
sections.save(
|
||||
ContentSection(chapterId = ch.id, title = title.trim(), slug = cleanSlug, position = maxPos + 1),
|
||||
)
|
||||
return SectionTreeNode(
|
||||
id = sec.id.toString(),
|
||||
title = sec.title,
|
||||
slug = sec.slug,
|
||||
position = sec.position,
|
||||
published = sec.published,
|
||||
articles = emptyList(),
|
||||
)
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun renameSection(
|
||||
sectionId: UUID,
|
||||
title: String,
|
||||
) {
|
||||
val sec = sections.findById(sectionId).orElseThrow { NotFoundException("Раздел не найден") }
|
||||
if (title.isBlank()) throw BadRequestException("Название не может быть пустым")
|
||||
sec.title = title.trim()
|
||||
sections.save(sec)
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun deleteSection(sectionId: UUID) {
|
||||
if (!sections.existsById(sectionId)) throw NotFoundException("Раздел не найден")
|
||||
sections.deleteById(sectionId)
|
||||
}
|
||||
|
||||
/** Переносит все статьи раздела в другой и удаляет раздел. */
|
||||
@Transactional
|
||||
fun moveArticlesAndDeleteSection(
|
||||
sectionId: UUID,
|
||||
targetSectionId: UUID,
|
||||
) {
|
||||
val src = sections.findById(sectionId).orElseThrow { NotFoundException("Раздел не найден") }
|
||||
val dst = sections.findById(targetSectionId).orElseThrow { NotFoundException("Раздел назначения не найден") }
|
||||
if (src.id == dst.id) throw BadRequestException("Раздел назначения совпадает с исходным")
|
||||
val moving = articles.findBySectionIdOrderByPositionAscIdAsc(src.id)
|
||||
var pos = articles.findBySectionIdOrderByPositionAscIdAsc(dst.id).maxOfOrNull { it.position } ?: 0
|
||||
for (a in moving) {
|
||||
pos += 1
|
||||
a.sectionId = dst.id
|
||||
a.position = pos
|
||||
articles.save(a)
|
||||
}
|
||||
sections.delete(src)
|
||||
}
|
||||
|
||||
/** Создаёт статью; если раздела нет — создаёт главу/раздел по названиям. */
|
||||
@Transactional
|
||||
fun createArticlePlaced(
|
||||
title: String,
|
||||
bodyMd: String,
|
||||
chapterId: UUID?,
|
||||
newChapterTitle: String?,
|
||||
sectionId: UUID?,
|
||||
newSectionTitle: String?,
|
||||
): ContentArticleView {
|
||||
val targetSection: ContentSection =
|
||||
when {
|
||||
sectionId != null && !newSectionTitle.isNullOrBlank() -> {
|
||||
val anchor =
|
||||
sections
|
||||
.findById(sectionId)
|
||||
.orElseThrow { NotFoundException("Раздел не найден") }
|
||||
val node = createSection(anchor.chapterId, newSectionTitle, null)
|
||||
sections
|
||||
.findById(UUID.fromString(node.id))
|
||||
.orElseThrow { NotFoundException("Раздел не найден") }
|
||||
}
|
||||
sectionId != null ->
|
||||
sections
|
||||
.findById(sectionId)
|
||||
.orElseThrow { NotFoundException("Раздел не найден") }
|
||||
!newSectionTitle.isNullOrBlank() -> {
|
||||
val chapter: ContentChapter =
|
||||
when {
|
||||
chapterId != null ->
|
||||
chapters
|
||||
.findById(chapterId)
|
||||
.orElseThrow { NotFoundException("Глава не найдена") }
|
||||
!newChapterTitle.isNullOrBlank() -> {
|
||||
val node = createChapter(newChapterTitle, null)
|
||||
chapters
|
||||
.findById(UUID.fromString(node.id))
|
||||
.orElseThrow { NotFoundException("Глава не найдена") }
|
||||
}
|
||||
else -> throw BadRequestException("Укажите главу")
|
||||
}
|
||||
val node = createSection(chapter.id, newSectionTitle, null)
|
||||
sections
|
||||
.findById(UUID.fromString(node.id))
|
||||
.orElseThrow { NotFoundException("Раздел не найден") }
|
||||
}
|
||||
else -> throw BadRequestException("Укажите раздел")
|
||||
}
|
||||
return createArticle(targetSection.id, title, bodyMd)
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun createArticle(
|
||||
sectionId: UUID,
|
||||
title: String,
|
||||
bodyMd: String,
|
||||
): ContentArticleView {
|
||||
val sec = sections.findById(sectionId).orElseThrow { NotFoundException("Раздел не найден") }
|
||||
val ch = chapters.findById(sec.chapterId).orElseThrow { NotFoundException("Глава не найдена") }
|
||||
if (title.isBlank()) throw BadRequestException("Заголовок не может быть пустым")
|
||||
val siblings = articles.findBySectionIdOrderByPositionAscIdAsc(sec.id)
|
||||
val slug = Slugs.uniqueSlug(title, siblings.map { it.slug }.toSet())
|
||||
val maxPos = siblings.maxOfOrNull { it.position } ?: 0
|
||||
val a =
|
||||
articles.save(
|
||||
ContentArticle(sectionId = sec.id, title = title.trim(), slug = slug, bodyMd = bodyMd, position = maxPos + 1),
|
||||
)
|
||||
return ContentArticleView(
|
||||
id = a.id.toString(),
|
||||
chapterSlug = ch.slug,
|
||||
chapterTitle = ch.title,
|
||||
sectionSlug = sec.slug,
|
||||
sectionTitle = sec.title,
|
||||
slug = a.slug,
|
||||
title = a.title,
|
||||
bodyMd = a.bodyMd,
|
||||
)
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun moveArticle(
|
||||
articleId: UUID,
|
||||
newSectionId: UUID,
|
||||
): ContentArticleView {
|
||||
val a = articles.findById(articleId).orElseThrow { NotFoundException("Статья не найдена") }
|
||||
val sec = sections.findById(newSectionId).orElseThrow { NotFoundException("Раздел не найден") }
|
||||
val ch = chapters.findById(sec.chapterId).orElseThrow { NotFoundException("Глава не найдена") }
|
||||
val siblings = articles.findBySectionIdOrderByPositionAscIdAsc(sec.id)
|
||||
a.sectionId = sec.id
|
||||
a.position = (siblings.maxOfOrNull { it.position } ?: 0) + 1
|
||||
articles.save(a)
|
||||
return ContentArticleView(
|
||||
id = a.id.toString(),
|
||||
chapterSlug = ch.slug,
|
||||
chapterTitle = ch.title,
|
||||
sectionSlug = sec.slug,
|
||||
sectionTitle = sec.title,
|
||||
slug = a.slug,
|
||||
title = a.title,
|
||||
bodyMd = a.bodyMd,
|
||||
)
|
||||
}
|
||||
|
||||
@Transactional
|
||||
fun importZip(
|
||||
filename: String,
|
||||
|
||||
@@ -12,11 +12,9 @@ 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.ContentAuthoringService
|
||||
import ru.ysu.lms.academic.content.ContentImportResult
|
||||
import ru.ysu.lms.academic.content.ContentService
|
||||
import ru.ysu.lms.academic.content.ReorderArticles
|
||||
import ru.ysu.lms.academic.content.ReorderSections
|
||||
import ru.ysu.lms.academic.content.ReorderTree
|
||||
import ru.ysu.lms.common.BadRequestException
|
||||
import java.util.UUID
|
||||
|
||||
@@ -25,6 +23,7 @@ import java.util.UUID
|
||||
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
|
||||
class ContentAdminController(
|
||||
private val contentService: ContentService,
|
||||
private val authoring: ContentAuthoringService,
|
||||
) {
|
||||
data class ArticleUpdatePayload(
|
||||
val title: String,
|
||||
@@ -111,32 +110,6 @@ class ContentAdminController(
|
||||
contentService.setArticlePublished(id, body.published)
|
||||
}
|
||||
|
||||
/** Новое расположение материалов (drag & drop глав, разделов и статей). */
|
||||
@PostMapping("/reorder")
|
||||
fun reorder(
|
||||
@RequestBody body: ReorderTree,
|
||||
) {
|
||||
contentService.reorder(body)
|
||||
}
|
||||
|
||||
/** Порядок разделов внутри главы (drag & drop на странице главы). */
|
||||
@PostMapping("/chapters/{id}/sections/reorder")
|
||||
fun reorderSections(
|
||||
@PathVariable id: UUID,
|
||||
@RequestBody body: ReorderSections,
|
||||
) {
|
||||
contentService.reorderSections(id, body.sections)
|
||||
}
|
||||
|
||||
/** Порядок статей внутри раздела (drag & drop на странице раздела). */
|
||||
@PostMapping("/sections/{id}/articles/reorder")
|
||||
fun reorderArticles(
|
||||
@PathVariable id: UUID,
|
||||
@RequestBody body: ReorderArticles,
|
||||
) {
|
||||
contentService.reorderArticles(id, body.articles)
|
||||
}
|
||||
|
||||
@PostMapping("/import")
|
||||
fun import(
|
||||
@RequestParam("file") file: MultipartFile,
|
||||
@@ -152,19 +125,19 @@ class ContentAdminController(
|
||||
@PostMapping("/chapters")
|
||||
fun createChapter(
|
||||
@RequestBody body: ChapterCreatePayload,
|
||||
) = contentService.createChapter(body.title, body.slug)
|
||||
) = authoring.createChapter(body.title, body.slug)
|
||||
|
||||
@PutMapping("/chapters/{id}")
|
||||
fun renameChapter(
|
||||
@PathVariable id: UUID,
|
||||
@RequestBody body: TitlePayload,
|
||||
) = contentService.renameChapter(id, body.title)
|
||||
) = authoring.renameChapter(id, body.title)
|
||||
|
||||
@DeleteMapping("/chapters/{id}")
|
||||
fun deleteChapter(
|
||||
@PathVariable id: UUID,
|
||||
): ResponseEntity<Void> {
|
||||
contentService.deleteChapter(id)
|
||||
authoring.deleteChapter(id)
|
||||
return ResponseEntity.noContent().build()
|
||||
}
|
||||
|
||||
@@ -172,13 +145,13 @@ class ContentAdminController(
|
||||
fun createSection(
|
||||
@RequestParam chapterId: UUID,
|
||||
@RequestBody body: SectionCreatePayload,
|
||||
) = contentService.createSection(chapterId, body.title, body.slug)
|
||||
) = authoring.createSection(chapterId, body.title, body.slug)
|
||||
|
||||
@PutMapping("/sections/{id}")
|
||||
fun renameSection(
|
||||
@PathVariable id: UUID,
|
||||
@RequestBody body: TitlePayload,
|
||||
) = contentService.renameSection(id, body.title)
|
||||
) = authoring.renameSection(id, body.title)
|
||||
|
||||
@DeleteMapping("/sections/{id}")
|
||||
fun deleteSection(
|
||||
@@ -186,9 +159,9 @@ class ContentAdminController(
|
||||
@RequestParam(required = false) moveTo: UUID?,
|
||||
): ResponseEntity<Void> {
|
||||
if (moveTo != null) {
|
||||
contentService.moveArticlesAndDeleteSection(id, moveTo)
|
||||
authoring.moveArticlesAndDeleteSection(id, moveTo)
|
||||
} else {
|
||||
contentService.deleteSection(id)
|
||||
authoring.deleteSection(id)
|
||||
}
|
||||
return ResponseEntity.noContent().build()
|
||||
}
|
||||
@@ -196,7 +169,7 @@ class ContentAdminController(
|
||||
@PostMapping("/articles")
|
||||
fun createArticle(
|
||||
@RequestBody body: ArticleCreatePayload,
|
||||
) = contentService.createArticlePlaced(
|
||||
) = authoring.createArticlePlaced(
|
||||
title = body.title,
|
||||
bodyMd = body.bodyMd,
|
||||
chapterId = body.chapterId?.let { UUID.fromString(it) },
|
||||
@@ -210,20 +183,20 @@ class ContentAdminController(
|
||||
@PathVariable id: UUID,
|
||||
@RequestBody body: ArticleUpdatePayload,
|
||||
) {
|
||||
contentService.updateArticle(id, body.title, body.bodyMd)
|
||||
authoring.updateArticle(id, body.title, body.bodyMd)
|
||||
}
|
||||
|
||||
@PostMapping("/articles/{id}/move")
|
||||
fun move(
|
||||
@PathVariable id: UUID,
|
||||
@RequestBody body: ArticleMovePayload,
|
||||
) = contentService.moveArticle(id, UUID.fromString(body.sectionId))
|
||||
) = authoring.moveArticle(id, UUID.fromString(body.sectionId))
|
||||
|
||||
@DeleteMapping("/articles/{id}")
|
||||
fun delete(
|
||||
@PathVariable id: UUID,
|
||||
): ResponseEntity<Void> {
|
||||
contentService.deleteArticle(id)
|
||||
authoring.deleteArticle(id)
|
||||
return ResponseEntity.noContent().build()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package ru.ysu.lms.academic.controller
|
||||
|
||||
import org.springframework.web.bind.annotation.PathVariable
|
||||
import org.springframework.web.bind.annotation.PostMapping
|
||||
import org.springframework.web.bind.annotation.RequestBody
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import ru.ysu.lms.academic.content.ContentOrderService
|
||||
import ru.ysu.lms.academic.content.ReorderArticles
|
||||
import ru.ysu.lms.academic.content.ReorderSections
|
||||
import ru.ysu.lms.academic.content.ReorderTree
|
||||
import java.util.UUID
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1")
|
||||
class ContentOrderController(
|
||||
private val order: ContentOrderService,
|
||||
) {
|
||||
/** Новое расположение материалов (drag & drop глав, разделов и статей). */
|
||||
@PostMapping("/reorder")
|
||||
fun reorder(
|
||||
@RequestBody body: ReorderTree,
|
||||
) {
|
||||
order.reorder(body)
|
||||
}
|
||||
|
||||
/** Порядок разделов внутри главы (drag & drop на странице главы). */
|
||||
@PostMapping("/chapters/{id}/sections/reorder")
|
||||
fun reorderSections(
|
||||
@PathVariable id: UUID,
|
||||
@RequestBody body: ReorderSections,
|
||||
) {
|
||||
order.reorderSections(id, body.sections)
|
||||
}
|
||||
|
||||
/** Порядок статей внутри раздела (drag & drop на странице раздела). */
|
||||
@PostMapping("/sections/{id}/articles/reorder")
|
||||
fun reorderArticles(
|
||||
@PathVariable id: UUID,
|
||||
@RequestBody body: ReorderArticles,
|
||||
) {
|
||||
order.reorderArticles(id, body.articles)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user