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
24 changes: 24 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ FRONTEND_URL=http://localhost:8080
|------|----------|
| Add API endpoint | `main.py` |
| Change grading logic | `grading/bulk.py` (`evaluate_student`, shared by single and bulk grading) |
| Team lab operations | `grading/teams.py` (`TeamRegistry`) |
| 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 @@ -119,6 +120,29 @@ 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`.

## Team (group) Lab Assignments

Labs with a `team` section in their config are done by teams: one repository per team, shared by
its members (see `docs/TEAM_ASSIGNMENTS_PLAN.md` for the full design and
`docs/PROJECT_DESCRIPTION.md` for the teacher-facing instructions).

- **A team is a repository.** `{github-prefix}-team-{N}` in the course organization; the roster is
its direct collaborators plus pending invitations, and the title/description live in the repo's
`description` field as `Название — описание`. No new storage: `grading/teams.py:TeamRegistry`
reads GitHub, caches the result for 30 s and mutates under a per-lab lock (single-worker backend
required, like `propagate.py`/`bulk.py`). Mutations re-read with `fresh=True` inside the lock.
- **Username only from the cookie.** The team endpoints take the student's GitHub login from the
signed `join_session` cookie (`require_join_session` in `main.py`) and never from the body, query
or path - anything else hands out access to a private repo under someone else's login. The
callback issues that cookie for a team lab instead of creating a repository.
- **`provision(access_username=...)`** separates the repo suffix (a team slug) from the student who
gets access; omitting it keeps the individual-lab behaviour untouched.
- **Grading**: `evaluate_student(..., repo_name=...)` grades the team's repository. It is called
exactly ONCE per team with a synthetic `SheetContext` (`current_cell_value=""`,
`student_order=None`); `can_overwrite_cell` is then applied per member against their own cell.
Calling it per member would triple the GitHub work. TASKID is off for team labs
(`taskid_column` returns None), and bulk `by_file` mode is refused.

## Bulk Grading (admin)

Grades a whole group for one lab in a single run, started from the admin lab list page
Expand Down
35 changes: 35 additions & 0 deletions docs/COURSE_CONFIG.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,41 @@ labs:

**Обновление стартового кода уже созданных репозиториев.** Только для `repo-provisioning: fork` - в админке (`/admin/courses/{course_id}/labs`) появляется кнопка «Обновить репозитории студентов», которая рассылает предложение обновления (pull request) во все репозитории-форки этой лабы. Слияние PR остаётся на усмотрение студента - это не принудительный push. Репозитории, созданные до переключения на `fork` (через `generate` или в GitHub Classroom), в рассылку не попадают - для них нет форк-связи с шаблоном. Подробности - в `docs/PROJECT_DESCRIPTION.md`, раздел «Рассылка обновлений стартового кода».

### `team`
**Тип:** `dict`
**Описание:** Признак командной (групповой) лабораторной работы. Присутствие секции - даже пустой (`team: {}`) - переключает ссылку `/join/{course_id}/{lab_id}` на командный сценарий: вместо немедленного создания личного репозитория студент видит список команд лабы и присоединяется к одной из них или создаёт новую. Репозиторий у команды один: `{github-prefix}-team-{N}`, все участники - его прямые коллабораторы. Формат ссылки для студентов не меняется.

Требования те же, что и для индивидуальной лабы: обязателен `template-repo`; `repo-provisioning` работает в обоих режимах (`template` и `fork`).

**Пример:**
```yaml
labs:
"5":
github-prefix: os-task5
short-name: ЛР5
template-repo: suai-os-2026/os-task5-template
repo-provisioning: fork
team:
size-max: 4
count-max: 8
```

Подробности - в `docs/PROJECT_DESCRIPTION.md`, раздел «Командные лабораторные работы» (в том числе как удалить студента из команды, переименовать её и закрыть создание новых).

### `team.size-max`
**Тип:** `int`
**По умолчанию:** без ограничения
**Описание:** Максимальное число участников в одной команде. Учитываются и принятые приглашения, и ожидающие: студент, получивший приглашение, но ещё не открывший его, уже занимает место. По достижении лимита кнопка «Присоединиться» у этой команды недоступна.

### `team.count-max`
**Тип:** `int`
**По умолчанию:** без ограничения
**Описание:** Максимальное число команд у лабы. По достижении лимита создание новых команд закрывается, вступление в уже созданные неполные команды продолжает работать.

**Валидация.** `size-max` и `count-max`, если заданы, должны быть целыми числами не меньше 1. Некорректное значение (`0`, строка, дробное) не игнорируется молча: ссылка `/join/...` для такой лабы отдаёт ошибку конфигурации, как и при отсутствующем `template-repo`.

**`taskid-max` для командной лабы игнорируется:** номер варианта выводится из порядкового номера студента в таблице, у команды такого номера нет, поэтому проверка TASKID для командных лаб не выполняется. При старте бэкенда такое сочетание пишется в лог как предупреждение.

---

## CI/CD опции
Expand Down
47 changes: 45 additions & 2 deletions docs/PROJECT_DESCRIPTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,9 +177,12 @@ lab_grader_web/
| GET | `/courses/{course_id}/groups/{group_id}/labs` | Список лабораторных работ |
| POST | `/courses/{course_id}/groups/{group_id}/register` | Регистрация студента |
| POST | `/courses/{course_id}/groups/{group_id}/labs/{lab_id}/grade` | Проверка лабораторной работы |
| GET | `/join/{course_id}/{lab_id}` | Публичная информация для страницы присоединения к лабе |
| GET | `/join/{course_id}/{lab_id}` | Публичная информация для страницы присоединения к лабе (в том числе признак командной работы и лимиты) |
| GET | `/join/{course_id}/{lab_id}/start` | Начало GitHub OAuth Flow для создания репозитория студента |
| GET | `/join/callback` | Колбэк GitHub OAuth: создание репозитория и починка доступа (см. `docs/REPO_GENERATION_PLAN.md`) |
| GET | `/join/callback` | Колбэк GitHub OAuth: создание репозитория и починка доступа (см. `docs/REPO_GENERATION_PLAN.md`); для командной лабы - выдача сессии студента |
| GET | `/join/{course_id}/{lab_id}/teams` | Список команд лабы, своя команда и лимиты (нужна cookie `join_session`) |
| POST | `/join/{course_id}/{lab_id}/teams` | Создание команды: `{"title": ..., "description": ...}` |
| POST | `/join/{course_id}/{lab_id}/teams/{slug}/join` | Вступление в команду либо починка доступа к своей |

### Административные маршруты

Expand Down Expand Up @@ -245,6 +248,46 @@ lab_grader_web/
- Ошибка на одном студенте (нет коммитов, неверный вариант, недоступный репозиторий) не останавливает работу - она попадает в его строку отчёта. Останавливают работу только общие сбои: нет столбца `GitHub` или столбца лабы, недоступен список репозиториев организации.
- Отмена проверяется между студентами: накопленная порция дописывается в таблицу, работа завершается со статусом `cancelled`.

### Командные лабораторные работы

Лаба, которую студенты выполняют командами: один репозиторий на команду, доступ у всех её участников. Замена group assignment в GitHub Classroom. Ссылка для студентов та же, что и для индивидуальных лаб - `https://<host>/join/{course_id}/{lab_id}`.

**Как перевести лабу в командный режим.** Добавить в конфиг лабы секцию `team` (см. `docs/COURSE_CONFIG.md`); в ней необязательные `size-max` (максимум участников в команде) и `count-max` (максимум команд). Присутствие секции, даже пустой, - и есть признак командной лабы. Остальное как у индивидуальной: обязателен `template-repo`, `repo-provisioning` работает в обоих режимах.

**Что видит студент.** После входа через GitHub - список уже созданных команд лабы: название, описание, логины участников, заполненность («3 из 4») и кнопку «Присоединиться»; плюс форму создания своей команды. Непринявшие приглашение участники помечены отдельно - видно, почему место занято. Повторный переход по той же ссылке приводит участника команды на карточку его команды с ссылкой на репозиторий и кнопкой «Восстановить доступ», которая пересоздаёт протухшее приглашение.

**Как устроены репозитории команд.** `{github-prefix}-team-{N}` в организации курса, приватные; `N` - наименьший свободный номер среди существующих команд лабы. Участники - прямые коллабораторы с правом `push`; владельцы организации и логины из `github.teachers` в состав команды не попадают. Название и описание команды хранятся в поле `description` репозитория в виде `Название — описание` и правятся преподавателем прямо на GitHub. Отдельного хранилища состав команд не имеет: источник истины - сам репозиторий.

**Как удалить студента из команды** (типовая ситуация: студент ошибся при выборе):

1. Открыть репозиторий команды: `https://github.com/{организация}/{github-prefix}-team-{N}`.
2. Settings → Collaborators and teams.
3. Если студент принял приглашение - напротив его логина нажать `Remove`.
4. Если приглашение ещё не принято - оно показано в разделе `Pending invitations`, нажать `Cancel invitation`. Этот шаг обязателен: непринятое приглашение продолжает занимать место в команде и удерживает студента привязанным к ней.
5. Сообщить студенту, чтобы он снова открыл ссылку `/join/...` - он увидит список команд и сможет выбрать другую.

Коммиты, которые студент успел сделать в старом репозитории, остаются в его истории; при необходимости преподаватель удаляет их обычными средствами Git.

**Как переименовать команду.** Отредактировать `description` репозитория: `Название — описание` (разделитель - пробел, длинное тире, пробел). Если разделителя нет, вся строка показывается студентам как название.

**Как удалить пустую команду.** Удалить репозиторий на GitHub. Номер `N` освободится и будет переиспользован следующей созданной командой.

**Как закрыть создание новых команд.** Выставить `team.count-max` равным текущему числу команд. Вступление в уже созданные неполные команды при этом остаётся доступным: отдельного признака «формирование команд закрыто» нет. Убирать секцию `team` для этого нельзя - лаба перестанет быть командной, и проверка работ перестанет находить репозитории команд.

**Проверка командных работ.** Оценка ставится каждому участнику в его собственную строку таблицы, но репозиторий проверяется один раз на команду: тяжёлая часть (файлы, коммиты, результаты CI, логи job'ов) не повторяется на каждого студента. Защита ячейки (`can_overwrite_cell`) при этом применяется индивидуально - у участника с уже выставленной оценкой она не перезаписывается, остальные оценку получают. Одиночная проверка (студент нажал «Проверить») находит репозиторий его команды по логину и пишет результат только в его строку; студент без команды получает понятное сообщение. В массовой проверке студенты без команды попадают в отчёт со статусом `no_team`, а режим сопоставления по файлу с ФИО для командных лаб недоступен - в репозитории один такой файл на несколько человек.

**Проверка варианта (TASKID) для командных лаб не выполняется:** номер варианта выводится из порядкового номера студента в таблице, у команды такого номера нет. Заданный вместе с `team` ключ `taskid-max` игнорируется, при старте бэкенда это пишется в лог.

**Технические детали и ограничения:**

- Список команд лабы собирается из репозиториев организации (`1 + 2 × число_команд` запросов к GitHub) и кэшируется на 30 секунд: группа из 30 человек, одновременно открывшая страницу, тратит около 21 запроса вместо 630.
- Изменяющие операции (создание команды, вступление) идут под блокировкой своей лабы и внутри неё перечитывают список команд, минуя кэш, - два студента не могут занять один номер, одно название или последнее свободное место. Как и состояние фоновых работ, это корректно только при **одном uvicorn-воркере**.
- Действия преподавателя напрямую на GitHub в момент операции студента блокировкой не охватываются; расхождение исправляется на следующем чтении списка. Лимиты - проверка на момент операции, а не жёсткий инвариант.
- Непринятое приглашение занимает место в команде осознанно: иначе один студент мог бы занять места во всех командах.
- Модерация названий команд не выполняется: текст очищается от управляющих символов и обрезается по длине, за содержание отвечает преподаватель (может отредактировать `description` или удалить репозиторий). Логин создателя команды пишется в лог.
- Личность студента на всех командных эндпоинтах берётся только из подписанной cookie `join_session` (HttpOnly, `SameSite=Lax`, `path=/join`, 30 минут), которую выставляет колбэк OAuth. Из тела запроса, query-параметров и пути логин не принимается никогда.
- Переиспользования состава команд между лабами (аналог «set of teams» в GHC) нет: у каждой лабы свой репозиторий, состав задаётся заново.

## Конфигурация курса

### Индексный файл курсов (`courses/index.yaml`)
Expand Down
Loading
Loading