Coverage for src/lexigram/admin/relations/manager.py: 0%

38 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-24 23:18 +0800

1"""Relation manager for lexigram-admin. 

2 

3Provides base class for managing entity relationships in admin views. 

4""" 

5 

6from __future__ import annotations 

7 

8from abc import ABC, abstractmethod 

9from typing import TYPE_CHECKING, Any, ClassVar 

10 

11if TYPE_CHECKING: 

12 from lexigram.ui.columns.types import Column 

13from lexigram.admin.exceptions import PermissionDeniedError 

14from lexigram.di.decorators import inject 

15from lexigram.result import Ok, Result 

16 

17 

18@inject 

19class AbstractRelationManager(ABC): 

20 """Base class for managing entity relationships. 

21 

22 Subclasses define how to display and query related entities 

23 for a parent resource. 

24 

25 Example: 

26 class UserPetsRelationManager(AbstractRelationManager): 

27 relationship_name = "pets" 

28 

29 @classmethod 

30 def table(cls, table_config=None): 

31 return [ 

32 TextColumn("name").sortable(), 

33 BadgeColumn("species"), 

34 ] 

35 

36 async def get_query(self): 

37 return await pet_service.list(user_id=self.parent_id) 

38 """ 

39 

40 relationship_name: ClassVar[str] = "" 

41 

42 def __init__( 

43 self, 

44 parent_id: Any = None, 

45 parent: Any = None, 

46 data_source: Any = None, 

47 ): 

48 """Initialize relation manager. 

49 

50 Args: 

51 parent_id: ID of the parent entity 

52 parent: The parent entity object (optional) 

53 data_source: Optional data source used for persisting 

54 pivot/relation operations. Subclasses may also attach 

55 one later via :meth:`set_data_source`. 

56 """ 

57 self.parent_id = parent_id 

58 self.parent = parent 

59 self._data_source = data_source 

60 

61 def set_data_source(self, data_source: Any) -> None: 

62 """Attach a data source for pivot persistence operations.""" 

63 self._data_source = data_source 

64 

65 async def get_parent(self) -> Any: 

66 """Resolve the parent entity for this relationship. 

67 

68 Returns: 

69 The resolved parent entity, or ``None`` when no parent object 

70 is declared. 

71 

72 Note: 

73 Override to resolve ``self.parent_id`` through a data source. 

74 """ 

75 return self.parent 

76 

77 def can_view_parent( 

78 self, parent: Any, user: Any | None = None 

79 ) -> Result[None, PermissionDeniedError]: 

80 """Check whether the user may view the given parent entity. 

81 

82 Defaults to allow; override to declare per-parent read policy. 

83 

84 Args: 

85 parent: The resolved parent entity. 

86 user: The requesting admin user. 

87 

88 Returns: 

89 ``Ok(None)`` when viewing is permitted, ``Err`` with a 

90 ``PermissionDeniedError`` when denied. 

91 """ 

92 return Ok(None) 

93 

94 @classmethod 

95 @abstractmethod 

96 def table(cls, table_config: Any = None) -> list[Column]: 

97 """Define columns for displaying related entities. 

98 

99 Args: 

100 table_config: Optional table configuration 

101 

102 Returns: 

103 List of Column instances 

104 """ 

105 ... 

106 

107 @abstractmethod 

108 async def get_query(self) -> list[Any]: 

109 """Get related entities for the parent. 

110 

111 Returns: 

112 List of related entities 

113 """ 

114 ... 

115 

116 async def count(self) -> int: 

117 """Count related entities. 

118 

119 Returns: 

120 Number of related entities 

121 """ 

122 items = await self.get_query() 

123 return len(items) if items else 0 

124 

125 async def get_items( 

126 self, 

127 *, 

128 page: int = 1, 

129 per_page: int = 20, 

130 **filters: Any, 

131 ) -> list[Any]: 

132 """Get paginated related entities. 

133 

134 Args: 

135 page: Page number (1-indexed) 

136 per_page: Items per page 

137 **filters: Additional filters 

138 

139 Returns: 

140 List of related entities for the page 

141 """ 

142 items = await self.get_query() 

143 if not items: 

144 return [] 

145 

146 start = (page - 1) * per_page 

147 end = start + per_page 

148 return items[start:end] 

149 

150 @classmethod 

151 def get_relationship_name(cls) -> str: 

152 """Get the name of this relationship.""" 

153 return cls.relationship_name or cls.__name__.lower().replace( 

154 "relationmanager", 

155 "", 

156 ) 

157 

158 

159__all__ = ["AbstractRelationManager"]