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

44 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-21 15:04 +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 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 async def list_view(self: ResourceController, request: Request) -> Response: 

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

28 async with AdminContextManager(request) as ctx: 

29 # Parse URL state 

30 url_state = URLState.from_request(request) 

31 

32 # Build query 

33 query = self._build_query(url_state) 

34 

35 # Fetch data 

36 data_source = self.get_data_source() 

37 result = await data_source.find_many(query) 

38 

39 # Check if HTMX request 

40 if ctx.is_htmx: 

41 # Return just the table/content 

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

43 

44 # Return full page 

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

46 

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

48 """Build QuerySpec from URL state.""" 

49 qs = QuerySpec() 

50 

51 # Pagination — cursor takes priority over page number 

52 if state.cursor: 

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

54 state.per_page or self.meta.per_page 

55 ) 

56 else: 

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

58 state.per_page or self.meta.per_page 

59 ) 

60 

61 # Sorting 

62 if state.sort: 

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

64 else: 

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

66 

67 # Search 

68 if state.search: 

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

70 

71 # Filters 

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

73 if isinstance(value, list): 

74 qs = qs.with_where_in(field, value) 

75 else: 

76 qs = qs.with_where_eq(field, value) 

77 

78 return qs 

79 def render_list(self: ResourceController, 

80 ctx: AdminContext, 

81 result: QueryResult[T], 

82 state: URLState, 

83 ) -> str: 

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

85 return f""" 

86<!DOCTYPE html> 

87<html> 

88<head> 

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

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

91</head> 

92<body> 

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

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

95</body> 

96</html> 

97""" 

98 

99 def render_list_partial(self: ResourceController, 

100 ctx: AdminContext, 

101 result: QueryResult[T], 

102 state: URLState, 

103 ) -> str: 

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

105 row_els: list[Any] = [] 

106 for item in result.items: 

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

108 

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

110 return f""" 

111<table> 

112 <thead><tr><th>Item</th></tr></thead> 

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

114</table> 

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

116"""