Coverage for src / lexigram / admin / relations / manager_ext.py: 35%

52 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-08-13 22:14 +0800

1"""Extended relation manager with inline editing support. 

2 

3.. experimental:: 

4""" 

5 

6from __future__ import annotations 

7 

8from typing import Any 

9 

10from lexigram.admin.exceptions import PermissionDeniedError 

11from lexigram.admin.relations.manager import AbstractRelationManager 

12from lexigram.result import Ok, Result 

13 

14 

15class RelationManager(AbstractRelationManager): 

16 """Relation manager with inline create/edit/delete support. 

17 

18 Extends AbstractRelationManager with inline editing methods, 

19 permission predicates, and HTMX route handlers. 

20 

21 Example: 

22 class UserPetsRelationManager(RelationManager): 

23 relationship_name = "pets" 

24 

25 @classmethod 

26 def table(cls, table_config=None): 

27 return [] 

28 

29 async def get_query(self): 

30 return [] 

31 """ 

32 

33 # Inline editing policy 

34 inline_create: bool = True 

35 inline_edit: bool = True 

36 inline_delete: bool = True 

37 inline_detach: bool = False 

38 

39 def create_form(self) -> str | None: 

40 """Return the HTML for a create form. 

41 

42 Override to provide a custom form. Returns None by default. 

43 """ 

44 return None 

45 

46 def edit_form(self, record: Any) -> str | None: 

47 """Return the HTML for an edit form pre-filled with the given record. 

48 

49 Override to provide a custom form. Returns None by default. 

50 """ 

51 return None 

52 

53 def can_create( 

54 self, user: Any | None = None 

55 ) -> Result[None, PermissionDeniedError]: 

56 """Check whether the user can create related records.""" 

57 return Ok(None) 

58 

59 def can_edit( 

60 self, record: Any, user: Any | None = None 

61 ) -> Result[None, PermissionDeniedError]: 

62 """Check whether the user can edit the given related record.""" 

63 return Ok(None) 

64 

65 def can_delete( 

66 self, record: Any, user: Any | None = None 

67 ) -> Result[None, PermissionDeniedError]: 

68 """Check whether the user can delete the given related record.""" 

69 return Ok(None) 

70 

71 def can_detach( 

72 self, record: Any, user: Any | None = None 

73 ) -> Result[None, PermissionDeniedError]: 

74 """Check whether the user can detach the given related record.""" 

75 return Ok(None) 

76 

77 async def render(self, request: Any, resource_name: str = "") -> str: 

78 """Render the relation panel as HTML.""" 

79 items = await self.get_query() 

80 rel_name = self.get_relationship_name() 

81 parent_id = self.parent_id 

82 

83 rows_html = "" 

84 for item in items: 

85 item_id = getattr(item, "id", str(id(item))) 

86 row_cells = "" 

87 cols = self.table() 

88 if cols: 

89 for col in cols: 

90 value = getattr(item, col.name, "") if hasattr(col, "name") else "" 

91 row_cells += f"<td>{value}</td>" 

92 else: 

93 row_cells = f"<td>{item}</td>" 

94 

95 actions_html = "" 

96 if self.inline_edit: 

97 edit_url = f"/admin/{resource_name}/{parent_id}/relations/{rel_name}/{item_id}/edit" 

98 actions_html += f'<a href="{edit_url}" hx-get="{edit_url}" hx-target="closest tr" hx-swap="outerHTML">Edit</a> ' 

99 if self.inline_delete: 

100 delete_url = ( 

101 f"/admin/{resource_name}/{parent_id}/relations/{rel_name}/{item_id}" 

102 ) 

103 actions_html += f'<a href="{delete_url}" hx-delete="{delete_url}" hx-confirm="Delete this record?" hx-target="closest tr" hx-swap="outerHTML">Delete</a>' 

104 if actions_html: 

105 row_cells += f"<td>{actions_html}</td>" 

106 

107 rows_html += f"<tr>{row_cells}</tr>" 

108 

109 header_html = "" 

110 if self.inline_create: 

111 create_url = f"/admin/{resource_name}/{parent_id}/relations/{rel_name}/new" 

112 header_html = f'<div><a href="{create_url}" hx-get="{create_url}" hx-target="this" hx-swap="outerHTML">+ Add {rel_name}</a></div>' 

113 

114 return f'<div class="relation-panel" id="relation-panel-{rel_name}">{header_html}<table><tbody>{rows_html}</tbody></table></div>' 

115 

116 

117__all__ = [ 

118 "RelationManager", 

119]