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
« 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."""
3from __future__ import annotations
5from typing import TYPE_CHECKING, Any
7from starlette.requests import Request
8from starlette.responses import HTMLResponse, Response
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
18if TYPE_CHECKING:
19 from lexigram.admin.controllers.resource import ResourceController
23class ResourceListMixin:
24 """List view, query building, and list rendering."""
26 # Host attributes provided by sibling mixins on ResourceController.
27 meta: ResourceMeta
29 get_data_source: Any
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)
37 # Build query
38 query = self._build_query(url_state)
40 # Fetch data
41 data_source = self.get_data_source()
42 result = await data_source.find_many(query)
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))
49 # Return full page
50 return HTMLResponse(self.render_list(ctx, result, url_state))
52 def _build_query(self, state: URLState) -> QuerySpec:
53 """Build QuerySpec from URL state."""
54 qs = QuerySpec()
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 )
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)
72 # Search
73 if state.search:
74 qs = qs.with_search(state.search, self.meta.searchable_fields or [])
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)
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"""
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))))
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"""