From 12383e7c397e1fa92f90630d36eb4bce35695094 Mon Sep 17 00:00:00 2001 From: Vladislav Kopnin Date: Fri, 11 Sep 2026 20:41:11 +0300 Subject: [PATCH] Fix learn dialog rendering, add lab publish/draft toggle, bump to v0.1.2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - learn/index.html: restructured dialogs inside section divs to fix scope - learn/index.html: edit-article dialogs via flat articles list in modal blocks - LearnController: sectionChoices for select options; secure article fetching - ContentService: createArticlePlaced: new section adjacent to anchor section - lab status: published = student-visible, draft = hidden - lab toggle: click badge in labs list or button in lab header to switch status - added badge-btn CSS class, removed inline styles - removed all th:onclick with expressions → data-modal attributes - footer v0.1.2 --- .../lms/academic/content/ContentService.kt | 7 + .../lms/academic/controller/LabController.kt | 6 + .../ru/ysu/lms/academic/lab/LabService.kt | 11 +- .../ru/ysu/lms/web/admin/AdminController.kt | 14 + .../kotlin/ru/ysu/lms/web/api/ApiClient.kt | 8 + .../ru/ysu/lms/web/learn/LearnController.kt | 19 +- web/src/main/resources/static/css/app.css | 1 + .../main/resources/templates/admin/lab.html | 16 +- .../main/resources/templates/admin/labs.html | 11 +- .../resources/templates/fragments/base.html | 4 +- .../main/resources/templates/learn/index.html | 253 +++++++++--------- 11 files changed, 219 insertions(+), 131 deletions(-) diff --git a/services/academic-service/src/main/kotlin/ru/ysu/lms/academic/content/ContentService.kt b/services/academic-service/src/main/kotlin/ru/ysu/lms/academic/content/ContentService.kt index 7b1c0f5..ebd40b7 100644 --- a/services/academic-service/src/main/kotlin/ru/ysu/lms/academic/content/ContentService.kt +++ b/services/academic-service/src/main/kotlin/ru/ysu/lms/academic/content/ContentService.kt @@ -231,6 +231,13 @@ class ContentService( 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() -> { diff --git a/services/academic-service/src/main/kotlin/ru/ysu/lms/academic/controller/LabController.kt b/services/academic-service/src/main/kotlin/ru/ysu/lms/academic/controller/LabController.kt index f733e86..0f1523f 100644 --- a/services/academic-service/src/main/kotlin/ru/ysu/lms/academic/controller/LabController.kt +++ b/services/academic-service/src/main/kotlin/ru/ysu/lms/academic/controller/LabController.kt @@ -55,6 +55,12 @@ class LabController(private val labService: LabService) { fun deleteVariant(@PathVariable id: UUID, @PathVariable variantId: UUID): LabDetail = labService.deleteVariant(id, variantId) + @PostMapping("/{id}/toggle-status") + @PreAuthorize("hasAuthority('ROLE_ADMIN')") + fun toggleStatus(@PathVariable id: UUID) { + labService.toggleStatus(id) + } + @PostMapping @PreAuthorize("hasAuthority('ROLE_ADMIN')") fun create(@Valid @RequestBody body: LabCreateRequest): LabDetail = labService.create(body) diff --git a/services/academic-service/src/main/kotlin/ru/ysu/lms/academic/lab/LabService.kt b/services/academic-service/src/main/kotlin/ru/ysu/lms/academic/lab/LabService.kt index e829c15..8ea3866 100644 --- a/services/academic-service/src/main/kotlin/ru/ysu/lms/academic/lab/LabService.kt +++ b/services/academic-service/src/main/kotlin/ru/ysu/lms/academic/lab/LabService.kt @@ -257,10 +257,10 @@ class LabService( return detail(labId) } - @Transactional +@Transactional fun addLevel(labId: UUID, request: LevelSaveRequest) { val lab = labs.findById(labId).orElseThrow { NotFoundException("Лабораторная не найдена") } - if (request.code.isNullOrBlank()) throw BadRequestException("Код уровня не может быть пустым") + if (request.code.isBlank()) throw BadRequestException("Код уровня не может быть пустым") val existing = labLevels.findByLabIdOrderByPosition(labId) labLevels.save( LabLevel( @@ -273,6 +273,13 @@ class LabService( ) } + @Transactional + fun toggleStatus(labId: UUID) { + val lab = labs.findById(labId).orElseThrow { NotFoundException("Лабораторная не найдена") } + lab.status = if (lab.status == "published") "draft" else "published" + labs.save(lab) + } + private fun findOrCreateSemester(title: String, year: Int): Semester { semesters.findFirstByActiveTrue()?.let { return it } val t = title.ifBlank { "осень" } diff --git a/web/src/main/kotlin/ru/ysu/lms/web/admin/AdminController.kt b/web/src/main/kotlin/ru/ysu/lms/web/admin/AdminController.kt index 9239875..ffaf518 100644 --- a/web/src/main/kotlin/ru/ysu/lms/web/admin/AdminController.kt +++ b/web/src/main/kotlin/ru/ysu/lms/web/admin/AdminController.kt @@ -213,6 +213,20 @@ class AdminController( } } + @PostMapping("/labs/{id}/toggle-status") + fun toggleStatus( + @PathVariable id: String, + session: HttpSession, + redirect: RedirectAttributes, + ): String = try { + api.toggleLabStatus(token(session), id) + redirect.addFlashAttribute("notice", "Статус лабораторной изменён") + "redirect:/admin/labs/$id" + } catch (ex: ApiException) { + redirect.addFlashAttribute("error", ex.message) + "redirect:/admin/labs/$id" + } + @PostMapping("/labs/{id}/delete") fun deleteLab( @PathVariable id: String, diff --git a/web/src/main/kotlin/ru/ysu/lms/web/api/ApiClient.kt b/web/src/main/kotlin/ru/ysu/lms/web/api/ApiClient.kt index c9f6082..04530d3 100644 --- a/web/src/main/kotlin/ru/ysu/lms/web/api/ApiClient.kt +++ b/web/src/main/kotlin/ru/ysu/lms/web/api/ApiClient.kt @@ -181,6 +181,14 @@ class ApiClient( } } + fun toggleLabStatus(token: String, labId: String) { + call { + client.post().uri("/api/v1/labs/{id}/toggle-status", labId) + .header(HttpHeaders.AUTHORIZATION, bearer(token)) + .retrieve().toBodilessEntity() + } + } + fun getLevels(token: String, labId: String): List { val type = object : ParameterizedTypeReference>() {} return call { diff --git a/web/src/main/kotlin/ru/ysu/lms/web/learn/LearnController.kt b/web/src/main/kotlin/ru/ysu/lms/web/learn/LearnController.kt index 29677cb..c053997 100644 --- a/web/src/main/kotlin/ru/ysu/lms/web/learn/LearnController.kt +++ b/web/src/main/kotlin/ru/ysu/lms/web/learn/LearnController.kt @@ -22,7 +22,24 @@ class LearnController( @GetMapping("/learn") fun index(session: HttpSession, model: Model): String { - model.addAttribute("tree", api.contentTree(token(session))) + val t = token(session) + val tree = api.contentTree(t) + model.addAttribute("tree", tree) + val isAdmin = (session.getAttribute(SessionKeys.USER) as? ru.ysu.lms.web.session.SessionUser)?.isAdmin == true + val articles = if (isAdmin && t != null) api.contentTreeList(t) else emptyList() + model.addAttribute("articles", articles) + model.addAttribute( + "sectionChoices", + tree.chapters.flatMap { chapter -> + chapter.sections.map { section -> + mapOf( + "id" to section.id, + "chapterId" to chapter.id, + "label" to "${chapter.title} / ${section.title}", + ) + } + }, + ) return "learn/index" } diff --git a/web/src/main/resources/static/css/app.css b/web/src/main/resources/static/css/app.css index 6d82942..946cefc 100644 --- a/web/src/main/resources/static/css/app.css +++ b/web/src/main/resources/static/css/app.css @@ -136,6 +136,7 @@ a:hover { color: var(--c-text-link-hover); } .badge { display: inline-flex; align-items: center; border-radius: 9999px; padding: 0.125rem 0.625rem; font-size: 0.75rem; font-weight: 500; } .badge-ok { background: var(--c-badge-ok-bg); color: var(--c-badge-ok-text); } .badge-warn { background: var(--c-badge-warn-bg); color: var(--c-badge-warn-text); } +.badge-btn { border: none; cursor: pointer; padding: 0.125rem 0.625rem; } /* Tables */ table { width: 100%; font-size: 0.875rem; border-collapse: collapse; } diff --git a/web/src/main/resources/templates/admin/lab.html b/web/src/main/resources/templates/admin/lab.html index d2d009e..bcf4293 100644 --- a/web/src/main/resources/templates/admin/lab.html +++ b/web/src/main/resources/templates/admin/lab.html @@ -9,6 +9,10 @@

ЛР 1

Статус: — +

+ + +
· Срок: · Защита: · Максимум: б. @@ -47,14 +51,14 @@
+ th:attr="data-modal=${'modal-rename-level-' + level.id}" onclick="openModalData(this)">Переименовать
+ th:attr="data-modal=${'modal-add-variant-' + level.id}" onclick="openModalData(this)">+ Вариант
@@ -66,7 +70,7 @@ - + diff --git a/web/src/main/resources/templates/fragments/base.html b/web/src/main/resources/templates/fragments/base.html index 2ff8558..d05cb7f 100644 --- a/web/src/main/resources/templates/fragments/base.html +++ b/web/src/main/resources/templates/fragments/base.html @@ -12,6 +12,8 @@ function toggleTheme(){ var e=document.documentElement; var t=e.getAttribute('data-theme')==='dark'?'light':'dark'; e.setAttribute('data-theme',t); localStorage.setItem('lms-theme',t); } function openModal(id){ var d=document.getElementById(id); if(d) d.showModal(); } function closeModal(id){ var d=document.getElementById(id); if(d) d.close(); } + function openModalData(btn){ openModal(btn.dataset.modal); } + function closeModalData(btn){ closeModal(btn.dataset.modal); } @@ -38,7 +40,7 @@
- LMS v0.1.1 + LMS v0.1.2
diff --git a/web/src/main/resources/templates/learn/index.html b/web/src/main/resources/templates/learn/index.html index 4904e0c..1a4fbf1 100644 --- a/web/src/main/resources/templates/learn/index.html +++ b/web/src/main/resources/templates/learn/index.html @@ -8,7 +8,7 @@
- +
@@ -17,31 +17,34 @@
Материалы ещё не добавлены.
-
+ +
-
+

Глава

-
+
+ th:data-modal="|modal-rename-chapter-${chapter.id}|" onclick="openModalData(this)">Переименовать
- +
-
-
+ +

Разделов пока нет

+ +
+

Раздел

-
+
+ th:data-modal="|modal-rename-section-${section.id}|" onclick="openModalData(this)">Переименовать + th:data-modal="|modal-move-${section.id}|" onclick="openModalData(this)">Перенести статьи
@@ -55,7 +58,7 @@ th:text="${article.title}">Статья + th:data-modal="|modal-edit-article-${article.id}|" onclick="openModalData(this)">Править @@ -65,131 +68,141 @@
  • Статей пока нет
  • + + + + + + + + + + +
    -

    Разделов пока нет

    + + + + + + +
    -
    - - - - - - - - - - - - + - - + + + + + + + + + + + + + - - - - - - - - - - - - +
    - \ No newline at end of file +
    — + th:attr="data-modal=${'modal-edit-variant-' + v.id}" onclick="openModalData(this)">Править
    @@ -189,7 +193,7 @@
    1 — 0— + + + + +
    Лабораторных пока нет.