Coverage for src/lexigram/admin/relations/morph_many.py: 97%
38 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-21 14:56 +0800
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-21 14:56 +0800
1"""MorphMany (polymorphic HasMany) relation manager."""
3from __future__ import annotations
5from typing import Any
7from lexigram.admin.relations.manager_ext import RelationManager
8from lexigram.ui import el, render_to_string
11class MorphManyRelationManager(RelationManager):
12 """Relation manager for polymorphic HasMany relationships.
14 Similar to HasMany but filters related records by morph type
15 and key. On create, automatically sets the morph type and key.
17 Example:
18 class PostCommentsRelationManager(MorphManyRelationManager):
19 relationship_name = "comments"
20 morph_name = "commentable"
21 morph_type_value = "post"
22 """
24 morph_name: str = ""
25 morph_type_value: str = ""
27 @classmethod
28 def table(cls, table_config: Any = None) -> list[Any]:
29 return []
31 async def get_query(self) -> list[Any]:
32 return []
34 async def render(self, request: Any, resource_name: str = "") -> str:
35 """Render the morph-many relation panel as HTML."""
36 items = await self.get_query()
37 rel_name = self.get_relationship_name()
38 parent_id = self.parent_id
40 rows: list[Any] = []
41 for item in items:
42 item_id = str(getattr(item, "id", str(id(item))))
43 label = str(getattr(item, "name", item_id))
45 actions: list[Any] = []
46 if self.inline_edit:
47 edit_url = f"/admin/{resource_name}/{parent_id}/relations/{rel_name}/{item_id}/edit"
48 actions.append(
49 el(
50 "a",
51 "Edit",
52 href=edit_url,
53 hx_get=edit_url,
54 hx_target="closest tr",
55 hx_swap="outerHTML",
56 class_="text-primary-600 hover:text-primary-800 text-sm mr-2",
57 )
58 )
59 if self.inline_delete:
60 delete_url = (
61 f"/admin/{resource_name}/{parent_id}/relations/{rel_name}/{item_id}"
62 )
63 actions.append(
64 el(
65 "a",
66 "Delete",
67 href=delete_url,
68 hx_delete=delete_url,
69 hx_confirm="Delete this record?",
70 hx_target="closest tr",
71 hx_swap="outerHTML",
72 class_="text-destructive hover:text-destructive/90 text-sm",
73 )
74 )
76 rows.append(
77 el(
78 "tr",
79 el("td", label, class_="px-4 py-2 text-sm text-foreground"),
80 el(
81 "td",
82 item_id,
83 class_="px-4 py-2 text-sm text-muted-foreground",
84 ),
85 el("td", *actions, class_="px-4 py-2 text-sm"),
86 )
87 )
89 header: list[Any] = []
90 if self.inline_create:
91 create_url = f"/admin/{resource_name}/{parent_id}/relations/{rel_name}/new"
92 header.append(
93 el(
94 "div",
95 el(
96 "a",
97 "+ Add ",
98 rel_name.replace("_", " ").title(),
99 href=create_url,
100 hx_get=create_url,
101 hx_target=f"#relation-panel-{rel_name}",
102 hx_swap="beforeend",
103 class_="inline-flex items-center px-3 py-1.5 text-sm font-medium text-primary-600 bg-primary-50 dark:bg-primary-900/30 dark:text-primary-400 rounded-lg hover:bg-primary-100 transition-colors",
104 ),
105 class_="mb-3",
106 )
107 )
109 table = el(
110 "table",
111 el(
112 "thead",
113 el(
114 "tr",
115 el(
116 "th",
117 "Record",
118 class_="px-4 py-2 text-left text-xs font-medium text-muted-foreground uppercase",
119 ),
120 el(
121 "th",
122 "ID",
123 class_="px-4 py-2 text-left text-xs font-medium text-muted-foreground uppercase",
124 ),
125 el(
126 "th",
127 "Actions",
128 class_="px-4 py-2 text-left text-xs font-medium text-muted-foreground uppercase",
129 ),
130 ),
131 class_="bg-muted dark:bg-card",
132 ),
133 el("tbody", *rows, class_="divide-y divide-border"),
134 class_="min-w-full divide-y divide-border",
135 )
137 return render_to_string(
138 el(
139 "div",
140 el(
141 "h3",
142 rel_name.replace("_", " ").title(),
143 class_="text-lg font-medium text-foreground mb-4",
144 ),
145 *header,
146 table,
147 self._render_empty_state(rows),
148 class_="relation-panel p-4",
149 id=f"relation-panel-{rel_name}",
150 )
151 )
153 def _render_empty_state(self, rows: list[Any]) -> Any:
154 """Return an empty-state paragraph element, or None when rows exist."""
155 if rows:
156 return None
157 return el(
158 "p",
159 "No related records found.",
160 class_="text-sm text-muted-foreground italic mt-4",
161 )