Compare commits
5 Commits
feature/hu
...
5eaad38076
| Author | SHA1 | Date | |
|---|---|---|---|
| 5eaad38076 | |||
|
|
af87fa8af3 | ||
| 26db5832fd | |||
|
|
7530cbdb60 | ||
| ce90414654 |
@@ -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.
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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():
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user