Скрытое в таблице не парсим
Правило: если строка или столбец в таблице скрыт, его данные не попадают в расписание — раньше скрытые столбцы пропускались только при поиске групп. - xlsx: у листа появился набор скрытых строк (Worksheet.hidden_rows); - расписание, недели: скрытые строки пропускаются; - аудитории и справочник ФИО: скрытые столбцы не читаются вовсе (убрана двухпроходная логика «сначала видимые, потом скрытые»); - тесты: билдер умеет скрытые строки, добавлены проверки «скрытое не парсится». Проверено на таблице: группы и всё расписание совпали байт в байт (скрытых строк нет), из аудиторий ушли только те, что лежат в скрытых столбцах (402, 405, 415), справочник ФИО стал короче на 2 скрытые записи, у 223 осталось описание из видимого столбца (20 мест, интерактивная панель). Тесты: 171.
This commit is contained in:
@@ -83,7 +83,9 @@ def extract_week_ranges(sheets: dict[str, Worksheet]) -> list[dict]:
|
|||||||
return []
|
return []
|
||||||
ranges: list[dict] = []
|
ranges: list[dict] = []
|
||||||
seen: set[tuple[str, date]] = set()
|
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)):
|
if not any(row[i] for i in range(4)):
|
||||||
continue
|
continue
|
||||||
for parity, start_col, end_col in (("ч", 0, 1), ("з", 2, 3)):
|
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
|
parity: str | None = None
|
||||||
rows_in_day = 0
|
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)):
|
if not any(row[i] for i in range(4)):
|
||||||
continue
|
continue
|
||||||
day = _normalize_header(row[_COL_DAY]).lower()
|
day = _normalize_header(row[_COL_DAY]).lower()
|
||||||
@@ -485,7 +489,7 @@ def parse_rooms_info(sheets: dict[str, Worksheet]) -> dict:
|
|||||||
return
|
return
|
||||||
label = f"ауд. {m.group(1)}"
|
label = f"ауд. {m.group(1)}"
|
||||||
if label in info:
|
if label in info:
|
||||||
return # видимый столбец важнее скрытого дубля с тем же номером
|
return # дубль с тем же номером: первый столбец важнее
|
||||||
text = _strip_marks(
|
text = _strip_marks(
|
||||||
" ".join(ln.strip() for ln in str(desc[col]).split("\n") if ln.strip())
|
" ".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),
|
"available_days": room_weekdays(text),
|
||||||
}
|
}
|
||||||
|
|
||||||
columns = range(_FIRST_GROUP_COL, max(len(header), len(desc)))
|
for col in range(_FIRST_GROUP_COL, max(len(header), len(desc))):
|
||||||
for col in columns: # сначала видимые
|
if col not in sheet.hidden_cols: # скрытое не парсим
|
||||||
if col not in sheet.hidden_cols:
|
|
||||||
add(col)
|
|
||||||
for col in columns: # потом скрытые: аудитория без видимого столбца тоже нужна
|
|
||||||
if col in sheet.hidden_cols:
|
|
||||||
add(col)
|
add(col)
|
||||||
return info
|
return info
|
||||||
|
|
||||||
@@ -541,6 +541,8 @@ def parse_teachers_full(sheets: dict[str, Worksheet]) -> dict:
|
|||||||
header = sheet.rows[1]
|
header = sheet.rows[1]
|
||||||
result: dict = {}
|
result: dict = {}
|
||||||
for col in range(_FIRST_GROUP_COL, len(header)):
|
for col in range(_FIRST_GROUP_COL, len(header)):
|
||||||
|
if col in sheet.hidden_cols: # скрытое не парсим
|
||||||
|
continue
|
||||||
value = _strip_marks(str(header[col]))
|
value = _strip_marks(str(header[col]))
|
||||||
parts = [p.strip() for p in re.split(r"[\s\n]+", value) if p.strip()]
|
parts = [p.strip() for p in re.split(r"[\s\n]+", value) if p.strip()]
|
||||||
if not parts:
|
if not parts:
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import io
|
|||||||
import re
|
import re
|
||||||
import zipfile
|
import zipfile
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass, field
|
||||||
from datetime import date, datetime, timedelta
|
from datetime import date, datetime, timedelta
|
||||||
|
|
||||||
from .errors import ScheduleError
|
from .errors import ScheduleError
|
||||||
@@ -32,6 +32,7 @@ _REF_RE = re.compile(r"^([A-Z]+)(\d+)$")
|
|||||||
class Worksheet:
|
class Worksheet:
|
||||||
rows: list[list[str]]
|
rows: list[list[str]]
|
||||||
hidden_cols: set[int]
|
hidden_cols: set[int]
|
||||||
|
hidden_rows: set[int] = field(default_factory=set)
|
||||||
|
|
||||||
def cell(self, row_idx: int, col_idx: int) -> str:
|
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":
|
if col.get("hidden") == "1":
|
||||||
hidden_cols.update(range(c_min - 1, c_max))
|
hidden_cols.update(range(c_min - 1, c_max))
|
||||||
|
|
||||||
|
hidden_rows: set[int] = set()
|
||||||
for row in root.iter(f"{{{XLSX_NS}}}row"):
|
for row in root.iter(f"{{{XLSX_NS}}}row"):
|
||||||
row_num = row.get("r")
|
row_num = row.get("r")
|
||||||
if not row_num:
|
if not row_num:
|
||||||
continue
|
continue
|
||||||
r = int(row_num) - 1
|
r = int(row_num) - 1
|
||||||
|
if row.get("hidden") == "1":
|
||||||
|
hidden_rows.add(r)
|
||||||
for cell in row.iter(f"{{{XLSX_NS}}}c"):
|
for cell in row.iter(f"{{{XLSX_NS}}}c"):
|
||||||
ref = cell.get("r")
|
ref = cell.get("r")
|
||||||
if not ref:
|
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)]
|
[cells.get((r, c), "") for c in range(max_col + 1)]
|
||||||
for r in range(max_row + 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):
|
def _iter_text(node: ET.Element):
|
||||||
|
|||||||
@@ -302,7 +302,7 @@ def test_lowercase_except_abbreviations():
|
|||||||
|
|
||||||
|
|
||||||
def test_room_visible_column_wins_over_hidden_duplicate():
|
def test_room_visible_column_wins_over_hidden_duplicate():
|
||||||
"""У 223 в таблице два столбца: видимый (с панелью) и скрытый (без). Берём видимый."""
|
"""Скрытый столбец-дубль (у 223) не парсится вовсе — берём видимый."""
|
||||||
from xlsx_builder import SheetSpec
|
from xlsx_builder import SheetSpec
|
||||||
|
|
||||||
spec = SheetSpec(
|
spec = SheetSpec(
|
||||||
@@ -322,8 +322,8 @@ def test_room_visible_column_wins_over_hidden_duplicate():
|
|||||||
assert room["type"] == "комп.класс linux/windows"
|
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
|
from xlsx_builder import SheetSpec
|
||||||
|
|
||||||
spec = SheetSpec(
|
spec = SheetSpec(
|
||||||
@@ -334,5 +334,24 @@ def test_room_only_in_hidden_column_still_parsed():
|
|||||||
hidden_cols=[(6, 7)],
|
hidden_cols=[(6, 7)],
|
||||||
)
|
)
|
||||||
info = parsing.parse_rooms_info(make_sheets({parsing.ROOMS_SHEET: spec}))
|
info = parsing.parse_rooms_info(make_sheets({parsing.ROOMS_SHEET: spec}))
|
||||||
assert info["ауд. 402"]["available_days"] == ["четверг", "пятница"]
|
assert info == {}
|
||||||
assert info["ауд. 410"]["capacity"] == 30
|
|
||||||
|
|
||||||
|
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]["ч"] # понедельник на месте
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ class SheetSpec:
|
|||||||
rows: list[list]
|
rows: list[list]
|
||||||
merges: list[str] = field(default_factory=list)
|
merges: list[str] = field(default_factory=list)
|
||||||
hidden_cols: list[tuple[int, int]] = field(default_factory=list) # 0-based, инклязивно
|
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:
|
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 ""
|
t = f' t="{t_attr}"' if t_attr else ""
|
||||||
cells.append(f'<c r="{col_letter(c)}{r}"{t}><v>{escape(str(v))}</v></c>')
|
cells.append(f'<c r="{col_letter(c)}{r}"{t}><v>{escape(str(v))}</v></c>')
|
||||||
if cells:
|
if cells:
|
||||||
row_xml.append(f'<row r="{r}">{"".join(cells)}</row>')
|
hidden = ' hidden="1"' if (r - 1) in spec.hidden_rows else ""
|
||||||
|
row_xml.append(f'<row r="{r}"{hidden}>{"".join(cells)}</row>')
|
||||||
|
|
||||||
merges_xml = ""
|
merges_xml = ""
|
||||||
if spec.merges:
|
if spec.merges:
|
||||||
|
|||||||
Reference in New Issue
Block a user