Coverage for src / lexigram / admin / relations / morph_many.py: 31%
35 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-13 22:14 +0800
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-13 22:14 +0800
1"""MorphMany (polymorphic HasMany) relation manager."""
3from __future__ import annotations
5from typing import Any
7from lexigram.admin.relations.manager_ext import RelationManager
10class MorphManyRelationManager(RelationManager):
11 """Relation manager for polymorphic HasMany relationships.
13 Similar to HasMany but filters related records by morph type
14 and key. On create, automatically sets the morph type and key.
16 Example:
17 class PostCommentsRelationManager(MorphManyRelationManager):
18 relationship_name = "comments"
19 morph_name = "commentable"
20 morph_type_value = "post"
21 """
23 morph_name: str = ""
24 morph_type_value: str = ""
26 @classmethod
27 def table(cls, table_config: Any = None) -> list[Any]:
28 return []
30 async def get_query(self) -> list[Any]:
31 return []
33 async def render(self, request: Any, resource_name: str = "") -> str:
34 items = await self.get_query()
35 rel_name = self.get_relationship_name()
37 rows_html = ""
38 for item in items:
39 item_id = getattr(item, "id", str(id(item)))
40 label = str(getattr(item, "name", item_id))
42 actions = ""
43 if self.inline_edit:
44 edit_url = f"/admin/{resource_name}/{self.parent_id}/relations/{rel_name}/{item_id}/edit"
45 actions += f'<a href="{edit_url}" hx-get="{edit_url}" hx-target="closest tr" hx-swap="outerHTML" class="text-primary-600 hover:text-primary-800 text-sm mr-2">Edit</a>'
46 if self.inline_delete:
47 delete_url = f"/admin/{resource_name}/{self.parent_id}/relations/{rel_name}/{item_id}"
48 actions += f'<a href="{delete_url}" hx-delete="{delete_url}" hx-confirm="Delete this record?" hx-target="closest tr" hx-swap="outerHTML" class="text-destructive hover:text-destructive/90 text-sm">Delete</a>'
50 rows_html += f"""<tr>
51 <td class="px-4 py-2 text-sm text-foreground">{label}</td>
52 <td class="px-4 py-2 text-sm text-muted-foreground">{item_id}</td>
53 <td class="px-4 py-2 text-sm">{actions}</td>
54 </tr>"""
56 header = ""
57 if self.inline_create:
58 create_url = (
59 f"/admin/{resource_name}/{self.parent_id}/relations/{rel_name}/new"
60 )
61 header = f"""<div class="mb-3">
62 <a href="{create_url}" hx-get="{create_url}" hx-target="#relation-panel-{rel_name}" hx-swap="beforeend"
63 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">
64 + Add {rel_name.replace("_", " ").title()}
65 </a>
66 </div>"""
68 return f"""<div class="relation-panel p-4" id="relation-panel-{rel_name}">
69 <h3 class="text-lg font-medium text-foreground mb-4">{rel_name.replace("_", " ").title()}</h3>
70 {header}
71 <table class="min-w-full divide-y divide-border">
72 <thead class="bg-muted dark:bg-card">
73 <tr>
74 <th class="px-4 py-2 text-left text-xs font-medium text-muted-foreground uppercase">Record</th>
75 <th class="px-4 py-2 text-left text-xs font-medium text-muted-foreground uppercase">ID</th>
76 <th class="px-4 py-2 text-left text-xs font-medium text-muted-foreground uppercase">Actions</th>
77 </tr>
78 </thead>
79 <tbody class="divide-y divide-border">{rows_html}</tbody>
80 </table>
81 {self._render_empty_state(rows_html)}
82 </div>"""
84 def _render_empty_state(self, rows_html: str) -> str:
85 if rows_html:
86 return ""
87 return '<p class="text-sm text-muted-foreground italic mt-4">No related records found.</p>'