Coverage for src/lexigram/admin/resources/permissions_renderer.py: 0%
23 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-24 23:18 +0800
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-24 23:18 +0800
1"""Renderer for the per-user permission editing page.
3Serves the direct-permission editor for admin users (the page the legacy
4RbacController served at ``/admin/users/{id}/permissions``) through the
5Resource handler path.
6"""
8from __future__ import annotations
10from typing import TYPE_CHECKING, Any
12from starlette.responses import HTMLResponse
14from lexigram.ui import el
16if TYPE_CHECKING:
17 from lexigram.admin.rbac.inventory import PermissionInventoryService
20def _checkbox(perm: str, checked: bool) -> Any:
21 """Build a single permission checkbox input."""
22 return el(
23 "input",
24 type="checkbox",
25 name="permissions",
26 value=perm,
27 checked=checked or None,
28 class_="h-4 w-4 rounded border-input text-primary focus:ring-ring bg-background",
29 )
32class UserPermissionsRenderer:
33 """Render the grouped user permission checkboxes inside the admin shell.
35 Attributes:
36 resource_name: The admin resource name (``users``).
37 renderer: The engine renderer used to wrap content in the admin
38 shell; overridable for tests.
39 """
41 def __init__(self, resource_name: str, engine_renderer: Any | None = None) -> None:
42 """Initialize the renderer.
44 Args:
45 resource_name: The admin resource name (``users``).
46 engine_renderer: Optional engine renderer; a fresh
47 :class:`~lexigram.admin.engine.renderer.AdminRenderer` is
48 built when omitted.
49 """
50 self.resource_name = resource_name
51 if engine_renderer is None:
52 from lexigram.admin.engine.renderer import (
53 AdminRenderer as EngineAdminRenderer,
54 )
56 engine_renderer = EngineAdminRenderer()
57 self._renderer = engine_renderer
59 def render_form(
60 self,
61 request: Any,
62 user: Any,
63 inventory: PermissionInventoryService,
64 item_id: str,
65 prefix: str,
66 ) -> HTMLResponse:
67 """Render the permissions form as a full admin page.
69 Args:
70 request: The current request (carries the CSRF token in
71 ``request.state``).
72 user: The admin user being edited (must not be ``None``).
73 inventory: Grouped permission inventory ``{resource: [perm]}``.
74 item_id: The user id from the URL path.
75 prefix: Admin resource prefix (``admin/users``).
77 Returns:
78 A full-page HTMLResponse wrapped in the admin shell.
79 """
80 options = inventory.options() or {}
81 selected = set(getattr(user, "permissions", None) or [])
82 all_options = {p for perms in options.values() for p in perms}
83 preserved = [
84 el("input", type="hidden", name="permissions", value=perm)
85 for perm in sorted(selected - all_options)
86 ]
88 groups = [
89 el(
90 "fieldset",
91 el("legend", perm_name, class_="font-semibold text-foreground"),
92 *[_checkbox(perm, perm in selected) for perm in perms],
93 class_="mb-5 border rounded-lg p-4",
94 )
95 for perm_name, perms in options.items()
96 ]
98 user_label = (
99 f"{getattr(user, 'name', '')} <{getattr(user, 'email', '')}>"
100 if user is not None
101 else "unknown user"
102 )
104 form = el(
105 "form",
106 el(
107 "input",
108 type="hidden",
109 name="csrf_token",
110 value=getattr(request.state, "csrf_token", ""),
111 ),
112 *preserved,
113 el("p", user_label, class_="mb-4 text-sm text-foreground"),
114 *groups,
115 el(
116 "button",
117 "Save Permissions",
118 type="submit",
119 class_="mt-4 inline-flex items-center rounded bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90",
120 ),
121 method="post",
122 action=f"/admin/{prefix}/{item_id}/permissions",
123 )
125 content = el(
126 "div",
127 el(
128 "h1",
129 "Edit Permissions",
130 class_="text-2xl font-bold text-foreground mb-4",
131 ),
132 form,
133 class_="resource-content bg-card shadow rounded-lg p-6",
134 )
136 return self._renderer.render_page(
137 content,
138 request=request,
139 title="User Permissions",
140 breadcrumbs=[
141 {"label": "Dashboard", "url": f"/admin/{prefix}"},
142 {"label": "Users", "url": f"/admin/{prefix}"},
143 {
144 "label": user_label,
145 "url": f"/admin/{prefix}/{item_id}/permissions",
146 },
147 ],
148 )