feat(M02): 개인 · 회사 층 양식 지우기 — DELETE /layers/{층}/templates/{종류}/{이름} · 개인 본인만 · 회사 관리자만 · 시스템 · 프로젝트 거절 · 판 다르면 409 · 이름 검사 400 + 까닭
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015LuapLYqN1GGFD8Y1PStD5
This commit is contained in:
@@ -279,6 +279,39 @@ async def save_layer_template(
|
||||
return {"종류": kind, "이름": name, "판": version, "층": layer}
|
||||
|
||||
|
||||
@router.delete("/layers/{layer}/templates/{kind}/{name}")
|
||||
async def delete_layer_template(
|
||||
layer: Layer,
|
||||
kind: str,
|
||||
name: str,
|
||||
판: str | None = Query(None),
|
||||
project_id: str | None = Query(None),
|
||||
session: dict[str, Any] = Depends(verify_session),
|
||||
) -> dict[str, Any]:
|
||||
"""개인 = 본인만 · 회사 = 회사 관리자만 · 시스템 · 프로젝트 층은 거절 · 판이 다르면 409."""
|
||||
if layer in ("system", "project"):
|
||||
raise HTTPException(status_code=403, detail="개인 · 회사 층 양식만 지울 수 있습니다.")
|
||||
folder = await _layer_dir(session, layer, project_id=project_id, write=True)
|
||||
try:
|
||||
layers.check_kind(kind)
|
||||
layers.check_name(name)
|
||||
except ValueError as error:
|
||||
raise _bad(error) from error
|
||||
if not 판:
|
||||
raise HTTPException(status_code=400, detail="지울 양식의 판이 필요합니다.")
|
||||
try:
|
||||
found = await asyncio.to_thread(
|
||||
layers.delete_template, folder, kind, name, version=판, check_version=True
|
||||
)
|
||||
except layers.StaleTemplate as error:
|
||||
raise HTTPException(
|
||||
status_code=409, detail={"message": str(error), "판": error.current}
|
||||
) from error
|
||||
if not found:
|
||||
raise HTTPException(status_code=404, detail="양식을 찾을 수 없습니다.")
|
||||
return {"종류": kind, "이름": name, "층": layer, "지움": True}
|
||||
|
||||
|
||||
# ── 프로젝트 길 ───────────────────────────────────────
|
||||
|
||||
|
||||
|
||||
@@ -100,7 +100,10 @@ def check_name(name: str) -> str:
|
||||
text = str(name or "").strip()
|
||||
bad = not text or text != name or text.startswith((".", "_")) or len(text) > 80
|
||||
if bad or any(ch in text for ch in '/\\:*?"<>|') or ".." in text:
|
||||
raise ValueError("양식 이름이 올바르지 않습니다.")
|
||||
raise ValueError(
|
||||
"양식 이름이 올바르지 않습니다 — 빈 이름 · 앞뒤 빈칸 · 80자 초과 · 앞머리 . _ · "
|
||||
'`/ \ .. : * ? " < > |` 는 못 씁니다.'
|
||||
)
|
||||
return text
|
||||
|
||||
|
||||
@@ -189,10 +192,20 @@ def write_template(
|
||||
return version_of(path) or ""
|
||||
|
||||
|
||||
def delete_template(layer_dir: str | Path, kind: str, name: str) -> bool:
|
||||
def delete_template(
|
||||
layer_dir: str | Path,
|
||||
kind: str,
|
||||
name: str,
|
||||
*,
|
||||
version: str | None = None,
|
||||
check_version: bool = False,
|
||||
) -> bool:
|
||||
"""양식 지움 · manifest 줄도 뺌. `check_version` 이면 지금 판과 달라 `StaleTemplate`."""
|
||||
path = template_path(layer_dir, kind, name)
|
||||
if not path.is_file():
|
||||
return False
|
||||
if check_version and version_of(path) != version:
|
||||
raise StaleTemplate(version_of(path))
|
||||
path.unlink()
|
||||
manifest = read_manifest(layer_dir)
|
||||
if manifest.pop(f"{kind}/{name}", None) is not None:
|
||||
|
||||
@@ -354,3 +354,51 @@ def test_뼈대_없는_문서는_400으로_거절하고_파일을_안_씀(world:
|
||||
table = _url()
|
||||
assert client.put(table, json={"판": None, "문서": {}}).status_code == 400
|
||||
assert client.put(url, json={"판": version, "문서": {"entities": []}}).status_code == 200
|
||||
|
||||
|
||||
def _mine(name: str = "내양식", layer: str = "personal") -> str:
|
||||
return f"/api/m02/layers/{layer}/templates/table/{name}?project_id={P1}"
|
||||
|
||||
|
||||
def test_개인_회사_양식_지우기_권한_판_이름_예외(world: dict[str, Any]) -> None:
|
||||
client, session = world["client"], world["session"]
|
||||
doc = {"열": []}
|
||||
for layer in ("personal", "company"):
|
||||
client.put(_mine(layer=layer), json={"판": None, "문서": doc})
|
||||
personal = layers.personal_dir(7, 42) / "table/내양식.json"
|
||||
company = layers.company_dir(7) / "table/내양식.json"
|
||||
assert personal.is_file() and not company.is_file() # 일반 사용자 회사 저장은 403
|
||||
version = layers.version_of(personal)
|
||||
# 시스템 · 프로젝트 층은 거절
|
||||
for layer in ("system", "project"):
|
||||
assert client.delete(_mine("구조물집계표", layer) + f"&판={version}").status_code == 403
|
||||
assert (_root(world, P1) / "templates/table/구조물집계표.json").is_file()
|
||||
# 이름 예외 · 판 없음 · 판 다름 · 없는 양식
|
||||
for bad in ("%2E%2E", "a%2E%2Eb", "a%2Fb", "a%5Cb", "_initial", ".x", "a*b"):
|
||||
got = client.delete(_mine(bad) + f"&판={version}")
|
||||
assert got.status_code in (400, 404) and personal.is_file(), bad
|
||||
assert client.delete(_mine("a%2Fb") + f"&판={version}").status_code in (400, 404)
|
||||
assert client.delete(_mine()).status_code == 400
|
||||
stale = client.delete(_mine() + "&판=0000000000000000")
|
||||
assert stale.status_code == 409 and stale.json()["detail"]["판"] == version
|
||||
assert client.delete(_mine("없는것") + f"&판={version}").status_code == 404
|
||||
# 다른 회사 사람은 못 지움 · 회사 층은 관리자만
|
||||
assert client.delete(_mine("내양식", "company") + f"&판={version}").status_code == 403
|
||||
# 지움 · 연달아 누르면 둘째는 404 · manifest 줄도 없어짐
|
||||
assert client.delete(_mine() + f"&판={version}").status_code == 200
|
||||
assert not personal.exists()
|
||||
assert client.delete(_mine() + f"&판={version}").status_code == 404
|
||||
session["role"] = "ADMIN"
|
||||
client.put(_mine(layer="company"), json={"판": None, "문서": doc})
|
||||
assert company.is_file()
|
||||
session.update(role="USER")
|
||||
assert (
|
||||
client.delete(_mine(layer="company") + f"&판={layers.version_of(company)}").status_code
|
||||
== 403
|
||||
)
|
||||
session.update(role="ADMIN")
|
||||
assert (
|
||||
client.delete(_mine(layer="company") + f"&판={layers.version_of(company)}").status_code
|
||||
== 200
|
||||
)
|
||||
assert "table/내양식" not in layers.read_manifest(layers.company_dir(7))
|
||||
|
||||
Reference in New Issue
Block a user