fix(academic): детерминированный предпросмотр при совпадении slug
This commit is contained in:
@@ -148,10 +148,12 @@ class ContentService(
|
|||||||
sectionSlug: String,
|
sectionSlug: String,
|
||||||
articleSlug: String,
|
articleSlug: String,
|
||||||
): ContentArticleView {
|
): ContentArticleView {
|
||||||
// Сначала глобальная глава: findBySlug может упасть на коллизии slug между курсами.
|
// Сначала глобальная глава, затем первая курсовая по id: slug может повторяться
|
||||||
|
// в разных курсах, а findBySlug на дублях бросает IncorrectResultSizeDataAccessException.
|
||||||
val ch =
|
val ch =
|
||||||
requireFound(
|
requireFound(
|
||||||
chapters.findBySlugAndCourseIdIsNull(chapterSlug) ?: chapters.findBySlug(chapterSlug),
|
chapters.findBySlugAndCourseIdIsNull(chapterSlug)
|
||||||
|
?: chapters.findFirstBySlugOrderByIdAsc(chapterSlug),
|
||||||
"Глава не найдена",
|
"Глава не найдена",
|
||||||
)
|
)
|
||||||
return articleView(ch, sectionSlug, articleSlug, includeHidden = true)
|
return articleView(ch, sectionSlug, articleSlug, includeHidden = true)
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ interface ContentChapterRepository : JpaRepository<ContentChapter, UUID> {
|
|||||||
|
|
||||||
fun findBySlug(slug: String): ContentChapter?
|
fun findBySlug(slug: String): ContentChapter?
|
||||||
|
|
||||||
|
fun findFirstBySlugOrderByIdAsc(slug: String): ContentChapter?
|
||||||
|
|
||||||
fun findBySlugAndCourseIdIsNull(slug: String): ContentChapter?
|
fun findBySlugAndCourseIdIsNull(slug: String): ContentChapter?
|
||||||
|
|
||||||
fun findByCourseIdOrderByPositionAscIdAsc(courseId: UUID): List<ContentChapter>
|
fun findByCourseIdOrderByPositionAscIdAsc(courseId: UUID): List<ContentChapter>
|
||||||
|
|||||||
@@ -56,6 +56,9 @@ class CourseContentIntegrationTest {
|
|||||||
fun assetsDir(registry: DynamicPropertyRegistry) {
|
fun assetsDir(registry: DynamicPropertyRegistry) {
|
||||||
registry.add("lms.content.assets-dir") { Files.createTempDirectory("lms-assets").toString() }
|
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
|
@Autowired
|
||||||
@@ -248,6 +251,20 @@ class CourseContentIntegrationTest {
|
|||||||
.andExpect(status().isUnauthorized)
|
.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 {
|
private fun newCourse(): TestCourse {
|
||||||
val detail =
|
val detail =
|
||||||
courseService.create(
|
courseService.create(
|
||||||
@@ -262,11 +279,12 @@ class CourseContentIntegrationTest {
|
|||||||
courseId: UUID?,
|
courseId: UUID?,
|
||||||
audience: String?,
|
audience: String?,
|
||||||
published: Boolean = true,
|
published: Boolean = true,
|
||||||
|
slug: String = "chapter-${UUID.randomUUID()}",
|
||||||
): ContentChapter =
|
): ContentChapter =
|
||||||
chapters.saveAndFlush(
|
chapters.saveAndFlush(
|
||||||
ContentChapter(
|
ContentChapter(
|
||||||
title = title,
|
title = title,
|
||||||
slug = "chapter-${UUID.randomUUID()}",
|
slug = slug,
|
||||||
position = 1,
|
position = 1,
|
||||||
published = published,
|
published = published,
|
||||||
courseId = courseId,
|
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(
|
private fun seedMaterial(
|
||||||
courseId: UUID?,
|
courseId: UUID?,
|
||||||
published: Boolean,
|
published: Boolean,
|
||||||
|
|||||||
Reference in New Issue
Block a user