update sea-orm and fix empty list when no searchable conditions

This commit is contained in:
Manuel Gugger 2023-06-15 11:33:08 +02:00
parent 473147e80a
commit 223ad29266
8 changed files with 145 additions and 99 deletions

View File

@ -23,23 +23,23 @@ actix-web = "^4.3.1"
actix-session = { version = "^0.7.2", features = [] } actix-session = { version = "^0.7.2", features = [] }
actix-multipart = "^0.6.0" actix-multipart = "^0.6.0"
actix-files = "^0.6.2" actix-files = "^0.6.2"
futures-util = "0.3.27" futures-util = "0.3.28"
chrono = "0.4.24" chrono = "0.4.26"
tera = "^1.18.1" tera = "^1.19.0"
async-trait = "^0.1.67" async-trait = "^0.1.68"
lazy_static = "^1.4.0" lazy_static = "^1.4.0"
itertools = "^0.10.5" itertools = "^0.10.5"
serde = "^1.0.158" serde = "^1.0.164"
serde_derive = "^1.0.158" serde_derive = "^1.0.164"
sea-orm = { version = "^0.10.6", features = [], default-features = false } sea-orm = { version = "^0.11.3", features = [], default-features = false }
actix-admin-macros = { version = "0.4.0", path = "actix_admin_macros" } actix-admin-macros = { version = "0.4.0", path = "actix_admin_macros" }
derive_more = "0.99.17" derive_more = "0.99.17"
regex = "1.7.1" regex = "1.8.4"
[dev-dependencies] [dev-dependencies]
sea-orm = { version = "^0.10.6", features = [ "sqlx-sqlite", "runtime-actix-native-tls", "macros" ], default-features = true } sea-orm = { version = "^0.11.3", features = [ "sqlx-sqlite", "runtime-actix-native-tls", "macros" ], default-features = true }
actix-rt = "2.8.0" actix-rt = "2.8.0"
azure_auth = { path = "./examples/azure_auth/azure_auth" } azure_auth = { path = "./examples/azure_auth/azure_auth" }
oauth2 = "4.3" oauth2 = "4.4.1"
dotenv = "0.15" dotenv = "0.15"
actix-session = { version = "0.7.2", features = ["cookie-session"] } actix-session = { version = "0.7.2", features = ["cookie-session"] }

View File

@ -15,12 +15,16 @@ mod attributes;
mod model_fields; mod model_fields;
#[proc_macro_derive(DeriveActixAdminEnumSelectList, attributes(actix_admin))] #[proc_macro_derive(DeriveActixAdminEnumSelectList, attributes(actix_admin))]
pub fn derive_actix_admin_enum_select_list(input: proc_macro::TokenStream) -> proc_macro::TokenStream { pub fn derive_actix_admin_enum_select_list(
input: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
get_select_list_from_enum(input) get_select_list_from_enum(input)
} }
#[proc_macro_derive(DeriveActixAdminModelSelectList, attributes(actix_admin))] #[proc_macro_derive(DeriveActixAdminModelSelectList, attributes(actix_admin))]
pub fn derive_actix_admin_model_select_list(input: proc_macro::TokenStream) -> proc_macro::TokenStream { pub fn derive_actix_admin_model_select_list(
input: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
get_select_list_from_model(input) get_select_list_from_model(input)
} }
@ -30,10 +34,10 @@ pub fn derive_actix_admin(_input: proc_macro::TokenStream) -> proc_macro::TokenS
use std::convert::From; use std::convert::From;
use actix_admin::prelude::*; use actix_admin::prelude::*;
use sea_orm::{ use sea_orm::{
ActiveValue::Set, ActiveValue::Set,
ConnectOptions, ConnectOptions,
DatabaseConnection, DatabaseConnection,
entity::*, entity::*,
query::*, query::*,
EntityTrait EntityTrait
}; };
@ -82,7 +86,7 @@ pub fn derive_actix_admin_view_model(input: proc_macro::TokenStream) -> proc_mac
let custom_errors = Entity::validate(&active_model); let custom_errors = Entity::validate(&active_model);
model.custom_errors = custom_errors; model.custom_errors = custom_errors;
} }
} }
async fn create_entity(db: &DatabaseConnection, mut model: ActixAdminModel) -> Result<ActixAdminModel, ActixAdminError> { async fn create_entity(db: &DatabaseConnection, mut model: ActixAdminModel) -> Result<ActixAdminModel, ActixAdminError> {
let new_model = ActiveModel::from(model.clone()); let new_model = ActiveModel::from(model.clone());
@ -108,8 +112,8 @@ pub fn derive_actix_admin_view_model(input: proc_macro::TokenStream) -> proc_mac
Some(e) => { Some(e) => {
let mut entity: ActiveModel = e.into(); let mut entity: ActiveModel = e.into();
#(#fields_for_edit_model);*; #(#fields_for_edit_model);*;
let entity: Model = entity.update(db).await?; let entity: Model = entity.update(db).await?;
Ok(model) Ok(model)
}, },
_ => Err(ActixAdminError::EntityDoesNotExistError) _ => Err(ActixAdminError::EntityDoesNotExistError)
} }
@ -143,28 +147,46 @@ pub fn derive_actix_admin_view_model(input: proc_macro::TokenStream) -> proc_mac
pub fn derive_actix_admin_model(input: proc_macro::TokenStream) -> proc_macro::TokenStream { pub fn derive_actix_admin_model(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let fields = get_fields_for_tokenstream(input); let fields = get_fields_for_tokenstream(input);
let field_names = get_fields_as_tokenstream(&fields, |model_field| -> String { model_field.ident.to_string() }); let field_names = get_fields_as_tokenstream(&fields, |model_field| -> String {
let field_html_input_type = get_fields_as_tokenstream(&fields, |model_field| -> String { model_field.html_input_type.to_string() }); model_field.ident.to_string()
let field_list_regex_mask = get_fields_as_tokenstream(&fields, |model_field| -> String { model_field.list_regex_mask.to_string() }); });
let field_select_list = get_fields_as_tokenstream(&fields, |model_field| -> String { model_field.select_list.to_string() }); let field_html_input_type = get_fields_as_tokenstream(&fields, |model_field| -> String {
let is_option_list = get_fields_as_tokenstream(&fields, |model_field| -> bool { model_field.is_option() }); model_field.html_input_type.to_string()
});
let field_list_regex_mask = get_fields_as_tokenstream(&fields, |model_field| -> String {
model_field.list_regex_mask.to_string()
});
let field_select_list = get_fields_as_tokenstream(&fields, |model_field| -> String {
model_field.select_list.to_string()
});
let is_option_list =
get_fields_as_tokenstream(&fields, |model_field| -> bool { model_field.is_option() });
let fields_for_create_model = get_fields_for_create_model(&fields); let fields_for_create_model = get_fields_for_create_model(&fields);
let fields_for_from_model = get_fields_for_from_model(&fields); let fields_for_from_model = get_fields_for_from_model(&fields);
let field_for_primary_key = get_field_for_primary_key(&fields); let field_for_primary_key = get_field_for_primary_key(&fields);
let fields_for_validate_model = get_fields_for_validate_model(&fields); let fields_for_validate_model = get_fields_for_validate_model(&fields);
let fields_type_path = get_fields_as_tokenstream(&fields, |model_field| -> String { model_field.get_type_path_string() }); let fields_type_path = get_fields_as_tokenstream(&fields, |model_field| -> String {
let fields_textarea = get_fields_as_tokenstream(&fields, |model_field| -> bool { model_field.textarea }); model_field.get_type_path_string()
let fields_file_upload = get_fields_as_tokenstream(&fields, |model_field| -> bool { model_field.file_upload }); });
let fields_textarea =
get_fields_as_tokenstream(&fields, |model_field| -> bool { model_field.textarea });
let fields_file_upload =
get_fields_as_tokenstream(&fields, |model_field| -> bool { model_field.file_upload });
let fields_match_name_to_columns = get_match_name_to_column(&fields); let fields_match_name_to_columns = get_match_name_to_column(&fields);
let fields_list_sort_positions = get_fields_as_tokenstream(&fields, |model_field| -> usize { model_field.list_sort_position }); let fields_list_sort_positions = get_fields_as_tokenstream(&fields, |model_field| -> usize {
let fields_list_hide_column = get_fields_as_tokenstream(&fields, |model_field| -> bool { model_field.list_hide_column }); model_field.list_sort_position
});
let fields_list_hide_column = get_fields_as_tokenstream(&fields, |model_field| -> bool {
model_field.list_hide_column
});
let fields_searchable = get_actix_admin_fields_searchable(&fields); let fields_searchable = get_actix_admin_fields_searchable(&fields);
let has_searchable_fields = fields_searchable.len() > 0;
let expanded = quote! { let expanded = quote! {
actix_admin::prelude::lazy_static! { actix_admin::prelude::lazy_static! {
pub static ref ACTIX_ADMIN_VIEWMODEL_FIELDS: Vec<ActixAdminViewModelField> = { pub static ref ACTIX_ADMIN_VIEWMODEL_FIELDS: Vec<ActixAdminViewModelField> = {
let mut vec = Vec::new(); let mut vec = Vec::new();
let field_names = stringify!( let field_names = stringify!(
#(#field_names),* #(#field_names),*
).split(",") ).split(",")
@ -207,9 +229,9 @@ pub fn derive_actix_admin_model(input: proc_macro::TokenStream) -> proc_macro::T
let list_regex_masks = [ let list_regex_masks = [
#(#field_list_regex_mask),* #(#field_list_regex_mask),*
]; ];
for (field_name, html_input_type, select_list, is_option_list, fields_type_path, is_textarea, is_file_upload, list_sort_position, list_hide_column, list_regex_mask) in actix_admin::prelude::izip!(&field_names, &html_input_types, &field_select_lists, is_option_lists, fields_type_paths, fields_textareas, fields_fileupload, list_sort_positions, list_hide_columns, list_regex_masks) { for (field_name, html_input_type, select_list, is_option_list, fields_type_path, is_textarea, is_file_upload, list_sort_position, list_hide_column, list_regex_mask) in actix_admin::prelude::izip!(&field_names, &html_input_types, &field_select_lists, is_option_lists, fields_type_paths, fields_textareas, fields_fileupload, list_sort_positions, list_hide_columns, list_regex_masks) {
let select_list = select_list.replace('"', "").replace(' ', "").to_string(); let select_list = select_list.replace('"', "").replace(' ', "").to_string();
let field_name = field_name.replace('"', "").replace(' ', "").to_string(); let field_name = field_name.replace('"', "").replace(' ', "").to_string();
let html_input_type = html_input_type.replace('"', "").replace(' ', "").to_string(); let html_input_type = html_input_type.replace('"', "").replace(' ', "").to_string();
@ -260,26 +282,28 @@ pub fn derive_actix_admin_model(input: proc_macro::TokenStream) -> proc_macro::T
#[actix_admin::prelude::async_trait] #[actix_admin::prelude::async_trait]
impl ActixAdminModelTrait for Entity { 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<ActixAdminModel>), ActixAdminError> { async fn list_model(db: &DatabaseConnection, page: u64, posts_per_page: u64, search: &str, sort_by: &str, sort_order: &SortOrder) -> Result<(u64, Vec<ActixAdminModel>), ActixAdminError> {
use sea_orm::{ query::* };
let sort_column = match sort_by { let sort_column = match sort_by {
#(#fields_match_name_to_columns)* #(#fields_match_name_to_columns)*
_ => panic!("Unknown column") _ => panic!("Unknown column")
}; };
let query = if sort_order.eq(&SortOrder::Asc) { let mut query = if sort_order.eq(&SortOrder::Asc) {
Entity::find().order_by_asc(sort_column) Entity::find().order_by_asc(sort_column)
} else { } else {
Entity::find().order_by_desc(sort_column) Entity::find().order_by_desc(sort_column)
}; };
let paginator = query if (#has_searchable_fields) {
query = query
.filter( .filter(
Condition::any() Condition::any()
#(#fields_searchable)* #(#fields_searchable)*
) )
.paginate(db, posts_per_page); }
let paginator = query.paginate(db, posts_per_page);
let num_pages = paginator.num_pages().await?; let num_pages = paginator.num_pages().await?;
let mut model_entities = Vec::new(); let mut model_entities = Vec::new();
if (num_pages == 0) { return Ok((num_pages, model_entities)) }; if (num_pages == 0) { return Ok((num_pages, model_entities)) };
let entities = paginator let entities = paginator
@ -290,7 +314,7 @@ pub fn derive_actix_admin_model(input: proc_macro::TokenStream) -> proc_macro::T
ActixAdminModel::from(entity) ActixAdminModel::from(entity)
); );
} }
Ok((num_pages, model_entities)) Ok((num_pages, model_entities))
} }

View File

@ -32,10 +32,10 @@ name = "actix-admin-example"
path = "main.rs" path = "main.rs"
[dependencies] [dependencies]
actix-web = "^4.2.1" actix-web = "^4.3.1"
actix-rt = "2.7.0" actix-rt = "2.7.0"
actix-multipart = "^0.4.0" actix-multipart = "^0.4.0"
sea-orm = { version = "^0.11.1", features = [ "sqlx-sqlite", "runtime-actix-native-tls", "macros" ], default-features = true } sea-orm = { version = "^0.11.3", features = [ "sqlx-sqlite", "runtime-actix-native-tls", "macros" ], default-features = true }
chrono = "0.4.23" chrono = "0.4.23"
tera = "^1.17.1" tera = "^1.17.1"
serde = "^1.0.152" serde = "^1.0.152"

View File

@ -1,10 +1,12 @@
// setup // setup
use sea_orm::sea_query::{ForeignKeyCreateStatement, ColumnDef, TableCreateStatement}; use sea_orm::sea_query::{ForeignKeyCreateStatement, ColumnDef, TableCreateStatement};
use sea_orm::{Set, EntityTrait, error::*, sea_query, ConnectionTrait, DbConn, ExecResult}; use sea_orm::{Set, EntityTrait, error::*, sea_query, ConnectionTrait, DbConn, ExecResult };
pub mod comment; pub mod comment;
pub mod post; pub mod post;
pub mod user;
pub use comment::Entity as Comment; pub use comment::Entity as Comment;
pub use post::Entity as Post; pub use post::Entity as Post;
pub use user::Entity as User;
use chrono::{Local, Duration, DurationRound}; use chrono::{Local, Duration, DurationRound};
use sea_orm::prelude::Decimal; use sea_orm::prelude::Decimal;
@ -61,7 +63,22 @@ pub async fn create_post_table(db: &DbConn) -> Result<ExecResult, DbErr> {
) )
.to_owned(); .to_owned();
let res = create_table(db, &stmt).await; let _res = create_table(db, &stmt).await;
let stmt = sea_query::Table::create()
.table(user::Entity)
.if_not_exists()
.col(
ColumnDef::new(post::Column::Id)
.integer()
.not_null()
.auto_increment()
.primary_key(),
)
.col(ColumnDef::new(user::Column::Name).string().not_null())
.to_owned();
let _res = create_table(db, &stmt).await;
for i in 1..1000 { for i in 1..1000 {
let row = post::ActiveModel { let row = post::ActiveModel {
@ -87,5 +104,13 @@ pub async fn create_post_table(db: &DbConn) -> Result<ExecResult, DbErr> {
let _res = Comment::insert(row).exec(db).await; let _res = Comment::insert(row).exec(db).await;
} }
res 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
} }

View File

@ -0,0 +1,20 @@
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
use actix_admin::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Deserialize, Serialize, DeriveActixAdmin, DeriveActixAdminModel, DeriveActixAdminViewModel)]
#[sea_orm(table_name = "user")]
pub struct Model {
#[sea_orm(primary_key)]
#[serde(skip_deserializing)]
#[actix_admin(primary_key)]
pub id: i32,
pub name: String
}
impl ActiveModelBehavior for ActiveModel {}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActixAdminModelValidationTrait<ActiveModel> for Entity {}

View File

@ -5,7 +5,7 @@ use actix_web::{web, App, HttpServer, middleware};
use sea_orm::{ConnectOptions, DatabaseConnection}; use sea_orm::{ConnectOptions, DatabaseConnection};
use std::time::Duration; use std::time::Duration;
mod entity; mod entity;
use entity::{Post, Comment}; use entity::{Post, Comment, User};
#[derive(Clone)] #[derive(Clone)]
pub struct AppState { pub struct AppState {
@ -33,13 +33,14 @@ fn create_actix_admin_builder() -> ActixAdminBuilder {
let mut admin_builder = ActixAdminBuilder::new(configuration); let mut admin_builder = ActixAdminBuilder::new(configuration);
let post_view_model = ActixAdminViewModel::from(Post); let post_view_model = ActixAdminViewModel::from(Post);
admin_builder.add_entity::<AppState, Post>(&post_view_model); admin_builder.add_entity::<AppState, Post>(&post_view_model);
let some_category = "Groupings"; let some_category = "Group";
let comment_view_model = ActixAdminViewModel::from(Comment); let comment_view_model = ActixAdminViewModel::from(Comment);
admin_builder.add_entity_to_category::<AppState, Comment>(&comment_view_model, some_category); admin_builder.add_entity_to_category::<AppState, Comment>(&comment_view_model, some_category);
let user_view_model = ActixAdminViewModel::from(User);
admin_builder.add_entity_to_category::<AppState, User>(&user_view_model, some_category);
admin_builder admin_builder
} }

View File

@ -39,8 +39,14 @@
} }
} }
function sort_by(column, order) { function sort_by(column) {
document.getElementsByName("sort_order").forEach((e) => e.value = order); console.log(column);
current_sort_order = document.getElementsByName("sort_order")[0].value;
if (current_sort_order == "Asc") {
document.getElementsByName("sort_order").forEach((e) => e.value = "Desc");
} else {
document.getElementsByName("sort_order").forEach((e) => e.value = "Asc");
}
document.getElementsByName("sort_by").forEach((e) => e.value = column); document.getElementsByName("sort_by").forEach((e) => e.value = column);
document.getElementById('table_form').requestSubmit(); document.getElementById('table_form').requestSubmit();
} }

View File

@ -71,63 +71,33 @@
<input type="hidden" name="entities_per_page" value="{{ entities_per_page }}"> <input type="hidden" name="entities_per_page" value="{{ entities_per_page }}">
<input type="hidden" name="search" value="{{ search }}"> <input type="hidden" name="search" value="{{ search }}">
<input type="hidden" name="page" value="{{ page }}"> <input type="hidden" name="page" value="{{ page }}">
<table class="table is-relative is-fullwidth is-hoverable is-striped"> <table class="table is-relative is-narrow is-fullwidth is-hoverable is-striped">
<thead> <thead>
<tr> <tr>
<th> <th>
<input type="checkbox" onclick="checkAll(this)"> <input type="checkbox" onclick="checkAll(this)">
</th> </th>
<th> <th onclick="sort_by('{{ view_model.primary_key }}');" class="is-clickable">{{
<div class="dropdown is-hoverable"> view_model.primary_key | title }}
<div class="dropdown-trigger"> {% if sort_by == view_model.primary_key %}
<div class="navbar-item has-dropdown" aria-haspopup="true" {% if sort_order == "Asc" %}
aria-controls="dropdown-menu4"> <i class="ml-1 fa-solid fa-caret-up"></i>
<a class="navbar-link"> {% elif sort_order == "Desc" %}
<span>{{ view_model.primary_key | split(pat="_") | join(sep=" ") | title <i class="ml-1 fa-solid fa-caret-down"></i>
}}</span> {% endif %}
</a> {% endif %}
</div>
</div>
<div class="dropdown-menu" id="dropdown-menu4" role="menu">
<div class="dropdown-content">
<div class="dropdown-item is-clickable"
onclick="sort_by('{{ view_model.primary_key }}', 'Desc');">
Sort by Desc <i class="ml-1 fa-solid fa-caret-down"></i>
</div>
<div class="dropdown-item is-clickable"
onclick="sort_by('{{ view_model.primary_key }}', 'Asc');">
Sort by Asc <i class="ml-1 fa-solid fa-caret-up"></i>
</div>
</div>
</div>
</div>
</th> </th>
{% for model_field in view_model.fields | filter(attribute="list_hide_column", value=false) | {% for model_field in view_model.fields | filter(attribute="list_hide_column", value=false) |
sort(attribute="list_sort_position") -%} sort(attribute="list_sort_position") -%}
<th> <th onclick="sort_by('{{ model_field.field_name }}');" class="is-clickable">{{
<div class="dropdown is-hoverable"> model_field.field_name | split(pat="_") | join(sep=" ") | title }}
<div class="dropdown-trigger"> {% if sort_by == model_field.field_name %}
<div class="navbar-item has-dropdown" aria-haspopup="true" {% if sort_order == "Asc" %}
aria-controls="dropdown-menu4"> <i class="ml-1 fa-solid fa-caret-up"></i>
<a class="navbar-link"> {% elif sort_order == "Desc" %}
<span>{{ model_field.field_name | split(pat="_") | join(sep=" ") | title <i class="ml-1 fa-solid fa-caret-down"></i>
}}</span> {% endif %}
</a> {% endif %}
</div>
</div>
<div class="dropdown-menu" id="dropdown-menu4" role="menu">
<div class="dropdown-content">
<div class="dropdown-item is-clickable"
onclick="sort_by('{{ model_field.field_name }}', 'Desc');">
Sort by Desc <i class="ml-1 fa-solid fa-caret-down"></i>
</div>
<div class="dropdown-item is-clickable"
onclick="sort_by('{{ model_field.field_name }}', 'Asc');">
Sort by Asc <i class="ml-1 fa-solid fa-caret-up"></i>
</div>
</div>
</div>
</div>
</th> </th>
{%- endfor %} {%- endfor %}
<th> <th>
@ -164,7 +134,7 @@
<td>{{ entity.values | get(key=model_field.field_name) }}</td> <td>{{ entity.values | get(key=model_field.field_name) }}</td>
{% endif %} {% endif %}
{%- endfor %} {%- endfor %}
<td> <td class="has-text-right">
<a hx-target="body" href="/admin/{{ entity_name }}/edit/{{ entity.primary_key }}" hx-vals='{ <a hx-target="body" href="/admin/{{ entity_name }}/edit/{{ entity.primary_key }}" hx-vals='{
"page" : "{{ page }}", "page" : "{{ page }}",
"entities_per_page" : "{{ entities_per_page }}", "entities_per_page" : "{{ entities_per_page }}",