Coverage for src/lexigram/admin/resources/users.py: 64%

58 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-21 14:56 +0800

1""" 

2User resource definition for Lexigram Admin. 

3 

4This module defines the UserResource class that provides admin interface 

5for managing users with date range filtering capabilities. 

6""" 

7 

8from __future__ import annotations 

9 

10from typing import TYPE_CHECKING, Any, cast 

11 

12from lexigram.admin.actions import ( 

13 CreateAction, 

14 DeleteAction, 

15 EditAction, 

16 ImpersonateAction, 

17 PermissionsAction, 

18) 

19from lexigram.admin.auth.entity import AdminUserEntity 

20from lexigram.admin.resources.base import Resource 

21from lexigram.admin.schema import BooleanField, DateField, SelectField, TextField 

22from lexigram.admin.ui.filters import SelectFilter, ToggleFilter 

23from lexigram.ui import BadgeColumn, DateColumn, TextColumn 

24 

25if TYPE_CHECKING: 

26 from lexigram.admin.rbac.inventory import PermissionInventoryService 

27 

28 

29class UserResource(Resource): 

30 """ 

31 Admin resource for managing users. 

32 

33 Provides comprehensive user management with filtering by registration date 

34 and last active date using DateRangeFilter. 

35 """ 

36 

37 model = cast("Any", AdminUserEntity) 

38 name = "users" 

39 label = "Users" 

40 icon = "users" 

41 category = "system" 

42 service = None # Will be set by DI or registry 

43 permission_inventory: PermissionInventoryService | None = None 

44 """Grouped RBAC permission inventory; wired at mount time for the 

45 per-user permission editor.""" 

46 # Secret/framework-managed columns must never render in generated forms 

47 form_exclude_fields = ( 

48 "id", 

49 "created_at", 

50 "updated_at", 

51 "hashed_password", 

52 ) 

53 

54 # UI Configuration 

55 layout_type = "sidebar" 

56 data_view = "tabular" 

57 

58 # New SchemaField declarations (canonical definition) 

59 fields: list[Any] = [ 

60 TextField(name="name", sortable=True, searchable=True), 

61 TextField(name="email", sortable=True, searchable=True), 

62 SelectField( 

63 name="role", 

64 options=[ 

65 ("admin", "Admin"), 

66 ("moderator", "Moderator"), 

67 ("user", "User"), 

68 ("guest", "Guest"), 

69 ], 

70 ), 

71 SelectField( 

72 name="status", 

73 options=[ 

74 ("active", "Active"), 

75 ("inactive", "Inactive"), 

76 ("pending", "Pending"), 

77 ("suspended", "Suspended"), 

78 ], 

79 ), 

80 DateField(name="created_at", label="Registration Date", sortable=True), 

81 DateField(name="last_active", sortable=True), 

82 BooleanField(name="is_active", label="Active"), 

83 ] 

84 

85 # Columns configuration (legacy — kept for backward compat) 

86 columns: list[Any] = [ 

87 TextColumn("name").sortable().searchable().weight("semibold"), 

88 TextColumn("email").sortable().searchable().copyable().color("blue"), 

89 BadgeColumn( 

90 "role", 

91 colors={ 

92 "admin": "purple", 

93 "moderator": "blue", 

94 "user": "gray", 

95 "guest": "slate", 

96 }, 

97 ), 

98 BadgeColumn( 

99 "status", 

100 colors={ 

101 "active": "green", 

102 "inactive": "red", 

103 "pending": "yellow", 

104 "suspended": "orange", 

105 }, 

106 ), 

107 DateColumn("created_at", label="Registration Date").datetime().sortable(), 

108 DateColumn("last_active").datetime().sortable(), 

109 ] 

110 

111 # Filter configuration 

112 @property 

113 def filter_options(self) -> dict[str, Any]: 

114 """Define filter options for the user resource.""" 

115 field_map: dict[str, Any] = {f.name: f for f in self.fields} 

116 return { 

117 "role": SelectFilter( 

118 "role", 

119 options=[v for v, _ in field_map["role"].options], 

120 label="Role", 

121 ), 

122 "status": SelectFilter( 

123 "status", 

124 options=[v for v, _ in field_map["status"].options], 

125 label="Status", 

126 ), 

127 "is_active": ToggleFilter("is_active", label="Active"), 

128 } 

129 

130 # Actions configuration 

131 actions: list[Any] = [ 

132 EditAction(), 

133 DeleteAction(), 

134 PermissionsAction(), 

135 ImpersonateAction(), 

136 ] 

137 

138 # Header actions 

139 header_actions: list[Any] = [ 

140 CreateAction(label="Add User"), 

141 ] 

142 

143 # Search configuration 

144 search_fields = ["name", "email"] 

145 

146 # Bulk actions 

147 bulk_actions: list[Any] = ["delete_selected", "export_csv"] 

148 

149 # Permissions 

150 def has_view_permission(self, user: Any) -> bool: 

151 """Check if user can view users.""" 

152 return user and (user.is_admin or user.role in ["admin", "moderator"]) 

153 

154 def has_add_permission(self, user: Any) -> bool: 

155 """Check if user can add users.""" 

156 return user and user.is_admin 

157 

158 def has_change_permission(self, user: Any) -> bool: 

159 """Check if user can change users.""" 

160 return user and (user.is_admin or user.role in ["admin", "moderator"]) 

161 

162 def has_delete_permission(self, user: Any) -> bool: 

163 """Check if user can delete users.""" 

164 return user and user.is_admin 

165 

166 # Custom methods 

167 async def get_queryset(self, request: Any) -> Any: 

168 """Get the base queryset for users.""" 

169 if self.service: 

170 return await self.service.list() 

171 return [] 

172 

173 async def get_object(self, pk: Any) -> Any: 

174 """Get a single user object.""" 

175 if self.service: 

176 return await self.service.get_by_id(pk) 

177 return None 

178 

179 async def create_object(self, data: dict[str, Any]) -> Any: 

180 """Create a new user.""" 

181 if self.service: 

182 return await self.service.create(data) 

183 return None 

184 

185 async def update_object(self, pk: Any, data: dict[str, Any]) -> Any: 

186 """Update an existing user.""" 

187 if self.service: 

188 return await self.service.update(pk, data) 

189 return None 

190 

191 async def delete_object(self, pk: Any) -> bool: 

192 """Delete a user.""" 

193 if self.service: 

194 return await self.service.delete(pk) 

195 return False