From d131adbcb8de86d2ee9a86aaa74a22e74a38028f Mon Sep 17 00:00:00 2001 From: Vladislav Kopnin Date: Mon, 12 Oct 2026 12:07:39 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A1=D0=BA=D1=80=D1=8B=D1=82=D0=BE=D0=B5=20?= =?UTF-8?q?=D0=B2=20=D1=82=D0=B0=D0=B1=D0=BB=D0=B8=D1=86=D0=B5=20=D0=BD?= =?UTF-8?q?=D0=B5=20=D0=BF=D0=B0=D1=80=D1=81=D0=B8=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Правило: если строка или столбец в таблице скрыт, его данные не попадают в расписание — раньше скрытые столбцы пропускались только при поиске групп. - xlsx: у листа появился набор скрытых строк (Worksheet.hidden_rows); - расписание, недели: скрытые строки пропускаются; - аудитории и справочник ФИО: скрытые столбцы не читаются вовсе (убрана двухпроходная логика «сначала видимые, потом скрытые»); - тесты: билдер умеет скрытые строки, добавлены проверки «скрытое не парсится». Проверено на таблице: группы и всё расписание совпали байт в байт (скрытых строк нет), из аудиторий ушли только те, что лежат в скрытых столбцах (402, 405, 415), справочник ФИО стал короче на 2 скрытые записи, у 223 осталось описание из видимого столбца (20 мест, интерактивная панель). Тесты: 171. --- src/backend/schedule/parsing.py | 20 +++++++++++--------- src/backend/schedule/xlsx.py | 8 ++++++-- tests/test_parsing.py | 29 ++++++++++++++++++++++++----- tests/xlsx_builder.py | 4 +++- 4 files changed, 44 insertions(+), 17 deletions(-) diff --git a/src/backend/schedule/parsing.py b/src/backend/schedule/parsing.py index ef68a22..1e2888e 100644 --- a/src/backend/schedule/parsing.py +++ b/src/backend/schedule/parsing.py @@ -83,7 +83,9 @@ def extract_week_ranges(sheets: dict[str, Worksheet]) -> list[dict]: return [] ranges: list[dict] = [] seen: set[tuple[str, date]] = set() - for row in sheet.rows[1:]: + for row_idx, row in enumerate(sheet.rows[1:], start=1): + if row_idx in sheet.hidden_rows: + continue # скрытые строки не парсим if not any(row[i] for i in range(4)): continue for parity, start_col, end_col in (("ч", 0, 1), ("з", 2, 3)): @@ -148,7 +150,9 @@ def parse_schedule(sheets: dict[str, Worksheet]) -> dict: parity: str | None = None rows_in_day = 0 - for row in rows[1:]: + for row_idx, row in enumerate(rows[1:], start=1): + if row_idx in sheet.hidden_rows: + continue # скрытые строки не парсим if not any(row[i] for i in range(4)): continue day = _normalize_header(row[_COL_DAY]).lower() @@ -485,7 +489,7 @@ def parse_rooms_info(sheets: dict[str, Worksheet]) -> dict: return label = f"ауд. {m.group(1)}" if label in info: - return # видимый столбец важнее скрытого дубля с тем же номером + return # дубль с тем же номером: первый столбец важнее text = _strip_marks( " ".join(ln.strip() for ln in str(desc[col]).split("\n") if ln.strip()) ) @@ -500,12 +504,8 @@ def parse_rooms_info(sheets: dict[str, Worksheet]) -> dict: "available_days": room_weekdays(text), } - columns = range(_FIRST_GROUP_COL, max(len(header), len(desc))) - for col in columns: # сначала видимые - if col not in sheet.hidden_cols: - add(col) - for col in columns: # потом скрытые: аудитория без видимого столбца тоже нужна - if col in sheet.hidden_cols: + for col in range(_FIRST_GROUP_COL, max(len(header), len(desc))): + if col not in sheet.hidden_cols: # скрытое не парсим add(col) return info @@ -541,6 +541,8 @@ def parse_teachers_full(sheets: dict[str, Worksheet]) -> dict: header = sheet.rows[1] result: dict = {} for col in range(_FIRST_GROUP_COL, len(header)): + if col in sheet.hidden_cols: # скрытое не парсим + continue value = _strip_marks(str(header[col])) parts = [p.strip() for p in re.split(r"[\s\n]+", value) if p.strip()] if not parts: diff --git a/src/backend/schedule/xlsx.py b/src/backend/schedule/xlsx.py index c960cfa..69740b2 100644 --- a/src/backend/schedule/xlsx.py +++ b/src/backend/schedule/xlsx.py @@ -14,7 +14,7 @@ import io import re import zipfile import xml.etree.ElementTree as ET -from dataclasses import dataclass +from dataclasses import dataclass, field from datetime import date, datetime, timedelta from .errors import ScheduleError @@ -32,6 +32,7 @@ _REF_RE = re.compile(r"^([A-Z]+)(\d+)$") class Worksheet: rows: list[list[str]] hidden_cols: set[int] + hidden_rows: set[int] = field(default_factory=set) def cell(self, row_idx: int, col_idx: int) -> str: """Значение ячейки, "" вне границ матрицы.""" @@ -135,11 +136,14 @@ def _parse_sheet(xml_bytes: bytes, shared: list[str]) -> Worksheet: if col.get("hidden") == "1": hidden_cols.update(range(c_min - 1, c_max)) + hidden_rows: set[int] = set() for row in root.iter(f"{{{XLSX_NS}}}row"): row_num = row.get("r") if not row_num: continue r = int(row_num) - 1 + if row.get("hidden") == "1": + hidden_rows.add(r) for cell in row.iter(f"{{{XLSX_NS}}}c"): ref = cell.get("r") if not ref: @@ -194,7 +198,7 @@ def _parse_sheet(xml_bytes: bytes, shared: list[str]) -> Worksheet: [cells.get((r, c), "") for c in range(max_col + 1)] for r in range(max_row + 1) ] - return Worksheet(rows=rows, hidden_cols=hidden_cols) + return Worksheet(rows=rows, hidden_cols=hidden_cols, hidden_rows=hidden_rows) def _iter_text(node: ET.Element): diff --git a/tests/test_parsing.py b/tests/test_parsing.py index 9ceca59..e8b846e 100644 --- a/tests/test_parsing.py +++ b/tests/test_parsing.py @@ -302,7 +302,7 @@ def test_lowercase_except_abbreviations(): def test_room_visible_column_wins_over_hidden_duplicate(): - """У 223 в таблице два столбца: видимый (с панелью) и скрытый (без). Берём видимый.""" + """Скрытый столбец-дубль (у 223) не парсится вовсе — берём видимый.""" from xlsx_builder import SheetSpec spec = SheetSpec( @@ -322,8 +322,8 @@ def test_room_visible_column_wins_over_hidden_duplicate(): assert room["type"] == "комп.класс linux/windows" -def test_room_only_in_hidden_column_still_parsed(): - """Аудитория, у которой есть только скрытый столбец, не пропадает.""" +def test_room_only_in_hidden_column_not_parsed(): + """Скрытое не парсим: аудитория со скрытым столбцом описания не получает.""" from xlsx_builder import SheetSpec spec = SheetSpec( @@ -334,5 +334,24 @@ def test_room_only_in_hidden_column_still_parsed(): hidden_cols=[(6, 7)], ) info = parsing.parse_rooms_info(make_sheets({parsing.ROOMS_SHEET: spec})) - assert info["ауд. 402"]["available_days"] == ["четверг", "пятница"] - assert info["ауд. 410"]["capacity"] == 30 + assert info == {} + + +def test_hidden_rows_not_parsed(monkeypatch): + """Скрытые строки листа в расписание не попадают.""" + from fixtures import fixture_sheets + from xlsx_builder import SheetSpec, build_workbook + + sheets = fixture_sheets() + main = sheets[parsing.MAIN_SHEET] + # строки: 0 заголовок, 1 с/по, 2..5 понедельник, 6..7 вторник — скроем строку с первым вторником + spec = SheetSpec( + rows=main.rows, + merges=main.merges, + hidden_cols=main.hidden_cols, + hidden_rows=[6], + ) + data = parsing.parse_schedule(make_sheets({parsing.MAIN_SHEET: spec})) + tuesday = data["ИВТ-11БО"]["days"].get("вторник", {}) + assert not tuesday, "строка вторника скрыта — занятий быть не должно" + assert data["ИВТ-11БО"]["days"]["понедельник"][1]["ч"] # понедельник на месте diff --git a/tests/xlsx_builder.py b/tests/xlsx_builder.py index e93b0f9..bbd6a7a 100644 --- a/tests/xlsx_builder.py +++ b/tests/xlsx_builder.py @@ -28,6 +28,7 @@ class SheetSpec: rows: list[list] merges: list[str] = field(default_factory=list) hidden_cols: list[tuple[int, int]] = field(default_factory=list) # 0-based, инклязивно + hidden_rows: list[int] = field(default_factory=list) # 0-based def build_workbook(sheets: dict[str, SheetSpec]) -> bytes: @@ -83,7 +84,8 @@ def build_workbook(sheets: dict[str, SheetSpec]) -> bytes: t = f' t="{t_attr}"' if t_attr else "" cells.append(f'{escape(str(v))}') if cells: - row_xml.append(f'{"".join(cells)}') + hidden = ' hidden="1"' if (r - 1) in spec.hidden_rows else "" + row_xml.append(f'{"".join(cells)}') merges_xml = "" if spec.merges: