944 lines
30 KiB
Python
944 lines
30 KiB
Python
#!/usr/bin/env python
|
|
# -*- coding: utf-8 -*-
|
|
"""父格 + 聚合驱动的数据源扩展(全表拓扑,支持跨行上父格 / 横向父格树)。"""
|
|
from __future__ import annotations
|
|
|
|
import copy
|
|
from dataclasses import dataclass
|
|
from typing import Any, Dict, List, Optional, Tuple
|
|
|
|
from online_dev.report_manager.engine.parent_cells import (
|
|
cell_key,
|
|
filter_rows_by_parents,
|
|
resolve_field_path,
|
|
resolve_parents,
|
|
_index_data_sources,
|
|
_get_nested_value,
|
|
_int_coord,
|
|
)
|
|
from online_dev.report_manager.engine.polymerize import BindData, build_bind_list, expand_span
|
|
|
|
CellKey = Tuple[str, int, int]
|
|
|
|
|
|
@dataclass
|
|
class ExpandedSlot:
|
|
"""单元格一次绑定扩展占用的行/列区间(左闭右开)。"""
|
|
|
|
value: Any
|
|
data_list: List[Dict[str, Any]]
|
|
start: int
|
|
end: int
|
|
|
|
|
|
def _expand_direction(custom: Dict[str, Any]) -> str:
|
|
expand = (custom.get("expand") or custom.get("expandDirection") or "").lower()
|
|
if expand in ("down", "list", "vertical"):
|
|
return "down"
|
|
if expand in ("right", "horizontal", "across"):
|
|
return "right"
|
|
fill = (custom.get("fillDirection") or "").lower()
|
|
if fill in ("portrait", "vertical", "down"):
|
|
return "down"
|
|
if fill in ("landscape", "horizontal", "right", "across"):
|
|
return "right"
|
|
return "none"
|
|
|
|
|
|
def _dataset_name(cell: Dict[str, Any]) -> str:
|
|
custom = cell.get("custom") or {}
|
|
name = str(
|
|
custom.get("dataSetName") or custom.get("dataSet") or custom.get("alias") or ""
|
|
)
|
|
if name:
|
|
return name
|
|
field = str(custom.get("field") or custom.get("bindField") or "")
|
|
if "." in field:
|
|
return field.split(".", 1)[0]
|
|
return ""
|
|
|
|
|
|
def _field_name(cell: Dict[str, Any]) -> str:
|
|
custom = cell.get("custom") or {}
|
|
return str(custom.get("field") or custom.get("bindField") or "")
|
|
|
|
|
|
def _poly_list(cell: Dict[str, Any]) -> bool:
|
|
return str((cell.get("custom") or {}).get("polymerizationType") or "1") == "1"
|
|
|
|
|
|
def _poly_summary(cell: Dict[str, Any]) -> bool:
|
|
return str((cell.get("custom") or {}).get("polymerizationType") or "1") == "3"
|
|
|
|
|
|
def _poly_group(cell: Dict[str, Any]) -> bool:
|
|
return str((cell.get("custom") or {}).get("polymerizationType") or "1") == "2"
|
|
|
|
|
|
def _should_merge_group(cell: Dict[str, Any]) -> bool:
|
|
"""分组列是否合并单元格(custom.mergeCell,默认开启)。"""
|
|
if not _poly_group(cell):
|
|
return False
|
|
merge = (cell.get("custom") or {}).get("mergeCell")
|
|
if merge is None:
|
|
return True
|
|
if isinstance(merge, str):
|
|
return merge.strip().lower() not in ("0", "false", "no", "")
|
|
return bool(merge)
|
|
|
|
|
|
def _add_vertical_merge_region(
|
|
sheets: Dict[str, Any],
|
|
sheet_id: str,
|
|
start_row: int,
|
|
end_row: int,
|
|
col: int,
|
|
) -> None:
|
|
"""分组列:同组多行合并为一个单元格(预览/导出)。"""
|
|
if end_row <= start_row:
|
|
return
|
|
sheet = sheets.get(sheet_id)
|
|
if not sheet:
|
|
return
|
|
region = {
|
|
"startRow": start_row,
|
|
"endRow": end_row,
|
|
"startColumn": col,
|
|
"endColumn": col,
|
|
}
|
|
merges = sheet.setdefault("mergeData", [])
|
|
if region not in merges:
|
|
merges.append(region)
|
|
|
|
|
|
def _compute_down_band_lengths(
|
|
down_by_sheet: Dict[str, List[Dict[str, Any]]],
|
|
datasets: Dict[str, List[Any]],
|
|
) -> Dict[Tuple[str, int], int]:
|
|
lengths: Dict[Tuple[str, int], int] = {}
|
|
for sheet_id, down_cells in down_by_sheet.items():
|
|
bands: Dict[int, List[Dict[str, Any]]] = {}
|
|
for cell in down_cells:
|
|
bands.setdefault(_int_coord(cell.get("row")), []).append(cell)
|
|
for start_row, band in bands.items():
|
|
max_len = max(
|
|
(len(datasets.get(_dataset_name(item)) or []) for item in band),
|
|
default=0,
|
|
)
|
|
lengths[(sheet_id, start_row)] = max_len
|
|
return lengths
|
|
|
|
|
|
def _shift_sheet_rows_down(
|
|
sheets: Dict[str, Any],
|
|
sheet_id: str,
|
|
from_row: int,
|
|
delta: int,
|
|
) -> None:
|
|
"""将 from_row 及以下的 cellData 整体下移 delta 行(列表扩展前保留汇总/static 行)。"""
|
|
if delta <= 0:
|
|
return
|
|
sheet = sheets.get(sheet_id)
|
|
if not sheet:
|
|
return
|
|
cell_data = sheet.get("cellData") or {}
|
|
shifted: Dict[str, Any] = {}
|
|
for row_key, row_obj in cell_data.items():
|
|
row = _int_coord(row_key, 0)
|
|
target_key = str(row + delta) if row >= from_row else row_key
|
|
if target_key in shifted and row >= from_row:
|
|
existing = shifted[target_key]
|
|
if isinstance(existing, dict) and isinstance(row_obj, dict):
|
|
merged = {**existing, **row_obj}
|
|
shifted[target_key] = merged
|
|
else:
|
|
shifted[target_key] = row_obj
|
|
else:
|
|
shifted[target_key] = row_obj
|
|
sheet["cellData"] = shifted
|
|
|
|
|
|
def _summary_filter_rows(
|
|
cell: Dict[str, Any],
|
|
rows: List[Dict[str, Any]],
|
|
by_pos: Dict[CellKey, Dict[str, Any]],
|
|
by_sheet: Dict[str, List[Dict[str, Any]]],
|
|
) -> List[Dict[str, Any]]:
|
|
"""汇总格:仅 custom 父格按切片统计;default/none 对整表数据集聚合(合计行)。"""
|
|
custom = cell.get("custom") or {}
|
|
left_pt = custom.get("leftParentCellType") or "default"
|
|
top_pt = custom.get("topParentCellType") or "default"
|
|
if left_pt != "custom" and top_pt != "custom":
|
|
return rows
|
|
left, top = resolve_parents(cell, by_pos, by_sheet)
|
|
left_bind = top_bind = None
|
|
if left:
|
|
lb = build_bind_list(left, rows)
|
|
left_bind = lb[0].data_list if lb else None
|
|
if top:
|
|
tb = build_bind_list(top, rows)
|
|
top_bind = tb[0].data_list if tb else None
|
|
return filter_rows_by_parents(
|
|
rows,
|
|
cell,
|
|
left_parent=left,
|
|
top_parent=top,
|
|
left_bind=left_bind,
|
|
top_bind=top_bind,
|
|
)
|
|
|
|
|
|
def _summary_output_row(
|
|
sheet_id: str,
|
|
template_row: int,
|
|
band_lengths: Dict[Tuple[str, int], int],
|
|
) -> int:
|
|
"""汇总格位于列表模板行下方时,输出到扩展后的末行(对齐 JNPF 合计行)。"""
|
|
anchor: Optional[Tuple[int, int]] = None
|
|
for (sid, start_row), max_len in band_lengths.items():
|
|
if sid != sheet_id or max_len <= 0 or template_row <= start_row:
|
|
continue
|
|
if anchor is None or start_row > anchor[0]:
|
|
anchor = (start_row, max_len)
|
|
if anchor is None:
|
|
return template_row
|
|
start_row, max_len = anchor
|
|
return template_row + (max_len - 1)
|
|
|
|
|
|
def _template_style_ref(
|
|
sheets: Dict[str, Any],
|
|
sheet_id: str,
|
|
row: int,
|
|
col: int,
|
|
) -> Optional[Any]:
|
|
sheet = sheets.get(sheet_id)
|
|
if not sheet:
|
|
return None
|
|
cell_data = sheet.get("cellData") or {}
|
|
cell = (cell_data.get(str(row)) or {}).get(str(col))
|
|
if not isinstance(cell, dict):
|
|
return None
|
|
return cell.get("s")
|
|
|
|
|
|
def _set_cell_value(
|
|
sheets: Dict[str, Any],
|
|
sheet_id: str,
|
|
row: int,
|
|
col: int,
|
|
value: Any,
|
|
*,
|
|
style_template_row: Optional[int] = None,
|
|
style_template_col: Optional[int] = None,
|
|
) -> None:
|
|
sheet = sheets.get(sheet_id)
|
|
if not sheet:
|
|
return
|
|
cell_data = sheet.setdefault("cellData", {})
|
|
row_data = cell_data.setdefault(str(row), {})
|
|
cell_obj = row_data.setdefault(str(col), {})
|
|
style_ref = None
|
|
if style_template_row is not None:
|
|
style_ref = _template_style_ref(sheets, sheet_id, style_template_row, col)
|
|
elif style_template_col is not None:
|
|
style_ref = _template_style_ref(sheets, sheet_id, row, style_template_col)
|
|
if style_ref is not None:
|
|
cell_obj["s"] = style_ref
|
|
if value is None:
|
|
cell_obj["v"] = ""
|
|
elif isinstance(value, (dict, list)):
|
|
cell_obj["v"] = str(value)
|
|
else:
|
|
cell_obj["v"] = value
|
|
|
|
|
|
def _apply_fill_empty(
|
|
sheets: Dict[str, Any],
|
|
sheet_id: str,
|
|
custom: Dict[str, Any],
|
|
*,
|
|
direction: str,
|
|
anchor_row: int,
|
|
anchor_col: int,
|
|
data_len: int,
|
|
) -> None:
|
|
if not custom.get("fillEmptyRows"):
|
|
return
|
|
try:
|
|
fill_n = int(custom.get("fillEmptyNum") or 1)
|
|
except (TypeError, ValueError):
|
|
fill_n = 1
|
|
if fill_n < 1:
|
|
return
|
|
for offset in range(fill_n):
|
|
if direction == "down":
|
|
_set_cell_value(
|
|
sheets, sheet_id, anchor_row + data_len + offset, anchor_col, ""
|
|
)
|
|
else:
|
|
_set_cell_value(
|
|
sheets, sheet_id, anchor_row, anchor_col + data_len + offset, ""
|
|
)
|
|
|
|
|
|
def _slot_at_index(slots: List[ExpandedSlot], index: int) -> Optional[ExpandedSlot]:
|
|
if 0 <= index < len(slots):
|
|
return slots[index]
|
|
return None
|
|
|
|
|
|
def _slot_covering(slots: List[ExpandedSlot], pos: int) -> Optional[ExpandedSlot]:
|
|
for s in slots:
|
|
if s.start <= pos < s.end:
|
|
return s
|
|
return None
|
|
|
|
|
|
def _children_of(
|
|
parent: Dict[str, Any],
|
|
candidates: List[Dict[str, Any]],
|
|
by_pos: Dict[CellKey, Dict[str, Any]],
|
|
by_sheet: Dict[str, List[Dict[str, Any]]],
|
|
) -> List[Dict[str, Any]]:
|
|
pk = cell_key(parent)
|
|
out: List[Dict[str, Any]] = []
|
|
for c in candidates:
|
|
left, top = resolve_parents(c, by_pos, by_sheet)
|
|
if (left and cell_key(left) == pk) or (top and cell_key(top) == pk):
|
|
out.append(c)
|
|
return sorted(out, key=lambda x: (_int_coord(x.get("row")), _int_coord(x.get("col"))))
|
|
|
|
|
|
def _is_down_root(
|
|
cell: Dict[str, Any],
|
|
down_keys: set[CellKey],
|
|
by_pos: Dict[CellKey, Dict[str, Any]],
|
|
by_sheet: Dict[str, List[Dict[str, Any]]],
|
|
) -> bool:
|
|
left, top = resolve_parents(cell, by_pos, by_sheet)
|
|
if left and cell_key(left) in down_keys:
|
|
return False
|
|
if top and cell_key(top) in down_keys:
|
|
return False
|
|
return True
|
|
|
|
|
|
def _rows_for_bind(
|
|
rows: List[Dict[str, Any]],
|
|
cell: Dict[str, Any],
|
|
*,
|
|
left_parent: Optional[Dict[str, Any]],
|
|
top_parent: Optional[Dict[str, Any]],
|
|
left_slot: Optional[ExpandedSlot],
|
|
top_slot: Optional[ExpandedSlot],
|
|
) -> List[Dict[str, Any]]:
|
|
left_bind = left_slot.data_list if left_slot else None
|
|
top_bind = top_slot.data_list if top_slot else None
|
|
return filter_rows_by_parents(
|
|
rows,
|
|
cell,
|
|
left_parent=left_parent,
|
|
top_parent=top_parent,
|
|
left_bind=left_bind,
|
|
top_bind=top_bind,
|
|
)
|
|
|
|
|
|
def apply_pending_group_merges(
|
|
snapshot: Dict[str, Any],
|
|
pending: List[Tuple[str, int, int, int]],
|
|
) -> Dict[str, Any]:
|
|
sheets = snapshot.get("sheets") or {}
|
|
for sheet_id, col, start_row, end_row in pending:
|
|
_add_vertical_merge_region(sheets, sheet_id, start_row, end_row, col)
|
|
return snapshot
|
|
|
|
|
|
def _visit_down(
|
|
cell: Dict[str, Any],
|
|
rows: List[Dict[str, Any]],
|
|
row_cursor: int,
|
|
*,
|
|
sheets: Dict[str, Any],
|
|
sheet_id: str,
|
|
down_cells: List[Dict[str, Any]],
|
|
datasets: Dict[str, List[Any]],
|
|
by_pos: Dict[CellKey, Dict[str, Any]],
|
|
by_sheet: Dict[str, List[Dict[str, Any]]],
|
|
row_registry: Dict[CellKey, List[ExpandedSlot]],
|
|
bind_index: int = 0,
|
|
left_slot: Optional[ExpandedSlot] = None,
|
|
top_slot: Optional[ExpandedSlot] = None,
|
|
parent_scoped: bool = False,
|
|
pending_group_merges: Optional[List[Tuple[str, int, int, int]]] = None,
|
|
) -> int:
|
|
custom = cell.get("custom") or {}
|
|
field = resolve_field_path(_field_name(cell), _dataset_name(cell))
|
|
col = _int_coord(cell.get("col"))
|
|
anchor_row = _int_coord(cell.get("row"))
|
|
left_p, top_p = resolve_parents(cell, by_pos, by_sheet)
|
|
|
|
if not parent_scoped:
|
|
if left_slot is None and left_p:
|
|
left_slot = _slot_at_index(
|
|
row_registry.get(cell_key(left_p), []), bind_index
|
|
)
|
|
if top_slot is None and top_p:
|
|
top_slot = _slot_at_index(
|
|
row_registry.get(cell_key(top_p), []), bind_index
|
|
)
|
|
filtered = _rows_for_bind(
|
|
rows,
|
|
cell,
|
|
left_parent=left_p,
|
|
top_parent=top_p,
|
|
left_slot=left_slot,
|
|
top_slot=top_slot,
|
|
)
|
|
else:
|
|
filtered = [r if isinstance(r, dict) else {} for r in rows]
|
|
|
|
binds = build_bind_list(cell, filtered)
|
|
children = _children_of(cell, down_cells, by_pos, by_sheet)
|
|
child_cols = {_int_coord(ch.get("col")) for ch in children}
|
|
slots: List[ExpandedSlot] = []
|
|
end_row = row_cursor
|
|
|
|
for bi, bind in enumerate(binds):
|
|
block_start = end_row
|
|
if children:
|
|
child_end = block_start
|
|
for ch in children:
|
|
child_end = _visit_down(
|
|
ch,
|
|
bind.data_list,
|
|
block_start,
|
|
sheets=sheets,
|
|
sheet_id=sheet_id,
|
|
down_cells=down_cells,
|
|
datasets=datasets,
|
|
by_pos=by_pos,
|
|
by_sheet=by_sheet,
|
|
row_registry=row_registry,
|
|
bind_index=bi,
|
|
parent_scoped=True,
|
|
pending_group_merges=pending_group_merges,
|
|
)
|
|
block_len = max(1, child_end - block_start)
|
|
else:
|
|
block_len = expand_span(bind, cell)
|
|
|
|
if col not in child_cols:
|
|
style_row = anchor_row
|
|
if _poly_list(cell):
|
|
for i in range(block_len):
|
|
r = block_start + i
|
|
val = (
|
|
_get_nested_value(bind.data_list[i], field)
|
|
if i < len(bind.data_list)
|
|
else ""
|
|
)
|
|
_set_cell_value(
|
|
sheets,
|
|
sheet_id,
|
|
r,
|
|
col,
|
|
val,
|
|
style_template_row=style_row,
|
|
)
|
|
elif _poly_group(cell):
|
|
for i in range(block_len):
|
|
r = block_start + i
|
|
_set_cell_value(
|
|
sheets,
|
|
sheet_id,
|
|
r,
|
|
col,
|
|
bind.value,
|
|
style_template_row=style_row,
|
|
)
|
|
if (
|
|
pending_group_merges is not None
|
|
and block_len > 1
|
|
and _should_merge_group(cell)
|
|
):
|
|
pending_group_merges.append(
|
|
(sheet_id, col, block_start, block_start + block_len - 1)
|
|
)
|
|
else:
|
|
for i in range(block_len):
|
|
r = block_start + i
|
|
_set_cell_value(
|
|
sheets,
|
|
sheet_id,
|
|
r,
|
|
col,
|
|
bind.value,
|
|
style_template_row=style_row,
|
|
)
|
|
|
|
slots.append(
|
|
ExpandedSlot(bind.value, bind.data_list, block_start, block_start + block_len)
|
|
)
|
|
end_row = block_start + block_len
|
|
|
|
key = cell_key(cell)
|
|
row_registry[key] = row_registry.get(key, []) + slots
|
|
|
|
if custom.get("fillEmptyRows"):
|
|
_apply_fill_empty(
|
|
sheets,
|
|
sheet_id,
|
|
custom,
|
|
direction="down",
|
|
anchor_row=anchor_row,
|
|
anchor_col=col,
|
|
data_len=end_row - row_cursor,
|
|
)
|
|
return end_row
|
|
|
|
|
|
def _expand_sheet_down(
|
|
sheets: Dict[str, Any],
|
|
sheet_id: str,
|
|
down_cells: List[Dict[str, Any]],
|
|
datasets: Dict[str, List[Any]],
|
|
by_pos: Dict[CellKey, Dict[str, Any]],
|
|
by_sheet: Dict[str, List[Dict[str, Any]]],
|
|
pending_group_merges: Optional[List[Tuple[str, int, int, int]]] = None,
|
|
) -> None:
|
|
down_keys = {cell_key(c) for c in down_cells}
|
|
row_registry: Dict[CellKey, List[ExpandedSlot]] = {}
|
|
roots = [c for c in down_cells if _is_down_root(c, down_keys, by_pos, by_sheet)]
|
|
if not roots:
|
|
roots = down_cells
|
|
|
|
bands: Dict[int, List[Dict[str, Any]]] = {}
|
|
for c in roots:
|
|
bands.setdefault(_int_coord(c.get("row")), []).append(c)
|
|
|
|
for start_row in sorted(bands.keys()):
|
|
cursor = start_row
|
|
for root in sorted(bands[start_row], key=lambda x: _int_coord(x.get("col"))):
|
|
ds = _dataset_name(root)
|
|
raw = datasets.get(ds) or []
|
|
rows = [r if isinstance(r, dict) else {} for r in raw]
|
|
cursor = max(
|
|
cursor,
|
|
_visit_down(
|
|
root,
|
|
rows,
|
|
cursor,
|
|
sheets=sheets,
|
|
sheet_id=sheet_id,
|
|
down_cells=down_cells,
|
|
datasets=datasets,
|
|
by_pos=by_pos,
|
|
by_sheet=by_sheet,
|
|
row_registry=row_registry,
|
|
pending_group_merges=pending_group_merges,
|
|
),
|
|
)
|
|
|
|
|
|
def _legacy_down_band(
|
|
sheets: Dict[str, Any],
|
|
sheet_id: str,
|
|
start_row: int,
|
|
band: List[Dict[str, Any]],
|
|
datasets: Dict[str, List[Any]],
|
|
pending_group_merges: Optional[List[Tuple[str, int, int, int]]] = None,
|
|
) -> None:
|
|
max_len = 0
|
|
for item in band:
|
|
max_len = max(max_len, len(datasets.get(_dataset_name(item)) or []))
|
|
for item in sorted(band, key=lambda x: _int_coord(x.get("col"))):
|
|
ds = _dataset_name(item)
|
|
field = resolve_field_path(_field_name(item), ds)
|
|
col = _int_coord(item.get("col"))
|
|
data_rows = [r if isinstance(r, dict) else {} for r in datasets.get(ds) or []]
|
|
custom = item.get("custom") or {}
|
|
if _poly_group(item):
|
|
binds = build_bind_list(item, data_rows)
|
|
row_cursor = start_row
|
|
for bind in binds:
|
|
span = max(1, len(bind.data_list))
|
|
for i in range(span):
|
|
_set_cell_value(
|
|
sheets,
|
|
sheet_id,
|
|
row_cursor + i,
|
|
col,
|
|
bind.value,
|
|
style_template_row=start_row,
|
|
)
|
|
if (
|
|
span > 1
|
|
and pending_group_merges is not None
|
|
and _should_merge_group(item)
|
|
):
|
|
pending_group_merges.append(
|
|
(sheet_id, col, row_cursor, row_cursor + span - 1)
|
|
)
|
|
row_cursor += span
|
|
if custom.get("fillEmptyRows"):
|
|
_apply_fill_empty(
|
|
sheets,
|
|
sheet_id,
|
|
custom,
|
|
direction="down",
|
|
anchor_row=start_row,
|
|
anchor_col=col,
|
|
data_len=row_cursor - start_row,
|
|
)
|
|
continue
|
|
for i in range(max_len):
|
|
if i < len(data_rows):
|
|
val = _get_nested_value(data_rows[i], field)
|
|
else:
|
|
val = ""
|
|
_set_cell_value(
|
|
sheets,
|
|
sheet_id,
|
|
start_row + i,
|
|
col,
|
|
val,
|
|
style_template_row=start_row,
|
|
)
|
|
if custom.get("fillEmptyRows"):
|
|
_apply_fill_empty(
|
|
sheets,
|
|
sheet_id,
|
|
custom,
|
|
direction="down",
|
|
anchor_row=start_row,
|
|
anchor_col=col,
|
|
data_len=max_len,
|
|
)
|
|
|
|
|
|
def _has_down_parent_link(
|
|
band: List[Dict[str, Any]],
|
|
down_keys: set[CellKey],
|
|
by_pos: Dict[CellKey, Dict[str, Any]],
|
|
by_sheet: Dict[str, List[Dict[str, Any]]],
|
|
) -> bool:
|
|
for c in band:
|
|
left, top = resolve_parents(c, by_pos, by_sheet)
|
|
if left and cell_key(left) in down_keys:
|
|
return True
|
|
if top and cell_key(top) in down_keys:
|
|
return True
|
|
return False
|
|
|
|
|
|
def _visit_right(
|
|
cell: Dict[str, Any],
|
|
rows: List[Dict[str, Any]],
|
|
col_cursor: int,
|
|
*,
|
|
sheets: Dict[str, Any],
|
|
sheet_id: str,
|
|
row: int,
|
|
right_cells: List[Dict[str, Any]],
|
|
by_pos: Dict[CellKey, Dict[str, Any]],
|
|
by_sheet: Dict[str, List[Dict[str, Any]]],
|
|
col_registry: Dict[CellKey, List[ExpandedSlot]],
|
|
bind_index: int = 0,
|
|
left_slot: Optional[ExpandedSlot] = None,
|
|
top_slot: Optional[ExpandedSlot] = None,
|
|
parent_scoped: bool = False,
|
|
) -> int:
|
|
custom = cell.get("custom") or {}
|
|
field = resolve_field_path(_field_name(cell), _dataset_name(cell))
|
|
col = _int_coord(cell.get("col"))
|
|
anchor_col = col
|
|
left_p, top_p = resolve_parents(cell, by_pos, by_sheet)
|
|
|
|
if not parent_scoped:
|
|
if left_slot is None and left_p:
|
|
left_slot = _slot_at_index(
|
|
col_registry.get(cell_key(left_p), []), bind_index
|
|
)
|
|
if top_slot is None and top_p:
|
|
top_slot = _slot_at_index(
|
|
col_registry.get(cell_key(top_p), []), bind_index
|
|
)
|
|
filtered = _rows_for_bind(
|
|
rows,
|
|
cell,
|
|
left_parent=left_p,
|
|
top_parent=top_p,
|
|
left_slot=left_slot,
|
|
top_slot=top_slot,
|
|
)
|
|
else:
|
|
filtered = [r if isinstance(r, dict) else {} for r in rows]
|
|
|
|
binds = build_bind_list(cell, filtered)
|
|
children = _children_of(cell, right_cells, by_pos, by_sheet)
|
|
child_cols = {_int_coord(ch.get("col")) for ch in children}
|
|
slots: List[ExpandedSlot] = []
|
|
end_col = col_cursor
|
|
|
|
for bi, bind in enumerate(binds):
|
|
block_start = end_col
|
|
if children:
|
|
child_end = block_start
|
|
for ch in children:
|
|
child_end = _visit_right(
|
|
ch,
|
|
bind.data_list,
|
|
block_start,
|
|
sheets=sheets,
|
|
sheet_id=sheet_id,
|
|
row=row,
|
|
right_cells=right_cells,
|
|
by_pos=by_pos,
|
|
by_sheet=by_sheet,
|
|
col_registry=col_registry,
|
|
bind_index=bi,
|
|
parent_scoped=True,
|
|
)
|
|
block_len = max(1, child_end - block_start)
|
|
else:
|
|
block_len = expand_span(bind, cell)
|
|
|
|
if col not in child_cols:
|
|
for i in range(block_len):
|
|
c = block_start + i
|
|
if _poly_list(cell):
|
|
val = (
|
|
_get_nested_value(bind.data_list[i], field)
|
|
if i < len(bind.data_list)
|
|
else ""
|
|
)
|
|
else:
|
|
val = bind.value
|
|
_set_cell_value(
|
|
sheets,
|
|
sheet_id,
|
|
row,
|
|
c,
|
|
val,
|
|
style_template_col=anchor_col,
|
|
)
|
|
|
|
slots.append(
|
|
ExpandedSlot(bind.value, bind.data_list, block_start, block_start + block_len)
|
|
)
|
|
end_col = block_start + block_len
|
|
|
|
key = cell_key(cell)
|
|
col_registry[key] = col_registry.get(key, []) + slots
|
|
|
|
if custom.get("fillEmptyRows"):
|
|
_apply_fill_empty(
|
|
sheets,
|
|
sheet_id,
|
|
custom,
|
|
direction="right",
|
|
anchor_row=row,
|
|
anchor_col=col,
|
|
data_len=end_col - col_cursor,
|
|
)
|
|
return end_col
|
|
|
|
|
|
def _is_right_root(
|
|
cell: Dict[str, Any],
|
|
right_keys: set[CellKey],
|
|
by_pos: Dict[CellKey, Dict[str, Any]],
|
|
by_sheet: Dict[str, List[Dict[str, Any]]],
|
|
) -> bool:
|
|
left, top = resolve_parents(cell, by_pos, by_sheet)
|
|
if left and cell_key(left) in right_keys:
|
|
return False
|
|
if top and cell_key(top) in right_keys:
|
|
return False
|
|
return True
|
|
|
|
|
|
def _expand_sheet_right(
|
|
sheets: Dict[str, Any],
|
|
sheet_id: str,
|
|
row: int,
|
|
right_cells: List[Dict[str, Any]],
|
|
datasets: Dict[str, List[Any]],
|
|
by_pos: Dict[CellKey, Dict[str, Any]],
|
|
by_sheet: Dict[str, List[Dict[str, Any]]],
|
|
) -> None:
|
|
right_keys = {cell_key(c) for c in right_cells}
|
|
col_registry: Dict[CellKey, List[ExpandedSlot]] = {}
|
|
roots = [c for c in right_cells if _is_right_root(c, right_keys, by_pos, by_sheet)]
|
|
if not roots:
|
|
roots = right_cells
|
|
|
|
cursor = min(_int_coord(c.get("col")) for c in roots)
|
|
for root in sorted(roots, key=lambda x: _int_coord(x.get("col"))):
|
|
ds = _dataset_name(root)
|
|
raw = datasets.get(ds) or []
|
|
rows_data = [r if isinstance(r, dict) else {} for r in raw]
|
|
cursor = max(
|
|
cursor,
|
|
_visit_right(
|
|
root,
|
|
rows_data,
|
|
cursor,
|
|
sheets=sheets,
|
|
sheet_id=sheet_id,
|
|
row=row,
|
|
right_cells=right_cells,
|
|
by_pos=by_pos,
|
|
by_sheet=by_sheet,
|
|
col_registry=col_registry,
|
|
),
|
|
)
|
|
|
|
|
|
def _legacy_right_band(
|
|
sheets: Dict[str, Any],
|
|
sheet_id: str,
|
|
row: int,
|
|
band: List[Dict[str, Any]],
|
|
datasets: Dict[str, List[Any]],
|
|
) -> None:
|
|
max_len = max((len(datasets.get(_dataset_name(c)) or []) for c in band), default=0)
|
|
for item in sorted(band, key=lambda x: _int_coord(x.get("col"))):
|
|
ds = _dataset_name(item)
|
|
field = resolve_field_path(_field_name(item), ds)
|
|
start_col = _int_coord(item.get("col"))
|
|
data_rows = datasets.get(ds) or []
|
|
for i in range(max_len):
|
|
if i < len(data_rows) and isinstance(data_rows[i], dict):
|
|
val = _get_nested_value(data_rows[i], field)
|
|
else:
|
|
val = ""
|
|
_set_cell_value(
|
|
sheets,
|
|
sheet_id,
|
|
row,
|
|
start_col + i,
|
|
val,
|
|
style_template_col=start_col,
|
|
)
|
|
|
|
|
|
def apply_data_source_cells(
|
|
snapshot: Dict[str, Any],
|
|
cells_meta: Dict[str, Any],
|
|
datasets: Dict[str, List[Any]],
|
|
) -> Tuple[Dict[str, Any], List[Tuple[str, int, int, int]]]:
|
|
result = copy.deepcopy(snapshot)
|
|
sheets = result.get("sheets") or {}
|
|
pending_group_merges: List[Tuple[str, int, int, int]] = []
|
|
cell_list = cells_meta.get("cells") or []
|
|
by_pos, by_sheet = _index_data_sources(cell_list)
|
|
|
|
down_by_sheet: Dict[str, List[Dict[str, Any]]] = {}
|
|
right_by_sheet_row: Dict[Tuple[str, int], List[Dict[str, Any]]] = {}
|
|
singles: List[dict] = []
|
|
|
|
for cell in cell_list:
|
|
if cell.get("type") != "dataSource":
|
|
continue
|
|
custom = cell.get("custom") or {}
|
|
if not _dataset_name(cell) or not _field_name(cell):
|
|
continue
|
|
direction = _expand_direction(custom)
|
|
sheet_id = str(cell.get("sheet", "sheet1"))
|
|
|
|
if direction == "down":
|
|
down_by_sheet.setdefault(sheet_id, []).append(cell)
|
|
elif direction == "right":
|
|
row = _int_coord(cell.get("row"), 0)
|
|
right_by_sheet_row.setdefault((sheet_id, row), []).append(cell)
|
|
else:
|
|
singles.append(cell)
|
|
|
|
band_lengths = _compute_down_band_lengths(down_by_sheet, datasets)
|
|
for (sheet_id, start_row), max_len in sorted(
|
|
band_lengths.items(), key=lambda x: x[0][1], reverse=True
|
|
):
|
|
delta = max(0, max_len - 1)
|
|
if delta > 0:
|
|
_shift_sheet_rows_down(sheets, sheet_id, start_row + 1, delta)
|
|
|
|
for sheet_id, down_cells in down_by_sheet.items():
|
|
down_keys = {cell_key(c) for c in down_cells}
|
|
bands: Dict[int, List[Dict[str, Any]]] = {}
|
|
for c in down_cells:
|
|
bands.setdefault(_int_coord(c.get("row")), []).append(c)
|
|
uses_tree = False
|
|
for band in bands.values():
|
|
if _has_down_parent_link(band, down_keys, by_pos, by_sheet):
|
|
uses_tree = True
|
|
break
|
|
if uses_tree:
|
|
_expand_sheet_down(
|
|
sheets,
|
|
sheet_id,
|
|
down_cells,
|
|
datasets,
|
|
by_pos,
|
|
by_sheet,
|
|
pending_group_merges,
|
|
)
|
|
else:
|
|
for start_row, band in bands.items():
|
|
_legacy_down_band(
|
|
sheets,
|
|
sheet_id,
|
|
start_row,
|
|
band,
|
|
datasets,
|
|
pending_group_merges,
|
|
)
|
|
|
|
for (sheet_id, row), band in right_by_sheet_row.items():
|
|
right_keys = {cell_key(c) for c in band}
|
|
if _has_down_parent_link(band, right_keys, by_pos, by_sheet):
|
|
_expand_sheet_right(
|
|
sheets, sheet_id, row, band, datasets, by_pos, by_sheet
|
|
)
|
|
else:
|
|
_legacy_right_band(sheets, sheet_id, row, band, datasets)
|
|
|
|
for cell in singles:
|
|
sheet_id = str(cell.get("sheet", "sheet1"))
|
|
template_row = _int_coord(cell.get("row"), 0)
|
|
col = _int_coord(cell.get("col"), 0)
|
|
row = (
|
|
_summary_output_row(sheet_id, template_row, band_lengths)
|
|
if _poly_summary(cell)
|
|
else template_row
|
|
)
|
|
raw = datasets.get(_dataset_name(cell)) or []
|
|
rows = [r if isinstance(r, dict) else {} for r in raw]
|
|
if _poly_summary(cell):
|
|
filtered = _summary_filter_rows(cell, rows, by_pos, by_sheet)
|
|
else:
|
|
left, top = resolve_parents(cell, by_pos, by_sheet)
|
|
left_bind = top_bind = None
|
|
if left:
|
|
lb = build_bind_list(left, rows)
|
|
left_bind = lb[0].data_list if lb else None
|
|
if top:
|
|
tb = build_bind_list(top, rows)
|
|
top_bind = tb[0].data_list if tb else None
|
|
filtered = filter_rows_by_parents(
|
|
rows,
|
|
cell,
|
|
left_parent=left,
|
|
top_parent=top,
|
|
left_bind=left_bind,
|
|
top_bind=top_bind,
|
|
)
|
|
binds = build_bind_list(cell, filtered)
|
|
val = binds[0].value if binds else ""
|
|
_set_cell_value(sheets, sheet_id, row, col, val)
|
|
|
|
result["sheets"] = sheets
|
|
return result, pending_group_merges
|