From 3095929d7646bf83e37c341dafc265b1cd12abef Mon Sep 17 00:00:00 2001 From: Manuel Gugger Date: Fri, 16 Jun 2023 16:17:28 +0200 Subject: [PATCH] initial filter implementation --- Cargo.toml | 1 + actix_admin_macros/src/lib.rs | 26 +- examples/azure_auth/entity/comment.rs | 4 +- examples/azure_auth/entity/post.rs | 4 +- examples/basic/entity/comment.rs | 24 +- examples/basic/entity/mod.rs | 86 +++--- examples/basic/entity/post.rs | 5 +- examples/basic/entity/user.rs | 6 +- src/lib.rs | 4 +- src/model.rs | 24 +- src/routes/list.rs | 21 +- src/templates/base.html | 2 +- src/templates/head.html | 10 +- src/templates/list.html | 373 ++++++++++++++------------ src/view_model.rs | 18 +- 15 files changed, 373 insertions(+), 235 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5dcb7e8..e2e5c68 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,7 @@ sea-orm = { version = "^0.11.3", features = [], default-features = false } actix-admin-macros = { version = "0.4.0", path = "actix_admin_macros" } derive_more = "0.99.17" regex = "1.8.4" +urlencoding = "2.1.2" [dev-dependencies] sea-orm = { version = "^0.11.3", features = [ "sqlx-sqlite", "runtime-actix-native-tls", "macros" ], default-features = true } diff --git a/actix_admin_macros/src/lib.rs b/actix_admin_macros/src/lib.rs index 88224d1..0fea694 100644 --- a/actix_admin_macros/src/lib.rs +++ b/actix_admin_macros/src/lib.rs @@ -66,15 +66,17 @@ pub fn derive_actix_admin_view_model(input: proc_macro::TokenStream) -> proc_mac entity_name: entity.table_name().to_string(), fields: Entity::get_fields(), show_search: #has_searchable_fields, - user_can_access: None + user_can_access: None, + default_show_aside: Entity::get_filter().len() > 0 } } } #[actix_admin::prelude::async_trait(?Send)] impl ActixAdminViewModelTrait for Entity { - async fn list(db: &DatabaseConnection, page: u64, entities_per_page: u64, search: &str, sort_by: &str, sort_order: &SortOrder) -> Result<(u64, Vec), ActixAdminError> { - let entities = Entity::list_model(db, page, entities_per_page, search, sort_by, sort_order).await; + async fn list(db: &DatabaseConnection, page: u64, entities_per_page: u64, viewmodel_filter: Vec, search: &str, sort_by: &str, sort_order: &SortOrder) -> Result<(u64, Vec), ActixAdminError> { + let filter_values: HashMap> = viewmodel_filter.iter().map(|f| (f.name.to_string(), f.value.clone())).collect(); + let entities = Entity::list_model(db, page, entities_per_page, filter_values, search, sort_by, sort_order).await; entities } @@ -96,6 +98,15 @@ pub fn derive_actix_admin_view_model(input: proc_macro::TokenStream) -> proc_mac Ok(model) } + async fn get_viewmodel_filter() -> HashMap { + Entity::get_filter().iter().map(|f| + (f.name.to_string(), ActixAdminViewModelFilter { + name: f.name.to_string(), + value: None + }) + ).collect() + } + async fn get_entity(db: &DatabaseConnection, id: i32) -> Result { // TODO: separate primary key from other keys let entity = Entity::find_by_id(id).one(db).await?; @@ -281,7 +292,7 @@ pub fn derive_actix_admin_model(input: proc_macro::TokenStream) -> proc_macro::T #[actix_admin::prelude::async_trait] impl ActixAdminModelTrait for Entity { - async fn list_model(db: &DatabaseConnection, page: u64, posts_per_page: u64, search: &str, sort_by: &str, sort_order: &SortOrder) -> Result<(u64, Vec), ActixAdminError> { + async fn list_model(db: &DatabaseConnection, page: u64, posts_per_page: u64, filter_values: HashMap>, search: &str, sort_by: &str, sort_order: &SortOrder) -> Result<(u64, Vec), ActixAdminError> { let sort_column = match sort_by { #(#fields_match_name_to_columns)* _ => panic!("Unknown column") @@ -301,6 +312,13 @@ pub fn derive_actix_admin_model(input: proc_macro::TokenStream) -> proc_macro::T ) } + let filters = Entity::get_filter(); + for filter in filters { + let myfn = filter.filter; + let value = filter_values.get(&filter.name).unwrap_or_else(|| &None); + query = myfn(query, value.clone()); + } + let paginator = query.paginate(db, posts_per_page); let num_pages = paginator.num_pages().await?; diff --git a/examples/azure_auth/entity/comment.rs b/examples/azure_auth/entity/comment.rs index a38535e..1eb0035 100644 --- a/examples/azure_auth/entity/comment.rs +++ b/examples/azure_auth/entity/comment.rs @@ -48,4 +48,6 @@ impl ActixAdminModelValidationTrait for Entity { } errors } -} \ No newline at end of file +} + +impl ActixAdminModelFilterTrait for Entity {} \ No newline at end of file diff --git a/examples/azure_auth/entity/post.rs b/examples/azure_auth/entity/post.rs index b62977c..29a1e7a 100644 --- a/examples/azure_auth/entity/post.rs +++ b/examples/azure_auth/entity/post.rs @@ -67,4 +67,6 @@ impl FromStr for Tea { } } -impl ActixAdminModelValidationTrait for Entity {} \ No newline at end of file +impl ActixAdminModelValidationTrait for Entity {} + +impl ActixAdminModelFilterTrait for Entity {} \ No newline at end of file diff --git a/examples/basic/entity/comment.rs b/examples/basic/entity/comment.rs index 33149b8..211cf1d 100644 --- a/examples/basic/entity/comment.rs +++ b/examples/basic/entity/comment.rs @@ -1,6 +1,6 @@ use sea_orm::entity::prelude::*; use serde::{Deserialize, Serialize}; -use actix_admin::prelude::*; +use actix_admin::{prelude::*}; use super::Post; #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Deserialize, Serialize, DeriveActixAdmin, DeriveActixAdminModel, DeriveActixAdminViewModel)] @@ -49,4 +49,24 @@ impl ActixAdminModelValidationTrait for Entity { errors } -} \ No newline at end of file +} + +impl ActixAdminModelFilterTrait for Entity { + fn get_filter() -> Vec> { + vec![ + ActixAdminModelFilter:: { + name: "Id".to_string(), + filter: |q: sea_orm::Select, v| -> sea_orm::Select { + q.apply_if(v, | query, val: String| query.filter(Column::Id.eq(val))) + } + }, + ActixAdminModelFilter:: { + name: "User".to_string(), + filter: |q: sea_orm::Select, v| -> sea_orm::Select { + q.apply_if(v, | query, val: String| query.filter(Column::User.eq(val))) + } + } + ] + } +} + diff --git a/examples/basic/entity/mod.rs b/examples/basic/entity/mod.rs index cec8803..e450a67 100644 --- a/examples/basic/entity/mod.rs +++ b/examples/basic/entity/mod.rs @@ -1,14 +1,15 @@ // setup -use sea_orm::sea_query::{ForeignKeyCreateStatement, ColumnDef, TableCreateStatement}; -use sea_orm::{Set, EntityTrait, error::*, sea_query, ConnectionTrait, DbConn, ExecResult }; +use sea_orm::sea_query::{ColumnDef, ForeignKeyCreateStatement, TableCreateStatement}; +use sea_orm::{ + error::*, sea_query, ConnectionTrait, DbConn, EntityTrait, ExecResult, Set}; pub mod comment; pub mod post; pub mod user; +use chrono::{Duration, DurationRound, Local}; pub use comment::Entity as Comment; pub use post::Entity as Post; -pub use user::Entity as User; -use chrono::{Local, Duration, DurationRound}; use sea_orm::prelude::Decimal; +pub use user::Entity as User; // setup async fn create_table(db: &DbConn, stmt: &TableCreateStatement) -> Result { @@ -29,7 +30,11 @@ pub async fn create_post_table(db: &DbConn) -> Result { ) .col(ColumnDef::new(post::Column::Title).string().not_null()) .col(ColumnDef::new(post::Column::Text).string().not_null()) - .col(ColumnDef::new(post::Column::TeaMandatory).string().not_null()) + .col( + ColumnDef::new(post::Column::TeaMandatory) + .string() + .not_null(), + ) .col(ColumnDef::new(post::Column::TeaOptional).string()) .col(ColumnDef::new(post::Column::InsertDate).date().not_null()) .col(ColumnDef::new(post::Column::Attachment).string()) @@ -49,9 +54,21 @@ pub async fn create_post_table(db: &DbConn) -> Result { ) .col(ColumnDef::new(comment::Column::Comment).string().not_null()) .col(ColumnDef::new(comment::Column::User).string().not_null()) - .col(ColumnDef::new(comment::Column::InsertDate).date_time().not_null()) - .col(ColumnDef::new(comment::Column::IsVisible).boolean().not_null()) - .col(ColumnDef::new(comment::Column::MyDecimal).decimal().not_null()) + .col( + ColumnDef::new(comment::Column::InsertDate) + .date_time() + .not_null(), + ) + .col( + ColumnDef::new(comment::Column::IsVisible) + .boolean() + .not_null(), + ) + .col( + ColumnDef::new(comment::Column::MyDecimal) + .decimal() + .not_null(), + ) .col(ColumnDef::new(comment::Column::PostId).integer()) .foreign_key( ForeignKeyCreateStatement::new() @@ -82,35 +99,38 @@ pub async fn create_post_table(db: &DbConn) -> Result { for i in 1..1000 { let row = post::ActiveModel { - title: Set(format!("Test {}", i)), - text: Set("some content".to_string()), - tea_mandatory: Set(post::Tea::EverydayTea), - tea_optional: Set(None), - insert_date: Set(Local::now().date_naive()), - ..Default::default() + title: Set(format!("Test {}", i)), + text: Set("some content".to_string()), + tea_mandatory: Set(post::Tea::EverydayTea), + tea_optional: Set(None), + insert_date: Set(Local::now().date_naive()), + ..Default::default() }; let _res = Post::insert(row).exec(db).await; - } + } - for i in 1..1000 { - let row = comment::ActiveModel { - comment: Set(format!("Test {}", i)), - user: Set("me@home.com".to_string()), - my_decimal: Set(Decimal::new(105, 0)), - insert_date: Set(Local::now().naive_utc().duration_round(Duration::minutes(1)).unwrap()), - is_visible: Set(i%2 == 0), - ..Default::default() - }; - let _res = Comment::insert(row).exec(db).await; - } + for i in 1..1000 { + let row = comment::ActiveModel { + comment: Set(format!("Test {}", i)), + user: Set("me@home.com".to_string()), + my_decimal: Set(Decimal::new(105, 0)), + insert_date: Set(Local::now() + .naive_utc() + .duration_round(Duration::minutes(1)) + .unwrap()), + is_visible: Set(i % 2 == 0), + ..Default::default() + }; + let _res = Comment::insert(row).exec(db).await; + } - for i in 1..100 { - let row = user::ActiveModel { - name: Set(format!("user {}", i)), - ..Default::default() - }; - let _res = User::insert(row).exec(db).await; -} + for i in 1..100 { + let row = user::ActiveModel { + name: Set(format!("user {}", i)), + ..Default::default() + }; + let _res = User::insert(row).exec(db).await; + } _res } diff --git a/examples/basic/entity/post.rs b/examples/basic/entity/post.rs index e23f4de..1387d12 100644 --- a/examples/basic/entity/post.rs +++ b/examples/basic/entity/post.rs @@ -1,3 +1,4 @@ +use actix_admin::model::ActixAdminModelFilterTrait; use sea_orm::entity::prelude::*; use serde::{Deserialize, Serialize}; use actix_admin::prelude::*; @@ -71,4 +72,6 @@ impl FromStr for Tea { } } -impl ActixAdminModelValidationTrait for Entity {} \ No newline at end of file +impl ActixAdminModelValidationTrait for Entity {} + +impl ActixAdminModelFilterTrait for Entity {} \ No newline at end of file diff --git a/examples/basic/entity/user.rs b/examples/basic/entity/user.rs index 19cce78..7156e97 100644 --- a/examples/basic/entity/user.rs +++ b/examples/basic/entity/user.rs @@ -1,6 +1,6 @@ use sea_orm::entity::prelude::*; use serde::{Deserialize, Serialize}; -use actix_admin::prelude::*; +use actix_admin::{prelude::*}; #[derive(Clone, Debug, PartialEq, DeriveEntityModel, Deserialize, Serialize, DeriveActixAdmin, DeriveActixAdminModel, DeriveActixAdminViewModel)] #[sea_orm(table_name = "user")] @@ -17,4 +17,6 @@ impl ActiveModelBehavior for ActiveModel {} #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] pub enum Relation {} -impl ActixAdminModelValidationTrait for Entity {} \ No newline at end of file +impl ActixAdminModelValidationTrait for Entity {} + +impl ActixAdminModelFilterTrait for Entity {} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index a940e24..9aba321 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -24,11 +24,11 @@ pub mod view_model; pub mod prelude { pub use crate::builder::{ActixAdminBuilder, ActixAdminBuilderTrait}; - pub use crate::model::{ActixAdminModel, ActixAdminModelTrait, ActixAdminModelValidationTrait}; + pub use crate::model::{ActixAdminModel, ActixAdminModelTrait, ActixAdminModelValidationTrait, ActixAdminModelFilter, ActixAdminModelFilterTrait}; pub use crate::routes::{create_or_edit_post, get_admin_ctx, SortOrder}; pub use crate::view_model::{ ActixAdminViewModel, ActixAdminViewModelField, ActixAdminViewModelFieldType, - ActixAdminViewModelSerializable, ActixAdminViewModelTrait, + ActixAdminViewModelSerializable, ActixAdminViewModelTrait, ActixAdminViewModelFilter }; pub use crate::{hashmap, ActixAdminSelectListTrait}; pub use crate::{ActixAdmin, ActixAdminAppDataTrait, ActixAdminConfiguration, ActixAdminError}; diff --git a/src/model.rs b/src/model.rs index 3329207..1c04a92 100644 --- a/src/model.rs +++ b/src/model.rs @@ -1,11 +1,12 @@ use crate::routes::SortOrder; +use crate::view_model::ActixAdminViewModelFilter; use crate::{ActixAdminError, ActixAdminViewModelField}; use actix_multipart::{Multipart, MultipartError}; use actix_web::web::Bytes; use async_trait::async_trait; use chrono::{NaiveDate, NaiveDateTime}; use futures_util::stream::StreamExt as _; -use sea_orm::DatabaseConnection; +use sea_orm::{DatabaseConnection, EntityTrait}; use serde::Serialize; use std::collections::HashMap; use std::fs::File; @@ -18,6 +19,7 @@ pub trait ActixAdminModelTrait { db: &DatabaseConnection, page: u64, posts_per_page: u64, + filter_values: HashMap>, search: &str, sort_by: &str, sort_order: &SortOrder @@ -32,6 +34,26 @@ pub trait ActixAdminModelValidationTrait { } } +pub struct ActixAdminModelFilter { + pub name: String, + pub filter: fn(sea_orm::Select, Option) -> sea_orm::Select +} + +pub trait ActixAdminModelFilterTrait { + fn get_filter() -> Vec> { + Vec::new() + } +} + +impl From> for ActixAdminViewModelFilter { + fn from(filter: ActixAdminModelFilter) -> Self { + ActixAdminViewModelFilter { + name: filter.name, + value: None + } + } +} + #[derive(Clone, Debug, Serialize)] pub struct ActixAdminModel { pub primary_key: Option, diff --git a/src/routes/list.rs b/src/routes/list.rs index bb06b56..d81a45e 100644 --- a/src/routes/list.rs +++ b/src/routes/list.rs @@ -1,5 +1,5 @@ use std::fmt; - +use urlencoding::decode; use crate::prelude::*; use actix_web::{error, web, Error, HttpRequest, HttpResponse}; use serde::Deserialize; @@ -81,7 +81,20 @@ pub async fn list( .unwrap_or(view_model.primary_key.to_string()); let sort_order = params.sort_order.as_ref().unwrap_or(&SortOrder::Asc); - let result = E::list(db, page, entities_per_page, &search, &sort_by, &sort_order).await; + let decoded_querystring = decode(req.query_string()).unwrap(); + let actixadminfilters: Vec = decoded_querystring + .split("&") + .filter(|qf| qf.starts_with("filter_")) + .map(|f| { + let mut kv = f.split("="); + let af = ActixAdminViewModelFilter { + name: kv.next().unwrap().strip_prefix("filter_").unwrap_or_default().to_string(), + value: kv.next().map(|s| s.to_string()).filter(|f| !f.is_empty()), + }; + af + }).collect(); + + let result = E::list(db, page, entities_per_page, actixadminfilters, &search, &sort_by, &sort_order).await; match result { Ok(res) => { @@ -130,6 +143,7 @@ pub async fn list( ctx.insert("notifications", ¬ifications); ctx.insert("entities_per_page", &entities_per_page); ctx.insert("render_partial", &render_partial); + ctx.insert("viewmodel_filter", &E::get_viewmodel_filter().await); ctx.insert( "view_model", &ActixAdminViewModelSerializable::from(view_model.clone()), @@ -138,7 +152,8 @@ pub async fn list( ctx.insert("sort_by", &sort_by); ctx.insert("sort_order", &sort_order); - let body = actix_admin.tera + let body = actix_admin + .tera .render("list.html", &ctx) .map_err(|err| error::ErrorInternalServerError(err))?; Ok(http_response_code.content_type("text/html").body(body)) diff --git a/src/templates/base.html b/src/templates/base.html index e3ce7f0..4537944 100644 --- a/src/templates/base.html +++ b/src/templates/base.html @@ -27,7 +27,7 @@
-