Skip to content

Перенести UI страницы /join из PR #48 в кодовую базу PR #49 #50

Description

@markpolyak

Контекст

#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» и пустую ссылку на репозиторий.

#48 (откуда берём) #49 (куда переносим) Где править
GET /join/{course}/{lab}, поле названия лабы lab_name lab_short_name JoinLab/index.jsx
GET /join/{course}/{lab}, «лаба не настроена» HTTP 409 HTTP 400 (_load_lab_for_join, main.py) src/api/index.js
Redirect после /join/callback, код ошибки ?status=error&error=<code> ?status=error&reason=<code> JoinLab/index.jsx
Redirect после успеха ?repository=<url> ?repo_url=<url>&username=<login> JoinLab/index.jsx
Словарь кодов ошибок см. state.js #48 см. таблицу в п.4 ниже JoinLab/state.js

Разница в HTTP-кодах (409 → 400) в предыдущей редакции задачи отсутствовала: если её не учесть, все конфигурационные ошибки лабы схлопнутся в «непредвиденная ошибка».

Что нужно сделать

1. Скопировать из ветки #48 (feature/public-join-github-oauth)

Файлы целиком:

  • frontend/courses-front/src/components/JoinLab/index.jsx
  • frontend/courses-front/src/components/JoinLab/styled.js
  • frontend/courses-front/src/components/JoinLab/state.js
  • frontend/courses-front/src/components/JoinLab/state.test.js
  • 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/src/App.jsxдва роута:

    <Route path="/join/error" element={<JoinLab />} />
    <Route path="/join/:courseId/:labId" element={<JoinLab />} />
  • 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) обходит директорию рекурсивно сам.

2. Удалить из #49 замещённое

3. Кнопка «Назад»

Страница #48 — самостоятельный лендинг без навигации; в #49 у неё были ButtonBack и Breadcrumb. Переносим только кнопку «Назад»:

  • в JoinLab/index.jsx добавить useNavigate и кнопку, ведущую на / (стиль взять из ../course-list/styledButtonBack, как это было в JoinLab/index.jsx ветки Автоматическое создание репозитория студента (замена GitHub Classroom) #49), разместив её над карточкой;
  • Breadcrumb не переносить: он делает лишний запрос fetchCourseDetails и дублирует блок «Курс / Лабораторная работа», который на странице уже есть.

Если кнопка заметно ломает вёрстку карточки (JoinPage — grid с place-items: center), допустимо её не добавлять — это необязательный элемент. В этом случае явно указать причину в описании коммита.

4. Адаптировать state.js под контракт #49

ERROR_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_denied join.errors.oauthDenied
missing_code, oauth_exchange_failed join.errors.oauthFailed
oauth_not_configured join.errors.oauthNotConfigured
invalid_state join.errors.oauthStateExpired
config, INVALID_TEMPLATE_CONFIG, join_not_configured join.errors.notConfigured
TEMPLATE_NOT_FOUND join.errors.templateUnavailable
CREATE_VALIDATION_FAILED, CREATE_FAILED join.errors.repositoryFailed
CREATE_FORBIDDEN 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)."""
        return f"{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 обрабатывает этот роут без доработок — перенести её как есть.

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) — правка бэкенда потянет за собой переделку его тестов и выйдет дороже, а не дешевле.

Проверка

Автоматически:

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 — скрипт написан неверно).

Вручную (стенд поднят у автора задачи, отдельно его собирать не нужно):

  1. Запушить изменения в claude/student-repo-auto-creation-stp506.
  2. Попросить автора задачи перезапустить локальный стенд из этой ветки (git pull + пересборка) и сообщить URL фронтенда. Дождаться подтверждения, дальше проверять по чек-листу.
  3. Чек-лист:
    • /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 → ссылка не отображается, показана ошибка о некорректной ссылке.

Не входит в объём

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions