Skip to content

Commit 8c43f7d

Browse files
authored
feat: add basic pagination support for admin panel (#217)
1 parent 942105d commit 8c43f7d

File tree

7 files changed

+270
-11
lines changed

7 files changed

+270
-11
lines changed

cot-macros/src/admin.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,13 +101,23 @@ impl AdminModelDeriveBuilder {
101101
self
102102
}
103103

104+
async fn get_total_object_counts(
105+
request: &#crate_ident::request::Request,
106+
) -> #crate_ident::Result<usize> {
107+
use #crate_ident::db::Model;
108+
use #crate_ident::request::RequestExt;
109+
110+
Ok(Self::objects().count(request.db()).await?)
111+
}
112+
104113
async fn get_objects(
105114
request: &#crate_ident::request::Request,
115+
pagination: #crate_ident::admin::Pagination,
106116
) -> #crate_ident::Result<::std::vec::Vec<Self>> {
107117
use #crate_ident::db::Model;
108118
use #crate_ident::request::RequestExt;
109119

110-
Ok(Self::objects().all(request.db()).await?)
120+
Ok(Self::objects().limit(pagination.limit()).offset(pagination.offset()).all(request.db()).await?)
111121
}
112122

113123
async fn get_object_by_id(

cot/src/admin.rs

Lines changed: 93 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
//! registered in the application, straight from the web interface.
55
66
use std::any::Any;
7+
use std::collections::HashMap;
78
use std::marker::PhantomData;
89

910
use async_trait::async_trait;
@@ -24,7 +25,7 @@ use crate::auth::{AuthRequestExt, Password};
2425
use crate::form::{
2526
Form, FormContext, FormErrorTarget, FormField, FormFieldValidationError, FormResult,
2627
};
27-
use crate::request::{Request, RequestExt};
28+
use crate::request::{query_pairs, Request, RequestExt};
2829
use crate::response::{Response, ResponseExt};
2930
use crate::router::Router;
3031
use crate::{reverse_redirect, static_files, App, Body, Method, RequestHandler, StatusCode};
@@ -135,6 +136,34 @@ async fn authenticate(request: &mut Request, login_form: LoginForm) -> cot::Resu
135136
}
136137
}
137138

139+
/// Struct representing the pagination of objects.
140+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
141+
pub struct Pagination {
142+
limit: usize,
143+
offset: usize,
144+
}
145+
146+
impl Pagination {
147+
fn new(limit: usize, page: usize) -> Self {
148+
assert!(page > 0, "Page number must be greater than 0");
149+
150+
Self {
151+
limit,
152+
offset: (page - 1) * limit,
153+
}
154+
}
155+
156+
/// Returns the limit of objects per page.
157+
pub fn limit(&self) -> usize {
158+
self.limit
159+
}
160+
161+
/// Returns the offset of objects.
162+
pub fn offset(&self) -> usize {
163+
self.offset
164+
}
165+
}
166+
138167
async fn view_model(request: Request) -> cot::Result<Response> {
139168
#[derive(Debug, Template)]
140169
#[template(path = "admin/model.html")]
@@ -144,16 +173,54 @@ async fn view_model(request: Request) -> cot::Result<Response> {
144173
model: &'a dyn AdminModelManager,
145174
#[debug("..")]
146175
objects: Vec<Box<dyn AdminModel>>,
176+
page: usize,
177+
page_size: &'a usize,
178+
total_object_counts: usize,
179+
total_pages: usize,
147180
}
148181

149182
let model_name: String = request.path_params().parse()?;
150183
let manager = get_manager(&request, &model_name)?;
151184

185+
let query_params: HashMap<String, String> = request
186+
.uri()
187+
.query()
188+
.map(|q| {
189+
query_pairs(&Bytes::copy_from_slice(q.as_bytes()))
190+
.map(|(k, v)| (k.to_string(), v.to_string()))
191+
.collect()
192+
})
193+
.unwrap_or_default();
194+
195+
let page: usize = query_params
196+
.get("page")
197+
.map_or(1, |p| p.parse().unwrap_or(1));
198+
199+
let limit = query_params
200+
.get("page_size")
201+
.map_or(10, |p| p.parse().unwrap_or(10));
202+
203+
let total_object_counts = manager.get_total_object_counts(&request).await?;
204+
let total_pages = total_object_counts.div_ceil(limit);
205+
206+
if page == 0 || page > total_pages {
207+
return Err(Error::not_found_message(format!("page {page} not found")));
208+
}
209+
210+
let pagination = Pagination::new(limit, page);
211+
212+
let objects = manager.get_objects(&request, pagination).await?;
213+
152214
let template = ModelTemplate {
153215
request: &request,
154216
model: &*manager,
155-
objects: manager.get_objects(&request).await?,
217+
objects,
218+
page,
219+
page_size: &limit,
220+
total_object_counts,
221+
total_pages,
156222
};
223+
157224
Ok(Response::new_html(
158225
StatusCode::OK,
159226
Body::fixed(template.render()?),
@@ -312,7 +379,14 @@ pub trait AdminModelManager: Send + Sync {
312379
fn url_name(&self) -> &str;
313380

314381
/// Returns the list of objects of this model.
315-
async fn get_objects(&self, request: &Request) -> cot::Result<Vec<Box<dyn AdminModel>>>;
382+
async fn get_objects(
383+
&self,
384+
request: &Request,
385+
pagination: Pagination,
386+
) -> cot::Result<Vec<Box<dyn AdminModel>>>;
387+
388+
/// Returns the total count of objects of this model.
389+
async fn get_total_object_counts(&self, request: &Request) -> cot::Result<usize>;
316390

317391
/// Returns the object with the given ID.
318392
async fn get_object_by_id(
@@ -387,9 +461,17 @@ impl<T: AdminModel + Send + Sync + 'static> AdminModelManager for DefaultAdminMo
387461
T::url_name()
388462
}
389463

390-
async fn get_objects(&self, request: &Request) -> cot::Result<Vec<Box<dyn AdminModel>>> {
464+
async fn get_total_object_counts(&self, request: &Request) -> cot::Result<usize> {
465+
T::get_total_object_counts(request).await
466+
}
467+
468+
async fn get_objects(
469+
&self,
470+
request: &Request,
471+
pagination: Pagination,
472+
) -> cot::Result<Vec<Box<dyn AdminModel>>> {
391473
#[allow(trivial_casts)] // Upcast to the correct Box type
392-
T::get_objects(request).await.map(|objects| {
474+
T::get_objects(request, pagination).await.map(|objects| {
393475
objects
394476
.into_iter()
395477
.map(|object| Box::new(object) as Box<dyn AdminModel>)
@@ -443,7 +525,12 @@ pub trait AdminModel: Any + Send + 'static {
443525
fn as_any(&self) -> &dyn Any;
444526

445527
/// Get the objects of this model.
446-
async fn get_objects(request: &Request) -> cot::Result<Vec<Self>>
528+
async fn get_objects(request: &Request, pagination: Pagination) -> cot::Result<Vec<Self>>
529+
where
530+
Self: Sized;
531+
532+
/// Get the total count of objects of this model.
533+
async fn get_total_object_counts(request: &Request) -> cot::Result<usize>
447534
where
448535
Self: Sized;
449536

cot/src/db.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -963,6 +963,8 @@ impl Database {
963963
let mut select = sea_query::Query::select();
964964
select.columns(columns_to_get).from(T::TABLE_NAME);
965965
query.add_filter_to_statement(&mut select);
966+
query.add_limit_to_statement(&mut select);
967+
query.add_offset_to_statement(&mut select);
966968

967969
let rows = self.fetch_all(&select).await?;
968970
let result = rows.into_iter().map(T::from_db).collect::<Result<_>>()?;

cot/src/db/query.rs

Lines changed: 107 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ use sea_query::{ExprTrait, IntoColumnRef};
77

88
use crate::db;
99
use crate::db::{
10-
Auto, DatabaseBackend, DbFieldValue, DbValue, ForeignKey, FromDbValue, Identifier, Model,
11-
StatementResult, ToDbFieldValue,
10+
Auto, Database, DatabaseBackend, DbFieldValue, DbValue, ForeignKey, FromDbValue, Identifier,
11+
Model, StatementResult, ToDbFieldValue,
1212
};
1313

1414
/// A query that can be executed on a database. Can be used to filter, update,
@@ -32,6 +32,8 @@ use crate::db::{
3232
/// ```
3333
pub struct Query<T> {
3434
filter: Option<Expr>,
35+
limit: Option<usize>,
36+
offset: Option<usize>,
3537
phantom_data: PhantomData<fn() -> T>,
3638
}
3739

@@ -40,6 +42,8 @@ impl<T> Debug for Query<T> {
4042
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
4143
f.debug_struct("Query")
4244
.field("filter", &self.filter)
45+
.field("limit", &self.limit)
46+
.field("offset", &self.offset)
4347
.field("phantom_data", &self.phantom_data)
4448
.finish()
4549
}
@@ -50,6 +54,8 @@ impl<T> Clone for Query<T> {
5054
fn clone(&self) -> Self {
5155
Self {
5256
filter: self.filter.clone(),
57+
limit: self.limit,
58+
offset: self.offset,
5359
phantom_data: PhantomData,
5460
}
5561
}
@@ -91,6 +97,8 @@ impl<T: Model> Query<T> {
9197
pub fn new() -> Self {
9298
Self {
9399
filter: None,
100+
limit: None,
101+
offset: None,
94102
phantom_data: PhantomData,
95103
}
96104
}
@@ -118,6 +126,52 @@ impl<T: Model> Query<T> {
118126
self
119127
}
120128

129+
/// Set the limit for the query.
130+
///
131+
/// # Example
132+
///
133+
/// ```
134+
/// use cot::db::model;
135+
/// use cot::db::query::{Expr, Query};
136+
///
137+
/// #[model]
138+
/// struct User {
139+
/// #[model(primary_key)]
140+
/// id: i32,
141+
/// name: String,
142+
/// age: i32,
143+
/// }
144+
///
145+
/// let query = Query::<User>::new().limit(10);
146+
/// ```
147+
pub fn limit(&mut self, limit: usize) -> &mut Self {
148+
self.limit = Some(limit);
149+
self
150+
}
151+
152+
/// Set the offset for the query.
153+
///
154+
/// # Example
155+
///
156+
/// ```
157+
/// use cot::db::model;
158+
/// use cot::db::query::{Expr, Query};
159+
///
160+
/// #[model]
161+
/// struct User {
162+
/// #[model(primary_key)]
163+
/// id: i32,
164+
/// name: String,
165+
/// age: i32,
166+
/// }
167+
///
168+
/// let query = Query::<User>::new().offset(10);
169+
/// ```
170+
pub fn offset(&mut self, offset: usize) -> &mut Self {
171+
self.offset = Some(offset);
172+
self
173+
}
174+
121175
/// Execute the query and return all results.
122176
///
123177
/// # Errors
@@ -137,6 +191,25 @@ impl<T: Model> Query<T> {
137191
db.get(self).await
138192
}
139193

194+
/// Execute the query and return the number of results.
195+
///
196+
/// # Errors
197+
///
198+
/// Returns an error if the query fails.
199+
pub async fn count(&self, db: &Database) -> db::Result<usize> {
200+
let mut select = sea_query::Query::select();
201+
select
202+
.from(T::TABLE_NAME)
203+
.expr(sea_query::Expr::col(sea_query::Asterisk).count());
204+
self.add_filter_to_statement(&mut select);
205+
let row = db.fetch_option(&select).await?;
206+
let count = match row {
207+
Some(row) => row.get::<i64>(0)? as usize,
208+
None => 0,
209+
};
210+
Ok(count)
211+
}
212+
140213
/// Execute the query and check if any results exist.
141214
///
142215
/// # Errors
@@ -163,6 +236,18 @@ impl<T: Model> Query<T> {
163236
statement.and_where(filter.as_sea_query_expr());
164237
}
165238
}
239+
240+
pub(super) fn add_limit_to_statement(&self, statement: &mut sea_query::SelectStatement) {
241+
if let Some(limit) = self.limit {
242+
statement.limit(limit as u64);
243+
}
244+
}
245+
246+
pub(super) fn add_offset_to_statement(&self, statement: &mut sea_query::SelectStatement) {
247+
if let Some(offset) = self.offset {
248+
statement.offset(offset as u64);
249+
}
250+
}
166251
}
167252

168253
/// An expression that can be used to filter, update, or delete rows.
@@ -1353,13 +1438,17 @@ mod tests {
13531438
let query: Query<MockModel> = Query::new();
13541439

13551440
assert!(query.filter.is_none());
1441+
assert!(query.limit.is_none());
1442+
assert!(query.offset.is_none());
13561443
}
13571444

13581445
#[test]
13591446
fn query_default() {
13601447
let query: Query<MockModel> = Query::default();
13611448

13621449
assert!(query.filter.is_none());
1450+
assert!(query.limit.is_none());
1451+
assert!(query.offset.is_none());
13631452
}
13641453

13651454
#[test]
@@ -1371,6 +1460,22 @@ mod tests {
13711460
assert!(query.filter.is_some());
13721461
}
13731462

1463+
#[test]
1464+
fn query_limit() {
1465+
let mut query: Query<MockModel> = Query::new();
1466+
query.limit(10);
1467+
assert!(query.limit.is_some());
1468+
assert_eq!(query.limit.unwrap(), 10);
1469+
}
1470+
1471+
#[test]
1472+
fn query_offset() {
1473+
let mut query: Query<MockModel> = Query::new();
1474+
query.offset(10);
1475+
assert!(query.offset.is_some());
1476+
assert_eq!(query.offset.unwrap(), 10);
1477+
}
1478+
13741479
#[cot::test]
13751480
async fn query_all() {
13761481
let mut db = MockDatabaseBackend::new();

0 commit comments

Comments
 (0)