Групповые (командные) лабораторные работы (#54) - #55
Open
markpolyak wants to merge 10 commits into
Open
Conversation
Присутствие секции 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #54. Продолжение #46: лабораторные работы, которые выполняются командами — один репозиторий на команду, доступ у всех участников.
План реализации — docs/TEAM_ASSIGNMENTS_PLAN.md, закоммичен первым коммитом ветки.
Что сделано
Команда — это репозиторий.
{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).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в групповой проверке отклоняется.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
Моками не закрывается:
template-repoв обоих режимахrepo-provisioning, приватный, создатель добавлен коллаборатором,descriptionсодержит название команды.Вне объёма
Известные мелочи, оставленные намеренно: заголовок команды, оканчивающийся на тире, ломает round-trip через
description;TEAM_SLUG_REиспользует\dи$вместо[0-9]и\Z(неэксплуатируемо — slug обязан совпасть с существующей командой). Файл.DS_Storeпопал в коммит8074f18и убран вf4e2c5d; в HEAD его нет, но в истории трёх промежуточных коммитов он остаётся.🤖 Generated with Claude Code