Update app.py
Browse files
app.py
CHANGED
|
@@ -65,14 +65,14 @@ DISPLAY_COLUMNS_ALL = [
|
|
| 65 |
|
| 66 |
# ==========================
|
| 67 |
# UI: КОЛОНКИ ТАБЛИЦЫ + НАСТРОЙКИ ОТОБРАЖЕНИЯ
|
|
|
|
|
|
|
| 68 |
# ==========================
|
| 69 |
|
| 70 |
UI_TABLE_COLUMNS = [
|
| 71 |
"Сходство",
|
| 72 |
"ФИО",
|
| 73 |
-
"ORCID",
|
| 74 |
"Название диссертации",
|
| 75 |
-
"ВАК",
|
| 76 |
"Организация",
|
| 77 |
"Тип",
|
| 78 |
"Год",
|
|
@@ -83,7 +83,6 @@ UI_TABLE_COLUMNS = [
|
|
| 83 |
"Работ",
|
| 84 |
"Цитат",
|
| 85 |
]
|
| 86 |
-
|
| 87 |
DEFAULT_VISIBLE_UI = {c: (c != "Организация") for c in UI_TABLE_COLUMNS}
|
| 88 |
|
| 89 |
|
|
@@ -147,11 +146,6 @@ def _safe_text(x: Any) -> str:
|
|
| 147 |
return s
|
| 148 |
|
| 149 |
|
| 150 |
-
def _safe_fragment(s: Any) -> str:
|
| 151 |
-
t = _safe_text(s).replace("#", " ").replace("\n", " ").strip()
|
| 152 |
-
return t
|
| 153 |
-
|
| 154 |
-
|
| 155 |
def _norm_openalex_url(x: Any) -> str:
|
| 156 |
s = _safe_text(x)
|
| 157 |
if not s:
|
|
@@ -168,7 +162,7 @@ def _norm_orcid_url(x: Any) -> str:
|
|
| 168 |
return ""
|
| 169 |
if s.startswith("http://") or s.startswith("https://"):
|
| 170 |
return s
|
| 171 |
-
m = re.search(r"(0000-[0-9X]{4}-[0-9X]{4}-[0-9X]{4})", s)
|
| 172 |
return f"https://orcid.org/{m.group(1)}" if m else ""
|
| 173 |
|
| 174 |
|
|
@@ -458,7 +452,11 @@ def build_result_df(results):
|
|
| 458 |
|
| 459 |
# ВАЖНО: используем author_org_short (fallback на author_org_name если вдруг короткого нет)
|
| 460 |
org_short = meta.get("author_org_short", None)
|
| 461 |
-
if
|
|
|
|
|
|
|
|
|
|
|
|
|
| 462 |
org_short = meta.get("author_org_name", None)
|
| 463 |
|
| 464 |
rows.append(
|
|
@@ -541,36 +539,26 @@ def run_search(
|
|
| 541 |
if only_orcid:
|
| 542 |
df_raw = df_raw[df_raw["orcid_url"].map(_safe_text) != ""]
|
| 543 |
|
| 544 |
-
df_raw = df_raw.reset_index(drop=True)
|
| 545 |
-
|
| 546 |
# Ограничиваем top_k уже после фильтрации
|
|
|
|
| 547 |
if len(df_raw) > int(top_k):
|
| 548 |
df_raw = df_raw.iloc[: int(top_k)].copy()
|
| 549 |
|
| 550 |
-
#
|
| 551 |
-
if "№" in df_raw.columns:
|
| 552 |
-
df_raw["№"] = np.arange(1, len(df_raw) + 1)
|
| 553 |
-
|
| 554 |
-
# Установим индекс как стабильный id
|
| 555 |
df_raw["reg_norm"] = df_raw["registration_number"].map(_norm_regnum)
|
| 556 |
df_raw = df_raw.drop_duplicates(subset=["reg_norm"]).set_index("reg_norm", drop=True)
|
| 557 |
|
| 558 |
-
#
|
| 559 |
-
|
| 560 |
-
|
| 561 |
-
fio_cell = np.where(orcid_url != "", orcid_url + "#" + fio_txt.map(_safe_fragment), fio_txt)
|
| 562 |
-
|
| 563 |
-
title_txt = df_raw["title"].map(_safe_text)
|
| 564 |
-
vak_link = df_raw["vak_link"].map(_safe_text)
|
| 565 |
-
title_cell = np.where(vak_link != "", vak_link + "#" + title_txt.map(_safe_fragment), title_txt)
|
| 566 |
|
|
|
|
|
|
|
| 567 |
df_ui = pd.DataFrame(
|
| 568 |
{
|
| 569 |
"Сходство": pd.to_numeric(df_raw["score"], errors="coerce").astype("float64"),
|
| 570 |
-
"ФИО":
|
| 571 |
-
"
|
| 572 |
-
"Название диссертации": title_cell,
|
| 573 |
-
"ВАК": df_raw["vak_link"].map(_safe_text),
|
| 574 |
"Организация": df_raw["author_org_short"].map(_safe_text),
|
| 575 |
"Тип": df_raw["dissertation_type"].map(_safe_text),
|
| 576 |
"Год": pd.to_numeric(df_raw["protection_year"], errors="coerce").astype("float64"),
|
|
@@ -585,8 +573,8 @@ def run_search(
|
|
| 585 |
)
|
| 586 |
df_ui.index.name = "reg_norm"
|
| 587 |
|
| 588 |
-
# Excel
|
| 589 |
-
df_excel_ru = df_raw.reset_index(drop=
|
| 590 |
output = io.BytesIO()
|
| 591 |
with pd.ExcelWriter(output, engine="xlsxwriter") as writer:
|
| 592 |
df_excel_ru.to_excel(writer, index=False)
|
|
@@ -604,7 +592,22 @@ def format_selected_list_from_raw(df_raw: pd.DataFrame, selected_regnorms: List[
|
|
| 604 |
fio = _safe_text(raw.get("fio"))
|
| 605 |
title = _safe_text(raw.get("title"))
|
| 606 |
year = _safe_text(raw.get("protection_year"))
|
| 607 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 608 |
return "\n".join(lines)
|
| 609 |
|
| 610 |
|
|
@@ -623,8 +626,12 @@ if "last_df_raw" not in st.session_state:
|
|
| 623 |
st.session_state.last_df_raw = None
|
| 624 |
if "last_excel" not in st.session_state:
|
| 625 |
st.session_state.last_excel = None
|
|
|
|
|
|
|
| 626 |
if "selected_regnorms" not in st.session_state:
|
| 627 |
st.session_state.selected_regnorms = set()
|
|
|
|
|
|
|
| 628 |
if "search_id" not in st.session_state:
|
| 629 |
st.session_state.search_id = 0
|
| 630 |
|
|
@@ -717,12 +724,14 @@ if do_search:
|
|
| 717 |
st.session_state.last_df_raw = df_raw
|
| 718 |
st.session_state.last_excel = excel_bytes
|
| 719 |
|
| 720 |
-
#
|
| 721 |
if isinstance(df_ui, pd.DataFrame) and not df_ui.empty:
|
| 722 |
st.session_state.selected_regnorms = set(st.session_state.selected_regnorms) & set(df_ui.index)
|
| 723 |
else:
|
| 724 |
st.session_state.selected_regnorms = set()
|
| 725 |
|
|
|
|
|
|
|
| 726 |
st.session_state.search_id += 1
|
| 727 |
|
| 728 |
df_ui_saved = st.session_state.last_df_ui
|
|
@@ -732,10 +741,10 @@ excel_saved = st.session_state.last_excel
|
|
| 732 |
if isinstance(df_ui_saved, pd.DataFrame) and not df_ui_saved.empty:
|
| 733 |
st.success(f"Найдено записей: {len(df_ui_saved)}")
|
| 734 |
|
| 735 |
-
# Соберём отображаемую таблицу + колонку выбора
|
| 736 |
selected_set = set(st.session_state.selected_regnorms) & set(df_ui_saved.index)
|
| 737 |
st.session_state.selected_regnorms = selected_set
|
| 738 |
|
|
|
|
| 739 |
df_display = df_ui_saved.copy()
|
| 740 |
df_display.insert(0, "Выбрать", df_display.index.map(lambda x: x in selected_set))
|
| 741 |
|
|
@@ -745,30 +754,8 @@ if isinstance(df_ui_saved, pd.DataFrame) and not df_ui_saved.empty:
|
|
| 745 |
full_column_config = {
|
| 746 |
"Выбрать": st.column_config.CheckboxColumn("Выбрать", width="small"),
|
| 747 |
"Сходство": st.column_config.NumberColumn("Сходство", format="%.4f", width="small"),
|
| 748 |
-
"ФИО": st.column_config.
|
| 749 |
-
|
| 750 |
-
display_text=r"(?:.*#)?(.*)$",
|
| 751 |
-
width="medium",
|
| 752 |
-
help="Если для автора найден ORCID — ФИО кликабельно и ведёт на ORCID.",
|
| 753 |
-
),
|
| 754 |
-
"ORCID": st.column_config.LinkColumn(
|
| 755 |
-
"ORCID",
|
| 756 |
-
display_text=r"https://orcid\.org/(0000-[0-9X]{4}-[0-9X]{4}-[0-9X]{4})",
|
| 757 |
-
width="small",
|
| 758 |
-
help="Профиль автора в ORCID (если найден).",
|
| 759 |
-
),
|
| 760 |
-
"Название диссертации": st.column_config.LinkColumn(
|
| 761 |
-
"Название диссертации",
|
| 762 |
-
display_text=r"(?:.*#)?(.*)$",
|
| 763 |
-
width="large",
|
| 764 |
-
help="Если есть ссылка ВАК — название кликабельно и ведёт на ВАК.",
|
| 765 |
-
),
|
| 766 |
-
"ВАК": st.column_config.LinkColumn(
|
| 767 |
-
"ВАК",
|
| 768 |
-
display_text=r".+",
|
| 769 |
-
width="small",
|
| 770 |
-
help="Прямая ссылка на карточку/страницу ВАК (если есть).",
|
| 771 |
-
),
|
| 772 |
"Организация": st.column_config.TextColumn("Организация", width="large"),
|
| 773 |
"Тип": st.column_config.TextColumn("Тип", width="small"),
|
| 774 |
"Год": st.column_config.NumberColumn("Год", format="%.0f", width="small"),
|
|
@@ -778,13 +765,13 @@ if isinstance(df_ui_saved, pd.DataFrame) and not df_ui_saved.empty:
|
|
| 778 |
display_text=r"https://openalex\.org/(A\d+)",
|
| 779 |
width="small",
|
| 780 |
help="Профиль автора в OpenAlex (если найден).",
|
|
|
|
| 781 |
),
|
| 782 |
"h-index": st.column_config.NumberColumn("h-index", format="%.0f", width="small"),
|
| 783 |
"i10-index": st.column_config.NumberColumn("i10-index", format="%.0f", width="small"),
|
| 784 |
"Работ": st.column_config.NumberColumn("Работ", format="%.0f", width="small"),
|
| 785 |
"Цитат": st.column_config.NumberColumn("Цитат", format="%.0f", width="small"),
|
| 786 |
}
|
| 787 |
-
|
| 788 |
column_config_filtered = {k: v for k, v in full_column_config.items() if k in df_edit.columns}
|
| 789 |
|
| 790 |
edited = st.data_editor(
|
|
@@ -797,21 +784,42 @@ if isinstance(df_ui_saved, pd.DataFrame) and not df_ui_saved.empty:
|
|
| 797 |
key=f"editor_{st.session_state.search_id}",
|
| 798 |
)
|
| 799 |
|
| 800 |
-
#
|
| 801 |
if isinstance(edited, pd.DataFrame) and "Выбрать" in edited.columns:
|
| 802 |
st.session_state.selected_regnorms = set(edited.index[edited["Выбрать"] == True].tolist())
|
| 803 |
|
| 804 |
selected_regnorms = sorted(list(st.session_state.selected_regnorms))
|
| 805 |
|
| 806 |
if selected_regnorms:
|
| 807 |
-
with st.expander("Полный текст (для выбранных строк)", expanded=False):
|
| 808 |
for reg_norm in selected_regnorms[:50]:
|
| 809 |
if df_raw_saved is None or reg_norm not in df_raw_saved.index:
|
| 810 |
continue
|
| 811 |
raw = df_raw_saved.loc[reg_norm]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 812 |
st.markdown(
|
| 813 |
-
f"- **{
|
| 814 |
-
f" \n {
|
| 815 |
)
|
| 816 |
|
| 817 |
if excel_saved is not None:
|
|
|
|
| 65 |
|
| 66 |
# ==========================
|
| 67 |
# UI: КОЛОНКИ ТАБЛИЦЫ + НАСТРОЙКИ ОТОБРАЖЕНИЯ
|
| 68 |
+
# ВАЖНО: ВАК и ORCID отдельными столбцами НЕ выводим.
|
| 69 |
+
# Ссылки показываем в блоке "Полный текст" / "Перечень диссертаций".
|
| 70 |
# ==========================
|
| 71 |
|
| 72 |
UI_TABLE_COLUMNS = [
|
| 73 |
"Сходство",
|
| 74 |
"ФИО",
|
|
|
|
| 75 |
"Название диссертации",
|
|
|
|
| 76 |
"Организация",
|
| 77 |
"Тип",
|
| 78 |
"Год",
|
|
|
|
| 83 |
"Работ",
|
| 84 |
"Цитат",
|
| 85 |
]
|
|
|
|
| 86 |
DEFAULT_VISIBLE_UI = {c: (c != "Организация") for c in UI_TABLE_COLUMNS}
|
| 87 |
|
| 88 |
|
|
|
|
| 146 |
return s
|
| 147 |
|
| 148 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 149 |
def _norm_openalex_url(x: Any) -> str:
|
| 150 |
s = _safe_text(x)
|
| 151 |
if not s:
|
|
|
|
| 162 |
return ""
|
| 163 |
if s.startswith("http://") or s.startswith("https://"):
|
| 164 |
return s
|
| 165 |
+
m = re.search(r"(0000-[0-9X]{4}-[0-9X]{4}-[0-9X]{4}-[0-9X]{4})", s)
|
| 166 |
return f"https://orcid.org/{m.group(1)}" if m else ""
|
| 167 |
|
| 168 |
|
|
|
|
| 452 |
|
| 453 |
# ВАЖНО: используем author_org_short (fallback на author_org_name если вдруг короткого нет)
|
| 454 |
org_short = meta.get("author_org_short", None)
|
| 455 |
+
if (
|
| 456 |
+
org_short is None
|
| 457 |
+
or (isinstance(org_short, float) and pd.isna(org_short))
|
| 458 |
+
or str(org_short).lower() in {"none", "nan"}
|
| 459 |
+
):
|
| 460 |
org_short = meta.get("author_org_name", None)
|
| 461 |
|
| 462 |
rows.append(
|
|
|
|
| 539 |
if only_orcid:
|
| 540 |
df_raw = df_raw[df_raw["orcid_url"].map(_safe_text) != ""]
|
| 541 |
|
|
|
|
|
|
|
| 542 |
# Ограничиваем top_k уже после фильтрации
|
| 543 |
+
df_raw = df_raw.reset_index(drop=True)
|
| 544 |
if len(df_raw) > int(top_k):
|
| 545 |
df_raw = df_raw.iloc[: int(top_k)].copy()
|
| 546 |
|
| 547 |
+
# Дедуп и индекс
|
|
|
|
|
|
|
|
|
|
|
|
|
| 548 |
df_raw["reg_norm"] = df_raw["registration_number"].map(_norm_regnum)
|
| 549 |
df_raw = df_raw.drop_duplicates(subset=["reg_norm"]).set_index("reg_norm", drop=True)
|
| 550 |
|
| 551 |
+
# Перенумеруем "№" (после дедупа)
|
| 552 |
+
if "№" in df_raw.columns:
|
| 553 |
+
df_raw["№"] = np.arange(1, len(df_raw) + 1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 554 |
|
| 555 |
+
# UI-таблица: ВАЖНО — "ФИО" и "Название" делаем ТЕКСТОВЫМИ,
|
| 556 |
+
# чтобы Streamlit не создавал "липовые" ссылки для строк без ORCID/ВАК.
|
| 557 |
df_ui = pd.DataFrame(
|
| 558 |
{
|
| 559 |
"Сходство": pd.to_numeric(df_raw["score"], errors="coerce").astype("float64"),
|
| 560 |
+
"ФИО": df_raw["fio"].map(_safe_text),
|
| 561 |
+
"Название диссертации": df_raw["title"].map(_safe_text),
|
|
|
|
|
|
|
| 562 |
"Организация": df_raw["author_org_short"].map(_safe_text),
|
| 563 |
"Тип": df_raw["dissertation_type"].map(_safe_text),
|
| 564 |
"Год": pd.to_numeric(df_raw["protection_year"], errors="coerce").astype("float64"),
|
|
|
|
| 573 |
)
|
| 574 |
df_ui.index.name = "reg_norm"
|
| 575 |
|
| 576 |
+
# Excel
|
| 577 |
+
df_excel_ru = df_raw.reset_index(drop=True).rename(columns=COLUMN_LABELS_RU_EXCEL)
|
| 578 |
output = io.BytesIO()
|
| 579 |
with pd.ExcelWriter(output, engine="xlsxwriter") as writer:
|
| 580 |
df_excel_ru.to_excel(writer, index=False)
|
|
|
|
| 592 |
fio = _safe_text(raw.get("fio"))
|
| 593 |
title = _safe_text(raw.get("title"))
|
| 594 |
year = _safe_text(raw.get("protection_year"))
|
| 595 |
+
|
| 596 |
+
vak = _safe_text(raw.get("vak_link"))
|
| 597 |
+
orcid = _safe_text(raw.get("orcid_url"))
|
| 598 |
+
oa = _safe_text(raw.get("openalex_url"))
|
| 599 |
+
|
| 600 |
+
link_parts = []
|
| 601 |
+
if vak:
|
| 602 |
+
link_parts.append(f"[ВАК]({vak})")
|
| 603 |
+
if orcid:
|
| 604 |
+
link_parts.append(f"[ORCID]({orcid})")
|
| 605 |
+
if oa:
|
| 606 |
+
link_parts.append(f"[OpenAlex]({oa})")
|
| 607 |
+
|
| 608 |
+
links_line = (" \n " + " ".join(link_parts)) if link_parts else ""
|
| 609 |
+
|
| 610 |
+
lines.append(f"- **{fio}** — {title} ({year}){links_line}")
|
| 611 |
return "\n".join(lines)
|
| 612 |
|
| 613 |
|
|
|
|
| 626 |
st.session_state.last_df_raw = None
|
| 627 |
if "last_excel" not in st.session_state:
|
| 628 |
st.session_state.last_excel = None
|
| 629 |
+
|
| 630 |
+
# выбор диссертаций хранится по reg_norm, чтобы не сбрасывался при фильтрации/перерисовке
|
| 631 |
if "selected_regnorms" not in st.session_state:
|
| 632 |
st.session_state.selected_regnorms = set()
|
| 633 |
+
|
| 634 |
+
# счётчик только для управляемого сброса состояния таблицы при НОВОМ поиске
|
| 635 |
if "search_id" not in st.session_state:
|
| 636 |
st.session_state.search_id = 0
|
| 637 |
|
|
|
|
| 724 |
st.session_state.last_df_raw = df_raw
|
| 725 |
st.session_state.last_excel = excel_bytes
|
| 726 |
|
| 727 |
+
# НЕ сбрасываем выбор: оставляем то, что есть в новых результатах
|
| 728 |
if isinstance(df_ui, pd.DataFrame) and not df_ui.empty:
|
| 729 |
st.session_state.selected_regnorms = set(st.session_state.selected_regnorms) & set(df_ui.index)
|
| 730 |
else:
|
| 731 |
st.session_state.selected_regnorms = set()
|
| 732 |
|
| 733 |
+
# новый поиск -> новый ключ таблицы (чтобы корректно пересобрать data_editor),
|
| 734 |
+
# но выбор переносится через selected_regnorms выше
|
| 735 |
st.session_state.search_id += 1
|
| 736 |
|
| 737 |
df_ui_saved = st.session_state.last_df_ui
|
|
|
|
| 741 |
if isinstance(df_ui_saved, pd.DataFrame) and not df_ui_saved.empty:
|
| 742 |
st.success(f"Найдено записей: {len(df_ui_saved)}")
|
| 743 |
|
|
|
|
| 744 |
selected_set = set(st.session_state.selected_regnorms) & set(df_ui_saved.index)
|
| 745 |
st.session_state.selected_regnorms = selected_set
|
| 746 |
|
| 747 |
+
# Таблица + колонка выбора
|
| 748 |
df_display = df_ui_saved.copy()
|
| 749 |
df_display.insert(0, "Выбрать", df_display.index.map(lambda x: x in selected_set))
|
| 750 |
|
|
|
|
| 754 |
full_column_config = {
|
| 755 |
"Выбрать": st.column_config.CheckboxColumn("Выбрать", width="small"),
|
| 756 |
"Сходство": st.column_config.NumberColumn("Сходство", format="%.4f", width="small"),
|
| 757 |
+
"ФИО": st.column_config.TextColumn("ФИО", width="medium"),
|
| 758 |
+
"Название диссертации": st.column_config.TextColumn("Название диссертации", width="large"),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 759 |
"Организация": st.column_config.TextColumn("Организация", width="large"),
|
| 760 |
"Тип": st.column_config.TextColumn("Тип", width="small"),
|
| 761 |
"Год": st.column_config.NumberColumn("Год", format="%.0f", width="small"),
|
|
|
|
| 765 |
display_text=r"https://openalex\.org/(A\d+)",
|
| 766 |
width="small",
|
| 767 |
help="Профиль автора в OpenAlex (если найден).",
|
| 768 |
+
validate=r"^https://openalex\.org/A\d+$|^$",
|
| 769 |
),
|
| 770 |
"h-index": st.column_config.NumberColumn("h-index", format="%.0f", width="small"),
|
| 771 |
"i10-index": st.column_config.NumberColumn("i10-index", format="%.0f", width="small"),
|
| 772 |
"Работ": st.column_config.NumberColumn("Работ", format="%.0f", width="small"),
|
| 773 |
"Цитат": st.column_config.NumberColumn("Цитат", format="%.0f", width="small"),
|
| 774 |
}
|
|
|
|
| 775 |
column_config_filtered = {k: v for k, v in full_column_config.items() if k in df_edit.columns}
|
| 776 |
|
| 777 |
edited = st.data_editor(
|
|
|
|
| 784 |
key=f"editor_{st.session_state.search_id}",
|
| 785 |
)
|
| 786 |
|
| 787 |
+
# Синхронизация выбора
|
| 788 |
if isinstance(edited, pd.DataFrame) and "Выбрать" in edited.columns:
|
| 789 |
st.session_state.selected_regnorms = set(edited.index[edited["Выбрать"] == True].tolist())
|
| 790 |
|
| 791 |
selected_regnorms = sorted(list(st.session_state.selected_regnorms))
|
| 792 |
|
| 793 |
if selected_regnorms:
|
| 794 |
+
with st.expander("Полный текст и ссылки (для выбранных строк)", expanded=False):
|
| 795 |
for reg_norm in selected_regnorms[:50]:
|
| 796 |
if df_raw_saved is None or reg_norm not in df_raw_saved.index:
|
| 797 |
continue
|
| 798 |
raw = df_raw_saved.loc[reg_norm]
|
| 799 |
+
|
| 800 |
+
fio = _safe_text(raw.get("fio"))
|
| 801 |
+
title = _safe_text(raw.get("title"))
|
| 802 |
+
org = _safe_text(raw.get("author_org_short"))
|
| 803 |
+
year = _safe_text(raw.get("protection_year"))
|
| 804 |
+
|
| 805 |
+
vak = _safe_text(raw.get("vak_link"))
|
| 806 |
+
orcid = _safe_text(raw.get("orcid_url"))
|
| 807 |
+
oa = _safe_text(raw.get("openalex_url"))
|
| 808 |
+
|
| 809 |
+
# Ссылки показываем ТОЛЬКО если они реально есть (ничего не "выдумываем")
|
| 810 |
+
links = []
|
| 811 |
+
if vak:
|
| 812 |
+
links.append(f"[ВАК]({vak})")
|
| 813 |
+
if orcid:
|
| 814 |
+
links.append(f"[ORCID]({orcid})")
|
| 815 |
+
if oa:
|
| 816 |
+
links.append(f"[OpenAlex]({oa})")
|
| 817 |
+
|
| 818 |
+
links_md = (" \n " + " ".join(links)) if links else ""
|
| 819 |
+
|
| 820 |
st.markdown(
|
| 821 |
+
f"- **{fio}** — {title}\n"
|
| 822 |
+
f" \n {org} ({year}){links_md}"
|
| 823 |
)
|
| 824 |
|
| 825 |
if excel_saved is not None:
|