fix(academic): детерминированный предпросмотр при совпадении slug
This commit is contained in:
@@ -148,10 +148,12 @@ class ContentService(
|
||||
sectionSlug: String,
|
||||
articleSlug: String,
|
||||
): ContentArticleView {
|
||||
// Сначала глобальная глава: findBySlug может упасть на коллизии slug между курсами.
|
||||
// Сначала глобальная глава, затем первая курсовая по id: slug может повторяться
|
||||
// в разных курсах, а findBySlug на дублях бросает IncorrectResultSizeDataAccessException.
|
||||
val ch =
|
||||
requireFound(
|
||||
chapters.findBySlugAndCourseIdIsNull(chapterSlug) ?: chapters.findBySlug(chapterSlug),
|
||||
chapters.findBySlugAndCourseIdIsNull(chapterSlug)
|
||||
?: chapters.findFirstBySlugOrderByIdAsc(chapterSlug),
|
||||
"Глава не найдена",
|
||||
)
|
||||
return articleView(ch, sectionSlug, articleSlug, includeHidden = true)
|
||||
|
||||
@@ -11,6 +11,8 @@ interface ContentChapterRepository : JpaRepository<ContentChapter, UUID> {
|
||||
|
||||
fun findBySlug(slug: String): ContentChapter?
|
||||
|
||||
fun findFirstBySlugOrderByIdAsc(slug: String): ContentChapter?
|
||||
|
||||
fun findBySlugAndCourseIdIsNull(slug: String): ContentChapter?
|
||||
|
||||
fun findByCourseIdOrderByPositionAscIdAsc(courseId: UUID): List<ContentChapter>
|
||||
|
||||
@@ -56,6 +56,9 @@ class CourseContentIntegrationTest {
|
||||
fun assetsDir(registry: DynamicPropertyRegistry) {
|
||||
registry.add("lms.content.assets-dir") { Files.createTempDirectory("lms-assets").toString() }
|
||||
}
|
||||
|
||||
private const val PREVIEW_SECTION_SLUG = "preview-section"
|
||||
private const val PREVIEW_ARTICLE_SLUG = "preview-article"
|
||||
}
|
||||
|
||||
@Autowired
|
||||
@@ -248,6 +251,20 @@ class CourseContentIntegrationTest {
|
||||
.andExpect(status().isUnauthorized)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `admin preview resolves same-slug course chapters deterministically`() {
|
||||
val sharedSlug = "shared-${UUID.randomUUID()}"
|
||||
val first = seedChapter(title = "Первая курсовая", courseId = newCourse().id, audience = null, slug = sharedSlug)
|
||||
val second = seedChapter(title = "Вторая курсовая", courseId = newCourse().id, audience = null, slug = sharedSlug)
|
||||
val firstArticle = seedPreviewArticle(first, "Статья первой главы")
|
||||
val secondArticle = seedPreviewArticle(second, "Статья второй главы")
|
||||
val expectedTitle = if (first.id < second.id) firstArticle.title else secondArticle.title
|
||||
|
||||
val view = contentService.findArticleForAdmin(sharedSlug, PREVIEW_SECTION_SLUG, PREVIEW_ARTICLE_SLUG)
|
||||
|
||||
assertThat(view.title).isEqualTo(expectedTitle)
|
||||
}
|
||||
|
||||
private fun newCourse(): TestCourse {
|
||||
val detail =
|
||||
courseService.create(
|
||||
@@ -262,11 +279,12 @@ class CourseContentIntegrationTest {
|
||||
courseId: UUID?,
|
||||
audience: String?,
|
||||
published: Boolean = true,
|
||||
slug: String = "chapter-${UUID.randomUUID()}",
|
||||
): ContentChapter =
|
||||
chapters.saveAndFlush(
|
||||
ContentChapter(
|
||||
title = title,
|
||||
slug = "chapter-${UUID.randomUUID()}",
|
||||
slug = slug,
|
||||
position = 1,
|
||||
published = published,
|
||||
courseId = courseId,
|
||||
@@ -274,6 +292,30 @@ class CourseContentIntegrationTest {
|
||||
),
|
||||
)
|
||||
|
||||
private fun seedPreviewArticle(
|
||||
chapter: ContentChapter,
|
||||
title: String,
|
||||
): ContentArticle {
|
||||
val section =
|
||||
sections.saveAndFlush(
|
||||
ContentSection(
|
||||
chapterId = chapter.id,
|
||||
title = "Раздел",
|
||||
slug = PREVIEW_SECTION_SLUG,
|
||||
position = 1,
|
||||
),
|
||||
)
|
||||
return articles.saveAndFlush(
|
||||
ContentArticle(
|
||||
sectionId = section.id,
|
||||
title = title,
|
||||
slug = PREVIEW_ARTICLE_SLUG,
|
||||
bodyMd = "Текст",
|
||||
position = 1,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun seedMaterial(
|
||||
courseId: UUID?,
|
||||
published: Boolean,
|
||||
|
||||
Reference in New Issue
Block a user