fix: handle council specialty changes without JavaScript
CI / test (pull_request) Successful in 10m18s
CI / Fingerprint diagnostics (production deployment temporarily disabled) (pull_request) Has been skipped

This commit is contained in:
codex committed 2026-10-12 01:48:34 +03:00
1 parent 3359ea2f86
commit 9d788b0663
10 files changed
+72 -10

No files matched your search

+2 -2
View File
@@ -169,7 +169,7 @@ def council(request: Request, q: str = "", offset: int = 0, sort: str = "total_c
authenticated = optional_user(request, settings, db)
try:
result = council_candidates(db, q=q, offset=offset, sort=sort, direction=direction, specialty=specialty,
branch=branch)
branch=branch, previous_specialty=request.query_params.get("previous_specialty"))
except CouncilInputError as exc:
return _render(request, "council.html", {"error": True, "public_page": True, "input_error": str(exc)}, status_code=422)
except (SQLAlchemyError, OSError, ValueError, KeyError):
@@ -303,7 +303,7 @@ def export_council(request: Request, q: str = "", sort: str = "total_count", dir
db: Session = Depends(get_db)):
try:
result = council_candidates(db, q=q, sort=sort, direction=direction, paginate=False, specialty=specialty,
branch=branch)
branch=branch, previous_specialty=request.query_params.get("previous_specialty"))
content = council_xlsx(result)
except CouncilInputError as exc:
return _render(request, "council.html", {"error": True, "public_page": True, "input_error": str(exc)}, status_code=422)
+8 -2
View File
@@ -253,7 +253,8 @@ def _unique_articles(articles: list[dict], start: date, end: date, corrections=N
def council_candidates(db: Session, today: date | None = None, q: str = "", offset: int = 0,
sort: str = "total_count", direction: str = "desc", paginate: bool = True, specialty: str = "",
branch: str = "технические", employee_id: int | None = None) -> dict:
branch: str = "технические", employee_id: int | None = None,
previous_specialty: str | None = None) -> dict:
if branch and branch not in BRANCHES:
raise CouncilInputError("Неизвестная отрасль науки")
today = today or datetime.now(ZoneInfo("Europe/Moscow")).date()
@@ -264,7 +265,12 @@ def council_candidates(db: Session, today: date | None = None, q: str = "", offs
branches = list(dict.fromkeys(name for s in specialties if not specialty or s["code"] == specialty
for name in s["branches"]))
if branch and branch not in branches:
raise CouncilInputError("Отрасль оценки не относится к выбранным специальностям ВАК")
previous_branches = {name for s in specialties if previous_specialty == "" or s["code"] == previous_specialty
for name in s["branches"]}
if previous_specialty is not None and previous_specialty != specialty and branch in previous_branches:
branch = branches[0]
else:
raise CouncilInputError("Отрасль оценки не относится к выбранным специальностям ВАК")
specialty_name = next((s["name"] for s in specialties if s["code"] == specialty), "")
degree_sections = Employee.current_data["sections"]
if db.get_bind().dialect.name == "postgresql":
+1
View File
@@ -34,6 +34,7 @@
</details>
<form class="council__search" method="get" action="/admin/council" role="search">
<input type="hidden" name="previous_specialty" value="{{ specialty }}">
<label class="directory__field" for="council-branch">Отрасль оценки
<select class="directory__input" id="council-branch" name="branch">
{% for name in branches %}<option value="{{ name }}"{% if branch == name %} selected{% endif %}>{{ name|capitalize }}</option>{% endfor %}
+3 -3
View File
@@ -1,3 +1,3 @@
APP_VERSION = "1.2.2"
FRONTEND_VERSION = "1.2.2"
BACKEND_VERSION = "1.2.2"
APP_VERSION = "1.2.3"
FRONTEND_VERSION = "1.2.3"
BACKEND_VERSION = "1.2.3"
+2
View File
@@ -81,3 +81,5 @@
Отрасли оценки ограничены двенадцатью специальностями сайта: 1.1.2 допускает только физико-математические науки, 2.3.2 — только технические; остальные десять допускают обе отрасли. Основание — поле «Наименование отрасли науки, по которой присуждаются ученые степени» в сохранённых PDF-паспортах `app/data/scientific-specialty-passports/pdf/`. Список хранится в `k1_specialties.json` и сохраняется при регенерации справочника. При выборе «Все специальности» доступны обе отрасли; при смене специальности форма сохраняет подходящую отрасль либо выбирает первую допустимую. Сервер отклоняет несовместимые пары для списка, карточек и XLSX.
Без JavaScript форма передаёт предыдущую специальность. Если специальность изменена, а отправленная отрасль допустима только для предыдущего выбора, сервер выбирает первую допустимую отрасль новой специальности. Прямые несовместимые пары без контекста смены специальности отклоняются.
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "miem-workers"
version = "1.2.2"
version = "1.2.3"
description = "MIEM employees parser, admin API, and web admin"
requires-python = ">=3.11"
dependencies = [
+21
View File
@@ -232,6 +232,27 @@ def main():
assert page.evaluate("document.documentElement.scrollWidth <= innerWidth"), width
page.screenshot(path=str(output / f"catalog-{width}.png"), full_page=True)
assert not errors, errors
no_js = browser.new_context(java_script_enabled=False)
no_js.route("**/*", serve)
fallback = no_js.new_page()
for previous, specialty, old_branch, new_branch in [
("2.3.2", "1.1.2", "технические", "физико-математические"),
("1.1.2", "2.3.2", "физико-математические", "технические"),
]:
url = f"http://miem.test/admin/council?specialty={previous}&branch={old_branch}&q=Тест&sort=full_name&direction=asc"
fallback.goto(url)
fallback.get_by_label("Специальность ВАК").select_option(specialty)
with fallback.expect_download() as pending_download:
fallback.get_by_role("button", name="Скачать XLSX", exact=True).click()
book = load_workbook(BytesIO(Path(pending_download.value.path()).read_bytes()))
assert dict(book["Параметры"].values)["Отрасль оценки"] == new_branch
fallback.get_by_role("button", name="Применить", exact=True).click()
expect(fallback.get_by_label("Отрасль оценки")).to_have_value(new_branch)
expect(fallback.get_by_label("Специальность ВАК")).to_have_value(specialty)
expect(fallback.get_by_label("Поиск по ФИО")).to_have_value("Тест")
expect(fallback.get_by_label("Сортировать по")).to_have_value("full_name")
expect(fallback.get_by_label("Направление")).to_have_value("asc")
no_js.close()
browser.close()
finally:
council._articles_by_author = original
+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"] == "1.2.2"
assert response.json()["backend_version"] == "1.2.3"
def test_api_employees_and_stats_require_admin_session():
+1 -1
View File
@@ -223,7 +223,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.2.2 · Frontend 1.2.2" in response.text
assert "Backend 1.2.3 · Frontend 1.2.3" 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
+32
View File
@@ -526,3 +526,35 @@ def test_specialty_limits_branch_options_and_rejects_invalid_requests(db_session
assert client.get(f"/admin/council/employees/{employee.id}/articles", params=params).status_code == 422
finally:
app.dependency_overrides.clear()
@pytest.mark.parametrize("previous,specialty,old_branch,new_branch", [
("2.3.2", "1.1.2", "технические", "физико-математические"),
("1.1.2", "2.3.2", "физико-математические", "технические"),
("", "1.1.2", "технические", "физико-математические"),
])
def test_specialty_change_without_js_uses_compatible_branch(db_session, proofs, previous, specialty, old_branch, new_branch):
from bs4 import BeautifulSoup
app.dependency_overrides[get_db] = lambda: db_session
try:
client = TestClient(app)
params = {"previous_specialty": previous, "specialty": specialty, "branch": old_branch,
"q": "Тест", "sort": "full_name", "direction": "asc"}
page = client.get("/admin/council", params=params)
assert page.status_code == 200
parsed = BeautifulSoup(page.text, "html.parser")
assert parsed.select_one('#council-branch option[selected]')["value"] == new_branch
assert parsed.select_one('[name=previous_specialty]')["value"] == specialty
assert parsed.select_one('[name=q]')["value"] == "Тест"
book = load_workbook(BytesIO(client.get("/admin/council/export.xlsx", params=params).content))
assert dict(book["Параметры"].values)["Отрасль оценки"] == new_branch
for invalid_previous in [None, specialty, "oops"]:
invalid = {**params, "previous_specialty": invalid_previous}
if invalid_previous is None:
del invalid["previous_specialty"]
assert client.get("/admin/council", params=invalid).status_code == 422
assert client.get("/admin/council/export.xlsx", params=invalid).status_code == 422
invalid = {**params, "branch": "экономические"}
assert client.get("/admin/council", params=invalid).status_code == 422
finally:
app.dependency_overrides.clear()