Coverage for src/lexigram/admin/controllers/resource/list.py: 0%

46 statements  

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

1"""List view and query building for the resource controller.""" 

2 

3from __future__ import annotations 

4 

5from typing import TYPE_CHECKING, Any 

6 

7from starlette.requests import Request 

8from starlette.responses import HTMLResponse, Response 

9 

10from lexigram.admin.controllers.resource.meta import ResourceMeta, T 

11from lexigram.admin.data.data_source import IDataSource as DataSourceProtocol 

12from lexigram.admin.data.data_source import QueryResult 

13from lexigram.admin.data.query import QuerySpec 

14from lexigram.admin.state.context import AdminContext, AdminContextManager 

15from lexigram.admin.state.url import URLState 

16from lexigram.ui import el, render_to_string 

17 

18if TYPE_CHECKING: 

19 from lexigram.admin.controllers.resource import ResourceController 

20 

21 

22 

23class ResourceListMixin: 

24 """List view, query building, and list rendering.""" 

25 

26 # Host attributes provided by sibling mixins on ResourceController. 

27 meta: ResourceMeta 

28 

29 get_data_source: Any 

30 

31 async def list_view(self, request: Request) -> Response: 

32 """List resources with pagination and filtering.""" 

33 async with AdminContextManager(request) as ctx: 

34 # Parse URL state 

35 url_state = URLState.from_request(request) 

36 

37 # Build query 

38 query = self._build_query(url_state) 

39 

40 # Fetch data 

41 data_source = self.get_data_source() 

42 result = await data_source.find_many(query) 

43 

44 # Check if HTMX request 

45 if ctx.is_htmx: 

46 # Return just the table/content 

47 return HTMLResponse(self.render_list_partial(ctx, result, url_state)) 

48 

49 # Return full page 

50 return HTMLResponse(self.render_list(ctx, result, url_state)) 

51 

52 def _build_query(self, state: URLState) -> QuerySpec: 

53 """Build QuerySpec from URL state.""" 

54 qs = QuerySpec() 

55 

56 # Pagination — cursor takes priority over page number 

57 if state.cursor: 

58 qs = qs.with_cursor(state.cursor).with_per_page( 

59 state.per_page or self.meta.per_page 

60 ) 

61 else: 

62 qs = qs.with_page(state.page).with_per_page( 

63 state.per_page or self.meta.per_page 

64 ) 

65 

66 # Sorting 

67 if state.sort: 

68 qs = qs.with_order_by(state.sort, state.order) 

69 else: 

70 qs = qs.with_order_by(self.meta.default_sort, self.meta.default_sort_order) 

71 

72 # Search 

73 if state.search: 

74 qs = qs.with_search(state.search, self.meta.searchable_fields or []) 

75 

76 # Filters 

77 for field, value in state.filters.items(): 

78 if isinstance(value, list): 

79 qs = qs.with_where_in(field, value) 

80 else: 

81 qs = qs.with_where_eq(field, value) 

82 

83 return qs 

84 def render_list(self, 

85 ctx: AdminContext, 

86 result: QueryResult[T], 

87 state: URLState, 

88 ) -> str: 

89 """Render full list page. Override in subclass.""" 

90 return f""" 

91<!DOCTYPE html> 

92<html> 

93<head> 

94 <title>{self.meta.label_plural}</title> 

95 <script src="https://unpkg.com/htmx.org@1.9.10"></script> 

96</head> 

97<body> 

98 <h1>{self.meta.label_plural}</h1> 

99 {self.render_list_partial(ctx, result, state)} 

100</body> 

101</html> 

102""" 

103 

104 def render_list_partial(self, 

105 ctx: AdminContext, 

106 result: QueryResult[T], 

107 state: URLState, 

108 ) -> str: 

109 """Render list content (for HTMX). Override in subclass.""" 

110 row_els: list[Any] = [] 

111 for item in result.items: 

112 row_els.append(el("tr", el("td", str(item)))) 

113 

114 total_pages = getattr(result, "total_pages", "?") 

115 return f""" 

116<table> 

117 <thead><tr><th>Item</th></tr></thead> 

118 {render_to_string(el("tbody", *row_els))} 

119</table> 

120<div>Page {result.page} of {total_pages}</div> 

121"""