Merge remote-tracking branch 'origin/dev' into sub_laptop_2
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
"""Z01 공종 저장소 — `master_work_item`·`master_work_item_row`·`master_revision` 읽기·쓰기(SQL 만).
|
||||
|
||||
무엇을 쓸지는 `Z01_MasterData_WorkItems` 순수 함수(`plan_rows`·`reset_plan`)가 정함 ·
|
||||
여기는 받은 대로 씀.
|
||||
표 모양 = `db_management/020_master_cost.sql`(PLAN 1-0 계약). 트랜잭션은 부르는 쪽(라우터)이 엶.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
import aiomysql
|
||||
|
||||
HEAD_COLUMNS = (
|
||||
"id, axis, work_item_key, code, number, name, path_name, parent_key, parent_mode, "
|
||||
"basis_quantity, basis_unit, status, notes, source, seed, edition, sort_order, "
|
||||
"updated_by, updated_at"
|
||||
)
|
||||
ROW_COLUMNS = (
|
||||
"id, variant, resource_kind, resource_key, resource_name, resource_spec, amount, "
|
||||
"amount_unit, alternative_amount, group_ratio_pct, raw_cell, pum_table_id, seed, "
|
||||
"sort_order, is_deleted"
|
||||
)
|
||||
_JSON = ("notes", "source", "seed")
|
||||
|
||||
|
||||
def _parsed(row: dict[str, Any]) -> dict[str, Any]:
|
||||
"""JSON 칸은 MariaDB 가 글자로 돌려줌 — 풀어서 씀."""
|
||||
for key in _JSON:
|
||||
if isinstance(row.get(key), (str, bytes)):
|
||||
row[key] = json.loads(row[key])
|
||||
return row
|
||||
|
||||
|
||||
def _like(q: str) -> str:
|
||||
return "%" + q.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_") + "%"
|
||||
|
||||
|
||||
async def list_items(
|
||||
connection: aiomysql.Connection, *, axis: str, status: str, q: str, page: int, size: int
|
||||
) -> dict[str, Any]:
|
||||
"""목록 한 쪽 + 상태별 개수(상태 거르기 전 · 탭 숫자용). total 은 개수에서 셈(질의 하나 덜)."""
|
||||
where, params = ["1"], []
|
||||
if axis:
|
||||
where.append("w.axis = %s")
|
||||
params.append(axis)
|
||||
if q:
|
||||
where.append("(w.work_item_key LIKE %s OR w.number LIKE %s OR w.path_name LIKE %s)")
|
||||
params += [_like(q)] * 3
|
||||
picked, picked_params = list(where), list(params)
|
||||
if status:
|
||||
picked.append("w.status = %s")
|
||||
picked_params.append(status)
|
||||
async with connection.cursor(aiomysql.DictCursor) as cursor:
|
||||
await cursor.execute(
|
||||
"SELECT w.status, COUNT(*) AS n FROM master_work_item w "
|
||||
f"WHERE {' AND '.join(where)} GROUP BY w.status",
|
||||
params,
|
||||
)
|
||||
counts = {row["status"]: int(row["n"]) for row in await cursor.fetchall()}
|
||||
await cursor.execute(
|
||||
"SELECT w.work_item_key, w.axis, w.number, w.name, w.path_name, w.status, "
|
||||
"(SELECT COUNT(*) FROM master_work_item_row r "
|
||||
" WHERE r.work_item_id = w.id AND r.is_deleted = 0) AS row_count "
|
||||
f"FROM master_work_item w WHERE {' AND '.join(picked)} "
|
||||
"ORDER BY w.axis, w.sort_order, w.id LIMIT %s OFFSET %s",
|
||||
[*picked_params, size, (page - 1) * size],
|
||||
)
|
||||
items = list(await cursor.fetchall())
|
||||
total = counts.get(status, 0) if status else sum(counts.values())
|
||||
return {"items": items, "total": total, "counts": counts}
|
||||
|
||||
|
||||
async def fetch_item(
|
||||
connection: aiomysql.Connection, key: str, *, lock: bool = False
|
||||
) -> dict[str, Any] | None:
|
||||
async with connection.cursor(aiomysql.DictCursor) as cursor:
|
||||
await cursor.execute(
|
||||
f"SELECT {HEAD_COLUMNS} FROM master_work_item WHERE work_item_key = %s"
|
||||
+ (" FOR UPDATE" if lock else ""),
|
||||
(key,),
|
||||
)
|
||||
row = await cursor.fetchone()
|
||||
return _parsed(row) if row else None
|
||||
|
||||
|
||||
async def fetch_rows(
|
||||
connection: aiomysql.Connection, item_id: int, *, with_deleted: bool = False
|
||||
) -> list[dict[str, Any]]:
|
||||
"""구성 줄 — 기본은 살아 있는 줄만 · [초기값으로] 는 숨긴 줄까지."""
|
||||
async with connection.cursor(aiomysql.DictCursor) as cursor:
|
||||
await cursor.execute(
|
||||
f"SELECT {ROW_COLUMNS} FROM master_work_item_row WHERE work_item_id = %s"
|
||||
+ ("" if with_deleted else " AND is_deleted = 0")
|
||||
+ " ORDER BY sort_order, id",
|
||||
(item_id,),
|
||||
)
|
||||
return [_parsed(row) for row in await cursor.fetchall()]
|
||||
|
||||
|
||||
async def revision(connection: aiomysql.Connection, scope: str, *, lock: bool = False) -> int:
|
||||
"""지금 revision(없으면 0). lock 이면 줄을 만들어 잡음 — 같은 공종 저장이 줄 서게."""
|
||||
async with connection.cursor() as cursor:
|
||||
if lock:
|
||||
await cursor.execute(
|
||||
"INSERT IGNORE INTO master_revision (scope, revision) VALUES (%s, 0)", (scope,)
|
||||
)
|
||||
await cursor.execute(
|
||||
"SELECT revision FROM master_revision WHERE scope = %s"
|
||||
+ (" FOR UPDATE" if lock else ""),
|
||||
(scope,),
|
||||
)
|
||||
row = await cursor.fetchone()
|
||||
return int(row[0]) if row else 0
|
||||
|
||||
|
||||
async def bump_revision(connection: aiomysql.Connection, scope: str) -> None:
|
||||
async with connection.cursor() as cursor:
|
||||
await cursor.execute(
|
||||
"INSERT INTO master_revision (scope, revision) VALUES (%s, 1) "
|
||||
"ON DUPLICATE KEY UPDATE revision = revision + 1",
|
||||
(scope,),
|
||||
)
|
||||
|
||||
|
||||
async def known_resource_keys(connection: aiomysql.Connection, keys: set[str]) -> set[str]:
|
||||
"""그중 기초단가(`master_base_price.row_key`)에 실재하는 열쇠."""
|
||||
if not keys:
|
||||
return set()
|
||||
marks = ", ".join(["%s"] * len(keys))
|
||||
async with connection.cursor() as cursor:
|
||||
await cursor.execute(
|
||||
"SELECT DISTINCT row_key FROM master_base_price "
|
||||
f"WHERE is_deleted = 0 AND row_key IN ({marks})",
|
||||
list(keys),
|
||||
)
|
||||
return {row[0] for row in await cursor.fetchall()}
|
||||
|
||||
|
||||
async def write(
|
||||
connection: aiomysql.Connection,
|
||||
item_id: int,
|
||||
head: dict[str, Any],
|
||||
plan: dict[str, list[Any]],
|
||||
by: Any,
|
||||
) -> None:
|
||||
"""머리 칸 + 줄 쓰기(`plan_rows`·`reset_plan` 모양).
|
||||
|
||||
칸 이름은 순수 함수의 고정 목록에서만 옴(요청 글자가 SQL 에 안 들어감).
|
||||
"""
|
||||
async with connection.cursor() as cursor:
|
||||
sets = "".join(f"{field} = %s, " for field in head)
|
||||
await cursor.execute(
|
||||
f"UPDATE master_work_item SET {sets}updated_by = %s, updated_at = NOW() WHERE id = %s",
|
||||
[*head.values(), by, item_id],
|
||||
)
|
||||
for row in plan["updates"]: # 줄마다 칸이 다를 수 있음(seed 에 있는 칸만) — 한 공종 수십 줄
|
||||
values = {k: v for k, v in row.items() if k != "id"}
|
||||
sets = "".join(f"{field} = %s, " for field in values)
|
||||
await cursor.execute(
|
||||
f"UPDATE master_work_item_row SET {sets}is_deleted = 0 "
|
||||
"WHERE id = %s AND work_item_id = %s",
|
||||
[*values.values(), row["id"], item_id],
|
||||
)
|
||||
if plan["inserts"]:
|
||||
fields = list(plan["inserts"][0])
|
||||
await cursor.executemany(
|
||||
f"INSERT INTO master_work_item_row (work_item_id, {', '.join(fields)}) "
|
||||
f"VALUES (%s, {', '.join(['%s'] * len(fields))})",
|
||||
[[item_id, *(row[f] for f in fields)] for row in plan["inserts"]],
|
||||
)
|
||||
for ids, sql in (
|
||||
(plan["hide"], "UPDATE master_work_item_row SET is_deleted = 1"),
|
||||
(plan["drop"], "DELETE FROM master_work_item_row"),
|
||||
):
|
||||
if ids:
|
||||
await cursor.execute(
|
||||
f"{sql} WHERE work_item_id = %s AND id IN ({', '.join(['%s'] * len(ids))})",
|
||||
[item_id, *ids],
|
||||
)
|
||||
@@ -0,0 +1,152 @@
|
||||
"""Z01 공종 편집 라우터 — 공종 머리·구성 줄을 관리자가 고치고 메움(2026-09-18 PLAN 1-0 계약).
|
||||
|
||||
GET /api/master-data/work-items?axis&status&q&page&size 목록 + 상태별 개수
|
||||
GET /api/master-data/work-items/{key} 머리(+revision)·구성 줄·원문 표·[주]
|
||||
PUT /api/master-data/work-items/{key} 전체 줄 보내기
|
||||
→ 409(낡은 revision) · 422(검사)
|
||||
POST /api/master-data/work-items/{key}/reset 머리·줄을 초기값으로(추가 줄 삭제)
|
||||
쓰기는 한 트랜잭션(전부 아니면 전무) · 응답은 새 GET 모양 그대로(화면이 다시 안 불러도 됨).
|
||||
⚠ 권한은 등록하는 쪽(`main.py`)이 시스템 관리자 전용 묶음으로 붙임.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from decimal import Decimal
|
||||
from typing import Any, Literal
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
from common_util.common_util_auth import verify_session
|
||||
from config.config_db import get_db_pool
|
||||
from Z01_MasterData import Z01_MasterData_Repository_WorkItems as repo
|
||||
from Z01_MasterData import Z01_MasterData_Tables as tables
|
||||
from Z01_MasterData import Z01_MasterData_WorkItems as work_items
|
||||
|
||||
router = APIRouter(prefix="/api/master-data", tags=["Z01 MasterData WorkItems"])
|
||||
|
||||
Status = Literal[work_items.STATUSES] # 튜플을 펼침 — 상태 이름은 한 곳(`STATUSES`)
|
||||
MAX_ROWS = 500
|
||||
|
||||
|
||||
class _Strict(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
|
||||
class ItemHead(_Strict):
|
||||
name: str = Field(min_length=1)
|
||||
basis_quantity: Decimal | None = Field(default=None, ge=0)
|
||||
basis_unit: str | None = None
|
||||
status: Status
|
||||
|
||||
|
||||
class ItemRow(_Strict):
|
||||
id: int | None = None # 있으면 수정 · 없으면 추가
|
||||
variant: str = ""
|
||||
resource_kind: Literal["labor", "machine", "material", "etc"]
|
||||
resource_key: str | None = Field(default=None, min_length=1) # 못 맞춘 줄은 null(빈 글자 금지)
|
||||
resource_name: str = ""
|
||||
resource_spec: str = ""
|
||||
amount: Decimal | None = Field(default=None, ge=0)
|
||||
amount_unit: str = ""
|
||||
alternative_amount: Decimal | None = Field(default=None, ge=0)
|
||||
group_ratio_pct: Decimal | None = Field(default=None, ge=0)
|
||||
raw_cell: str | None = None
|
||||
pum_table_id: str = ""
|
||||
|
||||
|
||||
class WorkItemSave(_Strict):
|
||||
base_revision: int
|
||||
item: ItemHead
|
||||
rows: list[ItemRow] = Field(max_length=MAX_ROWS) # 전체 줄 — 빠진 줄은 삭제
|
||||
|
||||
|
||||
@router.get("/work-items")
|
||||
async def list_work_items(
|
||||
axis: Literal["", "forest", "const"] = "",
|
||||
status: Status | Literal[""] = "",
|
||||
q: str = "",
|
||||
page: int = 1,
|
||||
size: int = tables.DEFAULT_PAGE_SIZE,
|
||||
) -> dict[str, Any]:
|
||||
page, size = max(page, 1), max(1, min(size, tables.MAX_PAGE_SIZE))
|
||||
async with get_db_pool().acquire() as connection:
|
||||
found = await repo.list_items(
|
||||
connection, axis=axis, status=status, q=q.strip(), page=page, size=size
|
||||
)
|
||||
counts = {s: found["counts"].get(s, 0) for s in work_items.STATUSES}
|
||||
return {"status": "success", **found, "counts": counts, "page": page, "size": size}
|
||||
|
||||
|
||||
async def _detail(connection: Any, key: str) -> dict[str, Any]:
|
||||
head = await repo.fetch_item(connection, key)
|
||||
if head is None:
|
||||
raise HTTPException(status_code=404, detail=f"없는 공종: {key}")
|
||||
rows = await repo.fetch_rows(connection, head["id"])
|
||||
revision = await repo.revision(connection, work_items.revision_scope(key))
|
||||
return {"status": "success", **work_items.detail(head, rows, revision)}
|
||||
|
||||
|
||||
@router.get("/work-items/{key}")
|
||||
async def get_work_item(key: str) -> dict[str, Any]:
|
||||
async with get_db_pool().acquire() as connection:
|
||||
return await _detail(connection, key)
|
||||
|
||||
|
||||
async def _locked(connection: Any, key: str) -> tuple[dict[str, Any], int]:
|
||||
"""공종 머리 + revision 을 잡음(같은 공종 저장이 줄 섬)."""
|
||||
head = await repo.fetch_item(connection, key, lock=True)
|
||||
if head is None:
|
||||
raise HTTPException(status_code=404, detail=f"없는 공종: {key}")
|
||||
return head, await repo.revision(connection, work_items.revision_scope(key), lock=True)
|
||||
|
||||
|
||||
@router.put("/work-items/{key}")
|
||||
async def put_work_item(
|
||||
key: str, body: WorkItemSave, session: dict = Depends(verify_session)
|
||||
) -> dict[str, Any]:
|
||||
rows = [row.model_dump() for row in body.rows]
|
||||
async with get_db_pool().acquire() as connection:
|
||||
await connection.begin()
|
||||
try:
|
||||
head, current = await _locked(connection, key)
|
||||
if current != body.base_revision:
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail="다른 곳에서 먼저 고쳤음 — 다시 불러올 것"
|
||||
f"(지금 {current} · 보낸 {body.base_revision})",
|
||||
)
|
||||
existing = await repo.fetch_rows(connection, head["id"])
|
||||
known = await repo.known_resource_keys(
|
||||
connection, {row["resource_key"] for row in rows if row["resource_key"]}
|
||||
)
|
||||
errors = work_items.check_rows(rows, {row["id"] for row in existing}, known)
|
||||
if errors:
|
||||
raise HTTPException(status_code=422, detail="\n".join(errors))
|
||||
plan = work_items.plan_rows(existing, rows)
|
||||
await repo.write(
|
||||
connection, head["id"], body.item.model_dump(), plan, session.get("user_id")
|
||||
)
|
||||
await repo.bump_revision(connection, work_items.revision_scope(key))
|
||||
await connection.commit()
|
||||
except BaseException:
|
||||
await connection.rollback()
|
||||
raise
|
||||
return await _detail(connection, key)
|
||||
|
||||
|
||||
@router.post("/work-items/{key}/reset")
|
||||
async def reset_work_item(key: str, session: dict = Depends(verify_session)) -> dict[str, Any]:
|
||||
async with get_db_pool().acquire() as connection:
|
||||
await connection.begin()
|
||||
try:
|
||||
head, _current = await _locked(connection, key)
|
||||
rows = await repo.fetch_rows(connection, head["id"], with_deleted=True)
|
||||
plan = work_items.reset_plan(head, rows)
|
||||
await repo.write(connection, head["id"], plan["head"], plan, session.get("user_id"))
|
||||
await repo.bump_revision(connection, work_items.revision_scope(key))
|
||||
await connection.commit()
|
||||
except BaseException:
|
||||
await connection.rollback()
|
||||
raise
|
||||
return await _detail(connection, key)
|
||||
@@ -7,11 +7,15 @@
|
||||
— 빠지면 발파암(절취 0.1 + 깍기 0.9 + 집토 1) 금액이 틀어짐.
|
||||
`@id` = 불변 열쇠(FW-·CW-) — 없는 줄은 목차 코드로 서고 알림에 드러냄.
|
||||
⚠ 읽기만 — 고칠 칸 없음(원문 값 풀기 8-7 뒤). 원본 `work_item_code`(FP-)는 B08·B09 가 씀 — 안 건드림.
|
||||
|
||||
공종 편집(DB 정본 · 2026-09-18 PLAN 1-0) — 맨 아래 「공종 편집」 절. `master_work_item`·`_row` 를
|
||||
고치는 규칙만 순수 함수로(DB 없이 시험) · SQL 은 `Z01_MasterData_Repository_WorkItems`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from decimal import Decimal
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
@@ -292,3 +296,131 @@ def extra(kind: str) -> dict[str, Any]:
|
||||
"units": len(rows),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# ── 공종 편집 — DB 표 `master_work_item`·`master_work_item_row` 를 고치는 규칙(PLAN 1-0 계약) ──
|
||||
|
||||
STATUSES = ("auto_ok", "partial", "needs_input", "not_item", "reviewed")
|
||||
#: 관리자가 고치는 칸 — [초기값으로] 도 이 칸만 seed 에서 되살림
|
||||
HEAD_FIELDS = ("name", "basis_quantity", "basis_unit", "status")
|
||||
ROW_FIELDS = (
|
||||
"variant",
|
||||
"resource_kind",
|
||||
"resource_key",
|
||||
"resource_name",
|
||||
"resource_spec",
|
||||
"amount",
|
||||
"amount_unit",
|
||||
"alternative_amount",
|
||||
"group_ratio_pct",
|
||||
"raw_cell",
|
||||
"pum_table_id",
|
||||
)
|
||||
_HIDDEN = ("id", "work_item_id", "seed", "source", "notes", "is_deleted")
|
||||
|
||||
|
||||
def revision_scope(key: str) -> str:
|
||||
return f"work_item:{key}"
|
||||
|
||||
|
||||
def _same(a: Any, b: Any) -> bool:
|
||||
"""칸 값이 같은가 — 숫자는 값으로(DB `1.500000` = 요청 `1.5` = seed `1.5`) · 빈 글자 = NULL."""
|
||||
numbers = (int, float, Decimal)
|
||||
if isinstance(a, numbers) and isinstance(b, numbers):
|
||||
return Decimal(str(a)) == Decimal(str(b))
|
||||
return (None if a == "" else a) == (None if b == "" else b)
|
||||
|
||||
|
||||
def changed_columns(current: dict[str, Any], seed: dict[str, Any] | None, fields) -> list[str]:
|
||||
"""초기값과 다른 칸 — 관리자 추가 줄(seed 없음)은 빈 목록(`is_added` 로 드러남)."""
|
||||
if seed is None:
|
||||
return []
|
||||
return [f for f in fields if f in seed and not _same(current.get(f), seed[f])]
|
||||
|
||||
|
||||
def detail(head: dict[str, Any], rows: list[dict[str, Any]], revision: int) -> dict[str, Any]:
|
||||
"""GET 응답 — 머리(+revision) · 구성 줄 · 원문 표 · [주]. seed 는 안 내보내고 고친 칸 이름만."""
|
||||
seed = head.get("seed")
|
||||
return {
|
||||
"item": {
|
||||
**{k: v for k, v in head.items() if k not in _HIDDEN},
|
||||
"revision": revision,
|
||||
"changed_columns": changed_columns(head, seed, HEAD_FIELDS),
|
||||
},
|
||||
"rows": [
|
||||
{
|
||||
**{k: v for k, v in row.items() if k not in _HIDDEN or k == "id"},
|
||||
"is_added": row.get("seed") is None,
|
||||
"changed_columns": changed_columns(row, row.get("seed"), ROW_FIELDS),
|
||||
}
|
||||
for row in rows
|
||||
],
|
||||
"source": head.get("source") or [],
|
||||
"notes": head.get("notes") or [],
|
||||
}
|
||||
|
||||
|
||||
def check_rows(
|
||||
rows: list[dict[str, Any]], existing_ids: set[int], known_keys: set[str]
|
||||
) -> list[str]:
|
||||
"""[저장] 서버 검사 중 DB 와 맞춰 볼 것 — 모양(숫자 ≥0 · 종류 · 모르는 칸)은 요청 모델이 봄."""
|
||||
errors, seen = [], set()
|
||||
for number, row in enumerate(rows, 1):
|
||||
row_id = row.get("id")
|
||||
if row_id is not None:
|
||||
if row_id not in existing_ids:
|
||||
errors.append(f"{number}번째 줄: 이 공종에 없는 줄(id {row_id})")
|
||||
elif row_id in seen:
|
||||
errors.append(f"{number}번째 줄: 같은 줄(id {row_id})이 두 번 옴")
|
||||
seen.add(row_id)
|
||||
key = row.get("resource_key")
|
||||
if key is not None and key not in known_keys:
|
||||
errors.append(f"{number}번째 줄: 기초단가에 없는 자원 열쇠 {key}")
|
||||
return errors
|
||||
|
||||
|
||||
def plan_rows(
|
||||
existing: list[dict[str, Any]], incoming: list[dict[str, Any]]
|
||||
) -> dict[str, list[Any]]:
|
||||
"""전체 줄 보내기 → 쓸 것. existing = 지금 살아 있는 줄 · incoming 순서 = sort_order.
|
||||
|
||||
id 있음 → 수정(바뀐 줄만) · id 없음 → 추가 · 빠진 줄 → 초기값 줄은 숨김(`is_deleted` ·
|
||||
[초기값으로] 가 살림) · 관리자 추가 줄은 지움.
|
||||
"""
|
||||
by_id = {row["id"]: row for row in existing}
|
||||
updates, inserts = [], []
|
||||
for order, row in enumerate(incoming):
|
||||
values = {f: row.get(f) for f in ROW_FIELDS} | {"sort_order": order}
|
||||
old = by_id.get(row.get("id"))
|
||||
if old is None:
|
||||
inserts.append(values)
|
||||
elif any(not _same(old.get(f), v) for f, v in values.items()):
|
||||
updates.append({"id": old["id"], **values})
|
||||
kept = {row.get("id") for row in incoming}
|
||||
gone = [row for row in existing if row["id"] not in kept]
|
||||
return {
|
||||
"updates": updates,
|
||||
"inserts": inserts,
|
||||
"hide": [row["id"] for row in gone if row.get("seed") is not None],
|
||||
"drop": [row["id"] for row in gone if row.get("seed") is None],
|
||||
}
|
||||
|
||||
|
||||
def reset_plan(head: dict[str, Any], rows: list[dict[str, Any]]) -> dict[str, Any]:
|
||||
"""[초기값으로] — 머리·줄을 seed 로(seed 에 있는 칸만) · 숨긴 초기값 줄도 살림 · 추가 줄은 지움.
|
||||
|
||||
rows = 숨긴 줄까지 전부. 쓰기 모양은 `plan_rows` 와 같음(+ `head`).
|
||||
"""
|
||||
seed = head.get("seed") or {}
|
||||
restore = (*ROW_FIELDS, "sort_order")
|
||||
return {
|
||||
"head": {f: seed[f] for f in HEAD_FIELDS if f in seed},
|
||||
"updates": [
|
||||
{"id": row["id"], **{f: row["seed"][f] for f in restore if f in row["seed"]}}
|
||||
for row in rows
|
||||
if row.get("seed") is not None
|
||||
],
|
||||
"inserts": [],
|
||||
"hide": [],
|
||||
"drop": [row["id"] for row in rows if row.get("seed") is None],
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user