Compare commits

...

3 Commits

7 changed files with 56 additions and 20 deletions

View File

@@ -110,4 +110,4 @@ docker compose exec postgres pg_dump -U miem miem_workers > backup.sql
docker compose down docker compose down
``` ```
Версия сервиса: `0.2.2`. Админка всегда показывает версии backend и frontend в footer. Версия сервиса: `0.2.4`. Админка всегда показывает версии backend и frontend в footer.

View File

@@ -20,18 +20,19 @@ EMPLOYEE_SORTS = {
def employee_display_payload(employee: Employee) -> dict[str, Any]: def employee_display_payload(employee: Employee) -> dict[str, Any]:
data = employee.current_data or {} data = _as_dict(employee.current_data)
contacts = data.get("contacts") or {} contacts = _as_dict(data.get("contacts"))
sections = data.get("sections") or [] sections = _as_list(data.get("sections"))
emails = contacts.get("emails") or [] positions = _clean_list(data.get("positions"))
phones = contacts.get("phones") or [] emails = _clean_list(contacts.get("emails"))
phones = _clean_list(contacts.get("phones"))
return { return {
"id": employee.id, "id": employee.id,
"full_name": employee.full_name, "full_name": employee.full_name,
"status": employee.status, "status": employee.status,
"canonical_url": employee.canonical_url, "canonical_url": employee.canonical_url,
"positions": data.get("positions") or [], "positions": positions,
"positions_text": "; ".join(data.get("positions") or []), "positions_text": "; ".join(positions),
"hse_start_year": data.get("hse_start_year"), "hse_start_year": data.get("hse_start_year"),
"emails": emails, "emails": emails,
"email_text": ", ".join(emails), "email_text": ", ".join(emails),
@@ -47,8 +48,8 @@ def employee_display_payload(employee: Employee) -> dict[str, Any]:
def employee_detail_payload(employee: Employee) -> dict[str, Any]: def employee_detail_payload(employee: Employee) -> dict[str, Any]:
data = employee.current_data or {} data = _as_dict(employee.current_data)
contacts = data.get("contacts") or {} contacts = _as_dict(data.get("contacts"))
return { return {
**employee_display_payload(employee), **employee_display_payload(employee),
"profile_type": employee.profile_type or data.get("profile_type"), "profile_type": employee.profile_type or data.get("profile_type"),
@@ -58,10 +59,10 @@ def employee_detail_payload(employee: Employee) -> dict[str, Any]:
"emails": _clean_list(contacts.get("emails")), "emails": _clean_list(contacts.get("emails")),
"phones": _clean_list(contacts.get("phones")), "phones": _clean_list(contacts.get("phones")),
"address": contacts.get("address"), "address": contacts.get("address"),
"items": _normalize_contact_items(contacts.get("items")), "contact_items": _normalize_contact_items(contacts.get("items")),
}, },
"external_ids": _normalize_external_ids(data.get("external_ids")), "external_ids": _normalize_external_ids(data.get("external_ids")),
"sections": [_normalize_section(section) for section in data.get("sections") or []], "sections": [_normalize_section(section) for section in _as_list(data.get("sections"))],
} }
@@ -179,11 +180,23 @@ def _count_section_items(sections: list[dict[str, Any]], section_type: str) -> i
def _clean_list(values: Any) -> list[str]: def _clean_list(values: Any) -> list[str]:
if not isinstance(values, list): if values is None:
return [] return []
if not isinstance(values, list):
values = [values]
return [str(value).strip() for value in values if str(value or "").strip()] return [str(value).strip() for value in values if str(value or "").strip()]
def _as_dict(value: Any) -> dict[str, Any]:
return value if isinstance(value, dict) else {}
def _as_list(value: Any) -> list[Any]:
if value is None:
return []
return value if isinstance(value, list) else [value]
def _normalize_contact_items(items: Any) -> list[str]: def _normalize_contact_items(items: Any) -> list[str]:
normalized = [] normalized = []
if not isinstance(items, list): if not isinstance(items, list):

View File

@@ -62,12 +62,12 @@
<dt class="employee-card__meta-label">Адрес</dt> <dt class="employee-card__meta-label">Адрес</dt>
<dd class="employee-card__meta-value">{{ employee_view.contacts.address or "Не указано" }}</dd> <dd class="employee-card__meta-value">{{ employee_view.contacts.address or "Не указано" }}</dd>
</div> </div>
{% if employee_view.contacts.items %} {% if employee_view.contacts.contact_items %}
<div class="employee-card__meta-item employee-card__meta-item--wide"> <div class="employee-card__meta-item employee-card__meta-item--wide">
<dt class="employee-card__meta-label">Прочее</dt> <dt class="employee-card__meta-label">Прочее</dt>
<dd class="employee-card__meta-value"> <dd class="employee-card__meta-value">
<ul class="employee-card__list"> <ul class="employee-card__list">
{% for item in employee_view.contacts.items %} {% for item in employee_view.contacts.contact_items %}
<li class="employee-card__list-item">{{ item }}</li> <li class="employee-card__list-item">{{ item }}</li>
{% endfor %} {% endfor %}
</ul> </ul>

View File

@@ -1,3 +1,3 @@
APP_VERSION = "0.2.2" APP_VERSION = "0.2.4"
FRONTEND_VERSION = "0.2.2" FRONTEND_VERSION = "0.2.4"
BACKEND_VERSION = "0.2.2" BACKEND_VERSION = "0.2.4"

View File

@@ -86,7 +86,7 @@ def test_employee_detail_payload_normalizes_human_readable_sections(db_session):
payload = employee_detail_payload(employee) payload = employee_detail_payload(employee)
assert payload["contacts"]["emails"] == ["person@hse.ru"] assert payload["contacts"]["emails"] == ["person@hse.ru"]
assert payload["contacts"]["items"] == ["consultation hours"] assert payload["contacts"]["contact_items"] == ["consultation hours"]
assert payload["external_ids"][0]["system"] == "ORCID" assert payload["external_ids"][0]["system"] == "ORCID"
assert payload["sections"][0]["year_entries"][0]["text"] == "Master degree" assert payload["sections"][0]["year_entries"][0]["text"] == "Master degree"
assert payload["sections"][1]["publications"][0]["title"] == "Paper" assert payload["sections"][1]["publications"][0]["title"] == "Paper"
@@ -94,6 +94,27 @@ def test_employee_detail_payload_normalizes_human_readable_sections(db_session):
assert payload["sections"][3]["paragraphs"] == ["Fallback text"] assert payload["sections"][3]["paragraphs"] == ["Fallback text"]
def test_employee_payloads_tolerate_malformed_current_data(db_session):
employee = Employee(
profile_key="staff:broken",
canonical_url="https://www.hse.ru/staff/broken",
full_name="Broken Data",
status="active",
first_seen_at=datetime.now(timezone.utc),
last_seen_at=datetime.now(timezone.utc),
current_data="not-a-dict",
)
display = employee_display_payload(employee)
detail = employee_detail_payload(employee)
assert display["positions"] == []
assert display["email_text"] == ""
assert detail["contacts"]["emails"] == []
assert detail["contacts"]["contact_items"] == []
assert detail["sections"] == []
def test_list_employees_page_filters_sorts_and_paginates(db_session): def test_list_employees_page_filters_sorts_and_paginates(db_session):
db_session.add( db_session.add(
Employee( Employee(

View File

@@ -18,7 +18,7 @@ def test_health_returns_versions():
response = client.get("/api/health") response = client.get("/api/health")
assert response.status_code == 200 assert response.status_code == 200
assert response.json()["backend_version"] == "0.2.2" assert response.json()["backend_version"] == "0.2.4"
def test_mcp_requires_token_and_lists_tools(): def test_mcp_requires_token_and_lists_tools():

View File

@@ -7,6 +7,8 @@ def test_employee_detail_template_is_human_readable():
assert "Current data" not in template assert "Current data" not in template
assert "<pre class=\"code\"" not in template assert "<pre class=\"code\"" not in template
assert ">Tabs<" not in template assert ">Tabs<" not in template
assert "contacts.items" not in template
assert "contacts.contact_items" in template
assert "Основная информация" in template assert "Основная информация" in template
assert "Контакты" in template assert "Контакты" in template
assert "Разделы профиля" in template assert "Разделы профиля" in template