From 0de6d3f2d8b28f4f9f0f2675376e08102d67fc34 Mon Sep 17 00:00:00 2001 From: Vladislav Kopnin Date: Thu, 10 Sep 2026 23:25:36 +0300 Subject: [PATCH] Lab assignment management UI and student variant view - progress-service: POST /api/v1/assign (manual variant), GET /api/v1/assignments?labId= (enriched assignments), fixed enrich() to return real variant number/title/statement - gateway: routes for /api/v1/assign and /api/v1/levels (exact paths) - web: lab detail page shows students grouped with level and variant selects, group filter, distribute/redistribute buttons, source badge (auto/manual) - web: student dashboard renders variant statement via libs/markdown (spoilers, code highlight, sanitized) - academic-service: unquote YAML front-matter values (variant numbers were quoted) - academic-service: LabLevelDetail.id for level references --- .../ysu/lms/academic/lab/LabImportParser.kt | 13 ++- .../src/main/resources/application.yml | 2 +- .../main/kotlin/ru/ysu/lms/progress/Dtos.kt | 41 ++++++++- .../ru/ysu/lms/progress/ProgressController.kt | 19 +++- .../ru/ysu/lms/progress/ProgressService.kt | 69 +++++++++++--- .../lms/progress/security/SecurityConfig.kt | 23 ++++- web/build.gradle.kts | 1 + .../ru/ysu/lms/web/admin/AdminController.kt | 54 ++++++++++- .../ru/ysu/lms/web/admin/AdminLabView.kt | 18 ++++ .../kotlin/ru/ysu/lms/web/api/ApiClient.kt | 18 ++++ .../main/kotlin/ru/ysu/lms/web/api/ApiDtos.kt | 26 +++++- .../ysu/lms/web/student/StudentController.kt | 33 ++++++- web/src/main/resources/static/css/app.css | 22 +++++ .../main/resources/templates/admin/lab.html | 90 +++++++++++++++---- .../main/resources/templates/admin/labs.html | 15 ++-- .../resources/templates/fragments/base.html | 1 - .../templates/student/dashboard.html | 30 ++++--- 17 files changed, 408 insertions(+), 67 deletions(-) create mode 100644 web/src/main/kotlin/ru/ysu/lms/web/admin/AdminLabView.kt diff --git a/services/academic-service/src/main/kotlin/ru/ysu/lms/academic/lab/LabImportParser.kt b/services/academic-service/src/main/kotlin/ru/ysu/lms/academic/lab/LabImportParser.kt index c286ee9..7c865f1 100644 --- a/services/academic-service/src/main/kotlin/ru/ysu/lms/academic/lab/LabImportParser.kt +++ b/services/academic-service/src/main/kotlin/ru/ysu/lms/academic/lab/LabImportParser.kt @@ -144,19 +144,28 @@ class LabImportParser { val fm = mutableMapOf() for (line in fmLines) { val parts = line.split(":", limit = 2) - if (parts.size == 2) fm[parts[0].trim()] = parts[1].trim() + if (parts.size == 2) fm[parts[0].trim()] = unquote(parts[1].trim()) } val levelCode = normalizeLevel(fm["level"] ?: "basic") if (levelMap[levelCode] == null) return null val body = lines.drop(secondSep + 1).joinToString("\n").trim() return VariantMeta( level = levelCode, - number = fm["number"] ?: return null, + number = fm["number"]?.takeIf { it.isNotBlank() } ?: return null, title = fm["title"] ?: "", statement = body.ifBlank { "—" }, ) } + private fun unquote(value: String): String = + if (value.length >= 2 && + ((value.first() == '"' && value.last() == '"') || (value.first() == '\'' && value.last() == '\'')) + ) { + value.substring(1, value.length - 1) + } else { + value + } + private fun normalizeLevel(s: String): String = when (s.lowercase().trim()) { "main", "basic", "обычная", "основная" -> "basic" "advanced", "повышенная", "повышенной", "hard" -> "advanced" diff --git a/services/gateway/src/main/resources/application.yml b/services/gateway/src/main/resources/application.yml index d33ed5b..b068f5c 100644 --- a/services/gateway/src/main/resources/application.yml +++ b/services/gateway/src/main/resources/application.yml @@ -20,7 +20,7 @@ spring: - id: progress uri: ${LMS_PROGRESS_URI:http://localhost:8083} predicates: - - Path=/api/v1/assignments/**,/api/v1/levels/**,/api/v1/me/**,/api/v1/distribute/** + - Path=/api/v1/assign,/api/v1/assign/**,/api/v1/assignments/**,/api/v1/levels,/api/v1/levels/**,/api/v1/me/**,/api/v1/distribute,/api/v1/distribute/** management: endpoints: diff --git a/services/progress-service/src/main/kotlin/ru/ysu/lms/progress/Dtos.kt b/services/progress-service/src/main/kotlin/ru/ysu/lms/progress/Dtos.kt index bb5a9c5..38d00a4 100644 --- a/services/progress-service/src/main/kotlin/ru/ysu/lms/progress/Dtos.kt +++ b/services/progress-service/src/main/kotlin/ru/ysu/lms/progress/Dtos.kt @@ -40,6 +40,7 @@ data class DistributeResult( data class StudentInfo( val id: String = "", val studentNo: String = "", + val fullName: String = "", ) @JsonIgnoreProperties(ignoreUnknown = true) @@ -54,21 +55,28 @@ data class LabInfo( val id: String = "", val number: Int = 0, val title: String = "", + val status: String = "", + val descriptionMd: String = "", + val deadlineAt: String? = null, + val defenseAt: String? = null, + val pointsMax: Any? = null, val levels: List = emptyList(), ) @JsonIgnoreProperties(ignoreUnknown = true) data class LevelInfo( + val id: String = "", val code: String = "", val title: String = "", - val id: String = "", val variants: List = emptyList(), ) +@JsonIgnoreProperties(ignoreUnknown = true) data class VariantInfo( val id: String = "", val number: String = "", val title: String = "", + val statementMd: String = "", ) data class MeAssignmentResponse( @@ -81,4 +89,35 @@ data class MeAssignmentResponse( val deadlineAt: String?, val defenseAt: String?, val pointsMax: Any?, +) + +data class AssignRequest( + val studentId: String, + val labId: String, + val variantId: String, +) + +data class EnrichedAssignment( + val studentId: String, + val studentName: String, + val studentNo: String, + val groupName: String, + val levelCode: String, + val levelTitle: String, + val variantId: String?, + val variantNumber: String?, + val manual: Boolean, +) + +data class LabStudentsResponse( + val labId: String, + val labNumber: Int, + val labTitle: String, + val levels: List, + val groups: List, +) + +data class GroupAssignmentResponse( + val groupName: String, + val students: List, ) \ No newline at end of file diff --git a/services/progress-service/src/main/kotlin/ru/ysu/lms/progress/ProgressController.kt b/services/progress-service/src/main/kotlin/ru/ysu/lms/progress/ProgressController.kt index df1a20a..f3705b5 100644 --- a/services/progress-service/src/main/kotlin/ru/ysu/lms/progress/ProgressController.kt +++ b/services/progress-service/src/main/kotlin/ru/ysu/lms/progress/ProgressController.kt @@ -9,7 +9,10 @@ import java.util.UUID @RestController @RequestMapping("/api/v1") -class ProgressController(private val service: ProgressService) { +class ProgressController( + private val service: ProgressService, + private val academic: AcademicClient, +) { @GetMapping("/me/assignments") fun meAssignments(@AuthenticationPrincipal jwt: Jwt): List { @@ -47,4 +50,18 @@ class ProgressController(private val service: ProgressService) { @Valid @RequestBody body: DistributeRequest, @AuthenticationPrincipal jwt: Jwt, ): DistributeResult = service.distribute(body, jwt.tokenValue) + + @PostMapping("/assign") + @PreAuthorize("hasAuthority('ROLE_ADMIN')") + fun assign( + @Valid @RequestBody body: AssignRequest, + @AuthenticationPrincipal jwt: Jwt, + ): VariantAssignmentResponse = service.assignWithInfo(body, jwt.tokenValue) + + @GetMapping("/assignments") + @PreAuthorize("hasAuthority('ROLE_ADMIN')") + fun assignmentsForLab( + @RequestParam labId: UUID, + @AuthenticationPrincipal jwt: Jwt, + ): List = service.assignmentsForLabEnriched(labId, jwt.tokenValue) } \ No newline at end of file diff --git a/services/progress-service/src/main/kotlin/ru/ysu/lms/progress/ProgressService.kt b/services/progress-service/src/main/kotlin/ru/ysu/lms/progress/ProgressService.kt index 4fad94b..1466367 100644 --- a/services/progress-service/src/main/kotlin/ru/ysu/lms/progress/ProgressService.kt +++ b/services/progress-service/src/main/kotlin/ru/ysu/lms/progress/ProgressService.kt @@ -75,7 +75,7 @@ class ProgressService( } } - val assigned = mutableSetOf() + val assigned = mutableSetOf() val variantIdsByLevel = lab.levels.associate { lv -> lv.id to lv.variants.map { it.id } } @@ -85,12 +85,12 @@ class ProgressService( val existing = assignments.findByStudentIdAndLabId(sid, labId) if (existing != null && existing.manual) { result.add(existing) - assigned.add(existing.variantId) + assigned.add(existing.variantId.toString()) continue } if (existing != null && !request.force) { result.add(existing) - assigned.add(existing.variantId) + assigned.add(existing.variantId.toString()) continue } @@ -107,7 +107,7 @@ class ProgressService( ) assignments.save(entity) result.add(entity) - assigned.add(UUID.fromString(chosen)) + assigned.add(chosen) } val title = lab.levels.firstOrNull()?.let { lab.title } ?: lab.title @@ -123,6 +123,48 @@ class ProgressService( fun getAssignmentsForLab(labId: UUID): List = assignments.findByLabId(labId) + @Transactional + fun assign(request: AssignRequest): VariantAssignment { + val sid = UUID.fromString(request.studentId) + val lid = UUID.fromString(request.labId) + val existing = assignments.findByStudentIdAndLabId(sid, lid) + val entity = (existing ?: VariantAssignment(studentId = sid, labId = lid)).apply { + variantId = UUID.fromString(request.variantId) + manual = true + } + return assignments.save(entity) + } + + @Transactional + fun assignWithInfo(request: AssignRequest, token: String): VariantAssignmentResponse { + assign(request) + val lab = academic.getLab(request.labId, token) + val variant = lab.levels.flatMap { it.variants }.firstOrNull { it.id == request.variantId } + return VariantAssignmentResponse( + studentId = request.studentId, + labId = request.labId, + variantId = request.variantId, + variantNumber = variant?.number ?: "", + manual = true, + ) + } + + @Transactional(readOnly = true) + fun assignmentsForLabEnriched(labId: UUID, token: String): List { + val lab = academic.getLab(labId.toString(), token) + val variants = lab.levels.flatMap { it.variants }.associateBy { it.id } + return assignments.findByLabId(labId).map { va -> + val v = variants[va.variantId.toString()] + VariantAssignmentResponse( + studentId = va.studentId.toString(), + labId = va.labId.toString(), + variantId = va.variantId.toString(), + variantNumber = v?.number ?: "", + manual = va.manual, + ) + } + } + @Transactional(readOnly = true) fun getStudentAssignments(studentId: UUID): List = assignments.findByStudentId(studentId) @@ -134,18 +176,19 @@ class ProgressService( } private fun enrich(va: VariantAssignment, token: String): MeAssignmentResponse? { - val lab = runCatching { academic.getLabSummary(va.labId.toString(), token) }.getOrNull() ?: return null - val firstLevel = lab.levels.firstOrNull() + val lab = runCatching { academic.getLab(va.labId.toString(), token) }.getOrNull() ?: return null + val variant = lab.levels.flatMap { it.variants }.firstOrNull { it.id == va.variantId.toString() } + val level = lab.levels.firstOrNull { lv -> lv.variants.any { it.id == va.variantId.toString() } } return MeAssignmentResponse( labNumber = lab.number, labTitle = lab.title, - variantNumber = "", - variantTitle = "", - statementMd = "", - levelTitle = firstLevel?.title ?: "", - deadlineAt = null, - defenseAt = null, - pointsMax = null, + variantNumber = variant?.number ?: "", + variantTitle = variant?.title ?: "", + statementMd = variant?.statementMd ?: "", + levelTitle = level?.title ?: "", + deadlineAt = lab.deadlineAt, + defenseAt = lab.defenseAt, + pointsMax = lab.pointsMax, ) } } \ No newline at end of file diff --git a/services/progress-service/src/main/kotlin/ru/ysu/lms/progress/security/SecurityConfig.kt b/services/progress-service/src/main/kotlin/ru/ysu/lms/progress/security/SecurityConfig.kt index 30dcfe3..119c471 100644 --- a/services/progress-service/src/main/kotlin/ru/ysu/lms/progress/security/SecurityConfig.kt +++ b/services/progress-service/src/main/kotlin/ru/ysu/lms/progress/security/SecurityConfig.kt @@ -2,12 +2,18 @@ package ru.ysu.lms.progress.security import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration +import org.springframework.core.convert.converter.Converter +import org.springframework.security.authentication.AbstractAuthenticationToken import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity import org.springframework.security.config.http.SessionCreationPolicy +import org.springframework.security.core.GrantedAuthority +import org.springframework.security.core.authority.SimpleGrantedAuthority +import org.springframework.security.oauth2.jwt.Jwt import org.springframework.security.oauth2.jwt.JwtDecoder import org.springframework.security.oauth2.jwt.NimbusJwtDecoder +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter import org.springframework.security.web.SecurityFilterChain @Configuration @@ -18,6 +24,17 @@ class SecurityConfig { @Bean fun jwtDecoder(): JwtDecoder = NimbusJwtDecoder.withJwkSetUri(JWK_URI).build() + @Bean + fun jwtAuthenticationConverter(): Converter { + val converter = JwtAuthenticationConverter() + converter.setJwtGrantedAuthoritiesConverter { jwt -> + val role = jwt.getClaimAsString("role") + if (role.isNullOrBlank()) emptyList() + else listOf(SimpleGrantedAuthority("ROLE_$role")) + } + return converter + } + @Bean fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { http @@ -28,7 +45,11 @@ class SecurityConfig { auth.requestMatchers("/actuator/**", "/v3/api-docs/**", "/swagger-ui/**").permitAll() auth.anyRequest().authenticated() } - .oauth2ResourceServer { it.jwt { } } + .oauth2ResourceServer { oauth -> + oauth.jwt { jwt -> + jwt.jwtAuthenticationConverter(jwtAuthenticationConverter()) + } + } return http.build() } diff --git a/web/build.gradle.kts b/web/build.gradle.kts index c69ecd3..097a33d 100644 --- a/web/build.gradle.kts +++ b/web/build.gradle.kts @@ -11,6 +11,7 @@ dependencies { implementation(libs.springBootStarterActuator) implementation(project(":libs:common-http")) + implementation(project(":libs:markdown")) developmentOnly(platform(libs.springBootBom)) developmentOnly(libs.springBootDevtools) 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 d545717..18c4617 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 @@ -15,6 +15,7 @@ import org.springframework.web.multipart.MultipartFile import org.springframework.web.servlet.mvc.support.RedirectAttributes import ru.ysu.lms.web.api.ApiClient import ru.ysu.lms.web.api.ApiException +import ru.ysu.lms.web.api.AssignVariantPayload import ru.ysu.lms.web.api.LabImportResultDto import ru.ysu.lms.web.api.DistributeResultDto import ru.ysu.lms.web.api.LevelAssignPayload @@ -173,13 +174,60 @@ class AdminController(private val api: ApiClient) { session: HttpSession, model: Model, ): String { - val lab = api.labDetail(token(session), id) - val levels = api.getLevels(token(session), id) + val t = token(session) + val lab = api.labDetail(t, id) + val levelAssignments = api.getLevels(t, id) + val variantAssignments = api.getLabAssignments(t, id) + + // Для каждой группы: студенты с их уровнем (по умолчанию — первый/самый простой) и вариантом + val groupSections = api.listGroups(t).map { group -> + val detail = api.groupDetail(t, group.id) + AdminLabGroup( + groupName = group.name, + students = detail.students.map { s -> + val level = levelAssignments.firstOrNull { it.studentId == s.id } + val levelId = level?.labLevelId ?: lab.levels.firstOrNull()?.id.orEmpty() + val levelTitle = lab.levels.firstOrNull { it.id == levelId }?.title ?: "—" + val va = variantAssignments.firstOrNull { it.studentId == s.id } + AdminLabStudent( + studentId = s.id, + fullName = s.fullName, + studentNo = s.studentNo, + levelId = levelId, + levelTitle = levelTitle, + levelChanged = level != null, + variantId = va?.variantId, + variantNumber = va?.variantNumber, + source = when { + va == null -> "не распределён" + va.manual -> "вручную" + else -> "авто" + }, + ) + }, + ) + } model.addAttribute("lab", lab) - model.addAttribute("levelAssignments", levels) + model.addAttribute("groups", groupSections) return "admin/lab" } + @PostMapping("/labs/{labId}/assign") + fun assignVariant( + @PathVariable labId: String, + @RequestParam studentId: String, + @RequestParam variantId: String, + session: HttpSession, + redirect: RedirectAttributes, + ): String = try { + val result = api.assignVariant(token(session), AssignVariantPayload(studentId, labId, variantId)) + redirect.addFlashAttribute("notice", "Назначен вариант ${result.variantNumber}") + "redirect:/admin/labs/$labId" + } catch (ex: ApiException) { + redirect.addFlashAttribute("error", ex.message) + "redirect:/admin/labs/$labId" + } + @PostMapping("/labs/{id}/distribute") fun distribute( @PathVariable id: String, diff --git a/web/src/main/kotlin/ru/ysu/lms/web/admin/AdminLabView.kt b/web/src/main/kotlin/ru/ysu/lms/web/admin/AdminLabView.kt new file mode 100644 index 0000000..90c2f53 --- /dev/null +++ b/web/src/main/kotlin/ru/ysu/lms/web/admin/AdminLabView.kt @@ -0,0 +1,18 @@ +package ru.ysu.lms.web.admin + +data class AdminLabGroup( + val groupName: String, + val students: List, +) + +data class AdminLabStudent( + val studentId: String, + val fullName: String, + val studentNo: String, + val levelId: String, + val levelTitle: String, + val levelChanged: Boolean, + val variantId: String?, + val variantNumber: String?, + val source: String, +) \ No newline at end of file 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 3c98de3..b75343a 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 @@ -208,6 +208,24 @@ class ApiClient( .retrieve().body(DistributeResultDto::class.java) } + fun assignVariant(token: String, payload: AssignVariantPayload): VariantAssignmentResultDto = + call { + client.post().uri("/api/v1/assign") + .header(HttpHeaders.AUTHORIZATION, bearer(token)) + .contentType(MediaType.APPLICATION_JSON) + .body(payload) + .retrieve().body(VariantAssignmentResultDto::class.java) + } + + fun getLabAssignments(token: String, labId: String): List { + val type = object : ParameterizedTypeReference>() {} + return call { + client.get().uri("/api/v1/assignments?labId={labId}", labId) + .header(HttpHeaders.AUTHORIZATION, bearer(token)) + .retrieve().body(type) + } ?: emptyList() + } + fun meAssignments(token: String): List { val type = object : ParameterizedTypeReference>() {} return call { diff --git a/web/src/main/kotlin/ru/ysu/lms/web/api/ApiDtos.kt b/web/src/main/kotlin/ru/ysu/lms/web/api/ApiDtos.kt index 038bcee..cb8b9ee 100644 --- a/web/src/main/kotlin/ru/ysu/lms/web/api/ApiDtos.kt +++ b/web/src/main/kotlin/ru/ysu/lms/web/api/ApiDtos.kt @@ -110,6 +110,28 @@ data class LevelAssignPayload( val labLevelId: String, ) +data class AssignVariantPayload( + val studentId: String, + val labId: String, + val variantId: String, +) + +@JsonIgnoreProperties(ignoreUnknown = true) +data class VariantAssignmentResultDto( + val studentId: String = "", + val labId: String = "", + val variantId: String = "", + val variantNumber: String = "", + val manual: Boolean = false, +) + +@JsonIgnoreProperties(ignoreUnknown = true) +data class LabLevelVariantDto( + val id: String = "", + val number: String = "", + val title: String = "", +) + @JsonIgnoreProperties(ignoreUnknown = true) data class LevelAssignmentResultDto( val studentId: String = "", @@ -129,7 +151,9 @@ data class MeAssignmentDto( val deadlineAt: String? = null, val defenseAt: String? = null, val pointsMax: Any? = null, -) +) { + var statementHtml: String = "" +} @JsonIgnoreProperties(ignoreUnknown = true) data class LabDetailDto( diff --git a/web/src/main/kotlin/ru/ysu/lms/web/student/StudentController.kt b/web/src/main/kotlin/ru/ysu/lms/web/student/StudentController.kt index 70582e2..862eacf 100644 --- a/web/src/main/kotlin/ru/ysu/lms/web/student/StudentController.kt +++ b/web/src/main/kotlin/ru/ysu/lms/web/student/StudentController.kt @@ -5,18 +5,47 @@ import org.springframework.stereotype.Controller import org.springframework.ui.Model import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RequestMapping +import ru.ysu.lms.markdown.MarkdownRenderer import ru.ysu.lms.web.api.ApiClient +import ru.ysu.lms.web.api.MeAssignmentDto import ru.ysu.lms.web.session.SessionKeys @Controller @RequestMapping("/student") -class StudentController(private val api: ApiClient) { +class StudentController( + private val api: ApiClient, + private val markdown: MarkdownRenderer, +) { @GetMapping fun dashboard(session: HttpSession, model: Model): String { val token = session.getAttribute(SessionKeys.TOKEN) as? String ?: return "redirect:/login" - val assignments = api.meAssignments(token) + val assignments = api.meAssignments(token).map { + it.apply { statementHtml = markdown.render(statementMd) } + } model.addAttribute("assignments", assignments) return "student/dashboard" } +} + +data class StudentAssignmentView( + val labNumber: Int, + val labTitle: String, + val variantNumber: String, + val variantTitle: String, + val statementHtml: String, + val levelTitle: String, + val deadlineAt: String?, + val defenseAt: String?, +) { + constructor(a: MeAssignmentDto, statementHtml: String) : this( + labNumber = a.labNumber, + labTitle = a.labTitle, + variantNumber = a.variantNumber, + variantTitle = a.variantTitle, + statementHtml = statementHtml, + levelTitle = a.levelTitle, + deadlineAt = a.deadlineAt, + defenseAt = a.defenseAt, + ) } \ No newline at end of file diff --git a/web/src/main/resources/static/css/app.css b/web/src/main/resources/static/css/app.css index c771193..bd9cbaa 100644 --- a/web/src/main/resources/static/css/app.css +++ b/web/src/main/resources/static/css/app.css @@ -158,6 +158,28 @@ tr:last-child td { border-bottom: none; } /* Theme toggle */ .theme-btn { padding: 0.25rem 0.5rem; font-size: 0.75rem; } +/* Group assignment page */ +.group-title { margin: 0 0 0.5rem; font-size: 1rem; font-weight: 600; } +.inline-form { display: inline-flex; align-items: center; gap: 0.375rem; margin: 0; } +.inline-form .input-sm { width: auto; min-width: 10rem; } +.input-sm { padding: 0.25rem 0.5rem; font-size: 0.8125rem; } +.filter-select { width: auto; } +.hidden { display: none !important; } + +/* Markdown body */ +.md-body { font-size: 0.9375rem; } +.md-body h1, .md-body h2, .md-body h3 { margin: 1rem 0 0.5rem; line-height: 1.3; } +.md-body h1 { font-size: 1.25rem; } +.md-body h2 { font-size: 1.125rem; } +.md-body p, .md-body ul, .md-body ol { margin: 0.5rem 0; } +.md-body ul, .md-body ol { padding-left: 1.5rem; } +.md-body pre { background: var(--c-bg); border: 1px solid var(--c-border); border-radius: 0.5rem; padding: 0.75rem 1rem; overflow-x: auto; font-size: 0.8125rem; } +.md-body code { font-family: monospace; } +.md-body details.spoiler { border: 1px solid var(--c-border); border-radius: 0.5rem; padding: 0.5rem 0.75rem; margin: 0.5rem 0; } +.md-body details.spoiler summary { cursor: pointer; font-weight: 500; color: var(--c-text-link); } +.md-body img { max-width: 100%; } +.md-body table { margin: 0.5rem 0; } + /* Empty state */ .empty { text-align: center; color: var(--c-text-muted); padding: 1.5rem; } diff --git a/web/src/main/resources/templates/admin/lab.html b/web/src/main/resources/templates/admin/lab.html index 15d6bd0..1a01099 100644 --- a/web/src/main/resources/templates/admin/lab.html +++ b/web/src/main/resources/templates/admin/lab.html @@ -3,13 +3,20 @@ Лабораторная
- ← Все лабораторные -
-

ЛР 1

Статус: —

-
- - -
+ ← Все лабораторные +
+
+

ЛР 1

+

Статус: —

+
+
+
+ +
+
+ +
+
@@ -20,24 +27,73 @@ КодНазваниеВариантов - — + — — 0 + Уровни не найдены.
-

Студенты и варианты

-

Нажмите «Распределить», чтобы закрепить варианты. Можно задать уровень студента.

-
- - - - - -
СтудентУровеньВариант
+
+

Студенты и варианты

+
+

Уровень по умолчанию — самый простой. Селекты позволяют выдать вариант посложнее или назначить вариант вручную.

+ +
+

Группа

+
+ + + + + + + + + + + + + +
СтудентЛогинУровеньВариантИсточник
—— +
+ + + +
+
+
+ + + +
+
——
В группе нет студентов.
+
+
+
\ No newline at end of file diff --git a/web/src/main/resources/templates/admin/labs.html b/web/src/main/resources/templates/admin/labs.html index a05ee0a..734df06 100644 --- a/web/src/main/resources/templates/admin/labs.html +++ b/web/src/main/resources/templates/admin/labs.html @@ -9,28 +9,23 @@

Импорт лабораторной

-

Принимаются .zip (lab.yaml + README.md + variants/*.md) или .json (старый формат).

+

Принимаются .zip (lab.yaml + README.md + variants/*.md) или .json.

- + - - + - + - +
№НазваниеСеместрВариантовСтатус
№НазваниеВариантовСтатус
1——— 0 — -
- -
-
Лабораторных пока нет.
Лабораторных пока нет.
diff --git a/web/src/main/resources/templates/fragments/base.html b/web/src/main/resources/templates/fragments/base.html index 0d82be6..a6b726b 100644 --- a/web/src/main/resources/templates/fragments/base.html +++ b/web/src/main/resources/templates/fragments/base.html @@ -17,7 +17,6 @@