fix: include VAK specialties in council XLSX rows
This commit is contained in:
1 parent
675a3b5c6d
commit
881aba6b35
7 files changed
+67
-15
No files matched your search
@@ -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, страницы и строки исходного перечня, историю
|
||||
|
||||
@@ -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
|
||||
|
||||
+3
-3
@@ -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"
|
||||
+1
-1
@@ -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 = [
|
||||
|
||||
@@ -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:
|
||||
|
||||
+1
-1
@@ -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():
|
||||
|
||||
+39
-3
@@ -211,7 +211,7 @@ def test_public_page_data_minimization_and_safe_links(db_session, export):
|
||||
assert "<script>" in response.text and "<script>" not in response.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 0.15.0 · Frontend 0.15.0" in response.text
|
||||
assert "Backend 0.15.1 · Frontend 0.15.1" 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
|
||||
@@ -256,11 +256,11 @@ def test_xlsx_exports_all_filtered_workers_in_order_and_deduplicated_evidence(db
|
||||
people, works = book["Сотрудники"], book["Публикации"]
|
||||
assert [cell.value for cell in people[1]] == [
|
||||
"№", "ФИО", "Учёная степень", "Q1", "Q2", "К1", "ВСЕГО", "Страница ВШЭ", "Все публикации",
|
||||
"Профиль ВШЭ", "Примечание"
|
||||
"Профиль ВШЭ", "Примечание", "Специальности ВАК", "Работ по выбранной специальности"
|
||||
]
|
||||
assert people["J2"].value == "https://www.hse.ru/staff/test"
|
||||
assert people["K2"].value is None
|
||||
assert people.max_column == 11
|
||||
assert people.max_column == 13
|
||||
assert works["L1"].value == "Страница ВШЭ"
|
||||
assert people.max_row == 29 and works.max_row == 29
|
||||
assert people["B2"].value == "Петров 27" and people["B29"].value == "Петров 00"
|
||||
@@ -529,3 +529,39 @@ def test_specialty_reference_integrity():
|
||||
assert journal["status"] == "ambiguous" and not journal["specialty_codes"]
|
||||
for specialty in data["specialties"]:
|
||||
assert any(specialty["code"] in j["specialty_codes"] for j in data["journals"])
|
||||
|
||||
|
||||
def test_xlsx_specialties_are_explicit_with_and_without_filter(db_session, export):
|
||||
export([row(title="Old journal", doi="10.1234/conflict", year="2020", **{"Название журнала": "Бизнес-информатика"}),
|
||||
row(title="Old journal", doi="10.1234/conflict", year="2024")])
|
||||
person = employee(db_session)
|
||||
for title in ['Matching', 'Second matching']:
|
||||
publication(db_session, person, title=title, kind='ARTICLE', raw_data={'journal': 'Бизнес-информатика'})
|
||||
publication(db_session, person, title='Book', kind='BOOK', raw_data={'journal': 'Бизнес-информатика'})
|
||||
publication(db_session, person, title='Unknown journal', kind='ARTICLE', raw_data={'journal': 'Other'})
|
||||
other = employee(db_session, name='Без соответствия', key='other')
|
||||
publication(db_session, other, title='Unknown', kind='ARTICLE')
|
||||
db_session.commit()
|
||||
from app.services.council_export import council_xlsx
|
||||
for specialty in ('', '1.2.2'):
|
||||
result = council.council_candidates(db_session, date(2026, 10, 8), specialty=specialty, paginate=False)
|
||||
book = load_workbook(BytesIO(council_xlsx(result)))
|
||||
staff = list(book['Сотрудники'].values)
|
||||
works = list(book['Публикации'].values)
|
||||
candidate = next(dict(zip(staff[0], values)) for values in staff[1:] if values[1] == person.full_name)
|
||||
assert candidate['Специальности ВАК'].count('1.2.2.') == 1
|
||||
assert 'Математическое моделирование' in candidate['Специальности ВАК']
|
||||
assert '2.3.5.' in candidate['Специальности ВАК']
|
||||
assert '5.2.2.' not in candidate['Специальности ВАК'] # Only the site's twelve specialties.
|
||||
assert candidate['Работ по выбранной специальности'] == (2 if specialty else None)
|
||||
records = {values[2]: dict(zip(works[0], values)) for values in works[1:]}
|
||||
assert '1.2.2.' in records['Matching']['Специальности ВАК']
|
||||
assert records['Matching']['По выбранной специальности'] == ('Да' if specialty else None)
|
||||
for title in ('Book', 'Unknown journal', 'Old journal'):
|
||||
assert records[title]['Специальности ВАК'] == 'Не подтверждены'
|
||||
assert records[title]['По выбранной специальности'] == ('Нет' if specialty else None)
|
||||
if not specialty:
|
||||
unmatched = next(dict(zip(staff[0], values)) for values in staff[1:] if values[1] == other.full_name)
|
||||
assert unmatched['Специальности ВАК'] == 'Не подтверждены'
|
||||
assert book['Сотрудники'].auto_filter.ref.endswith(f'M{len(staff)}')
|
||||
assert book['Публикации'].auto_filter.ref.endswith(f'Q{len(works)}')
|
||||
Reference in new issue
Block a user