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»).
|
главы, которых нет в архиве — архив считается источником истины («удалено устаревших: N»).
|
||||||
* Без галки импорт работает в режиме **merge**: только добавляет и обновляет то, что есть
|
* Без галки импорт работает в режиме **merge**: только добавляет и обновляет то, что есть
|
||||||
|
|||||||
@@ -503,11 +503,17 @@ class ContentService(
|
|||||||
for (p in parsed) {
|
for (p in parsed) {
|
||||||
if (currentChapter?.title != p.chapterDir) {
|
if (currentChapter?.title != p.chapterDir) {
|
||||||
val slug = chapterSlug(p) ?: "chapter-${chCount + 1}"
|
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
|
title = p.chapterDir
|
||||||
position = p.chapterPos
|
position = p.chapterPos
|
||||||
} ?: chapters.save(
|
} ?: 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!!)
|
currentChapter = chapters.save(currentChapter!!)
|
||||||
touchedChapters.add(currentChapter!!.id)
|
touchedChapters.add(currentChapter!!.id)
|
||||||
@@ -517,11 +523,17 @@ class ContentService(
|
|||||||
val chapter = currentChapter ?: throw BadRequestException("Не удалось определить главу")
|
val chapter = currentChapter ?: throw BadRequestException("Не удалось определить главу")
|
||||||
if (currentSection?.title != p.sectionDir) {
|
if (currentSection?.title != p.sectionDir) {
|
||||||
val slug = sectionSlug(p) ?: "section-${secCount + 1}"
|
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
|
title = p.sectionDir
|
||||||
position = p.sectionPos
|
position = p.sectionPos
|
||||||
} ?: sections.save(
|
} ?: 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!!)
|
currentSection = sections.save(currentSection!!)
|
||||||
seenSections.add(currentSection!!.id)
|
seenSections.add(currentSection!!.id)
|
||||||
@@ -536,7 +548,8 @@ class ContentService(
|
|||||||
val slug = p.fmSlug?.takeIf { it.isNotBlank() }
|
val slug = p.fmSlug?.takeIf { it.isNotBlank() }
|
||||||
?: articleSlug(p)
|
?: articleSlug(p)
|
||||||
?: "article-${artCount + 1}"
|
?: "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 {
|
val article = (existing ?: ContentArticle(sectionId = section.id, slug = slug)).apply {
|
||||||
title = p.fileTitle
|
title = p.fileTitle
|
||||||
bodyMd = body
|
bodyMd = body
|
||||||
@@ -575,14 +588,13 @@ class ContentService(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun chapterSlug(p: ParsedArticle): String? =
|
// Slug без числового префикса — такой же, как при ручном создании через интерфейс.
|
||||||
slugify("%02d".format(p.chapterPos) + "-" + p.chapterDir)
|
// Номер каталога по-прежнему задаёт порядок (position), но не попадает в адрес.
|
||||||
|
private fun chapterSlug(p: ParsedArticle): String? = slugify(p.chapterDir)
|
||||||
|
|
||||||
private fun sectionSlug(p: ParsedArticle): String? =
|
private fun sectionSlug(p: ParsedArticle): String? = slugify(p.sectionDir)
|
||||||
slugify("%02d".format(p.sectionPos) + "-" + p.sectionDir)
|
|
||||||
|
|
||||||
private fun articleSlug(p: ParsedArticle): String? =
|
private fun articleSlug(p: ParsedArticle): String? = slugify(p.fileTitle)
|
||||||
slugify("%02d".format(p.articlePos) + "-" + p.fileTitle)
|
|
||||||
|
|
||||||
private fun readZip(bytes: ByteArray): Map<String, ByteArray> {
|
private fun readZip(bytes: ByteArray): Map<String, ByteArray> {
|
||||||
val result = mutableMapOf<String, ByteArray>()
|
val result = mutableMapOf<String, ByteArray>()
|
||||||
|
|||||||
@@ -9,16 +9,19 @@ import java.util.UUID
|
|||||||
interface ContentChapterRepository : JpaRepository<ContentChapter, UUID> {
|
interface ContentChapterRepository : JpaRepository<ContentChapter, UUID> {
|
||||||
fun findAllByOrderByPositionAscIdAsc(): List<ContentChapter>
|
fun findAllByOrderByPositionAscIdAsc(): List<ContentChapter>
|
||||||
fun findBySlug(slug: String): ContentChapter?
|
fun findBySlug(slug: String): ContentChapter?
|
||||||
|
fun findByTitleIgnoreCase(title: String): ContentChapter?
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ContentSectionRepository : JpaRepository<ContentSection, UUID> {
|
interface ContentSectionRepository : JpaRepository<ContentSection, UUID> {
|
||||||
fun findAllByOrderByPositionAscIdAsc(): List<ContentSection>
|
fun findAllByOrderByPositionAscIdAsc(): List<ContentSection>
|
||||||
fun findByChapterIdOrderByPositionAscIdAsc(chapterId: UUID): List<ContentSection>
|
fun findByChapterIdOrderByPositionAscIdAsc(chapterId: UUID): List<ContentSection>
|
||||||
fun findByChapterIdAndSlug(chapterId: UUID, slug: String): ContentSection?
|
fun findByChapterIdAndSlug(chapterId: UUID, slug: String): ContentSection?
|
||||||
|
fun findByChapterIdAndTitleIgnoreCase(chapterId: UUID, title: String): ContentSection?
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ContentArticleRepository : JpaRepository<ContentArticle, UUID> {
|
interface ContentArticleRepository : JpaRepository<ContentArticle, UUID> {
|
||||||
fun findBySectionIdOrderByPositionAscIdAsc(sectionId: UUID): List<ContentArticle>
|
fun findBySectionIdOrderByPositionAscIdAsc(sectionId: UUID): List<ContentArticle>
|
||||||
fun findBySectionIdAndSlug(sectionId: UUID, slug: String): ContentArticle?
|
fun findBySectionIdAndSlug(sectionId: UUID, slug: String): ContentArticle?
|
||||||
|
fun findBySectionIdAndTitleIgnoreCase(sectionId: UUID, title: String): ContentArticle?
|
||||||
fun findAllByOrderByPositionAscIdAsc(): List<ContentArticle>
|
fun findAllByOrderByPositionAscIdAsc(): List<ContentArticle>
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user