From 881aba6b35dfdce01ebc2eaa84baf4c5d1c58b00 Mon Sep 17 00:00:00 2001 From: codex Date: Thu, 8 Oct 2026 13:10:38 +0300 Subject: [PATCH 1/8] fix: include VAK specialties in council XLSX rows --- README.md | 5 +++- app/services/council_export.py | 21 ++++++++++++----- app/version.py | 6 ++--- pyproject.toml | 2 +- tests/browser_council.py | 4 ++++ tests/test_api.py | 2 +- tests/test_council.py | 42 +++++++++++++++++++++++++++++++--- 7 files changed, 67 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 291aa81..e350ca8 100644 --- a/README.md +++ b/README.md @@ -155,7 +155,7 @@ node --check app/static/admin.js Тесты с данными используют временную SQLite; отдельные API smoke-тесты запускают приложение с его текущей конфигурацией. Браузерная проверка: `pip install playwright`, `python -m playwright install chromium`, затем `python tests/browser_admin.py`. Она проверяет реальный рендеринг страниц, клавиатуру, диалог, фильтры и восстановление прогресса после ошибки. Скриншоты сохраняются во временную папку; путь выводится в конце. -Версия сервиса: `0.15.0`. Админка всегда показывает версии backend и frontend в footer. +Версия сервиса: `0.15.1`. Админка всегда показывает версии backend и frontend в footer. ## Разовый импорт публикаций @@ -173,6 +173,9 @@ node --check app/static/admin.js известным типом, отличным от статьи, не подтверждают специальность. Счётчики Q1/Q2/К1 сохраняют прежний смысл; число работ по выбранной специальности указано внутри карточки сотрудника. Фильтр сохраняется при поиске, смене страницы и экспорте XLSX. +На листах «Сотрудники» и «Публикации» XLSX отдельная колонка содержит коды и названия +подтверждённых специальностей, включая выгрузку без фильтра. При выбранном фильтре +добавлены число подходящих работ сотрудника и отметка «Да/Нет» для каждой публикации. `app/data/k1_specialties.json` содержит все 773 журнала из `k1_journals.json`, коды действующих специальностей, ISSN, страницы и строки исходного перечня, историю diff --git a/app/services/council_export.py b/app/services/council_export.py index 04c1589..17fd87e 100644 --- a/app/services/council_export.py +++ b/app/services/council_export.py @@ -22,21 +22,28 @@ def council_xlsx(result: dict) -> bytes: employees.title = "Сотрудники" publications = workbook.create_sheet("Публикации") parameters = workbook.create_sheet("Параметры") + specialty_labels = {item["code"]: f"{item['code']}. — {item['name']}" for item in result["specialties"]} + def specialties_text(codes): + return "\n".join(label for code, label in specialty_labels.items() if code in codes) or "Не подтверждены" + _append(employees, ["№", "ФИО", "Учёная степень", "Q1", "Q2", "К1", "ВСЕГО", "Страница ВШЭ", "Все публикации", - "Профиль ВШЭ", "Примечание"]) + "Профиль ВШЭ", "Примечание", "Специальности ВАК", "Работ по выбранной специальности"]) _append(publications, ["№ сотрудника", "ФИО", "Название публикации", "Год", "Тип материала", "Источники", - "Критерии", "Q1", "Q2", "К1", "В ВСЕГО", "Страница ВШЭ", "DOI", "Ссылка на сайте", "Строки CSV"]) + "Критерии", "Q1", "Q2", "К1", "В ВСЕГО", "Страница ВШЭ", "DOI", "Ссылка на сайте", "Строки CSV", "Специальности ВАК", "По выбранной специальности"]) for number, candidate in enumerate(result["candidates"], 1): note = "Совпадение ФИО: публикации CSV не присвоены автоматически" if candidate["ambiguous"] else "" + specialty_codes = {code for article in candidate["articles"] for code in article["specialty_codes"]} _append(employees, [number, candidate["name"], candidate["degree"], candidate["q1_count"], candidate["q2_count"], candidate["k1_count"], candidate["total_count"], candidate["db_count"], candidate["count"], - candidate["profile_url"], note]) + candidate["profile_url"], note, specialties_text(specialty_codes), + candidate["specialty_count"] if result["specialty"] else None]) for article in candidate["articles"]: _append(publications, [number, candidate["name"], article["title"], article["year"], article["type_label"], "; ".join(article["origins"]), "; ".join(article["criteria"]), *["Да" if article[key] else "Нет" for key in ("q1", "q2", "k1", "qualifies", "in_db")], - article["doi_url"], article["url"], ", ".join(map(str, article["rows"]))]) + article["doi_url"], article["url"], ", ".join(map(str, article["rows"])), specialties_text(article["specialty_codes"]), + ("Да" if article["matches_specialty"] else "Нет") if result["specialty"] else None]) _append(parameters, ["Параметр", "Значение"]) for values in [ ["Специальность ВАК", f"{result['specialty']}. — {result['specialty_name']}" if result["specialty"] else "Все специальности"], @@ -52,13 +59,15 @@ def council_xlsx(result: dict) -> bytes: "Материалы с известным другим типом исключены. К1 — совпадение названия журнала с итоговой категорией " "в справочнике из ВАК категории.pdf; историческая категория не подтверждается. " "Без названия журнала К1 не подтверждён."], + ["Специальности ВАК в строках", "Коды и названия из 12 специальностей, доступных на сайте. Для публикации — специальности релевантного журнала К1, учитываемые за пять лет; для сотрудника — объединение этих специальностей без дублей. Не подтверждены — нет достаточных данных по этим правилам."], + ["Работ / По выбранной специальности", "При выбранном фильтре показаны число соответствующих публикаций сотрудника и отметка Да/Нет для каждой работы. Без фильтра ячейки пустые; список специальностей остаётся заполненным."], ["ВСЕГО", "Уникальные публикации, входящие в Q1 ИЛИ Q2 ИЛИ К1; пересечения учитываются один раз."], ["Страница ВШЭ / Все публикации", "Все годы и категории; общий перечень объединяет CSV и БД без дублей. " "Страница ВШЭ учитывает сохранённые публикации, включая импортированные из CSV; источник указан на листе Публикации."], ]: _append(parameters, values) - for sheet, widths in [(employees, [7, 34, 40, 10, 10, 10, 12, 20, 22, 45, 55]), - (publications, [16, 34, 65, 12, 40, 45, 65, 10, 10, 10, 12, 20, 45, 45, 22]), + for sheet, widths in [(employees, [7, 34, 40, 10, 10, 10, 12, 20, 22, 45, 55, 80, 28]), + (publications, [16, 34, 65, 12, 40, 45, 65, 10, 10, 10, 12, 20, 45, 45, 22, 80, 28]), (parameters, [32, 95])]: sheet.freeze_panes = "A2" sheet.auto_filter.ref = sheet.dimensions diff --git a/app/version.py b/app/version.py index 4d74b00..b1b98a4 100644 --- a/app/version.py +++ b/app/version.py @@ -1,3 +1,3 @@ -APP_VERSION = "0.15.0" -FRONTEND_VERSION = "0.15.0" -BACKEND_VERSION = "0.15.0" +APP_VERSION = "0.15.1" +FRONTEND_VERSION = "0.15.1" +BACKEND_VERSION = "0.15.1" diff --git a/pyproject.toml b/pyproject.toml index 0da2f20..69a4cda 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "miem-workers" -version = "0.15.0" +version = "0.15.1" description = "MIEM employees parser, admin API, and web admin" requires-python = ">=3.11" dependencies = [ diff --git a/tests/browser_council.py b/tests/browser_council.py index 1a02c08..838a3ba 100644 --- a/tests/browser_council.py +++ b/tests/browser_council.py @@ -157,6 +157,10 @@ def main(): filtered = load_workbook(BytesIO(Path(pending_download.value.path()).read_bytes())) assert filtered["Сотрудники"].max_row == 2 assert dict(filtered["Параметры"].values)["Специальность ВАК"].startswith("1.2.2.") + headers = {cell.value: cell.column for cell in filtered["Сотрудники"][1]} + assert filtered["Сотрудники"].cell(2, headers["Специальности ВАК"]).value.startswith("1.2.2.") + assert filtered["Сотрудники"].cell(2, headers["Работ по выбранной специальности"]).value == 1 + pending_download.value.save_as(str(output / "specialty-export.xlsx")) assert not errors, errors browser.close() finally: diff --git a/tests/test_api.py b/tests/test_api.py index 5428a13..6eca2f7 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"] == "0.15.0" + assert response.json()["backend_version"] == "0.15.1" def test_api_employees_and_stats_require_admin_session(): diff --git a/tests/test_council.py b/tests/test_council.py index 667114f..06249c4 100644 --- a/tests/test_council.py +++ b/tests/test_council.py @@ -211,7 +211,7 @@ def test_public_page_data_minimization_and_safe_links(db_session, export): assert "<script>" in response.text and "