yogl commited on
Commit
14599c9
·
verified ·
1 Parent(s): 6735e1e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +62 -34
app.py CHANGED
@@ -63,10 +63,13 @@ DISPLAY_COLUMNS_ALL = [
63
  "registration_number",
64
  ]
65
 
 
66
  # ==========================
67
  # UI: КОЛОНКИ ТАБЛИЦЫ + НАСТРОЙКИ ОТОБРАЖЕНИЯ
68
- # ВАЖНО: ВАК и ORCID отдельными столбцами НЕ выводим.
69
- # Ссылки показываем в блоке "Полный текст" / "Перечень диссертаций".
 
 
70
  # ==========================
71
 
72
  UI_TABLE_COLUMNS = [
@@ -77,13 +80,14 @@ UI_TABLE_COLUMNS = [
77
  "Тип",
78
  "Год",
79
  "Регистрационный номер",
80
- "OpenAlex",
81
  "h-index",
82
  "i10-index",
83
  "Работ",
84
  "Цитат",
85
  ]
86
- DEFAULT_VISIBLE_UI = {c: (c != "Организация") for c in UI_TABLE_COLUMNS}
 
 
87
 
88
 
89
  # ==========================
@@ -115,7 +119,6 @@ st.markdown(
115
  .stDataFrame { font-size: 80% !important; }
116
  .gdg-w, .gdg-canvas { font-size: 80% !important; }
117
 
118
- /* data_editor */
119
  div[data-testid="stDataEditor"] { font-size: 80% !important; }
120
  div[data-testid="stDataEditor"] * { font-size: 80% !important; }
121
  </style>
@@ -146,6 +149,12 @@ def _safe_text(x: Any) -> str:
146
  return s
147
 
148
 
 
 
 
 
 
 
149
  def _norm_openalex_url(x: Any) -> str:
150
  s = _safe_text(x)
151
  if not s:
@@ -450,7 +459,6 @@ def build_result_df(results):
450
 
451
  protection_year = extract_year_int(meta.get("protection_date", None))
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
@@ -491,7 +499,7 @@ def run_search(
491
  ):
492
  mask = build_filter_mask(candidate_selected, doctor_selected, science_selected, year_range)
493
 
494
- # Берём с запасом, чтобы после фильтров (OA/ORCID) всё равно получить до top_k
495
  prefetch_k = min(max(int(top_k) * 5, int(top_k)), 500)
496
  results = search_core(query, prefetch_k, mask=mask)
497
 
@@ -506,7 +514,6 @@ def run_search(
506
  out.seek(0)
507
  return empty_ui, out, df_raw
508
 
509
- # reg_norm — стабильный идентификатор строки для сохранения выбранных чекбоксов
510
  df_raw["reg_norm"] = df_raw["registration_number"].map(_norm_regnum)
511
 
512
  # OA/ORCID enrichment
@@ -533,37 +540,55 @@ def run_search(
533
  df_raw["works_count"] = np.nan
534
  df_raw["cited_by_count"] = np.nan
535
 
536
- # Фильтры "только с OpenAlex/ORCID"
537
  if only_openalex:
538
  df_raw = df_raw[df_raw["openalex_url"].map(_safe_text) != ""]
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"),
565
  "Регистрационный номер": df_raw["registration_number"].map(_safe_text),
566
- "OpenAlex": df_raw["openalex_url"].map(_safe_text),
567
  "h-index": df_raw["h_index"],
568
  "i10-index": df_raw["i10_index"],
569
  "Работ": df_raw["works_count"],
@@ -586,8 +611,9 @@ def run_search(
586
  def format_selected_list_from_raw(df_raw: pd.DataFrame, selected_regnorms: List[str]) -> str:
587
  lines = []
588
  for reg_norm in selected_regnorms:
589
- if reg_norm not in df_raw.index:
590
  continue
 
591
  raw = df_raw.loc[reg_norm]
592
  fio = _safe_text(raw.get("fio"))
593
  title = _safe_text(raw.get("title"))
@@ -627,11 +653,11 @@ if "last_df_raw" not in st.session_state:
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
 
@@ -702,7 +728,7 @@ with st.form("search_form"):
702
  # Выбранные для отображения колонки (после формы)
703
  visible_ui_cols = [c for c in UI_TABLE_COLUMNS if st.session_state.get(_show_col_key(c), True)]
704
  if not visible_ui_cols:
705
- visible_ui_cols = [c for c in UI_TABLE_COLUMNS if c != "Организация"]
706
 
707
  if do_search:
708
  if not candidate_selected and not doctor_selected:
@@ -724,14 +750,12 @@ if do_search:
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
@@ -754,19 +778,24 @@ if isinstance(df_ui_saved, pd.DataFrame) and not df_ui_saved.empty:
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"),
762
  "Регистрационный номер": st.column_config.TextColumn("Регистрационный номер", width="small"),
763
- "OpenAlex": st.column_config.LinkColumn(
764
- "OpenAlex",
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"),
@@ -806,7 +835,6 @@ if isinstance(df_ui_saved, pd.DataFrame) and not df_ui_saved.empty:
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})")
 
63
  "registration_number",
64
  ]
65
 
66
+
67
  # ==========================
68
  # UI: КОЛОНКИ ТАБЛИЦЫ + НАСТРОЙКИ ОТОБРАЖЕНИЯ
69
+ # ВАЖНО:
70
+ # - отдельного столбца OpenAlex НЕТ
71
+ # - ФИО кликабельно и ведёт на OpenAlex (если есть), иначе на ORCID (если есть), иначе на ВАК (если есть)
72
+ # - Название диссертации кликабельно и ведёт на ВАК
73
  # ==========================
74
 
75
  UI_TABLE_COLUMNS = [
 
80
  "Тип",
81
  "Год",
82
  "Регистрационный номер",
 
83
  "h-index",
84
  "i10-index",
85
  "Работ",
86
  "Цитат",
87
  ]
88
+
89
+ # По умолчанию: все включены, кроме "Организация" и "Тип"
90
+ DEFAULT_VISIBLE_UI = {c: (c != "Организация" and c != "Тип") for c in UI_TABLE_COLUMNS}
91
 
92
 
93
  # ==========================
 
119
  .stDataFrame { font-size: 80% !important; }
120
  .gdg-w, .gdg-canvas { font-size: 80% !important; }
121
 
 
122
  div[data-testid="stDataEditor"] { font-size: 80% !important; }
123
  div[data-testid="stDataEditor"] * { font-size: 80% !important; }
124
  </style>
 
149
  return s
150
 
151
 
152
+ def _safe_fragment(s: Any) -> str:
153
+ # фрагмент для вставки после # в "url#text" — убираем опасные символы
154
+ t = _safe_text(s).replace("#", " ").replace("\n", " ").replace("\r", " ").strip()
155
+ return t
156
+
157
+
158
  def _norm_openalex_url(x: Any) -> str:
159
  s = _safe_text(x)
160
  if not s:
 
459
 
460
  protection_year = extract_year_int(meta.get("protection_date", None))
461
 
 
462
  org_short = meta.get("author_org_short", None)
463
  if (
464
  org_short is None
 
499
  ):
500
  mask = build_filter_mask(candidate_selected, doctor_selected, science_selected, year_range)
501
 
502
+ # С запасом, чтобы после OA/ORCID фильтров осталось до top_k
503
  prefetch_k = min(max(int(top_k) * 5, int(top_k)), 500)
504
  results = search_core(query, prefetch_k, mask=mask)
505
 
 
514
  out.seek(0)
515
  return empty_ui, out, df_raw
516
 
 
517
  df_raw["reg_norm"] = df_raw["registration_number"].map(_norm_regnum)
518
 
519
  # OA/ORCID enrichment
 
540
  df_raw["works_count"] = np.nan
541
  df_raw["cited_by_count"] = np.nan
542
 
543
+ # Фильтры "только с ..."
544
  if only_openalex:
545
  df_raw = df_raw[df_raw["openalex_url"].map(_safe_text) != ""]
546
  if only_orcid:
547
  df_raw = df_raw[df_raw["orcid_url"].map(_safe_text) != ""]
548
 
 
549
  df_raw = df_raw.reset_index(drop=True)
550
  if len(df_raw) > int(top_k):
551
  df_raw = df_raw.iloc[: int(top_k)].copy()
552
 
553
+ # Дедуп и индекс-идентификатор для стабильного выбора
554
  df_raw["reg_norm"] = df_raw["registration_number"].map(_norm_regnum)
555
  df_raw = df_raw.drop_duplicates(subset=["reg_norm"]).set_index("reg_norm", drop=True)
556
 
557
+ # Перенумерация "№"
558
  if "№" in df_raw.columns:
559
  df_raw["№"] = np.arange(1, len(df_raw) + 1)
560
 
561
+ # ====== UI значения со ссылками без "выдумывания" ======
562
+ fio_txt = df_raw["fio"].map(_safe_text)
563
+ fio_frag = fio_txt.map(_safe_fragment)
564
+
565
+ oa_url = df_raw["openalex_url"].map(_safe_text)
566
+ orcid_url = df_raw["orcid_url"].map(_safe_text)
567
+ vak_url = df_raw["vak_link"].map(_safe_text)
568
+
569
+ # Приоритет: OpenAlex -> ORCID -> ВАК
570
+ fio_href = np.where(
571
+ oa_url != "",
572
+ oa_url,
573
+ np.where(orcid_url != "", orcid_url, vak_url),
574
+ )
575
+ # Делаем "url#ФИО" (чтобы показывать ФИО как display_text)
576
+ fio_cell = np.where(fio_href != "", fio_href + "#" + fio_frag, fio_txt)
577
+
578
+ title_txt = df_raw["title"].map(_safe_text)
579
+ title_frag = title_txt.map(_safe_fragment)
580
+ # Название всегда ведёт на ВАК (если vak_link пуст — будет просто текст)
581
+ title_cell = np.where(vak_url != "", vak_url + "#" + title_frag, title_txt)
582
+
583
  df_ui = pd.DataFrame(
584
  {
585
  "Сходство": pd.to_numeric(df_raw["score"], errors="coerce").astype("float64"),
586
+ "ФИО": fio_cell,
587
+ "Название диссертации": title_cell,
588
  "Организация": df_raw["author_org_short"].map(_safe_text),
589
  "Тип": df_raw["dissertation_type"].map(_safe_text),
590
  "Год": pd.to_numeric(df_raw["protection_year"], errors="coerce").astype("float64"),
591
  "Регистрационный номер": df_raw["registration_number"].map(_safe_text),
 
592
  "h-index": df_raw["h_index"],
593
  "i10-index": df_raw["i10_index"],
594
  "Работ": df_raw["works_count"],
 
611
  def format_selected_list_from_raw(df_raw: pd.DataFrame, selected_regnorms: List[str]) -> str:
612
  lines = []
613
  for reg_norm in selected_regnorms:
614
+ if df_raw is None or reg_norm not in df_raw.index:
615
  continue
616
+
617
  raw = df_raw.loc[reg_norm]
618
  fio = _safe_text(raw.get("fio"))
619
  title = _safe_text(raw.get("title"))
 
653
  if "last_excel" not in st.session_state:
654
  st.session_state.last_excel = None
655
 
656
+ # Выбор диссертаций хранится по reg_norm табильный ключ) — не сбрасывается при перерисовке
657
  if "selected_regnorms" not in st.session_state:
658
  st.session_state.selected_regnorms = set()
659
 
660
+ # Ключ для data_editor (обновляем на новый поиск)
661
  if "search_id" not in st.session_state:
662
  st.session_state.search_id = 0
663
 
 
728
  # Выбранные для отображения колонки (после формы)
729
  visible_ui_cols = [c for c in UI_TABLE_COLUMNS if st.session_state.get(_show_col_key(c), True)]
730
  if not visible_ui_cols:
731
+ visible_ui_cols = [c for c in UI_TABLE_COLUMNS if c not in {"Организация", "Тип"}]
732
 
733
  if do_search:
734
  if not candidate_selected and not doctor_selected:
 
750
  st.session_state.last_df_raw = df_raw
751
  st.session_state.last_excel = excel_bytes
752
 
753
+ # Выбор не сбрасываем оставляем только те, что присутствуют в новых результатах
754
  if isinstance(df_ui, pd.DataFrame) and not df_ui.empty:
755
  st.session_state.selected_regnorms = set(st.session_state.selected_regnorms) & set(df_ui.index)
756
  else:
757
  st.session_state.selected_regnorms = set()
758
 
 
 
759
  st.session_state.search_id += 1
760
 
761
  df_ui_saved = st.session_state.last_df_ui
 
778
  full_column_config = {
779
  "Выбрать": st.column_config.CheckboxColumn("Выбрать", width="small"),
780
  "Сходство": st.column_config.NumberColumn("Сходство", format="%.4f", width="small"),
781
+ "ФИО": st.column_config.LinkColumn(
782
+ "ФИО",
783
+ display_text=r"(?:.*#)?(.*)$",
784
+ width="medium",
785
+ help="Если найден OpenAlex — ведёт на OpenAlex; иначе (если есть) на ORCID; иначе (если есть) на ВАК.",
786
+ validate=r"^https?://.+#.+$|^.+$",
787
+ ),
788
+ "Название диссертации": st.column_config.LinkColumn(
789
+ "Название диссертации",
790
+ display_text=r"(?:.*#)?(.*)$",
791
+ width="large",
792
+ help="Название ведёт на ВАК (если ссылка есть).",
793
+ validate=r"^https?://.+#.+$|^.+$",
794
+ ),
795
  "Организация": st.column_config.TextColumn("Организация", width="large"),
796
  "Тип": st.column_config.TextColumn("Тип", width="small"),
797
  "Год": st.column_config.NumberColumn("Год", format="%.0f", width="small"),
798
  "Регистрационный номер": st.column_config.TextColumn("Регистрационный номер", width="small"),
 
 
 
 
 
 
 
799
  "h-index": st.column_config.NumberColumn("h-index", format="%.0f", width="small"),
800
  "i10-index": st.column_config.NumberColumn("i10-index", format="%.0f", width="small"),
801
  "Работ": st.column_config.NumberColumn("Работ", format="%.0f", width="small"),
 
835
  orcid = _safe_text(raw.get("orcid_url"))
836
  oa = _safe_text(raw.get("openalex_url"))
837
 
 
838
  links = []
839
  if vak:
840
  links.append(f"[ВАК]({vak})")