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
« 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."""
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 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 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)
32 # Build query
33 query = self._build_query(url_state)
35 # Fetch data
36 data_source = self.get_data_source()
37 result = await data_source.find_many(query)
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))
44 # Return full page
45 return HTMLResponse(self.render_list(ctx, result, url_state))
47 def _build_query(self: ResourceController, state: URLState) -> QuerySpec:
48 """Build QuerySpec from URL state."""
49 qs = QuerySpec()
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 )
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)
67 # Search
68 if state.search:
69 qs = qs.with_search(state.search, self.meta.searchable_fields or [])
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)
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"""
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))))
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"""