Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ FRONTEND_URL=http://localhost:8080
| Task | Location |
|------|----------|
| Add API endpoint | `main.py` |
| Change grading logic | `grading/bulk.py` (`evaluate_student`, shared by single and bulk grading) |
| Add React component | `frontend/courses-front/src/components/` |
| Add/edit course | `courses/` directory + `index.yaml` |
| Add translation | `frontend/courses-front/src/locales/{en,ru,zh}/` |
Expand Down Expand Up @@ -118,6 +119,25 @@ Orchestration lives in `grading/propagate.py` (in-memory job state, single-worke
`docs/PROJECT_DESCRIPTION.md`). All `/admin/...` and course-management routes require the `require_admin`
FastAPI dependency in `main.py`, not just the frontend's `ProtectedRoute`.

## Bulk Grading (admin)

Grades a whole group for one lab in a single run, started from the admin lab list page
(`/admin/courses/{course_id}/labs`) - see `docs/PROJECT_DESCRIPTION.md` for the full behaviour.

- `grading/bulk.py:evaluate_student` holds the grading decision and is shared by `grade_lab` and the
bulk run, so the two cannot drift apart. It never touches Sheets: the spreadsheet context arrives
through a lazily-invoked provider, which is what lets `grade_lab` still answer repository and CI
errors without opening a Sheets connection.
- Endpoints: `POST /admin/courses/{id}/groups/{g}/labs/{l}/bulk-grade` (202 + `job_id`),
`GET /admin/bulk-grade-jobs/{job_id}`, `POST /admin/bulk-grade-jobs/{job_id}/cancel`.
- Job state mirrors `grading/propagate.py` (in-memory, single-worker backend required, 409 on a
second run for the same course/group/lab).
- With `name_file` set, repos are discovered by the lab's prefix and matched to sheet rows by the
first line of that file; without it, only students who already have a username in the sheet are
graded. Name matching is exact after normalization - no fuzzy matching, by design.
- Reads the worksheet once via `get_all_values()` and writes grades in batches of 10: the per-cell
helpers spend ~6 Sheets requests per student, over the 60 reads/minute quota for a group of 30.

## CI/CD

- **Tests**: Run on every push via `.github/workflows/tests.yml`
Expand Down
57 changes: 57 additions & 0 deletions docs/BULK_GRADING_PLAN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Массовая проверка лабораторных: разбор решений

Запись принятых решений по фиче. Поведение и API описаны в `docs/PROJECT_DESCRIPTION.md` (раздел «Массовая проверка лабораторной»); здесь - только то, почему сделано именно так.

## 1. Постановка задачи

Преподаватель выбирает курс, группу и лабораторную и нажимает кнопку. Анализируются репозитории лабы в организации курса, результат проверки проставляется в Google Таблицу. Необязательное поле «файл с ФИО» переключает режим: с ним обходятся все репозитории лабы и GitHub-логины проставляются в таблицу по ФИО из файла, без него проверяются только студенты с уже указанным логином.

## 2. Одно ядро для одиночной и массовой проверки

`grade_lab` (main.py) - функция на 240 строк, в которой перемешаны HTTP-слой, поячеечное чтение Google Таблицы и логика выставления оценки. Массовая проверка не может переиспользовать её как есть: ей нужен тот же алгоритм без `HTTPException` и без обращений к Sheets на каждого студента.

Дублировать алгоритм нельзя - разойдутся. Поэтому решение об оценке вынесено в `grading/bulk.py:evaluate_student`, а `grade_lab` переписан в тонкую обёртку над ним.

### Почему контекст таблицы передаётся через провайдер

`grade_lab` намеренно устроен так, что ошибки репозитория и CI возвращаются **до** подключения к Google Sheets: при отсутствии коммитов или незавершённом CI соединение с таблицей не открывается вовсе. Если бы `evaluate_student` принимала готовый `SheetContext`, вызывающему пришлось бы читать таблицу заранее - и это свойство потерялось бы.

Поэтому `evaluate_student` принимает не значение, а функцию, возвращающую `SheetContext`, и вызывает её не раньше, чем CI даст результат, который есть смысл записывать. `grade_lab` передаёт замыкание, открывающее лист; массовая проверка - функцию, отдающую уже прочитанную строку сетки.

## 3. Чтение таблицы одним запросом

Квота Google Sheets API - 60 запросов чтения в минуту. Поячеечные хелперы (`sheet.cell`, `row_values`, `col_values`, `worksheet.find`) тратят ~6 запросов на студента: на группе из 30 человек это ~180 запросов и гарантированное превышение квоты.

Массовый режим читает лист один раз через `get_all_values()` и ищет по сетке в памяти. В `grading/sheets_client.py` добавлены grid-варианты существующих хелперов; поячеечные не тронуты, ими продолжает пользоваться `grade_lab`. Парсинг дедлайна вынесен в общую `parse_deadline`, чтобы обе ветки разбирали даты одинаково.

Запись - пакетами по 10 ячеек **по ходу работы**, а не одним блоком в конце: падение, перезапуск или отмена сохраняют уже проставленные оценки.

## 4. Сопоставление ФИО - только точное

ФИО берётся из файла, который студент заполнял руками, поэтому сравнение нормализует пробелы (включая неразрывные), регистр и `ё`/`е`. Дальше - только точное совпадение.

Нечёткий подбор (по фамилии, по расстоянию Левенштейна) сознательно не реализован: цена ошибки - оценка, записанная не тому студенту, и обнаружится она нескоро. Несопоставленные репозитории попадают в отчёт с прочитанным ФИО, чтобы преподаватель исправил вручную - это дешевле, чем разбирать последствия неверного совпадения.

По той же причине совпадение ФИО у двух студентов - не повод выбрать первого: строка пропускается со статусом `ambiguous`.

## 5. Конфликт GitHub-логинов

Если в строке студента уже указан **другой** логин, репозиторий не проверяется ни под одним из них и попадает в отчёт со статусом `conflict`. Молча перезаписать логин нельзя (потеряется факт, что студент регистрировался сам), а проверить репозиторий под логином из строки - значит проверить чужую работу.

## 6. Защита оценок

Правило `can_overwrite_cell` (перезаписываются только пустые ячейки, `x` и значения с `?` в начале) действует и здесь. Флага принудительной перезаписи нет: одна случайная галочка затирала бы оценки всей группы, а пересдача - штучная операция, для которой достаточно очистить ячейку в таблице.

## 7. Последовательная обработка

Параллелизм не вводится. GitHub применяет secondary rate limits к всплескам параллельных запросов от одного токена, а работа и так фоновая с прогресс-баром: выигрыш во времени не стоит риска упереться в лимит на середине группы.

## 8. Границы отказа

Ошибка на одном студенте (нет коммитов, неверный вариант, недоступный репозиторий, неожиданное исключение) записывается в его строку отчёта и не останавливает работу. Останавливают только сбои, общие для всего запуска: нет столбца `GitHub` или столбца лабы, недоступен список репозиториев организации - продолжать после них бессмысленно.

## 9. Где живёт интерфейс

Отдельный экран не заводился: преподаватель уже выбирает курс и лабораторную на странице лабораторных в админке (`/admin/courses/{course_id}/labs`), откуда запускается рассылка обновлений шаблона. Массовая проверка добавлена туда же вторым действием в строке. Группа спрашивается в диалоге, потому что список лаб общий для курса, а лист - у каждой группы свой.

Устройство фоновой работы (хранилище в памяти под блокировкой, `202` с `job_id`, опрос статуса, `409` на повторный запуск) повторяет `grading/propagate.py` - тот же сценарий и те же ограничения.
15 changes: 15 additions & 0 deletions docs/COURSE_CONFIG.md
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,21 @@ files:
- README.md
```

### `student-name-file`
**Тип:** `string`
**Описание:** Файл в репозитории студента, первая строка которого содержит его ФИО. Используется только массовой проверкой из админки: по этому ФИО находится строка студента в Google Таблице и в неё записывается GitHub-логин из имени репозитория (см. «Массовая проверка лабораторной» в `docs/PROJECT_DESCRIPTION.md`). Если ключ задан, поле «Файл с ФИО студента» в диалоге проверки предзаполняется этим значением; преподаватель может его изменить или очистить.
**По умолчанию:** не задан - массовая проверка тогда работает только по логинам, уже указанным в таблице
**Пример:**
```yaml
labs:
"1":
github-prefix: os-task1
short-name: ЛР1
files:
- info.md
student-name-file: info.md
```

### `forbidden-modifications`
**Тип:** `list[string]`
**Описание:** Список файлов/директорий, которые студент не может изменять
Expand Down
Loading
Loading