You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
#48 и #49 реализуют одну и ту же фичу (автосоздание репозитория студента через GitHub OAuth, #46). После ручного сравнения обоих вариантов на локальном стенде принято решение:
Задача — перенести фронтенд из #48 в ветку #49. PR #48 после мержа #49 закрывается как заменённый.
Куда коммитить: прямо в ветку claude/student-repo-auto-creation-stp506 (ветка PR #49), чтобы PR #49 стал готов к мержу целиком. Новых PR не создавать.
Почему это не копирование файлов
Бэкенд-контракты /join/... в #48 и #49 разошлись при одинаковых путях эндпоинтов. Компонент JoinLab из #48 читает поля и query-параметры своего бэкенда; скопированный as-is, он покажет «unknown error» и пустую ссылку на репозиторий.
Разница в HTTP-кодах (409 → 400) в предыдущей редакции задачи отсутствовала: если её не учесть, все конфигурационные ошибки лабы схлопнутся в «непредвиденная ошибка».
Что нужно сделать
1. Скопировать из ветки #48 (feature/public-join-github-oauth)
frontend/courses-front/src/language.js — обязателен: index.jsx импортирует из него SUPPORTED_LANGUAGES, без этого файла сборка падает
frontend/courses-front/src/language.test.js
Частично:
frontend/courses-front/src/i18n.js — перенести правку из Добавить выдачу репозиториев через GitHub OAuth #48: lng: readStoredLanguage(), хук i18n.on("languageChanged", applyDocumentLanguage), вызов applyDocumentLanguage(i18n.language). Это то, что делает выбор языка на странице /join запоминаемым.
frontend/courses-front/src/api/index.js — функции fetchJoinLab и getJoinStartUrl (вместе с константой JOIN_REQUEST_TIMEOUT_MS и AbortController).
frontend/courses-front/package.json — добавить только скрипт запуска тестов. Не переносить из Добавить выдачу репозиториев через GitHub OAuth #48 бамп react-router-dom с ^6.23.0 до ^7.18.3 и прочие изменения зависимостей: используемые API (useParams, useSearchParams, useNavigate) есть и в v6, лишний мажорный апгрейд роутера в эту задачу не входит.
Скрипт писать так:
"test": "node --test src"
Не копировать вариант node --test src/**/*.test.js из Добавить выдачу репозиториев через GitHub OAuth #48: без globstar в bash этот glob не разворачивается рекурсивно и не находит src/components/JoinLab/state.test.js. Node 22 (frontend.Dockerfile) обходит директорию рекурсивно сам.
frontend/courses-front/src/components/JoinLab/joinLabWrapper.jsx — в Добавить выдачу репозиториев через GitHub OAuth #48 другая архитектура: компонент сам читает useParams/useSearchParams, отдельный wrapper не нужен (и не может обслужить роут /join/error, у которого нет параметров). Импорт в App.jsx заменить на import { JoinLab } from "./components/JoinLab";.
Плоские ключи переводов Автоматическое создание репозитория студента (замена GitHub Classroom) #49 из src/locales/{ru,en,zh}/translation.json: joinPageTitle, joinCourseLabel, joinLabLabel, joinSignInButton, joinSuccessMessage, joinOpenRepoButton, joinTryAgain, joinLoadError, joinErrorAccessDenied, joinErrorConfig, joinErrorOauthFailed, joinErrorTemplateNotFound, joinErrorForbidden, joinErrorRateLimited, joinErrorGeneric. Это не дубли ключей Добавить выдачу репозиториев через GitHub OAuth #48, а другое пространство имён — после переноса они становятся мёртвыми.
3. Кнопка «Назад»
Страница #48 — самостоятельный лендинг без навигации; в #49 у неё были ButtonBack и Breadcrumb. Переносим только кнопку «Назад»:
Breadcrumbне переносить: он делает лишний запрос fetchCourseDetails и дублирует блок «Курс / Лабораторная работа», который на странице уже есть.
Если кнопка заметно ломает вёрстку карточки (JoinPage — grid с place-items: center), допустимо её не добавлять — это необязательный элемент. В этом случае явно указать причину в описании коммита.
join.errors.createForbidden — новый ключ, текст взять из joinErrorForbidden ветки #49
RATE_LIMITED, rate_limit
join.errors.rateLimit
INVITATIONS_FETCH_FAILED, REINVITE_DELETE_FAILED
join.errors.accessFailed
INVITE_FAILED
join.errors.invitationFailed
join_not_found
join.errors.notFound
request_timeout
join.errors.githubUnavailable
provision_failed, unknown, всё неизвестное
join.errors.unknown
В src/api/index.js в fetchJoinLab заменить ветку response.status === 409 на response.status === 400 (см. таблицу контрактов выше).
Ключи join.errors.oauthStateMissing, oauthStateInvalid, oauthStateMismatch, oauthUnavailable в #49 источника не имеют — удалить их из state.js и из всех трёх файлов переводов. Первый тест в state.test.js проверяет именно эти четыре ключа — переписать его под фактический словарь (осмысленная проверка: каждый код из списка выше имеет ключ, и getSafeRepositoryUrl/shouldShowJoinAction не сломаны).
getSafeRepositoryUrl перенести без изменений — это защита от подделанного repo_url в адресной строке. В #49 значение подставлялось в href без проверки, терять эту валидацию нельзя.
В success-панели вывести username из query-параметра (в #48 такого параметра не было): строка вида GitHub: <strong>{username}</strong>, ключ перевода добавить в ru/en/zh.
5. Точечная правка бэкенда: невалидный state
Сейчас в main.py_parse_join_state бросает HTTPException(400), и студент с протухшей ссылкой видит сырой JSON вместо страницы. Именно под этот случай в #48 существует роут /join/error (в момент ошибки course_id/lab_id неизвестны — они лежат внутри самого state).
Сделать:
добавить хелпер рядом с _join_result_redirect:
def_join_error_redirect(reason: str) ->str:
"""Результат для случая, когда course_id/lab_id ещё неизвестны (битый state)."""returnf"{FRONTEND_URL.rstrip('/')}/join/error?{urlencode({'status': 'error', 'reason': reason})}"
в join_callback обернуть вызов _parse_join_state(state) в try/except HTTPException и вернуть RedirectResponse(_join_error_redirect("invalid_state"));
добавить тест в tests/test_join_endpoints.py: GET /join/callback?code=...&state=<мусор> → редирект на /join/error с reason=invalid_state.
Ветка isStandaloneError / hasLabContext в index.jsx#48 обрабатывает этот роут без доработок — перенести её как есть.
main.py (кроме п.5), grading/repo_provisioning.py, grading/github_client.py уже прошли ревью. Вариант «поправить не фронт, а параметры редиректа в бэкенде» из предыдущей редакции задачи отменён: lab_short_name отдаёт join_lab_info, а не _join_result_redirect, и оба поля покрыты тестами (tests/test_join_endpoints.py, проверки lab_short_name и repo_url) — правка бэкенда потянет за собой переделку его тестов и выйдет дороже, а не дешевле.
Проверка
Автоматически:
pytest tests/ -v
cd frontend/courses-front && npm install --legacy-peer-deps && npm run lint && npm test&& npm run build
Все три фронтовых шага должны быть зелёными; npm test должен реально находить и выполнять state.test.js и language.test.js (если тестов найдено 0 — скрипт написан неверно).
Вручную (стенд поднят у автора задачи, отдельно его собирать не нужно):
Запушить изменения в claude/student-repo-auto-creation-stp506.
Попросить автора задачи перезапустить локальный стенд из этой ветки (git pull + пересборка) и сообщить URL фронтенда. Дождаться подтверждения, дальше проверять по чек-листу.
Чек-лист:
/join/{course}/{lab} открывается, показывает курс и короткое название лабы;
переключатель языка меняет весь текст страницы и переживает перезагрузку (проверить ru → en → zh);
happy path: вход через GitHub → создание репозитория → success-панель с рабочей ссылкой и логином студента;
повторный проход по той же ссылке тем же студентом (репозиторий уже существует) — success, не ошибка;
отказ на экране авторизации GitHub → человекочитаемая ошибка (access_denied), кнопка «Попробовать снова» на месте;
лаба без template-repo в YAML → понятная ошибка «не настроено», не «непредвиденная ошибка»;
/join/error?status=error&reason=invalid_state → страница с ошибкой, а не пустой экран;
подделанный ?status=success&repo_url=https://example.com/a/b → ссылка не отображается, показана ошибка о некорректной ссылке.
Контекст
#48 и #49 реализуют одну и ту же фичу (автосоздание репозитория студента через GitHub OAuth, #46). После ручного сравнения обоих вариантов на локальном стенде принято решение:
grading/github_client.py, а не заводит параллельный клиент);/join/:courseId/:labIdиз Добавить выдачу репозиториев через GitHub OAuth #48 — переносим в Автоматическое создание репозитория студента (замена GitHub Classroom) #49 (главное преимущество — переключатель языка прямо на странице студента и полный набор переводов ошибок).Задача — перенести фронтенд из #48 в ветку #49. PR #48 после мержа #49 закрывается как заменённый.
Куда коммитить: прямо в ветку
claude/student-repo-auto-creation-stp506(ветка PR #49), чтобы PR #49 стал готов к мержу целиком. Новых PR не создавать.Почему это не копирование файлов
Бэкенд-контракты
/join/...в #48 и #49 разошлись при одинаковых путях эндпоинтов. КомпонентJoinLabиз #48 читает поля и query-параметры своего бэкенда; скопированный as-is, он покажет «unknown error» и пустую ссылку на репозиторий.GET /join/{course}/{lab}, поле названия лабыlab_namelab_short_nameJoinLab/index.jsxGET /join/{course}/{lab}, «лаба не настроена»_load_lab_for_join,main.py)src/api/index.js/join/callback, код ошибки?status=error&error=<code>?status=error&reason=<code>JoinLab/index.jsx?repository=<url>?repo_url=<url>&username=<login>JoinLab/index.jsxstate.js#48JoinLab/state.jsРазница в HTTP-кодах (409 → 400) в предыдущей редакции задачи отсутствовала: если её не учесть, все конфигурационные ошибки лабы схлопнутся в «непредвиденная ошибка».
Что нужно сделать
1. Скопировать из ветки #48 (
feature/public-join-github-oauth)Файлы целиком:
frontend/courses-front/src/components/JoinLab/index.jsxfrontend/courses-front/src/components/JoinLab/styled.jsfrontend/courses-front/src/components/JoinLab/state.jsfrontend/courses-front/src/components/JoinLab/state.test.jsfrontend/courses-front/src/language.js— обязателен:index.jsxимпортирует из негоSUPPORTED_LANGUAGES, без этого файла сборка падаетfrontend/courses-front/src/language.test.jsЧастично:
frontend/courses-front/src/i18n.js— перенести правку из Добавить выдачу репозиториев через GitHub OAuth #48:lng: readStoredLanguage(), хукi18n.on("languageChanged", applyDocumentLanguage), вызовapplyDocumentLanguage(i18n.language). Это то, что делает выбор языка на странице/joinзапоминаемым.frontend/courses-front/src/api/index.js— функцииfetchJoinLabиgetJoinStartUrl(вместе с константойJOIN_REQUEST_TIMEOUT_MSиAbortController).frontend/courses-front/src/App.jsx— два роута:frontend/courses-front/package.json— добавить только скрипт запуска тестов. Не переносить из Добавить выдачу репозиториев через GitHub OAuth #48 бампreact-router-domс^6.23.0до^7.18.3и прочие изменения зависимостей: используемые API (useParams,useSearchParams,useNavigate) есть и в v6, лишний мажорный апгрейд роутера в эту задачу не входит.Скрипт писать так:
Не копировать вариант
node --test src/**/*.test.jsиз Добавить выдачу репозиториев через GitHub OAuth #48: безglobstarв bash этот glob не разворачивается рекурсивно и не находитsrc/components/JoinLab/state.test.js. Node 22 (frontend.Dockerfile) обходит директорию рекурсивно сам.2. Удалить из #49 замещённое
frontend/courses-front/src/components/JoinLab/joinLabWrapper.jsx— в Добавить выдачу репозиториев через GitHub OAuth #48 другая архитектура: компонент сам читаетuseParams/useSearchParams, отдельный wrapper не нужен (и не может обслужить роут/join/error, у которого нет параметров). Импорт вApp.jsxзаменить наimport { JoinLab } from "./components/JoinLab";.fetchJoinLabInfoизsrc/api/index.js— её полностью заменяетfetchJoinLabиз Добавить выдачу репозиториев через GitHub OAuth #48. Не оставлять обе.src/locales/{ru,en,zh}/translation.json:joinPageTitle,joinCourseLabel,joinLabLabel,joinSignInButton,joinSuccessMessage,joinOpenRepoButton,joinTryAgain,joinLoadError,joinErrorAccessDenied,joinErrorConfig,joinErrorOauthFailed,joinErrorTemplateNotFound,joinErrorForbidden,joinErrorRateLimited,joinErrorGeneric. Это не дубли ключей Добавить выдачу репозиториев через GitHub OAuth #48, а другое пространство имён — после переноса они становятся мёртвыми.3. Кнопка «Назад»
Страница #48 — самостоятельный лендинг без навигации; в #49 у неё были
ButtonBackиBreadcrumb. Переносим только кнопку «Назад»:JoinLab/index.jsxдобавитьuseNavigateи кнопку, ведущую на/(стиль взять из../course-list/styled→ButtonBack, как это было вJoinLab/index.jsxветки Автоматическое создание репозитория студента (замена GitHub Classroom) #49), разместив её над карточкой;Breadcrumbне переносить: он делает лишний запросfetchCourseDetailsи дублирует блок «Курс / Лабораторная работа», который на странице уже есть.Если кнопка заметно ломает вёрстку карточки (
JoinPage— grid сplace-items: center), допустимо её не добавлять — это необязательный элемент. В этом случае явно указать причину в описании коммита.4. Адаптировать
state.jsпод контракт #49ERROR_TRANSLATION_KEYSпереписать под коды ошибок #49. Полный список источников кодов:/join/callback(main.py):access_denied,missing_code,config,oauth_not_configured,oauth_exchange_failed,provision_failed,invalid_state(новый, см. п.5);result.error_code(grading/repo_provisioning.py):INVALID_TEMPLATE_CONFIG,CREATE_VALIDATION_FAILED,CREATE_FORBIDDEN,RATE_LIMITED,TEMPLATE_NOT_FOUND,CREATE_FAILED,INVITATIONS_FETCH_FAILED,REINVITE_DELETE_FAILED,INVITE_FAILED;fetchJoinLab(src/api/index.js):join_not_found,join_not_configured,rate_limit,request_timeout,unknown.Целевой маппинг (переводы #48 переиспользуются там, где смысл совпадает):
access_deniedjoin.errors.oauthDeniedmissing_code,oauth_exchange_failedjoin.errors.oauthFailedoauth_not_configuredjoin.errors.oauthNotConfiguredinvalid_statejoin.errors.oauthStateExpiredconfig,INVALID_TEMPLATE_CONFIG,join_not_configuredjoin.errors.notConfiguredTEMPLATE_NOT_FOUNDjoin.errors.templateUnavailableCREATE_VALIDATION_FAILED,CREATE_FAILEDjoin.errors.repositoryFailedCREATE_FORBIDDENjoin.errors.createForbidden— новый ключ, текст взять изjoinErrorForbiddenветки #49RATE_LIMITED,rate_limitjoin.errors.rateLimitINVITATIONS_FETCH_FAILED,REINVITE_DELETE_FAILEDjoin.errors.accessFailedINVITE_FAILEDjoin.errors.invitationFailedjoin_not_foundjoin.errors.notFoundrequest_timeoutjoin.errors.githubUnavailableprovision_failed,unknown, всё неизвестноеjoin.errors.unknownВ
src/api/index.jsвfetchJoinLabзаменить веткуresponse.status === 409наresponse.status === 400(см. таблицу контрактов выше).Ключи
join.errors.oauthStateMissing,oauthStateInvalid,oauthStateMismatch,oauthUnavailableв #49 источника не имеют — удалить их изstate.jsи из всех трёх файлов переводов. Первый тест вstate.test.jsпроверяет именно эти четыре ключа — переписать его под фактический словарь (осмысленная проверка: каждый код из списка выше имеет ключ, иgetSafeRepositoryUrl/shouldShowJoinActionне сломаны).getSafeRepositoryUrlперенести без изменений — это защита от подделанногоrepo_urlв адресной строке. В #49 значение подставлялось вhrefбез проверки, терять эту валидацию нельзя.В success-панели вывести
usernameиз query-параметра (в #48 такого параметра не было): строка видаGitHub: <strong>{username}</strong>, ключ перевода добавить в ru/en/zh.5. Точечная правка бэкенда: невалидный
stateСейчас в
main.py_parse_join_stateбросаетHTTPException(400), и студент с протухшей ссылкой видит сырой JSON вместо страницы. Именно под этот случай в #48 существует роут/join/error(в момент ошибкиcourse_id/lab_idнеизвестны — они лежат внутри самогоstate).Сделать:
_join_result_redirect:join_callbackобернуть вызов_parse_join_state(state)вtry/except HTTPExceptionи вернутьRedirectResponse(_join_error_redirect("invalid_state"));tests/test_join_endpoints.py:GET /join/callback?code=...&state=<мусор>→ редирект на/join/errorсreason=invalid_state.Ветка
isStandaloneError/hasLabContextвindex.jsx#48 обрабатывает этот роут без доработок — перенести её как есть.6. Остальной бэкенд #49 не трогать
main.py(кроме п.5),grading/repo_provisioning.py,grading/github_client.pyуже прошли ревью. Вариант «поправить не фронт, а параметры редиректа в бэкенде» из предыдущей редакции задачи отменён:lab_short_nameотдаётjoin_lab_info, а не_join_result_redirect, и оба поля покрыты тестами (tests/test_join_endpoints.py, проверкиlab_short_nameиrepo_url) — правка бэкенда потянет за собой переделку его тестов и выйдет дороже, а не дешевле.Проверка
Автоматически:
Все три фронтовых шага должны быть зелёными;
npm testдолжен реально находить и выполнятьstate.test.jsиlanguage.test.js(если тестов найдено 0 — скрипт написан неверно).Вручную (стенд поднят у автора задачи, отдельно его собирать не нужно):
claude/student-repo-auto-creation-stp506.git pull+ пересборка) и сообщить URL фронтенда. Дождаться подтверждения, дальше проверять по чек-листу./join/{course}/{lab}открывается, показывает курс и короткое название лабы;access_denied), кнопка «Попробовать снова» на месте;template-repoв YAML → понятная ошибка «не настроено», не «непредвиденная ошибка»;/join/error?status=error&reason=invalid_state→ страница с ошибкой, а не пустой экран;?status=success&repo_url=https://example.com/a/b→ ссылка не отображается, показана ошибка о некорректной ссылке.Не входит в объём
react-router-domдо v7 и прочие изменения зависимостей из Добавить выдачу репозиториев через GitHub OAuth #48.repository_uses_templateпоtemplate_repository.full_name, в Автоматическое создание репозитория студента (замена GitHub Classroom) #49 её нет, там толькоrepo_exists). Отдельная задача, к переносу UI отношения не имеет.repo-provisioning: fork(Поддержать создание репозитория студента через fork как альтернативу template, выбор в конфиге лабы #51) и propagate обновлений шаблона (Распространение обновлений стартового репозитория на репозитории студентов через fork-PR (админка) #52)./register,grade_lab, старых модулях оценивания.