Content: match imported chapters/sections/articles by title; slug without number
This commit is contained in:
@@ -107,7 +107,13 @@ printf("Ответ: 42\n");
|
||||
|
||||
## Импорт: что происходит с материалами
|
||||
|
||||
* Главы, разделы и статьи из архива **обновляются** (совпадение по главе + разделу + slug статьи).
|
||||
* Главы, разделы и статьи из архива **обновляются**. Совпадение ищется **по названию**
|
||||
(глава — по названию главы, раздел — по названию раздела внутри главы, статья — по названию
|
||||
внутри раздела); если по названию не нашли, для совместимости проверяется slug. Поэтому один
|
||||
и тот же материал, созданный вручную и загруженный из архива, не дублируется.
|
||||
* Номера в именах каталогов архива (`02-Матрицы`, `01-Двумерные массивы`) задают только
|
||||
**порядок** (позицию). В адрес страницы они не попадают: slug формируется из названия, как и
|
||||
при ручном создании (`Матрицы` → `matricy`, `Функции` → `funkcii`).
|
||||
* Режим **синхронизации** (галка в форме импорта) дополнительно **удаляет** разделы и статьи
|
||||
главы, которых нет в архиве — архив считается источником истины («удалено устаревших: N»).
|
||||
* Без галки импорт работает в режиме **merge**: только добавляет и обновляет то, что есть
|
||||
|
||||
@@ -503,11 +503,17 @@ class ContentService(
|
||||
for (p in parsed) {
|
||||
if (currentChapter?.title != p.chapterDir) {
|
||||
val slug = chapterSlug(p) ?: "chapter-${chCount + 1}"
|
||||
currentChapter = chapters.findBySlug(slug)?.apply {
|
||||
// Ищем по названию, затем по slug: так импорт находит главу, созданную
|
||||
// вручную или импортированную ранее, и не плодит дубли.
|
||||
currentChapter = (chapters.findByTitleIgnoreCase(p.chapterDir) ?: chapters.findBySlug(slug))?.apply {
|
||||
title = p.chapterDir
|
||||
position = p.chapterPos
|
||||
} ?: chapters.save(
|
||||
ContentChapter(title = p.chapterDir, slug = slug, position = p.chapterPos),
|
||||
ContentChapter(
|
||||
title = p.chapterDir,
|
||||
slug = uniqueSlug(p.chapterDir, chapters.findAllByOrderByPositionAscIdAsc().map { it.slug }.toSet()),
|
||||
position = p.chapterPos,
|
||||
),
|
||||
)
|
||||
currentChapter = chapters.save(currentChapter!!)
|
||||
touchedChapters.add(currentChapter!!.id)
|
||||
@@ -517,11 +523,17 @@ class ContentService(
|
||||
val chapter = currentChapter ?: throw BadRequestException("Не удалось определить главу")
|
||||
if (currentSection?.title != p.sectionDir) {
|
||||
val slug = sectionSlug(p) ?: "section-${secCount + 1}"
|
||||
currentSection = sections.findByChapterIdAndSlug(chapter.id, slug)?.apply {
|
||||
currentSection = (sections.findByChapterIdAndTitleIgnoreCase(chapter.id, p.sectionDir)
|
||||
?: sections.findByChapterIdAndSlug(chapter.id, slug))?.apply {
|
||||
title = p.sectionDir
|
||||
position = p.sectionPos
|
||||
} ?: sections.save(
|
||||
ContentSection(chapterId = chapter.id, title = p.sectionDir, slug = slug, position = p.sectionPos),
|
||||
ContentSection(
|
||||
chapterId = chapter.id,
|
||||
title = p.sectionDir,
|
||||
slug = uniqueSlug(p.sectionDir, sections.findByChapterIdOrderByPositionAscIdAsc(chapter.id).map { it.slug }.toSet()),
|
||||
position = p.sectionPos,
|
||||
),
|
||||
)
|
||||
currentSection = sections.save(currentSection!!)
|
||||
seenSections.add(currentSection!!.id)
|
||||
@@ -536,7 +548,8 @@ class ContentService(
|
||||
val slug = p.fmSlug?.takeIf { it.isNotBlank() }
|
||||
?: articleSlug(p)
|
||||
?: "article-${artCount + 1}"
|
||||
val existing = articles.findBySectionIdAndSlug(section.id, slug)
|
||||
val existing = articles.findBySectionIdAndTitleIgnoreCase(section.id, p.fileTitle)
|
||||
?: articles.findBySectionIdAndSlug(section.id, slug)
|
||||
val article = (existing ?: ContentArticle(sectionId = section.id, slug = slug)).apply {
|
||||
title = p.fileTitle
|
||||
bodyMd = body
|
||||
@@ -575,14 +588,13 @@ class ContentService(
|
||||
)
|
||||
}
|
||||
|
||||
private fun chapterSlug(p: ParsedArticle): String? =
|
||||
slugify("%02d".format(p.chapterPos) + "-" + p.chapterDir)
|
||||
// Slug без числового префикса — такой же, как при ручном создании через интерфейс.
|
||||
// Номер каталога по-прежнему задаёт порядок (position), но не попадает в адрес.
|
||||
private fun chapterSlug(p: ParsedArticle): String? = slugify(p.chapterDir)
|
||||
|
||||
private fun sectionSlug(p: ParsedArticle): String? =
|
||||
slugify("%02d".format(p.sectionPos) + "-" + p.sectionDir)
|
||||
private fun sectionSlug(p: ParsedArticle): String? = slugify(p.sectionDir)
|
||||
|
||||
private fun articleSlug(p: ParsedArticle): String? =
|
||||
slugify("%02d".format(p.articlePos) + "-" + p.fileTitle)
|
||||
private fun articleSlug(p: ParsedArticle): String? = slugify(p.fileTitle)
|
||||
|
||||
private fun readZip(bytes: ByteArray): Map<String, ByteArray> {
|
||||
val result = mutableMapOf<String, ByteArray>()
|
||||
|
||||
@@ -9,16 +9,19 @@ import java.util.UUID
|
||||
interface ContentChapterRepository : JpaRepository<ContentChapter, UUID> {
|
||||
fun findAllByOrderByPositionAscIdAsc(): List<ContentChapter>
|
||||
fun findBySlug(slug: String): ContentChapter?
|
||||
fun findByTitleIgnoreCase(title: String): ContentChapter?
|
||||
}
|
||||
|
||||
interface ContentSectionRepository : JpaRepository<ContentSection, UUID> {
|
||||
fun findAllByOrderByPositionAscIdAsc(): List<ContentSection>
|
||||
fun findByChapterIdOrderByPositionAscIdAsc(chapterId: UUID): List<ContentSection>
|
||||
fun findByChapterIdAndSlug(chapterId: UUID, slug: String): ContentSection?
|
||||
fun findByChapterIdAndTitleIgnoreCase(chapterId: UUID, title: String): ContentSection?
|
||||
}
|
||||
|
||||
interface ContentArticleRepository : JpaRepository<ContentArticle, UUID> {
|
||||
fun findBySectionIdOrderByPositionAscIdAsc(sectionId: UUID): List<ContentArticle>
|
||||
fun findBySectionIdAndSlug(sectionId: UUID, slug: String): ContentArticle?
|
||||
fun findBySectionIdAndTitleIgnoreCase(sectionId: UUID, title: String): ContentArticle?
|
||||
fun findAllByOrderByPositionAscIdAsc(): List<ContentArticle>
|
||||
}
|
||||
Reference in New Issue
Block a user