fix: clarify HSE sources and remove reconciled export duplicates
CI / test (pull_request) Successful in 9m26s
CI / Fingerprint diagnostics (production deployment temporarily disabled) (pull_request) Has been skipped

This commit is contained in:
codex committed 2026-10-08 19:25:08 +03:00
1 parent c548998522
commit 33f5a61865
15 files changed
+4493 -25797

No files matched your search

+5
View File
@@ -1,5 +1,10 @@
# Changelog
## 0.16.5
- Источники публикаций названы «Данные ВШЭ» и «ЛК сотрудника ВШЭ» на странице совета и в XLSX. Счётчик личного кабинета не включает записи, полученные только из выгрузки ВШЭ.
- Из выгрузки ВШЭ удалены 797 строк, однозначно найденных у всех указанных авторов в локальной БД workers; метаданные сохранены в 1226 существующих записях, резервные копии созданы. Неоднозначные совпадения и неполные связи авторов оставлены.
## 0.16.4
- Вступление страницы совета и пояснения занимают всю доступную ширину своих блоков; убрана flex-разметка заголовка и ограничения ширины текста.
+6 -6
View File
@@ -94,7 +94,7 @@ Compose запускает `api`, `worker` и `postgres`. API привязан
Отдельно показаны предварительные Q1/Q2/К1/К2/RSCI, их объединение ВСЕГО и подтверждённое число, порог, монографии, пункт и причины проверки. Одна работа учитывается один раз, основания сохраняют исходные записи. Без подтверждения высшей степени, даты, отрасли и достаточного числа проверенных работ результат — «Требует проверки». Недостаток записей не доказывает отсутствие работ; соответствие публикационным критериям не означает автоматического включения в совет.
Сервис читает подготовленный `app/data/council_articles.json` и таблицу `employee_publications`. Справочник преобразуется из CSV скриптом `scripts/prepare_council_articles.py`, проверяет схему и индекс авторов; импорт/парсинг CSV не объявляется доказательством выполнения критериев. Сопоставление CSV по полному ФИО разрешено только при единственном совпадении. Дедупликация использует DOI либо нормализованные название и год при отсутствии неоднозначных DOI. Источники с разными годами не переносят категории между периодами.
Сервис читает подготовленный `app/data/council_articles.json` и таблицу `employee_publications`. Справочник преобразуется из CSV скриптом `scripts/prepare_council_articles.py`, проверяет схему и индекс авторов; импорт/парсинг CSV не объявляется доказательством выполнения критериев. Сопоставление данных ВШЭ по полному ФИО разрешено только при единственном совпадении. Дедупликация использует DOI либо нормализованные название и год при отсутствии неоднозначных DOI. Источники с разными годами не переносят категории между периодами.
К1/К2 сопоставляются по точным нормализованным названиям журнала и документированным алиасам; название статьи, переводы и нечёткое совпадение не используются. Справочник К1 содержит 773 журнала, К2 — 1539; экстрактор проверяет все 3134 строки, даты и SHA-256 источника. Это текущие категории, а не историческое доказательство. Q1/Q2 читаются также из двух тематических JSON-каталогов (схемы 1.0.0/1.1.0, 229 уникальных журналов): ISSN/eISSN, категории CiteScore/JCR, источники и версии паспортов. Снимок метрик 2025 не распространяется автоматически на другие годы; текущая индексация не проверена. Каталоги не являются белым списком. Аналитические прямые/условные связи журнала показываются отдельно от рассмотрения статьи по паспорту.
@@ -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.16.4`. Админка всегда показывает версии backend и frontend в footer.
Версия сервиса: `0.16.5`. Админка всегда показывает версии backend и frontend в footer.
## Разовый импорт публикаций
@@ -210,11 +210,11 @@ JSON-справочника записываются с LF для воспрои
Исходный `app/data/council_articles.csv` сохранён для проверки и отдельного импорта
в БД. Данные файла и индекс кэшируются на процесс, CSV не преобразуется при
HTTP-запросах. Название источника «Файл CSV» и номера строк в интерфейсе и XLSX
сохранены как происхождение данных. Перед разбором публикаций сотрудников из БД
проверяется докторская степень; публикации остальных сотрудников не разбираются.
HTTP-запросах. В интерфейсе и XLSX используются названия «Данные ВШЭ» (выгрузка из базы публикаций ВШЭ) и «ЛК сотрудника ВШЭ» (публикации из личного кабинета). Номера строк выгрузки сохраняются для проверки источника. Показываются сотрудники с обнаруженной или подтверждённой степенью доктора, кандидата наук или PhD.
После обновления CSV пересоздайте JSON до сборки/развёртывания сервиса:
При сверке 08.10.2026 с локальной БД workers из исходной выгрузки удалены 797 строк, однозначно найденных у всех указанных авторов. Их метаданные перенесены в `raw_data.csv_enrichment` 1226 существующих публикаций; парсер сохраняет это обогащение при обновлении записи. Выгрузка теперь содержит 1000 строк и 776 ФИО. Резервные копии исходной выгрузки, JSON и записей БД сохранены в `.impeccable/review/hse-source-backup/` и `.impeccable/review/workers-enrichment-backup.json` (не публикуются в Git). Строки с конфликтами или неполными связями авторов сохранены. Для другого экземпляра БД перенос метаданных из исходной выгрузки необходимо выполнить до использования сокращённого файла: сам Git-коммит данные БД не переносит.
После обновления выгрузки ВШЭ пересоздайте JSON до сборки/развёртывания сервиса:
```sh
python scripts/prepare_council_articles.py app/data/council_articles.csv
File diff suppressed because it is too large. Load diff
+4447 -24959
View File
File diff suppressed because it is too large. Load diff
+4 -4
View File
@@ -116,10 +116,10 @@ def _articles_by_author() -> dict[str, list[dict]]:
def _database_article(publication) -> dict:
raw = publication.raw_data if isinstance(publication.raw_data, dict) else {}
imported = raw.get("csv_enrichment")
from_site = bool(publication.publication_id or publication.url or any(key != "csv_enrichment" for key in raw))
origins = ["БД miem-workers · сайт ВШЭ"] if from_site else []
from_site = not imported or bool(publication.publication_id or publication.url or any(key != "csv_enrichment" for key in raw))
origins = ["ЛК сотрудника ВШЭ"] if from_site else []
if imported:
origins.append("БД miem-workers · импорт CSV")
origins.append("Данные ВШЭ")
kind = publication.publication_type
description = publication.description if isinstance(publication.description, dict) else raw.get("description")
journals = _journal_names(raw)
@@ -132,7 +132,7 @@ def _database_article(publication) -> dict:
"quartiles": _quartiles(imported) if isinstance(imported, dict) else [],
"journals": journals, "issns": [raw[key] for key in ("issn", "eissn") if isinstance(raw.get(key), str)],
"source_hashes": [publication.source_hash],
"origins": origins or ["БД miem-workers"], "rows": [], "in_csv": False, "in_db": True,
"origins": origins, "rows": [], "in_csv": bool(imported), "in_db": from_site,
"url": _safe_url(publication.url)}
+6 -6
View File
@@ -26,16 +26,16 @@ def council_xlsx(result: dict) -> bytes:
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(employees, ["№", "ФИО", "Учёная степень", "Q1", "Q2", "К1", "ВСЕГО", "ЛК сотрудника ВШЭ", "Все публикации",
"Профиль ВШЭ", "Примечание", "Специальности ВАК", "Работ по выбранной специальности",
"Текущая степень", "Степень подтверждена", "Пункт", "Отрасль оценки", "Минимум",
"Подтверждено", "К2 предварительно", "RSCI предварительно", "Итог", "Причины проверки"])
_append(publications, ["№ сотрудника", "ФИО", "Название публикации", "Год", "Тип материала", "Источники",
"Критерии", "Q1", "Q2", "К1", "В ВСЕГО", "Страница ВШЭ", "DOI", "Ссылка на сайте", "Строки CSV", "Специальности ВАК", "По выбранной специальности",
"Критерии", "Q1", "Q2", "К1", "В ВСЕГО", "ЛК сотрудника ВШЭ", "DOI", "Ссылка на сайте", "Строки выгрузки ВШЭ", "Специальности ВАК", "По выбранной специальности",
"К2", "RSCI", "Подтверждённый зачёт", "Дата публикации", "Причины проверки", "Подтверждённые основания",
"Журнал и идентификаторы", "Квартильные категории и источники", "Аналитическая связь журнала", "Рассмотрение статьи"])
for number, candidate in enumerate(result["candidates"], 1):
note = "Совпадение ФИО: публикации CSV не присвоены автоматически" if candidate["ambiguous"] else ""
note = "Совпадение ФИО: данные ВШЭ не присвоены автоматически" 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"],
@@ -69,7 +69,7 @@ def council_xlsx(result: dict) -> bytes:
["Подбор по специальности", "Хотя бы одна публикация за пять лет в релевантном журнале К1 по Перечню ВАК на 15.09.2026; содержание статьи и исторические специальности не проверяются."],
["Начало периода", result["start"]], ["Конец периода", result["end"]], ["Поиск по ФИО", result["q"]],
["Сортировать по", {"full_name": "ФИО", "q1_count": "Q1", "q2_count": "Q2", "k1_count": "К1", "k2_count": "К2", "rsci_count": "RSCI", "confirmed_count": "Подтверждено",
"total_count": "ВСЕГО", "db_count": "Страница ВШЭ", "count": "Все публикации"}[result["sort"]]],
"total_count": "ВСЕГО", "db_count": "ЛК сотрудника ВШЭ", "count": "Все публикации"}[result["sort"]]],
["Направление", "По возрастанию" if result["direction"] == "asc" else "По убыванию"],
["Сотрудников в выгрузке", result["matched"]], ["Всего сотрудников в списке", result["total"]],
["Отбор по степени", "Высшая степень из истории: доктор выше кандидата/PhD; отсутствие подтверждения доктора не переводит оценку на пункт 6. Отсутствие публикаций не исключает сотрудника."],
@@ -81,8 +81,8 @@ def council_xlsx(result: dict) -> bytes:
["Специальности ВАК в строках", "Коды и названия из 12 специальностей, доступных на сайте. Для публикации — специальности релевантного журнала К1, учитываемые за пять лет; для сотрудника — объединение этих специальностей без дублей. Не подтверждены — нет достаточных данных по этим правилам."],
["Работ / По выбранной специальности", "При выбранном фильтре показаны число соответствующих публикаций сотрудника и отметка Да/Нет для каждой работы. Без фильтра ячейки пустые; список специальностей остаётся заполненным."],
["ВСЕГО", "Предварительные уникальные работы Q1/Q2/К1/RSCI либо К2 для доктора; пересечения учитываются один раз."],
["Страница ВШЭ / Все публикации", "Все годы и категории; общий перечень объединяет CSV и БД без дублей. "
"Страница ВШЭ учитывает сохранённые публикации, включая импортированные из CSV; источник указан на листе Публикации."],
["ЛК сотрудника ВШЭ / Все публикации", "Все годы и категории. ЛК сотрудника ВШЭ — публикации из личного кабинета. "
"Все публикации объединяет записи личного кабинета и выгрузку из базы ВШЭ без дублей; источники указаны на листе Публикации."],
]:
_append(parameters, values)
_append(parameters, ["Пункт 5 · технические", "9 публикаций; К1/К2/RSCI/Q1/Q2"])
+1 -1
View File
@@ -98,7 +98,7 @@ def load_reference(path: Path) -> dict[str, list[dict]]:
expected[author].append(index)
articles.append({"title": record["title"], "year": record["year"], "doi": record["doi"],
"types": [], "quartiles": record["quartiles"], "journals": record["journals"],
"origins": ["Файл CSV"], "rows": [record["source_row"]],
"origins": ["Данные ВШЭ"], "rows": [record["source_row"]],
"in_csv": True, "in_db": False, "url": None})
if by_author != dict(expected) or any(type(i) is not int for indexes in by_author.values() for i in indexes):
raise ValueError("Council author index disagrees with publication authors")
+6 -6
View File
@@ -21,11 +21,11 @@
<details class="council__notice">
<summary><strong>Как рассчитываются показатели</strong></summary>
<p>Период оценки: {{ start.strftime('%d.%m.%Y') }} — {{ end.strftime('%d.%m.%Y') }}, последние пять лет относительно текущей даты Москвы. Для доктора наук применяется минимум 9 публикаций по пункту 5, для кандидата наук или PhD — 20 по пункту 6. Используется высшая обнаруженная степень; её документальное подтверждение проверяется отдельно.</p>
<p>Q1, Q2, К1, К2 и RSCI — предварительные показатели. Квартили определяются по отметкам CSV, применимым данным каталогов и проверенным основаниям; категории К1/К2 — по сопоставлению журнала со справочниками ВАК. RSCI учитывается при наличии отдельной записи об этом основании; отметка РИНЦ сама по себе его не подтверждает. Сведения из профиля сотрудника дополняются внешними источниками.</p>
<p>Q1, Q2, К1, К2 и RSCI — предварительные показатели. Квартили определяются по данным ВШЭ, применимым данным каталогов и проверенным основаниям; категории К1/К2 — по сопоставлению журнала со справочниками ВАК. RSCI учитывается при наличии отдельной записи об этом основании; отметка РИНЦ сама по себе его не подтверждает. Сведения из профиля сотрудника дополняются внешними источниками.</p>
<p>ВСЕГО — число уникальных работ с основаниями Q1/Q2/К1/RSCI; для доктора наук учитывается также К2. Пересечения между основаниями не увеличивают итог. Для кандидата наук или PhD одной категории К2 недостаточно. Известные другие виды материалов не входят в этот подсчёт без отдельного подтверждения их допустимости.</p>
<p>«Подтверждено» — работы с проверенными видом материала, периодом и допустимым основанием. Для пограничных лет нужно уточнить дату публикации; текущая категория журнала не подтверждает его категорию в год выхода работы. «Требует проверки» означает, что для вывода не хватает подтверждений: например, степени, даты или основания зачёта работы. Это не означает, что у сотрудника нет нужных публикаций. Соответствие публикационному минимуму не означает автоматического включения в совет.</p>
<p>Фильтр «Специальность ВАК» подбирает сотрудников с хотя бы одной публикацией за пять лет в подходящем журнале К1. Связь определяется по журналу из Перечня ВАК на 15.09.2026, а не по содержанию статьи. Квартильные каталоги содержат ограниченные подборки и метрики 2025 года; они не подтверждают другие годы или текущую индексацию. Аналитическая связь журнала со специальностью и отдельное рассмотрение статьи показаны в публикациях.</p>
<p>«В БД» — количество сохранённых публикаций сотрудника за все годы. «Все публикации» — общий перечень из БД и CSV без дублей, также за все годы и независимо от категории. При совпадении ФИО нескольких сотрудников записи CSV не присваиваются автоматически. XLSX содержит всех найденных сотрудников и их публикации, включая другие страницы списка.</p>
<p>«ЛК сотрудника ВШЭ» — количество публикаций из личного кабинета сотрудника за все годы. «Данные ВШЭ» — выгрузка из базы публикаций ВШЭ, которая дополняет сведения из личного кабинета. «Все публикации» объединяет оба источника без дублей за все годы и независимо от категории. При совпадении ФИО нескольких сотрудников данные ВШЭ не присваиваются автоматически. XLSX содержит всех найденных сотрудников и их публикации, включая другие страницы списка.</p>
</details>
<form class="council__search" method="get" action="/admin/council" role="search">
@@ -42,7 +42,7 @@
</label>
<label class="directory__field">Сортировать по
<select class="directory__input" name="sort">
{% for value, label in [('full_name', 'ФИО'), ('confirmed_count', 'Подтверждено'), ('q1_count', 'Q1'), ('q2_count', 'Q2'), ('k1_count', 'К1'), ('k2_count', 'К2'), ('rsci_count', 'RSCI'), ('total_count', 'ВСЕГО'), ('db_count', 'Публикации в БД'), ('count', 'Все публикации')] %}
{% for value, label in [('full_name', 'ФИО'), ('confirmed_count', 'Подтверждено'), ('q1_count', 'Q1'), ('q2_count', 'Q2'), ('k1_count', 'К1'), ('k2_count', 'К2'), ('rsci_count', 'RSCI'), ('total_count', 'ВСЕГО'), ('db_count', 'ЛК сотрудника ВШЭ'), ('count', 'Все публикации')] %}
<option value="{{ value }}"{% if sort == value %} selected{% endif %}>{{ label }}</option>
{% endfor %}
</select>
@@ -74,7 +74,7 @@
<span class="council__candidate-count"><strong>{{ candidate.q2_count }}</strong><small>Q2</small></span>
<span class="council__candidate-count"><strong>{{ candidate.k1_count }}</strong><small>К1</small></span>
<span class="council__candidate-count"><strong>{{ candidate.total_count }}</strong><small>ВСЕГО (предв.)</small></span>
<span class="council__candidate-count"><strong>{{ candidate.db_count }}</strong><small>В БД</small></span>
<span class="council__candidate-count"><strong>{{ candidate.db_count }}</strong><small>ЛК сотрудника ВШЭ</small></span>
<span class="council__candidate-count"><strong>{{ candidate.count }}</strong><small>Все публикации</small></span>
</span>
<span class="council__candidate-cue" aria-hidden="true"></span>
@@ -86,7 +86,7 @@
{% if not candidate.articles %}<p class="council__hint">Доступных публикаций нет. Это не доказывает отсутствие работ сотрудника.</p>{% endif %}
{% if specialty %}<p class="council__hint">Публикаций по выбранной специальности: {{ candidate.specialty_count }}</p>{% endif %}
{% if candidate.profile_url %}<a class="admin__link" href="{{ candidate.profile_url }}" target="_blank" rel="noopener noreferrer">Профиль на сайте ВШЭ</a>{% endif %}
{% if candidate.ambiguous %}<p class="council__hint">В базе есть сотрудники с одинаковым ФИО. Публикации из файла не присвоены автоматически; показаны записи, связанные с этим сотрудником в БД.</p>{% endif %}
{% if candidate.ambiguous %}<p class="council__hint">Есть сотрудники с одинаковым ФИО. Публикации из данных ВШЭ не присвоены автоматически; показаны записи из личного кабинета этого сотрудника.</p>{% endif %}
<ol class="council__articles">
{% for article in candidate.articles %}
<li class="council__article">
@@ -99,7 +99,7 @@
<span>{{ article.type_label }}</span>
{% if article.doi_url %}<a class="admin__link" href="{{ article.doi_url }}" target="_blank" rel="noopener noreferrer">DOI</a>{% endif %}
{% if article.url %}<a class="admin__link" href="{{ article.url }}" target="_blank" rel="noopener noreferrer">Публикация на сайте</a>{% endif %}
{% if article.rows %}<span>Строки CSV: {{ article.rows|join(', ') }}</span>{% endif %}
{% if article.rows %}<span>Строки выгрузки ВШЭ: {{ article.rows|join(', ') }}</span>{% endif %}
</div>
<ul class="council__evidence" aria-label="Критерии и подтверждения">
<li>Зачёт в подтверждённое число: {{ 'да' if article.confirmed else 'требует проверки' }}{% if article.publication_date %}; точная дата: {{ article.publication_date.strftime('%d.%m.%Y') }}{% endif %}</li>
+3 -3
View File
@@ -1,3 +1,3 @@
APP_VERSION = "0.16.4"
FRONTEND_VERSION = "0.16.4"
BACKEND_VERSION = "0.16.4"
APP_VERSION = "0.16.5"
FRONTEND_VERSION = "0.16.5"
BACKEND_VERSION = "0.16.5"
+1 -1
View File
@@ -2,7 +2,7 @@
Страница и XLSX читают один результат сервиса. Правила и ограничения исходного материала описаны в `app/data/COUNCIL_CRITERIA.md`, каталогов — в `app/data/Q1_Q2_CATALOG_GUIDE.md`. Публичная страница и XLSX настроены на **технические науки**: для доктора — 9 публикаций, для кандидата/PhD — 20. Пятилетний период рассчитывается относительно текущей даты Москвы. Дата ходатайства, монографии и связанные причины проверки не отображаются; подтверждение степени, вида, периода и основания работ по-прежнему требуется. Внутренний сервис сохраняет правила других отраслей для проверки исходной инструкции.
По умолчанию `app/data/council_evidence.json` содержит пустой набор подтверждений. Это намеренно: профиль, CSV и текущие списки журналов не заменяют документы. Без достаточных подтверждений вывод — «Требует проверки». Источники считаются неполными; отрицательное заключение из малого количества найденных работ не формируется.
По умолчанию `app/data/council_evidence.json` содержит пустой набор подтверждений. Это намеренно: профиль, данные ВШЭ и текущие списки журналов не заменяют документы. Без достаточных подтверждений вывод — «Требует проверки». Источники считаются неполными; отрицательное заключение из малого количества найденных работ не формируется.
## Хранение и привязка
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "miem-workers"
version = "0.16.4"
version = "0.16.5"
description = "MIEM employees parser, admin API, and web admin"
requires-python = ">=3.11"
dependencies = [
+3 -3
View File
@@ -58,8 +58,8 @@ def main():
original = council._articles_by_author
council._articles_by_author = lambda: {"сотрудник 26": [
{"title": f"Публикация из CSV {number}", "year": 2024, "doi": f"10.1234/test-{number}",
"types": [], "journals": [], "quartiles": ["Scopus Q1"], "origins": ["Файл CSV"], "rows": [number + 2],
{"title": f"Публикация из данных ВШЭ {number}", "year": 2024, "doi": f"10.1234/test-{number}",
"types": [], "journals": [], "quartiles": ["Scopus Q1"], "origins": ["Данные ВШЭ"], "rows": [number + 2],
"in_csv": True, "in_db": False, "url": None} for number in range(2)]}
app.dependency_overrides[get_db] = database
client = TestClient(app)
@@ -93,7 +93,7 @@ def main():
summary = page.locator(".council__candidate-summary").first
summary.focus()
page.keyboard.press("Enter")
expect(page.get_by_text("БД miem-workers · сайт ВШЭ").first).to_be_visible()
expect(page.locator(".council__source").filter(has_text="ЛК сотрудника ВШЭ").first).to_be_visible()
expect(page.get_by_text("Q1/Q2 и К1 не подтверждены доступными данными").first).to_be_visible()
assert page.locator(".council__candidate").first.evaluate("e => e.open")
page.screenshot(path=str(output / "desktop.png"), full_page=True)
+1 -1
View File
@@ -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.16.4"
assert response.json()["backend_version"] == "0.16.5"
def test_api_employees_and_stats_require_admin_session():
+7 -7
View File
@@ -72,14 +72,14 @@ def test_council_union_counts_provenance_and_types(db_session, export):
result = council.council_candidates(db_session, date(2026, 9, 30))
assert result["total"] == 1
candidate = result["candidates"][0]
assert (candidate["total_count"], candidate["db_count"], candidate["count"]) == (3, 3, 5)
assert (candidate["total_count"], candidate["db_count"], candidate["count"]) == (3, 2, 5)
shared = next(a for a in candidate["articles"] if a["doi"] == "10.1234/shared")
assert shared["type_label"] == "Статья"
assert shared["quartiles"] == ["Scopus Q1", "Web of Science Q2"]
assert shared["rows"] == [2, 3]
assert "Файл CSV" in shared["origins"] and "БД miem-workers · сайт ВШЭ" in shared["origins"]
assert "Данные ВШЭ" in shared["origins"] and "ЛК сотрудника ВШЭ" in shared["origins"]
imported = next(a for a in candidate["articles"] if a["title"] == "Imported")
assert imported["origins"] == ["БД miem-workers · импорт CSV"]
assert imported["origins"] == ["Данные ВШЭ"]
assert imported["qualifies"] and imported["q2"]
assert "Тип не указан" in imported["type_label"]
assert council.period(date(2024, 2, 29))[0] == date(2019, 2, 28)
@@ -218,7 +218,7 @@ def test_public_page_data_minimization_and_safe_links(db_session, export):
assert "&lt;script&gt;" 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.16.4 · Frontend 0.16.4" in response.text
assert "Backend 0.16.5 · Frontend 0.16.5" 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
@@ -262,18 +262,18 @@ def test_xlsx_exports_all_filtered_workers_in_order_and_deduplicated_evidence(db
assert book.sheetnames == ["Сотрудники", "Публикации", "Параметры"]
people, works = book["Сотрудники"], book["Публикации"]
assert [cell.value for cell in people[1]][:13] == [
"№", "ФИО", "Учёная степень", "Q1", "Q2", "К1", "ВСЕГО", "Страница ВШЭ", "Все публикации",
"№", "ФИО", "Учёная степень", "Q1", "Q2", "К1", "ВСЕГО", "ЛК сотрудника ВШЭ", "Все публикации",
"Профиль ВШЭ", "Примечание", "Специальности ВАК", "Работ по выбранной специальности"
]
assert people["J2"].value == "https://www.hse.ru/staff/test"
assert people["K2"].value is None
assert people.max_column == 23
assert works["L1"].value == "Страница ВШЭ"
assert works["L1"].value == "ЛК сотрудника ВШЭ"
assert people.max_row == 29 and works.max_row == 29
assert people["B2"].value == "Петров 27" and people["B29"].value == "Петров 00"
assert [people.cell(2, n).value for n in [4, 5, 6, 7, 8, 9]] == [1, 1, 0, 1, 1, 1]
assert works["C2"].value == "Shared work" and works["E2"].value == "Статья"
assert "Файл CSV" in works["F2"].value and "сайт ВШЭ" in works["F2"].value
assert "Данные ВШЭ" in works["F2"].value and "ЛК сотрудника ВШЭ" in works["F2"].value
assert "Scopus Q1" in works["G2"].value and "Web of Science Q2" in works["G2"].value
assert works["M2"].value == "https://doi.org/10.1234/shared" and works["O2"].value == "2, 3"
assert people["D2"].data_type == "n" and people.freeze_panes == "A2"
+2 -2
View File
@@ -44,7 +44,7 @@ def test_prepared_reference_preserves_quoted_fields_metadata_and_matching(tmp_pa
assert articles['елкин петр'][0]['rows'] == [2]
assert articles['другой автор'][0]['title'] == record['title']
assert len(articles['елкин петр']) == 1
assert articles['елкин петр'][0]['origins'] == ['Файл CSV']
assert articles['елкин петр'][0]['origins'] == ['Данные ВШЭ']
@pytest.mark.parametrize('change', ['author', 'index', 'bool_index', 'year', 'schema', 'source_row', 'list'])
@@ -69,7 +69,7 @@ def test_committed_reference_is_reproducible_and_loaded_once(monkeypatch):
source = council.EXPORT.with_suffix('.csv')
data = json.loads(council.EXPORT.read_text(encoding='utf-8'))
assert data == prepare_reference(source)
assert data['source_rows'] == 1797 and len(data['by_author']) == 825
assert data['source_rows'] == 1000 and len(data['by_author']) == 776
calls = []
original = council.load_reference
def counted(path):