Admin group: fix script loading (extra div), total submissions counter, label cleanup

This commit is contained in:
2026-09-28 17:22:09 +03:00
parent 9df1af7647
commit f630b0267e
6 changed files with 35 additions and 7 deletions

View File

@@ -56,6 +56,13 @@ class SubmissionController(
@RequestParam labId: UUID, @RequestParam labId: UUID,
): List<SubmissionDto> = submissions.latestPerStudent(labId) ): List<SubmissionDto> = submissions.latestPerStudent(labId)
/** Общее число решений (всех попыток) по лабораторной. */
@GetMapping("/submissions/count")
@PreAuthorize("hasAuthority('ROLE_ADMIN')")
fun submissionCount(
@RequestParam labId: UUID,
): Map<String, Long> = mapOf("count" to submissions.countForLab(labId))
@GetMapping("/submissions/{id}/download") @GetMapping("/submissions/{id}/download")
@PreAuthorize("hasAuthority('ROLE_ADMIN')") @PreAuthorize("hasAuthority('ROLE_ADMIN')")
fun downloadForAdmin( fun downloadForAdmin(

View File

@@ -101,6 +101,10 @@ class SubmissionService(
return zip to name return zip to name
} }
/** Общее число решений (всех попыток) по лабораторной. */
@Transactional(readOnly = true)
fun countForLab(labId: UUID): Long = submissions.countByLabId(labId)
@Transactional(readOnly = true) @Transactional(readOnly = true)
fun latestPerStudent(labId: UUID): List<SubmissionDto> { fun latestPerStudent(labId: UUID): List<SubmissionDto> {
val perStudent = mutableMapOf<UUID, Submission>() val perStudent = mutableMapOf<UUID, Submission>()

View File

@@ -14,6 +14,8 @@ interface SubmissionRepository : JpaRepository<Submission, UUID> {
fun findByLabIdOrderByCreatedAtDesc(labId: UUID): List<Submission> fun findByLabIdOrderByCreatedAtDesc(labId: UUID): List<Submission>
fun countByLabId(labId: UUID): Long
fun findFirstByStudentIdAndLabIdOrderBySeqDesc( fun findFirstByStudentIdAndLabIdOrderBySeqDesc(
studentId: UUID, studentId: UUID,
labId: UUID, labId: UUID,

View File

@@ -22,7 +22,6 @@ import ru.ysu.lms.web.api.GradeSetPayload
import ru.ysu.lms.web.api.GroupDetailDto import ru.ysu.lms.web.api.GroupDetailDto
import ru.ysu.lms.web.api.LabSummaryDto import ru.ysu.lms.web.api.LabSummaryDto
import ru.ysu.lms.web.api.StudentCandidateDto import ru.ysu.lms.web.api.StudentCandidateDto
import ru.ysu.lms.web.api.SubmissionDto
import ru.ysu.lms.web.api.createGroup import ru.ysu.lms.web.api.createGroup
import ru.ysu.lms.web.api.downloadSubmissionAsAdmin import ru.ysu.lms.web.api.downloadSubmissionAsAdmin
import ru.ysu.lms.web.api.getLabAssignments import ru.ysu.lms.web.api.getLabAssignments
@@ -38,6 +37,7 @@ import ru.ysu.lms.web.api.listGroups
import ru.ysu.lms.web.api.listLabs import ru.ysu.lms.web.api.listLabs
import ru.ysu.lms.web.api.publishGrades import ru.ysu.lms.web.api.publishGrades
import ru.ysu.lms.web.api.setGrade import ru.ysu.lms.web.api.setGrade
import ru.ysu.lms.web.api.submissionCount
import ru.ysu.lms.web.api.suggestComments import ru.ysu.lms.web.api.suggestComments
import tools.jackson.databind.ObjectMapper import tools.jackson.databind.ObjectMapper
import java.net.URLEncoder import java.net.URLEncoder
@@ -192,7 +192,7 @@ class AdminGroupController(
) )
} }
fillGroupModel(model, groupViewModel(t, id, GroupRenderData(group, labs, rows, publishedLabs, submissionsBy))) fillGroupModel(model, groupViewModel(t, id, GroupRenderData(group, labs, rows, publishedLabs)))
return "admin/group" return "admin/group"
} }
@@ -206,7 +206,6 @@ class AdminGroupController(
val labs: List<LabSummaryDto>, val labs: List<LabSummaryDto>,
val rows: List<GroupStudentRow>, val rows: List<GroupStudentRow>,
val publishedLabs: Set<String>, val publishedLabs: Set<String>,
val submissionsBy: Map<LabSummaryDto, Map<String, SubmissionDto>>,
) )
private fun groupViewModel( private fun groupViewModel(
@@ -214,7 +213,7 @@ class AdminGroupController(
id: String, id: String,
data: GroupRenderData, data: GroupRenderData,
): GroupViewModel { ): GroupViewModel {
val submissionCounts = data.labs.associate { it.id to (data.submissionsBy[it]?.size ?: 0) } val submissionCounts = data.labs.associate { it.id to api.submissionCount(token, it.id) }
val candidates = runCatching { api.groupCandidates(token, id) }.getOrDefault(emptyList()) val candidates = runCatching { api.groupCandidates(token, id) }.getOrDefault(emptyList())
return GroupViewModel(data.group, data.labs, candidates, data.rows, data.publishedLabs, submissionCounts) return GroupViewModel(data.group, data.labs, candidates, data.rows, data.publishedLabs, submissionCounts)
} }

View File

@@ -6,6 +6,23 @@ import org.springframework.http.HttpHeaders
import org.springframework.http.MediaType import org.springframework.http.MediaType
import org.springframework.util.LinkedMultiValueMap import org.springframework.util.LinkedMultiValueMap
fun ApiClient.submissionCount(
token: String,
labId: String,
): Int {
val type = object : ParameterizedTypeReference<Map<String, Long>>() {}
val result =
call {
client
.get()
.uri("/api/v1/submissions/count?labId={id}", labId)
.header(HttpHeaders.AUTHORIZATION, bearer(token))
.retrieve()
.body(type)
}
return (result["count"] ?: 0L).toInt()
}
fun ApiClient.distribute( fun ApiClient.distribute(
token: String, token: String,
payload: DistributeRequestPayload, payload: DistributeRequestPayload,

View File

@@ -12,7 +12,7 @@
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
<button class="btn btn-primary" type="button" onclick="openModal('modal-add-student')">+ Студент</button> <button class="btn btn-primary" type="button" onclick="openModal('modal-add-student')">+ Студент</button>
<a class="btn btn-secondary" th:href="@{/admin/groups/{id}/credentials(id=${group.id}, format='xlsx')}">Скачать пароли</a> <a class="btn btn-secondary" th:href="@{/admin/groups/{id}/credentials(id=${group.id}, format='xlsx')}">Скачать пароли</a>
<button class="btn btn-secondary" type="button" onclick="openModal('modal-defense-import')">Импорт заданий (CSV)</button> <button class="btn btn-secondary" type="button" onclick="openModal('modal-defense-import')">Импорт заданий</button>
</div> </div>
</div> </div>
@@ -225,7 +225,6 @@
</form> </form>
</div> </div>
</div> </div>
</div>
</div> </div>
</dialog> </dialog>
@@ -234,7 +233,7 @@
<!-- Диалог импорта CSV --> <!-- Диалог импорта CSV -->
<dialog id="modal-defense-import" class="modal"> <dialog id="modal-defense-import" class="modal">
<div class="modal-header"> <div class="modal-header">
<h2 class="modal-title">Импорт заданий (CSV)</h2> <h2 class="modal-title">Импорт заданий</h2>
<button class="modal-close" type="button" onclick="closeModal('modal-defense-import')">✕</button> <button class="modal-close" type="button" onclick="closeModal('modal-defense-import')">✕</button>
</div> </div>
<div class="modal-body"> <div class="modal-body">