fix: края архива, истории, пагинации и лимита попыток
This commit is contained in:
@@ -152,7 +152,7 @@ Authorization: Bearer <JWT>
|
|||||||
- `author` может быть `null`, если автор не сохранён (старые данные).
|
- `author` может быть `null`, если автор не сохранён (старые данные).
|
||||||
- `averageScore` — среднее по выставленным оценкам, `null` если оценок нет.
|
- `averageScore` — среднее по выставленным оценкам, `null` если оценок нет.
|
||||||
- `submissions` — последние попытки по убыванию `seq`, по умолчанию не более
|
- `submissions` — последние попытки по убыванию `seq`, по умолчанию не более
|
||||||
50 (`submissionLimit`, максимум 500); `submissionsTotal` — всего попыток,
|
50 (`submissionLimit`, максимум 500; `0` — все); `submissionsTotal` — всего попыток,
|
||||||
`attemptsCount` — тоже полное число. Файлы — имена без пути.
|
`attemptsCount` — тоже полное число. Файлы — имена без пути.
|
||||||
- `downloadUrl` относительный; web-BFF добавляет авторизацию.
|
- `downloadUrl` относительный; web-BFF добавляет авторизацию.
|
||||||
|
|
||||||
@@ -247,7 +247,7 @@ GET /api/v1/internal/courses/{courseId}/labs
|
|||||||
- публикации — `grade_publications`/`grade_publication_students` по группе и лабам.
|
- публикации — `grade_publications`/`grade_publication_students` по группе и лабам.
|
||||||
- Список лаб курса кэшируется на время запроса.
|
- Список лаб курса кэшируется на время запроса.
|
||||||
- Для больших курсов — лимит попыток: `submissionLimit` (по умолчанию 50,
|
- Для больших курсов — лимит попыток: `submissionLimit` (по умолчанию 50,
|
||||||
максимум 500) и `submissionsTotal` для индикатора «показаны последние N»;
|
максимум 500, `0` — все) и `submissionsTotal` для индикатора «показаны последние N»;
|
||||||
`attemptsCount` не зависит от лимита.
|
`attemptsCount` не зависит от лимита.
|
||||||
- Индексы: `submissions(student_id, lab_id, seq)` уже уникален;
|
- Индексы: `submissions(student_id, lab_id, seq)` уже уникален;
|
||||||
`grade_comments(student_id, lab_id)` уже есть; `lab_grades(lab_id)` добавлен;
|
`grade_comments(student_id, lab_id)` уже есть; `lab_grades(lab_id)` добавлен;
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ row-level XLSX-импорт и audit-события ростера.
|
|||||||
| Mutation `CourseAccessService` | 75% test strength (порог 70%) |
|
| Mutation `CourseAccessService` | 75% test strength (порог 70%) |
|
||||||
| Mutation `PromotionPlanner` | 69% test strength (порог 60%) |
|
| Mutation `PromotionPlanner` | 69% test strength (порог 60%) |
|
||||||
| Browser E2E | 18/18 (Chromium/Firefox/WebKit) |
|
| Browser E2E | 18/18 (Chromium/Firefox/WebKit) |
|
||||||
| Load smoke | 125 запросов, 0 ошибок, p95 0.942 с (порог < 1 с) |
|
| Load smoke | 125 запросов, 0 ошибок, p95 0.918 с (порог < 1 с) |
|
||||||
|
|
||||||
## Что добавлено в этой итерации
|
## Что добавлено в этой итерации
|
||||||
|
|
||||||
|
|||||||
@@ -290,7 +290,7 @@ class GroupService(
|
|||||||
MembershipHistoryRow(
|
MembershipHistoryRow(
|
||||||
id = event.id.toString(),
|
id = event.id.toString(),
|
||||||
studentId = event.studentId.toString(),
|
studentId = event.studentId.toString(),
|
||||||
studentName = studentsById[event.studentId]?.fullName.orEmpty(),
|
studentName = studentsById[event.studentId]?.fullName?.takeIf { it.isNotBlank() } ?: event.studentId.toString(),
|
||||||
fromGroupId = event.fromGroupId?.toString(),
|
fromGroupId = event.fromGroupId?.toString(),
|
||||||
fromGroupName = event.fromGroupId?.let { groupsById[it]?.name },
|
fromGroupName = event.fromGroupId?.let { groupsById[it]?.name },
|
||||||
toGroupId = event.toGroupId?.toString(),
|
toGroupId = event.toGroupId?.toString(),
|
||||||
@@ -306,8 +306,8 @@ class GroupService(
|
|||||||
@Transactional
|
@Transactional
|
||||||
fun restoreGroup(groupId: UUID): GroupDetail {
|
fun restoreGroup(groupId: UUID): GroupDetail {
|
||||||
val group = groups.findById(groupId).orElseThrow { NotFoundException("Группа не найдена") }
|
val group = groups.findById(groupId).orElseThrow { NotFoundException("Группа не найдена") }
|
||||||
if (group.status == GroupStatus.ACTIVE) {
|
if (group.status != GroupStatus.ARCHIVED) {
|
||||||
throw BadRequestException("Группа ${group.name} уже активна")
|
throw BadRequestException("Группа ${group.name} не находится в архиве")
|
||||||
}
|
}
|
||||||
group.status = GroupStatus.ACTIVE
|
group.status = GroupStatus.ACTIVE
|
||||||
groups.save(group)
|
groups.save(group)
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ class StudentProfileService(
|
|||||||
submissionLimit: Int = DEFAULT_SUBMISSION_LIMIT,
|
submissionLimit: Int = DEFAULT_SUBMISSION_LIMIT,
|
||||||
): StudentCourseProfile {
|
): StudentCourseProfile {
|
||||||
val role = normalizeRole(callerRole)
|
val role = normalizeRole(callerRole)
|
||||||
val effectiveLimit = submissionLimit.coerceIn(1, MAX_SUBMISSION_LIMIT)
|
val effectiveLimit = if (submissionLimit <= 0) Int.MAX_VALUE else submissionLimit.coerceIn(1, MAX_SUBMISSION_LIMIT)
|
||||||
val course = academic.getCourse(courseId.toString(), token)
|
val course = academic.getCourse(courseId.toString(), token)
|
||||||
val context = identity.getStudentContext(studentId.toString())
|
val context = identity.getStudentContext(studentId.toString())
|
||||||
requireProfileAccess(course, context, studentId, callerId, role)
|
requireProfileAccess(course, context, studentId, callerId, role)
|
||||||
|
|||||||
@@ -288,6 +288,10 @@ class StudentProfileIntegrationTest {
|
|||||||
assertThat(lab.submissionsTotal).isEqualTo(3)
|
assertThat(lab.submissionsTotal).isEqualTo(3)
|
||||||
assertThat(lab.attemptsCount).isEqualTo(3)
|
assertThat(lab.attemptsCount).isEqualTo(3)
|
||||||
assertThat(lab.submissions.map { it.seq }).containsExactly(3)
|
assertThat(lab.submissions.map { it.seq }).containsExactly(3)
|
||||||
|
|
||||||
|
val unlimited = profiles.profile(courseId, studentId, UUID.randomUUID(), "ADMIN", TEACHER_TOKEN, submissionLimit = 0)
|
||||||
|
val unlimitedLab = unlimited.labs.single()
|
||||||
|
assertThat(unlimitedLab.submissions.map { it.seq }).containsExactly(3, 2, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun stubCourse(
|
private fun stubCourse(
|
||||||
|
|||||||
@@ -113,10 +113,11 @@ class AdminGroupController(
|
|||||||
model: Model,
|
model: Model,
|
||||||
): String {
|
): String {
|
||||||
val safePage = page.coerceAtLeast(0)
|
val safePage = page.coerceAtLeast(0)
|
||||||
val rows = api.listGroups(token(session), safePage, GROUP_PAGE_SIZE)
|
val loaded = api.listGroups(token(session), safePage, GROUP_PAGE_SIZE + 1)
|
||||||
|
val rows = loaded.take(GROUP_PAGE_SIZE)
|
||||||
model.addAttribute("groups", rows)
|
model.addAttribute("groups", rows)
|
||||||
model.addAttribute("page", safePage)
|
model.addAttribute("page", safePage)
|
||||||
model.addAttribute("hasNext", rows.size == GROUP_PAGE_SIZE)
|
model.addAttribute("hasNext", loaded.size > GROUP_PAGE_SIZE)
|
||||||
return "admin/groups"
|
return "admin/groups"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user