diff --git a/.codex-session.md b/.codex-session.md index 8ea3844..0f05d38 100644 --- a/.codex-session.md +++ b/.codex-session.md @@ -1,20 +1,21 @@ ## Сделано -- Ветка `feature/council-xlsx-selection`, создана от обновлённого `main` (`7c02b08`) после слияния PR #71. Версия приложения/frontend/backend `1.4.1`. +- Ветка `feature/council-xlsx-selection`, создана от обновлённого `main` (`7c02b08`) после слияния PR #71. Версия приложения/frontend/backend `1.4.2`. - «Всего» в XLSX использует `candidate.total_count`, как сайт: Q1/Q2/К1/RSCI, у доктора также К2; самостоятельный К2 у кандидата/PhD исключается, пересечения не увеличивают итог. - Первый лист «Подбор диссовета»: специальность ВАК, допустимая отрасль с автоматическим выбором совместимой, поиск, степень, статус, сортировка, счётчики, подтверждённый зачёт, минимум и профиль ВШЭ. Без макросов и новых зависимостей. - Для переключения фильтров выгружаются все сотрудники с учёной степенью и их публикации. Начальные фильтры берутся с сайта; подробные листы сохраняют названия и столбцы. Данные являются снимком; изменения Excel не отправляются на сайт. - Исходные строки остаются текстом; формулы записываются отдельно. Формат и поведение описаны в README и docs/council-evidence.md. +- Исправлено замечание P2 ревью SHA `962e936`: общий сервис отклоняет явно пустую отрасль до построения результата. Список, карточка и экспорт возвращают 422; отсутствие параметра сохраняет техническую отрасль. Регрессии охватывают пустую/непустую БД и параметры специальности/предыдущего выбора. - На первом листе добавлена памятка под списком и ссылки к ней/обратно к фильтрам. Памятка поясняет работу с листом, счётчики, К2, подтверждения и пороги, ограничения специальностей, источников, снимка и Excel; правила расчёта и адреса фильтров не менялись. -- Проверки: полный pytest — 416 passed, 11 skipped; синтаксис четырёх JS-файлов и двух shell-скриптов CI — passed; `git diff --check main` — passed. -- В установленном Windows Excel 16.0 проверены 14 сценариев на синтетических данных и пустой файл: результаты, сортировка, поиск (включая буквальный *), специальности, совместимость отраслей, степень, статусы и пороги совпали с сервисом сайта. Дополнительно проверены переходы к памятке и к фильтрам; памятка экспортирована в PDF, проверено наличие разделов и завершающей ссылки. Артефакты: `.impeccable/review/council-xlsx/sample-checked.xlsx`, `memo-preview.pdf`, `cases.json`, `check.ps1` (игнорируются Git). +- Проверки: после исправления P2 полный pytest — 418 passed, 11 skipped; синтаксис четырёх JS-файлов и двух shell-скриптов CI — passed; `git diff --check main` — passed. +- До исправления P2 в установленном Windows Excel 16.0 проверены 14 сценариев на синтетических данных и пустой файл: результаты, сортировка, поиск (включая буквальный *), специальности, совместимость отраслей, степень, статусы и пороги совпали с сервисом сайта. Дополнительно проверены переходы к памятке и к фильтрам; памятка экспортирована в PDF, проверено наличие разделов и завершающей ссылки. Артефакты: `.impeccable/review/council-xlsx/sample-checked.xlsx`, `memo-preview.pdf`, `cases.json`, `check.ps1` (игнорируются Git). ## Остановились на -- Открыт PR #72: https://git.vakanaut.ru/admin/miem_workers/pulls/72, исходная реализация — `2baa2e1`. Пользователь попросил добавить памятку на первый лист; дополнение готово к коммиту и push в тот же PR. +- Открыт PR #72: https://git.vakanaut.ru/admin/miem_workers/pulls/72. Ревью SHA `962e936` требует исправить один P2; пользователь поручил исправление. Оно готово к коммиту и push в ту же ветку/PR. - PostgreSQL-тесты пропущены без отдельной тестовой БД; production не изменялся. Тестирование Excel проводилось только на синтетическом наборе. ## Следующий шаг -- Сохранить дополнение коммитом и отправить `git push -u origin feature/council-xlsx-selection` в существующий PR #72. +- Сохранить исправление коммитом и отправить `git push -u origin feature/council-xlsx-selection` в существующий PR #72. - После публикации остановиться и ждать независимого ревью в отдельной сессии и дальнейших указаний пользователя. Не выполнять ревью из авторской сессии и не сливать PR. diff --git a/README.md b/README.md index 5a53267..c3daab9 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,7 @@ Compose запускает `api`, `worker` и `postgres`. API привязан Страница `/admin/council` доступна без авторизации. Показывает докторов, кандидатов наук и PhD, включая сотрудников без доступных публикаций; уволенные отмечены отдельно. Высшая степень выбирается по всей истории: доктор выше кандидата/PhD, кандидат и PhD имеют одинаковый уровень. Неподтверждённого доктора нельзя заменить подтверждённым кандидатом. Звание профессора или доцента не заменяет степень. -На странице и в XLSX выбирается отрасль оценки; по умолчанию технические науки. Для доктора порог зависит от отрасли, для кандидата/PhD — 20; самостоятельного К2 недостаточно. Период — пять календарных лет, включая текущий год Москвы. Некорректную специальность или отрасль отклоняет HTTP 422. +На странице и в XLSX выбирается отрасль оценки; по умолчанию технические науки. Для доктора порог зависит от отрасли, для кандидата/PhD — 20; самостоятельного К2 недостаточно. Период — пять календарных лет, включая текущий год Москвы. Некорректную специальность или отрасль отклоняет HTTP 422. Явно пустая отрасль (`branch=`) также отклоняется для списка, карточки и XLSX; отсутствие параметра сохраняет значение по умолчанию «технические». Отдельно показаны предварительные Q1/Q2/К1/К2/RSCI, их объединение ВСЕГО и подтверждённое число, порог, монографии, пункт и причины проверки. Одна работа учитывается один раз, основания сохраняют исходные записи. Без подтверждения высшей степени, даты, отрасли и достаточного числа проверенных работ результат — «Требует проверки». Недостаток записей не доказывает отсутствие работ; соответствие публикационным критериям не означает автоматического включения в совет. diff --git a/app/services/council.py b/app/services/council.py index 7e0c40b..1adbeae 100644 --- a/app/services/council.py +++ b/app/services/council.py @@ -255,7 +255,7 @@ def council_candidates(db: Session, today: date | None = None, q: str = "", offs sort: str = "total_count", direction: str = "desc", paginate: bool = True, specialty: str = "", branch: str = "технические", employee_id: int | None = None, previous_specialty: str | None = None) -> dict: - if branch and branch not in BRANCHES: + if branch not in BRANCHES: raise CouncilInputError("Неизвестная отрасль науки") today = today or datetime.now(ZoneInfo("Europe/Moscow")).date() start, end = period(today) diff --git a/app/version.py b/app/version.py index bca63e0..e4e7483 100644 --- a/app/version.py +++ b/app/version.py @@ -1,3 +1,3 @@ -APP_VERSION = "1.4.1" -FRONTEND_VERSION = "1.4.1" -BACKEND_VERSION = "1.4.1" +APP_VERSION = "1.4.2" +FRONTEND_VERSION = "1.4.2" +BACKEND_VERSION = "1.4.2" diff --git a/pyproject.toml b/pyproject.toml index c2cb2ad..b204836 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "miem-workers" -version = "1.4.1" +version = "1.4.2" description = "MIEM employees parser, admin API, and web admin" requires-python = ">=3.11" dependencies = [ diff --git a/tests/test_api.py b/tests/test_api.py index 1d473a5..6e42b7f 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -18,7 +18,7 @@ def test_health_returns_versions(): response = TestClient(app).get("/api/health") assert response.status_code == 200 - assert response.json()["backend_version"] == "1.4.1" + assert response.json()["backend_version"] == "1.4.2" def test_api_employees_and_stats_require_admin_session(): diff --git a/tests/test_council.py b/tests/test_council.py index a1302dc..e4df701 100644 --- a/tests/test_council.py +++ b/tests/test_council.py @@ -230,7 +230,7 @@ def test_public_page_data_minimization_and_safe_links(db_session, export): assert "javascript:" not in fragment.text and "private@example.org" not in fragment.text assert "javascript:" not in response.text and "private@example.org" not in response.text assert "/admin/employees/" not in response.text and "/admin/logout" not in response.text - assert "Backend 1.4.1 · Frontend 1.4.1" in response.text + assert "Backend 1.4.2 · Frontend 1.4.2" in response.text assert "Вход в систему" in response.text assert client.get("/admin/directory", follow_redirects=False).status_code in (302, 303, 401, 403) assert "Сотрудники не найдены" in client.get("/admin/council?q=missing").text diff --git a/tests/test_council_assessment.py b/tests/test_council_assessment.py index fa90009..8469d9c 100644 --- a/tests/test_council_assessment.py +++ b/tests/test_council_assessment.py @@ -203,6 +203,33 @@ def test_http_validation_context_and_export_parity(db_session, proofs, monkeypat app.dependency_overrides.clear() +@pytest.mark.parametrize("populated", [False, True]) +def test_empty_branch_is_rejected_before_export_and_omitted_branch_keeps_default(db_session, proofs, populated): + if populated: + employee = person(db_session, ["Доктор технических наук"]) + work(db_session, employee) + db_session.commit() + with pytest.raises(council.CouncilInputError, match="Неизвестная отрасль науки"): + council.council_candidates(db_session, branch="") + app.dependency_overrides[get_db] = lambda: db_session + try: + client = TestClient(app) + for query in ("branch=", "branch=&specialty=1.1.2", "branch=&specialty=1.1.2&previous_specialty="): + for path in ("/admin/council", "/admin/council/export.xlsx", "/admin/council/employees/1/articles"): + response = client.get(path + "?" + query) + assert response.status_code == 422 + assert "Неизвестная отрасль науки" in response.text + assert "content-disposition" not in response.headers + assert client.get("/admin/council").status_code == 200 + response = client.get("/admin/council/export.xlsx") + assert response.status_code == 200 + book = load_workbook(BytesIO(response.content)) + assert book["Подбор диссовета"]["C4"].value == "технические" + assert book["Сотрудники"].max_row == (2 if populated else 1) + finally: + app.dependency_overrides.clear() + + def test_unknown_kind_conflicts_and_safe_source_url(db_session, proofs): employee = person(db_session, ["Доктор технических наук"]) record = work(db_session, employee)