mirror of
https://github.com/hansjone/netx.git
synced 2026-10-08 23:33:21 +08:00
Align folder permissions: roots deletable, root maps rename-only, world map locked.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
1b592490a5
commit
77832b271d
4 changed files with 85 additions and 13 deletions
|
|
@ -519,9 +519,27 @@ def update_folder(db: Session, folder_id: str, body: TopologyFolderUpdate) -> To
|
|||
name = str(body.name or "").strip()
|
||||
if not name:
|
||||
raise HTTPException(status_code=400, detail="name_required")
|
||||
if bool(row.is_system) and str(row.kind or "") == "root":
|
||||
ext = str(getattr(row, "external_ref", None) or "").strip()
|
||||
# Manual「根图」is auto-created but user-renamable (still not directly deletable).
|
||||
is_manual_root_map = (
|
||||
bool(row.is_system)
|
||||
and not ext
|
||||
and str(row.kind or "") == "region"
|
||||
)
|
||||
if str(row.kind or "") == "root":
|
||||
row.name = name[:256]
|
||||
elif is_manual_root_map:
|
||||
row.name = name[:256]
|
||||
from .topology_region_canvas import region_canvas_node_id
|
||||
|
||||
nid = region_canvas_node_id(row.id)
|
||||
for vn in (
|
||||
db.query(TopoViewNode).filter(TopoViewNode.fabric_node_id == nid).all()
|
||||
):
|
||||
vn.label = name[:256]
|
||||
vn.updated_at = _utcnow()
|
||||
elif bool(row.is_system):
|
||||
# UME World / World drill / synced SBN folders: not free-rename.
|
||||
raise HTTPException(status_code=400, detail="cannot_rename_system_folder")
|
||||
else:
|
||||
row.name = name[:256]
|
||||
|
|
@ -547,15 +565,17 @@ def update_folder(db: Session, folder_id: str, body: TopologyFolderUpdate) -> To
|
|||
|
||||
|
||||
def delete_folder(db: Session, folder_id: str, *, force: bool = False) -> dict[str, Any]:
|
||||
"""Delete a region and cascade-delete nested regions + maps.
|
||||
"""Delete a folder and cascade nested regions + maps.
|
||||
|
||||
Not user-deletable (structure / auto-managed):
|
||||
- kind=root
|
||||
- manual system「根图」(is_system, no UME external_ref)
|
||||
- UME World container and World drill (ume:world / ume:world:drill)
|
||||
User-deletable:
|
||||
- Manual「根」(nav region under the hidden tree root) — cascades its「根图」
|
||||
- Manual / UME-synced sub-regions under a root map or World drill
|
||||
|
||||
User-deletable: manual sub-regions and UME-synced SBN folders under World.
|
||||
World map view is never a folder — it is auto created/suppressed by reconcile.
|
||||
Not directly deletable (auto-managed):
|
||||
- Hidden tree ``kind=root``
|
||||
- Manual system「根图」(deleted only with parent「根」)
|
||||
- UME World / World drill
|
||||
- World map view (auto create/suppress via reconcile; see ``delete_view``)
|
||||
"""
|
||||
row = _get_folder_or_404(db, folder_id)
|
||||
if str(row.kind or "") == "root":
|
||||
|
|
@ -566,7 +586,7 @@ def delete_folder(db: Session, folder_id: str, *, force: bool = False) -> dict[s
|
|||
ext = str(getattr(row, "external_ref", None) or "").strip()
|
||||
if is_ume_world_container(row) or ext in (WORLD_FOLDER_REF, WORLD_DRILL_REF):
|
||||
raise HTTPException(status_code=400, detail="cannot_delete_system_folder")
|
||||
# Manual「根图」and other non-UME system folders stay protected except via parent cascade.
|
||||
# Manual「根图」: only removable when cascading from parent「根」.
|
||||
if bool(row.is_system) and not ext:
|
||||
raise HTTPException(status_code=400, detail="cannot_delete_system_folder")
|
||||
_ = force
|
||||
|
|
@ -838,6 +858,10 @@ def create_view(db: Session, body: TopologyViewCreate) -> TopologyViewOut:
|
|||
def update_view(db: Session, view_id: str, body: TopologyViewUpdate) -> TopologyViewOut:
|
||||
row = _get_view_or_404(db, view_id)
|
||||
if body.name is not None:
|
||||
from .ume_topology_world import is_world_flat_view
|
||||
|
||||
if is_world_flat_view(row):
|
||||
raise HTTPException(status_code=400, detail="cannot_rename_world_map_view")
|
||||
name = str(body.name or "").strip()
|
||||
if not name:
|
||||
raise HTTPException(status_code=400, detail="name_required")
|
||||
|
|
|
|||
|
|
@ -666,6 +666,31 @@ class FabricTopologyTests(unittest.TestCase):
|
|||
child_ids = {c.id for c in root_map.children}
|
||||
self.assertEqual(child_ids, {a.id, b.id})
|
||||
|
||||
def test_root_map_rename_ok_but_not_direct_delete(self) -> None:
|
||||
from fastapi import HTTPException
|
||||
|
||||
from netx_api.topology_schemas import TopologyFolderUpdate
|
||||
|
||||
top = svc.create_folder(
|
||||
self.db, TopologyFolderCreate(name="North", kind="region")
|
||||
)
|
||||
tree = svc.get_topology_tree(self.db)
|
||||
assert tree.root is not None
|
||||
north = next(c for c in tree.root.children if c.id == top.id)
|
||||
root_map = north.children[0]
|
||||
self.assertTrue(root_map.is_system)
|
||||
renamed = svc.update_folder(
|
||||
self.db, root_map.id, TopologyFolderUpdate(name="主画布")
|
||||
)
|
||||
self.assertEqual(renamed.name, "主画布")
|
||||
with self.assertRaises(HTTPException) as ctx:
|
||||
svc.delete_folder(self.db, root_map.id)
|
||||
self.assertEqual(ctx.exception.detail, "cannot_delete_system_folder")
|
||||
# Deleting the manual「根」cascades the「根图」.
|
||||
out = svc.delete_folder(self.db, top.id)
|
||||
self.assertTrue(out.get("deleted"))
|
||||
self.assertIsNone(self.db.get(TopoFolder, root_map.id))
|
||||
|
||||
def test_classify_role_region_and_slices(self) -> None:
|
||||
from netx_api import topology_classify as clf
|
||||
from netx_api.topology_schemas import (
|
||||
|
|
|
|||
|
|
@ -450,6 +450,12 @@ class UmeHierarchyTests(unittest.TestCase):
|
|||
with self.assertRaises(HTTPException) as ctx2:
|
||||
delete_view(self.db, flat.id)
|
||||
self.assertEqual(ctx2.exception.detail, "cannot_delete_world_map_view")
|
||||
from netx_api.topology_schemas import TopologyViewUpdate
|
||||
from netx_api.topology_views_tree import update_view
|
||||
|
||||
with self.assertRaises(HTTPException) as ctx3:
|
||||
update_view(self.db, flat.id, TopologyViewUpdate(name="Nope"))
|
||||
self.assertEqual(ctx3.exception.detail, "cannot_rename_world_map_view")
|
||||
|
||||
def test_flat_slots_do_not_overlap(self):
|
||||
self._seed_tree()
|
||||
|
|
|
|||
|
|
@ -196,6 +196,12 @@ function isUmeStructuralFolder(folder: TopologyTreeFolderItem | null | undefined
|
|||
return ext === "ume:world" || ext === "ume:world:drill";
|
||||
}
|
||||
|
||||
/** Auto「根图」under a manual 根 — rename OK, delete only with parent 根. */
|
||||
function isManualRootMapFolder(folder: TopologyTreeFolderItem | null | undefined): boolean {
|
||||
if (!folder || isUmeStructuralFolder(folder)) return false;
|
||||
return Boolean(folder.is_system) && !String(folder.external_ref || "").trim();
|
||||
}
|
||||
|
||||
/** UME-synced SBN under World — deletable like a manual sub-region. */
|
||||
function isUmeSyncedSubRegion(folder: TopologyTreeFolderItem | null | undefined): boolean {
|
||||
if (!folder || isUmeStructuralFolder(folder)) return false;
|
||||
|
|
@ -2414,10 +2420,18 @@ export function TopologyPage() {
|
|||
</span>
|
||||
</span>
|
||||
</button>
|
||||
{!isUmeStructuralFolder(folder) &&
|
||||
(!folder.is_system || isUmeSyncedSubRegion(folder)) ? (
|
||||
{(() => {
|
||||
const canRenameFolder =
|
||||
!isUmeStructuralFolder(folder) &&
|
||||
(!folder.is_system || isManualRootMapFolder(folder));
|
||||
const canDeleteFolder =
|
||||
!isUmeStructuralFolder(folder) &&
|
||||
!isManualRootMapFolder(folder) &&
|
||||
(!folder.is_system || isUmeSyncedSubRegion(folder));
|
||||
if (!canRenameFolder && !canDeleteFolder) return null;
|
||||
return (
|
||||
<div className="topo-map-list__actions">
|
||||
{!folder.is_system ? (
|
||||
{canRenameFolder ? (
|
||||
<button
|
||||
type="button"
|
||||
className="topo-map-list__icon"
|
||||
|
|
@ -2429,6 +2443,7 @@ export function TopologyPage() {
|
|||
<PencilIcon />
|
||||
</button>
|
||||
) : null}
|
||||
{canDeleteFolder ? (
|
||||
<button
|
||||
type="button"
|
||||
className="topo-map-list__icon"
|
||||
|
|
@ -2447,9 +2462,11 @@ export function TopologyPage() {
|
|||
>
|
||||
<CloseIcon />
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
{open ? (
|
||||
<ul className="topo-map-list topo-region-list__maps">
|
||||
{containerFolder ? (
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue