Skip to content

Групповые (командные) лабораторные работы (#54) - #55

Open
markpolyak wants to merge 10 commits into
feature/bulk-gradingfrom
feature/team-assignments
Open

Групповые (командные) лабораторные работы (#54)#55
markpolyak wants to merge 10 commits into
feature/bulk-gradingfrom
feature/team-assignments

Conversation

@markpolyak

Copy link
Copy Markdown
Owner

Closes #54. Продолжение #46: лабораторные работы, которые выполняются командами — один репозиторий на команду, доступ у всех участников.

План реализации — docs/TEAM_ASSIGNMENTS_PLAN.md, закоммичен первым коммитом ветки.

База — feature/bulk-grading (#53), а не main. Этап проверки работ правит grading/bulk.py, который существует только в той ветке. После вливания #53 базу PR нужно переключить на main.

Что сделано

Команда — это репозиторий. {github-prefix}-team-{N} в организации курса; состав — прямые коллабораторы плюс непринятые приглашения; название и описание живут в поле description репозитория как Название — описание. Нового хранилища не появляется, а RepoProvisioner._ensure_access вместе с починкой протухших приглашений (замена github-reinvite) переиспользуется для каждого участника.

GitHub teams (подход GitHub Classroom) сознательно не используются: членство в team требует членства в организации, что изменило бы модель доступа всего проекта — разбор в §3.2 плана.

  • Конфигурация: секция team с size-max и count-max в конфиге лабы (docs/COURSE_CONFIG.md).
  • Сессия студента: после OAuth для командной лабы вместо создания репозитория выставляется подписанная cookie join_session (HttpOnly, SameSite=Lax, path=/join, 30 минут). Логин студента берётся только из неё — никогда из тела запроса, query или пути.
  • Эндпоинты: GET /join/{c}/{l}/teams, POST /join/{c}/{l}/teams, POST /join/{c}/{l}/teams/{slug}/join.
  • Проверка работ: evaluate_student(..., repo_name=...) вызывается один раз на команду, оценка разносится по строкам участников с индивидуальной проверкой защиты ячейки. TASKID для командных лаб отключён, режим by_file в групповой проверке отклоняется.
  • Frontend: экран выбора команды (список, создание, карточка своей команды), переводы ru/en/zh.
  • Инструкция преподавателю (docs/PROJECT_DESCRIPTION.md): как удалить студента из команды, чтобы тот смог выбрать другую, как переименовать команду, как закрыть создание новых.

Исправления по код-ревью (96e0b1a)

  • Лок лабы брался по сырому lab_id из URL, а find_lab_config приводит 5, 05, ЛР5 и lab5 к одной лабе — два студента получали два разных лока и одновременно проходили count-max, size-max, TITLE_TAKEN и ALREADY_IN_TEAM. Теперь лочится по каноническому ключу конфига; заодно перестал неограниченно расти _lab_locks.
  • Флаг members_unknown игнорировался: сбой GitHub на чтении состава читался как «студент ни в какой команде не состоит». В create_team проверки не было вовсе (студент заводил вторую команду со вторым репозиторием), в join_team она смотрела только на целевую команду, а групповая проверка сообщала преподавателю ложный no_team для целой команды.
  • Определений членства было два: _ensure_access пропускал выдачу приглашения любому, у кого есть доступ на любом уровне, а состав команды считался по прямым коллабораторам с push. Студент с правом чтения — или с записью, унаследованной от базовых прав организации — получал «доступ выдан» на каждом заходе и навсегда оставался для проверки работ студентом без команды. Единственным определением членства объявлен состав команды; join_team передаёт force_invite для отсутствующих в нём, add_collaborator запрашивает push явно.

Тесты

pytest tests/ -v — 621 зелёный (461 до задачи). Новое: tests/test_teams.py, плюс командные разделы в tests/test_join_endpoints.py, tests/test_bulk_grading.py, tests/test_repo_provisioning.py. Проверка индивидуальных лаб не менялась: tests/test_grade_lab_characterization.py не тронут и зелёный.

Требует ручной проверки на живом GitHub

Моками не закрывается:

  1. Репозиторий реально создаётся из template-repo в обоих режимах repo-provisioning, приватный, создатель добавлен коллаборатором, description содержит название команды.
  2. Кнопка «Восстановить доступ» пересоздаёт протухшее приглашение.
  3. Полный сценарий двумя тестовыми аккаунтами: первый создаёт команду, второй присоединяется, оба получают доступ к одному репозиторию.
  4. После удаления студента из коллабораторов и отмены непринятого приглашения он действительно может выбрать другую команду.

Вне объёма

  • Этап 7 плана — управление командами из админки (список команд, удаление участника). Пока только инструкция преподавателю через интерфейс GitHub.
  • Самостоятельный выход студента из команды: приглашённый, нажавший «вступить» не в ту команду, разблокируется только преподавателем.
  • Вариант задания (TASKID) на команду, минимальный размер команды, признак «формирование команд закрыто».

Известные мелочи, оставленные намеренно: заголовок команды, оканчивающийся на тире, ломает round-trip через description; TEAM_SLUG_RE использует \d и $ вместо [0-9] и \Z (неэксплуатируемо — slug обязан совпасть с существующей командой). Файл .DS_Store попал в коммит 8074f18 и убран в f4e2c5d; в HEAD его нет, но в истории трёх промежуточных коммитов он остаётся.

🤖 Generated with Claude Code

markpolyak and others added 10 commits September 8, 2026 19:43
План реализации групповых лабораторных работ (issue #54), продолжение #46.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Присутствие секции team (даже пустой) делает лабу командной. Ограничения
size-max и count-max необязательны, но если заданы - должны быть целыми
числами не меньше 1: некорректное значение отдаёт понятную ошибку
конфигурации на /join, а не 500.

Для командной лабы проверка TASKID не выполняется: номер варианта
выводится из порядкового номера студента в таблице, у команды такого
номера нет. Сочетание team и taskid-max пишется в лог при старте.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Колбэк OAuth для командной лабы больше не создаёт репозиторий: он
выставляет подписанную cookie join_session (HttpOnly, SameSite=Lax,
path=/join, 30 минут) и возвращает студента на страницу лабы со
статусом authenticated - выбор команды впереди.

require_join_session берёт GitHub-логин только из этой cookie и сверяет
курс и лабу из неё с путём запроса. Из тела запроса, query-параметров и
пути логин не принимается никогда: иначе доступ к приватному
репозиторию команды выдавался бы по чужому логину.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Команда - это репозиторий {github-prefix}-team-{N}: имя даёт slug,
поле description - название и описание, прямые коллабораторы и
непринятые приглашения - состав. Отдельного хранилища не появляется.

Участником считается коллаборатор с правом push и без admin - фильтр
отсекает владельцев организации, попадающих в список по организационной
роли; логины из github.teachers исключаются дополнительно. Недоступный
состав одной команды помечается флагом и не ломает весь экран.

Список кэшируется на 30 секунд: группа в 30 человек, одновременно
открывшая страницу, тратит один набор запросов вместо тридцати. Ссылка
на репозиторий отдаётся только для своей команды.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Изменяющие операции идут под блокировкой своей лабы и внутри неё
перечитывают список команд с fresh=True, минуя кэш: два студента не
могут занять один номер или одно название. После успешной мутации кэш
сбрасывается.

Номер команды - наименьший свободный среди существующих репозиториев,
поэтому отдельного счётчика не нужно, а удалённая команда освобождает
свой номер. Slug из запроса проверяется по TEAM_SLUG_RE, имя
репозитория собирает сервер: имя репозитория из запроса не принимается
никогда.

RepoProvisioner.provision получил необязательный access_username:
репозиторий называется по команде, а доступ выдаётся студенту. Для
индивидуальных лаб параметр не передаётся и поведение не меняется.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Репозиторий у команды один, поэтому проверяется он один раз, а результат
разносится по строкам участников. evaluate_student вызывается ровно один
раз на команду с синтетическим SheetContext (пустая ячейка, нет
порядкового номера), а защита ячейки применяется потом к каждому
участнику по его собственной ячейке. Тяжёлая часть - файлы, коммиты,
check-runs, логи job'ов - не повторяется на каждого студента.

Одиночная проверка находит репозиторий команды по логину студента и
пишет оценку только в его строку: публичный эндпоинт не пишет в чужие
строки. Студент без команды получает понятное сообщение, а не
«репозиторий не найден».

Режим by_file для командных лаб отклоняется: в репозитории один файл с
ФИО на несколько человек. Студенты без команды попадают в отчёт со
статусом no_team, в отчёт добавлена колонка команды.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Страница /join для командной лабы проходит четыре состояния: лендинг с
кнопкой входа, выбор команды со списком и формой создания, карточка
своей команды с кнопкой «Восстановить доступ», экран ошибки. Признак
«студент авторизован» - успешный ответ GET .../teams, а не
query-параметр: cookie join_session помечена HttpOnly и странице не
видна, зато переживает перезагрузку.

Заполненность команды показывается как «3 из 4», непринявшие
приглашение участники помечены отдельно - видно, почему место занято.
Кнопка присоединения блокируется для полных команд и для студента,
который уже в команде; форма создания скрывается при достижении
count-max с пояснением.

Коды ошибок командных эндпоинтов переведены на ru, en и zh. Инструкция
преподавателю (как удалить студента из команды, переименовать её,
удалить пустую и закрыть создание новых) - в PROJECT_DESCRIPTION.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Файл попал в коммит этапа 3 по недосмотру: он служебный, macOS создаёт
его сам и в репозитории ему не место. На диске файл остаётся, но снова
становится неотслеживаемым.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Колонка «Команда» и статус no_team в таблице отчёта. Колонка появляется
только у командной лабы: у индивидуальной поле пустое у всех строк, и
лишний столбец только мешает.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Три дефекта, найденных ревью реализации issue #54.

Лок лабы брался по сырому lab_id из URL, а find_lab_config приводит "5",
"05", "ЛР5" и "lab5" к одной лабе - два студента получали два разных лока
на одну лабу и одновременно проходили count-max, size-max, TITLE_TAKEN и
ALREADY_IN_TEAM. _load_lab_for_join теперь отдаёт канонический ключ, и
командные мутации лочатся по нему; заодно перестаёт неограниченно расти
_lab_locks.

Флаг members_unknown игнорировался: сбой GitHub на чтении состава читался
как "студент ни в какой команде не состоит". В create_team проверки не было
вовсе (студент заводил вторую команду со вторым репозиторием), в join_team
она смотрела только на целевую команду, а групповая проверка сообщала
преподавателю ложный no_team для целой команды.

Определений членства было два: _ensure_access пропускал выдачу приглашения
любому, у кого есть доступ на любом уровне, а состав команды считался по
прямым коллабораторам с push. Студент с правом чтения (или с записью,
унаследованной от базовых прав организации) получал "доступ выдан" на каждом
заходе и навсегда оставался для проверки работ студентом без команды.
Единственным определением членства объявлен состав команды; join_team
передаёт force_invite для отсутствующих в нём, add_collaborator запрашивает
push явно.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant