Update app.py
Browse files
app.py
CHANGED
|
@@ -63,6 +63,29 @@ DISPLAY_COLUMNS_ALL = [
|
|
| 63 |
"registration_number",
|
| 64 |
]
|
| 65 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
|
| 67 |
# ==========================
|
| 68 |
# ПРОВЕРКА СЕКРЕТОВ
|
|
@@ -88,13 +111,14 @@ except Exception:
|
|
| 88 |
st.markdown(
|
| 89 |
"""
|
| 90 |
<style>
|
| 91 |
-
/* уменьшение шрифта вокруг dataframes */
|
| 92 |
div[data-testid="stDataFrame"] { font-size: 80% !important; }
|
| 93 |
div[data-testid="stDataFrame"] * { font-size: 80% !important; }
|
| 94 |
-
|
| 95 |
-
/* попытка зацепить внутренние классы грида (зависит от версии Streamlit) */
|
| 96 |
.stDataFrame { font-size: 80% !important; }
|
| 97 |
.gdg-w, .gdg-canvas { font-size: 80% !important; }
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
</style>
|
| 99 |
""",
|
| 100 |
unsafe_allow_html=True,
|
|
@@ -152,6 +176,10 @@ def _keyify(label: str) -> str:
|
|
| 152 |
return "k_" + "".join(ch if ch.isalnum() else "_" for ch in label).strip("_")
|
| 153 |
|
| 154 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
# ==========================
|
| 156 |
# ФИЛЬТРЫ (UI)
|
| 157 |
# ==========================
|
|
@@ -460,14 +488,32 @@ def run_search(
|
|
| 460 |
doctor_selected: bool,
|
| 461 |
science_selected: List[str],
|
| 462 |
year_range: Optional[Tuple[int, int]],
|
|
|
|
|
|
|
| 463 |
):
|
| 464 |
mask = build_filter_mask(candidate_selected, doctor_selected, science_selected, year_range)
|
| 465 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 466 |
df_raw = build_result_df(results)
|
| 467 |
|
| 468 |
-
if
|
| 469 |
-
|
| 470 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 471 |
|
| 472 |
def _get(col: str, default):
|
| 473 |
if col in en.columns:
|
|
@@ -489,6 +535,27 @@ def run_search(
|
|
| 489 |
df_raw["works_count"] = np.nan
|
| 490 |
df_raw["cited_by_count"] = np.nan
|
| 491 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 492 |
fio_txt = df_raw["fio"].map(_safe_text)
|
| 493 |
orcid_url = df_raw["orcid_url"].map(_safe_text)
|
| 494 |
fio_cell = np.where(orcid_url != "", orcid_url + "#" + fio_txt.map(_safe_fragment), fio_txt)
|
|
@@ -499,22 +566,27 @@ def run_search(
|
|
| 499 |
|
| 500 |
df_ui = pd.DataFrame(
|
| 501 |
{
|
| 502 |
-
"_rid": np.arange(len(df_raw)),
|
| 503 |
"Сходство": pd.to_numeric(df_raw["score"], errors="coerce").astype("float64"),
|
| 504 |
"ФИО": fio_cell,
|
|
|
|
| 505 |
"Название диссертации": title_cell,
|
| 506 |
-
|
| 507 |
"Организация": df_raw["author_org_short"].map(_safe_text),
|
|
|
|
| 508 |
"Год": pd.to_numeric(df_raw["protection_year"], errors="coerce").astype("float64"),
|
|
|
|
| 509 |
"OpenAlex": df_raw["openalex_url"].map(_safe_text),
|
| 510 |
"h-index": df_raw["h_index"],
|
| 511 |
"i10-index": df_raw["i10_index"],
|
| 512 |
"Работ": df_raw["works_count"],
|
| 513 |
"Цитат": df_raw["cited_by_count"],
|
| 514 |
-
}
|
| 515 |
-
|
|
|
|
|
|
|
| 516 |
|
| 517 |
-
|
|
|
|
| 518 |
output = io.BytesIO()
|
| 519 |
with pd.ExcelWriter(output, engine="xlsxwriter") as writer:
|
| 520 |
df_excel_ru.to_excel(writer, index=False)
|
|
@@ -523,10 +595,12 @@ def run_search(
|
|
| 523 |
return df_ui, output, df_raw
|
| 524 |
|
| 525 |
|
| 526 |
-
def format_selected_list_from_raw(df_raw: pd.DataFrame,
|
| 527 |
lines = []
|
| 528 |
-
for
|
| 529 |
-
|
|
|
|
|
|
|
| 530 |
fio = _safe_text(raw.get("fio"))
|
| 531 |
title = _safe_text(raw.get("title"))
|
| 532 |
year = _safe_text(raw.get("protection_year"))
|
|
@@ -549,8 +623,8 @@ if "last_df_raw" not in st.session_state:
|
|
| 549 |
st.session_state.last_df_raw = None
|
| 550 |
if "last_excel" not in st.session_state:
|
| 551 |
st.session_state.last_excel = None
|
| 552 |
-
if "
|
| 553 |
-
st.session_state.
|
| 554 |
if "search_id" not in st.session_state:
|
| 555 |
st.session_state.search_id = 0
|
| 556 |
|
|
@@ -600,10 +674,29 @@ with st.form("search_form"):
|
|
| 600 |
step=1,
|
| 601 |
)
|
| 602 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 603 |
c1, c2, c3 = st.columns([1, 1, 1])
|
| 604 |
with c2:
|
| 605 |
do_search = st.form_submit_button("🔍 Поиск", type="primary", use_container_width=True)
|
| 606 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 607 |
if do_search:
|
| 608 |
if not candidate_selected and not doctor_selected:
|
| 609 |
st.warning("Выключены оба типа диссертаций. Включите «Кандидатские» и/или «Докторские».")
|
|
@@ -616,12 +709,20 @@ if do_search:
|
|
| 616 |
doctor_selected=doctor_selected,
|
| 617 |
science_selected=science_selected,
|
| 618 |
year_range=year_range,
|
|
|
|
|
|
|
| 619 |
)
|
| 620 |
|
| 621 |
st.session_state.last_df_ui = df_ui
|
| 622 |
st.session_state.last_df_raw = df_raw
|
| 623 |
st.session_state.last_excel = excel_bytes
|
| 624 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 625 |
st.session_state.search_id += 1
|
| 626 |
|
| 627 |
df_ui_saved = st.session_state.last_df_ui
|
|
@@ -631,61 +732,83 @@ excel_saved = st.session_state.last_excel
|
|
| 631 |
if isinstance(df_ui_saved, pd.DataFrame) and not df_ui_saved.empty:
|
| 632 |
st.success(f"Найдено записей: {len(df_ui_saved)}")
|
| 633 |
|
| 634 |
-
|
| 635 |
-
|
| 636 |
-
|
| 637 |
-
|
| 638 |
-
|
| 639 |
-
|
| 640 |
-
|
| 641 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 642 |
"ФИО",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 643 |
"Название диссертации",
|
| 644 |
-
"
|
| 645 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 646 |
"OpenAlex",
|
| 647 |
-
"
|
| 648 |
-
"
|
| 649 |
-
"
|
| 650 |
-
|
| 651 |
-
|
| 652 |
-
column_config=
|
| 653 |
-
|
| 654 |
-
|
| 655 |
-
|
| 656 |
-
|
| 657 |
-
|
| 658 |
-
|
| 659 |
-
|
| 660 |
-
|
| 661 |
-
|
| 662 |
-
|
| 663 |
-
|
| 664 |
-
|
| 665 |
-
|
| 666 |
-
|
| 667 |
-
"Год": st.column_config.NumberColumn("Год", format="%.0f", width="small"),
|
| 668 |
-
"OpenAlex": st.column_config.LinkColumn(
|
| 669 |
-
"OpenAlex",
|
| 670 |
-
display_text=r"https://openalex\.org/(A\d+)",
|
| 671 |
-
width="small",
|
| 672 |
-
help="Профиль автора в OpenAlex (если найден).",
|
| 673 |
-
),
|
| 674 |
-
"h-index": st.column_config.NumberColumn("h-index", format="%.0f", width="small"),
|
| 675 |
-
"i10-index": st.column_config.NumberColumn("i10-index", format="%.0f", width="small"),
|
| 676 |
-
"Работ": st.column_config.NumberColumn("Работ", format="%.0f", width="small"),
|
| 677 |
-
"Цитат": st.column_config.NumberColumn("Цитат", format="%.0f", width="small"),
|
| 678 |
-
},
|
| 679 |
)
|
| 680 |
|
| 681 |
-
|
| 682 |
-
if
|
| 683 |
-
st.session_state.
|
| 684 |
|
| 685 |
-
|
|
|
|
|
|
|
| 686 |
with st.expander("Полный текст (для выбранных строк)", expanded=False):
|
| 687 |
-
for
|
| 688 |
-
|
|
|
|
|
|
|
| 689 |
st.markdown(
|
| 690 |
f"- **{_safe_text(raw.get('fio'))}** — {_safe_text(raw.get('title'))}\n"
|
| 691 |
f" \n {_safe_text(raw.get('author_org_short'))} ({_safe_text(raw.get('protection_year'))})"
|
|
@@ -702,17 +825,15 @@ if isinstance(df_ui_saved, pd.DataFrame) and not df_ui_saved.empty:
|
|
| 702 |
st.markdown("---")
|
| 703 |
st.subheader("Запрос расширенной информации")
|
| 704 |
|
| 705 |
-
selected_rids = st.session_state.selected_rids or []
|
| 706 |
-
|
| 707 |
with st.form("request_form"):
|
| 708 |
requester_fio = st.text_input("Ваше ФИО", placeholder="Иванов Иван Иванович")
|
| 709 |
requester_email = st.text_input("Email", placeholder="name@example.com")
|
| 710 |
|
| 711 |
st.markdown("**Перечень диссертаций:**")
|
| 712 |
-
if
|
| 713 |
-
st.markdown(format_selected_list_from_raw(df_raw_saved,
|
| 714 |
else:
|
| 715 |
-
st.info("Отметьте диссертации чекбокс
|
| 716 |
|
| 717 |
comment = st.text_area(
|
| 718 |
"Комментарий",
|
|
@@ -728,12 +849,14 @@ if isinstance(df_ui_saved, pd.DataFrame) and not df_ui_saved.empty:
|
|
| 728 |
if send_request:
|
| 729 |
if not requester_fio.strip() or not requester_email.strip():
|
| 730 |
st.warning("Поля «Ваше ФИО» и «Email» обязательны. Заполните их, чтобы отправить запрос.")
|
| 731 |
-
elif len(
|
| 732 |
-
st.warning("Выберите хотя бы одну диссертацию (чекбокс
|
| 733 |
else:
|
| 734 |
items = []
|
| 735 |
-
for
|
| 736 |
-
|
|
|
|
|
|
|
| 737 |
items.append(
|
| 738 |
{
|
| 739 |
"author_fio": raw.get("fio"),
|
|
|
|
| 63 |
"registration_number",
|
| 64 |
]
|
| 65 |
|
| 66 |
+
# ==========================
|
| 67 |
+
# UI: КОЛОНКИ ТАБЛИЦЫ + НАСТРОЙКИ ОТОБРАЖЕНИЯ
|
| 68 |
+
# ==========================
|
| 69 |
+
|
| 70 |
+
UI_TABLE_COLUMNS = [
|
| 71 |
+
"Сходство",
|
| 72 |
+
"ФИО",
|
| 73 |
+
"ORCID",
|
| 74 |
+
"Название диссертации",
|
| 75 |
+
"ВАК",
|
| 76 |
+
"Организация",
|
| 77 |
+
"Тип",
|
| 78 |
+
"Год",
|
| 79 |
+
"Регистрационный номер",
|
| 80 |
+
"OpenAlex",
|
| 81 |
+
"h-index",
|
| 82 |
+
"i10-index",
|
| 83 |
+
"Работ",
|
| 84 |
+
"Цитат",
|
| 85 |
+
]
|
| 86 |
+
|
| 87 |
+
DEFAULT_VISIBLE_UI = {c: (c != "Организация") for c in UI_TABLE_COLUMNS}
|
| 88 |
+
|
| 89 |
|
| 90 |
# ==========================
|
| 91 |
# ПРОВЕРКА СЕКРЕТОВ
|
|
|
|
| 111 |
st.markdown(
|
| 112 |
"""
|
| 113 |
<style>
|
|
|
|
| 114 |
div[data-testid="stDataFrame"] { font-size: 80% !important; }
|
| 115 |
div[data-testid="stDataFrame"] * { font-size: 80% !important; }
|
|
|
|
|
|
|
| 116 |
.stDataFrame { font-size: 80% !important; }
|
| 117 |
.gdg-w, .gdg-canvas { font-size: 80% !important; }
|
| 118 |
+
|
| 119 |
+
/* data_editor */
|
| 120 |
+
div[data-testid="stDataEditor"] { font-size: 80% !important; }
|
| 121 |
+
div[data-testid="stDataEditor"] * { font-size: 80% !important; }
|
| 122 |
</style>
|
| 123 |
""",
|
| 124 |
unsafe_allow_html=True,
|
|
|
|
| 176 |
return "k_" + "".join(ch if ch.isalnum() else "_" for ch in label).strip("_")
|
| 177 |
|
| 178 |
|
| 179 |
+
def _show_col_key(col: str) -> str:
|
| 180 |
+
return _keyify("show_" + col)
|
| 181 |
+
|
| 182 |
+
|
| 183 |
# ==========================
|
| 184 |
# ФИЛЬТРЫ (UI)
|
| 185 |
# ==========================
|
|
|
|
| 488 |
doctor_selected: bool,
|
| 489 |
science_selected: List[str],
|
| 490 |
year_range: Optional[Tuple[int, int]],
|
| 491 |
+
only_openalex: bool,
|
| 492 |
+
only_orcid: bool,
|
| 493 |
):
|
| 494 |
mask = build_filter_mask(candidate_selected, doctor_selected, science_selected, year_range)
|
| 495 |
+
|
| 496 |
+
# Берём с запасом, чтобы после фильтров (OA/ORCID) всё равно получить до top_k
|
| 497 |
+
prefetch_k = min(max(int(top_k) * 5, int(top_k)), 500)
|
| 498 |
+
results = search_core(query, prefetch_k, mask=mask)
|
| 499 |
+
|
| 500 |
df_raw = build_result_df(results)
|
| 501 |
|
| 502 |
+
if df_raw.empty:
|
| 503 |
+
empty_ui = pd.DataFrame(columns=["Выбрать"] + UI_TABLE_COLUMNS)
|
| 504 |
+
empty_ui.index.name = "reg_norm"
|
| 505 |
+
out = io.BytesIO()
|
| 506 |
+
with pd.ExcelWriter(out, engine="xlsxwriter") as writer:
|
| 507 |
+
pd.DataFrame().to_excel(writer, index=False)
|
| 508 |
+
out.seek(0)
|
| 509 |
+
return empty_ui, out, df_raw
|
| 510 |
+
|
| 511 |
+
# reg_norm — стабильный идентификатор строки для сохранения выбранных чекбоксов
|
| 512 |
+
df_raw["reg_norm"] = df_raw["registration_number"].map(_norm_regnum)
|
| 513 |
+
|
| 514 |
+
# OA/ORCID enrichment
|
| 515 |
+
if not oa_enrich.empty:
|
| 516 |
+
en = oa_enrich.reindex(df_raw["reg_norm"]).reset_index(drop=True)
|
| 517 |
|
| 518 |
def _get(col: str, default):
|
| 519 |
if col in en.columns:
|
|
|
|
| 535 |
df_raw["works_count"] = np.nan
|
| 536 |
df_raw["cited_by_count"] = np.nan
|
| 537 |
|
| 538 |
+
# Фильтры "только с OpenAlex/ORCID"
|
| 539 |
+
if only_openalex:
|
| 540 |
+
df_raw = df_raw[df_raw["openalex_url"].map(_safe_text) != ""]
|
| 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 |
+
# UI-таблица
|
| 559 |
fio_txt = df_raw["fio"].map(_safe_text)
|
| 560 |
orcid_url = df_raw["orcid_url"].map(_safe_text)
|
| 561 |
fio_cell = np.where(orcid_url != "", orcid_url + "#" + fio_txt.map(_safe_fragment), fio_txt)
|
|
|
|
| 566 |
|
| 567 |
df_ui = pd.DataFrame(
|
| 568 |
{
|
|
|
|
| 569 |
"Сходство": pd.to_numeric(df_raw["score"], errors="coerce").astype("float64"),
|
| 570 |
"ФИО": fio_cell,
|
| 571 |
+
"ORCID": df_raw["orcid_url"].map(_safe_text),
|
| 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"),
|
| 577 |
+
"Регистрационный номер": df_raw["registration_number"].map(_safe_text),
|
| 578 |
"OpenAlex": df_raw["openalex_url"].map(_safe_text),
|
| 579 |
"h-index": df_raw["h_index"],
|
| 580 |
"i10-index": df_raw["i10_index"],
|
| 581 |
"Работ": df_raw["works_count"],
|
| 582 |
"Цитат": df_raw["cited_by_count"],
|
| 583 |
+
},
|
| 584 |
+
index=df_raw.index,
|
| 585 |
+
)
|
| 586 |
+
df_ui.index.name = "reg_norm"
|
| 587 |
|
| 588 |
+
# Excel (как раньше)
|
| 589 |
+
df_excel_ru = df_raw.reset_index(drop=False).rename(columns=COLUMN_LABELS_RU_EXCEL)
|
| 590 |
output = io.BytesIO()
|
| 591 |
with pd.ExcelWriter(output, engine="xlsxwriter") as writer:
|
| 592 |
df_excel_ru.to_excel(writer, index=False)
|
|
|
|
| 595 |
return df_ui, output, df_raw
|
| 596 |
|
| 597 |
|
| 598 |
+
def format_selected_list_from_raw(df_raw: pd.DataFrame, selected_regnorms: List[str]) -> str:
|
| 599 |
lines = []
|
| 600 |
+
for reg_norm in selected_regnorms:
|
| 601 |
+
if reg_norm not in df_raw.index:
|
| 602 |
+
continue
|
| 603 |
+
raw = df_raw.loc[reg_norm]
|
| 604 |
fio = _safe_text(raw.get("fio"))
|
| 605 |
title = _safe_text(raw.get("title"))
|
| 606 |
year = _safe_text(raw.get("protection_year"))
|
|
|
|
| 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 |
|
|
|
|
| 674 |
step=1,
|
| 675 |
)
|
| 676 |
|
| 677 |
+
st.markdown("**Фильтрация по профилям:**")
|
| 678 |
+
only_openalex = st.checkbox("Отображать только с OpenAlex", value=False, key="only_openalex")
|
| 679 |
+
only_orcid = st.checkbox("Отображать только с ORCID", value=False, key="only_orcid")
|
| 680 |
+
|
| 681 |
+
st.markdown("**Настройки отображения:**")
|
| 682 |
+
disp_cols = st.columns(3)
|
| 683 |
+
for i, col in enumerate(UI_TABLE_COLUMNS):
|
| 684 |
+
with disp_cols[i % 3]:
|
| 685 |
+
st.checkbox(
|
| 686 |
+
col,
|
| 687 |
+
value=DEFAULT_VISIBLE_UI.get(col, True),
|
| 688 |
+
key=_show_col_key(col),
|
| 689 |
+
)
|
| 690 |
+
|
| 691 |
c1, c2, c3 = st.columns([1, 1, 1])
|
| 692 |
with c2:
|
| 693 |
do_search = st.form_submit_button("🔍 Поиск", type="primary", use_container_width=True)
|
| 694 |
|
| 695 |
+
# Выбранные для отображения колонки (после формы)
|
| 696 |
+
visible_ui_cols = [c for c in UI_TABLE_COLUMNS if st.session_state.get(_show_col_key(c), True)]
|
| 697 |
+
if not visible_ui_cols:
|
| 698 |
+
visible_ui_cols = [c for c in UI_TABLE_COLUMNS if c != "Организация"]
|
| 699 |
+
|
| 700 |
if do_search:
|
| 701 |
if not candidate_selected and not doctor_selected:
|
| 702 |
st.warning("Выключены оба типа диссертаций. Включите «Кандидатские» и/или «Докторские».")
|
|
|
|
| 709 |
doctor_selected=doctor_selected,
|
| 710 |
science_selected=science_selected,
|
| 711 |
year_range=year_range,
|
| 712 |
+
only_openalex=only_openalex,
|
| 713 |
+
only_orcid=only_orcid,
|
| 714 |
)
|
| 715 |
|
| 716 |
st.session_state.last_df_ui = df_ui
|
| 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 |
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 |
+
|
| 742 |
+
show_cols = [c for c in visible_ui_cols if c in df_display.columns]
|
| 743 |
+
df_edit = df_display[["Выбрать"] + show_cols].copy()
|
| 744 |
+
|
| 745 |
+
full_column_config = {
|
| 746 |
+
"Выбрать": st.column_config.CheckboxColumn("Выбрать", width="small"),
|
| 747 |
+
"Сходство": st.column_config.NumberColumn("Сходство", format="%.4f", width="small"),
|
| 748 |
+
"ФИО": st.column_config.LinkColumn(
|
| 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"),
|
| 775 |
+
"Регистрационный номер": st.column_config.TextColumn("Регистрационный номер", width="small"),
|
| 776 |
+
"OpenAlex": st.column_config.LinkColumn(
|
| 777 |
"OpenAlex",
|
| 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(
|
| 791 |
+
df_edit,
|
| 792 |
+
use_container_width=True,
|
| 793 |
+
hide_index=True,
|
| 794 |
+
num_rows="fixed",
|
| 795 |
+
column_config=column_config_filtered,
|
| 796 |
+
disabled=[c for c in df_edit.columns if c != "Выбрать"],
|
| 797 |
+
key=f"editor_{st.session_state.search_id}",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 798 |
)
|
| 799 |
|
| 800 |
+
# Обновим выбранные reg_norm из чекбоксов — это не будет "сбрасываться" при фильтрах
|
| 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"- **{_safe_text(raw.get('fio'))}** — {_safe_text(raw.get('title'))}\n"
|
| 814 |
f" \n {_safe_text(raw.get('author_org_short'))} ({_safe_text(raw.get('protection_year'))})"
|
|
|
|
| 825 |
st.markdown("---")
|
| 826 |
st.subheader("Запрос расширенной информации")
|
| 827 |
|
|
|
|
|
|
|
| 828 |
with st.form("request_form"):
|
| 829 |
requester_fio = st.text_input("Ваше ФИО", placeholder="Иванов Иван Иванович")
|
| 830 |
requester_email = st.text_input("Email", placeholder="name@example.com")
|
| 831 |
|
| 832 |
st.markdown("**Перечень диссертаций:**")
|
| 833 |
+
if selected_regnorms:
|
| 834 |
+
st.markdown(format_selected_list_from_raw(df_raw_saved, selected_regnorms))
|
| 835 |
else:
|
| 836 |
+
st.info("Отметьте диссертации чекбоксом «Выбрать» — здесь появится перечень.")
|
| 837 |
|
| 838 |
comment = st.text_area(
|
| 839 |
"Комментарий",
|
|
|
|
| 849 |
if send_request:
|
| 850 |
if not requester_fio.strip() or not requester_email.strip():
|
| 851 |
st.warning("Поля «Ваше ФИО» и «Email» обязательны. Заполните их, чтобы отправить запрос.")
|
| 852 |
+
elif len(selected_regnorms) == 0:
|
| 853 |
+
st.warning("Выберите хотя бы одну диссертацию (чекбокс «Выбрать»).")
|
| 854 |
else:
|
| 855 |
items = []
|
| 856 |
+
for reg_norm in selected_regnorms:
|
| 857 |
+
if df_raw_saved is None or reg_norm not in df_raw_saved.index:
|
| 858 |
+
continue
|
| 859 |
+
raw = df_raw_saved.loc[reg_norm].to_dict()
|
| 860 |
items.append(
|
| 861 |
{
|
| 862 |
"author_fio": raw.get("fio"),
|