From a600ff5c9cfaeb700965a7d90020cfcd234d29b9 Mon Sep 17 00:00:00 2001 From: Manuel Gugger Date: Thu, 29 Dec 2022 19:56:20 +0100 Subject: [PATCH] implement file_upload field --- .gitignore | 5 +- Cargo.toml | 18 ++- actix_admin_macros/src/attributes.rs | 4 +- actix_admin_macros/src/lib.rs | 9 +- actix_admin_macros/src/model_fields.rs | 5 +- actix_admin_macros/src/struct_fields.rs | 69 ++++++--- .../.env.example | 1 + .../azure_auth/Cargo.toml | 0 .../azure_auth/src/lib.rs | 0 .../src => azure_auth}/entity/comment.rs | 0 .../{basic/src => azure_auth}/entity/mod.rs | 0 .../{basic/src => azure_auth}/entity/post.rs | 0 .../src => azure_auth}/main.rs | 6 +- .../templates/custom_handler.html | 0 .../templates/custom_index.html | 0 .../templates/example_base.html | 0 .../templates/index.html | 0 examples/basic/Cargo.toml | 12 -- .../src => basic}/entity/comment.rs | 0 .../src => basic}/entity/mod.rs | 1 + .../src => basic}/entity/post.rs | 4 +- examples/basic/{src => }/main.rs | 3 +- examples/basic/templates/index.html | 15 -- examples/with_azure_auth/Cargo.toml | 17 --- examples/with_azure_auth/README.md | 1 - src/builder.rs | 9 +- src/lib.rs | 4 +- src/model.rs | 133 +++++++++++++----- src/routes/create_or_edit_get.rs | 2 +- src/routes/create_or_edit_post.rs | 56 +++++--- src/routes/delete.rs | 40 ++++-- src/routes/mod.rs | 5 +- src/routes/show.rs | 21 +-- src/routes/static_content.rs | 85 +++++++++++ src/view_model.rs | 14 +- templates/create_or_edit.html | 2 +- templates/form_elements/input.html | 30 ++-- templates/list.html | 4 +- templates/show.html | 4 +- tests/test_setup/helper.rs | 7 +- 40 files changed, 399 insertions(+), 187 deletions(-) rename examples/{with_azure_auth => azure_auth}/.env.example (60%) rename examples/{with_azure_auth => azure_auth}/azure_auth/Cargo.toml (100%) rename examples/{with_azure_auth => azure_auth}/azure_auth/src/lib.rs (100%) rename examples/{basic/src => azure_auth}/entity/comment.rs (100%) rename examples/{basic/src => azure_auth}/entity/mod.rs (100%) rename examples/{basic/src => azure_auth}/entity/post.rs (100%) rename examples/{with_azure_auth/src => azure_auth}/main.rs (96%) rename examples/{with_azure_auth => azure_auth}/templates/custom_handler.html (100%) rename examples/{with_azure_auth => azure_auth}/templates/custom_index.html (100%) rename examples/{with_azure_auth => azure_auth}/templates/example_base.html (100%) rename examples/{with_azure_auth => azure_auth}/templates/index.html (100%) delete mode 100644 examples/basic/Cargo.toml rename examples/{with_azure_auth/src => basic}/entity/comment.rs (100%) rename examples/{with_azure_auth/src => basic}/entity/mod.rs (97%) rename examples/{with_azure_auth/src => basic}/entity/post.rs (95%) rename examples/basic/{src => }/main.rs (96%) delete mode 100644 examples/basic/templates/index.html delete mode 100644 examples/with_azure_auth/Cargo.toml delete mode 100644 examples/with_azure_auth/README.md create mode 100644 src/routes/static_content.rs diff --git a/.gitignore b/.gitignore index 3729aaf..378983e 100644 --- a/.gitignore +++ b/.gitignore @@ -26,4 +26,7 @@ _site/ .jekyll-metadata # Ignore folders generated by Bundler .bundle/ -vendor/ \ No newline at end of file +vendor/ + +# Ignore File Uploads +file_uploads/ \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index df2d75a..79ac9a5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,18 +6,21 @@ version = "0.2.0" repository = "https://github.com/mgugger/actix-admin" edition = "2021" exclude = [ - "example/*", + "examples/*", "actix_admin_macros/*", "tests/*", - "README.md", - "static/*", - "azure_auth/*", + "README.md" ] +[lib] +name = "actix_admin" +path = "src/lib.rs" + [dependencies] actix-web = "^4.0.1" actix-session = { version = "^0.7.1", features = [] } actix-multipart = "^0.4.0" +actix-files = "^0.6.2" futures-util = "0.3.21" chrono = "0.4.20" tera = "^1.16.0" @@ -31,4 +34,9 @@ actix-admin-macros = { version = "0.2.0", path = "actix_admin_macros" } derive_more = "0.99.17" [dev-dependencies] -sea-orm = { version = "^0.9.1", features = [ "sqlx-sqlite", "runtime-actix-native-tls", "macros" ], default-features = true } \ No newline at end of file +sea-orm = { version = "^0.9.1", features = [ "sqlx-sqlite", "runtime-actix-native-tls", "macros" ], default-features = true } +actix-rt = "2.7.0" +azure_auth = { path = "./examples/azure_auth/azure_auth" } +oauth2 = "4.1" +dotenv = "0.15" +actix-session = { version = "0.7.1", features = ["cookie-session"] } \ No newline at end of file diff --git a/actix_admin_macros/src/attributes.rs b/actix_admin_macros/src/attributes.rs index 7e90346..1fbdd49 100644 --- a/actix_admin_macros/src/attributes.rs +++ b/actix_admin_macros/src/attributes.rs @@ -14,7 +14,9 @@ pub mod derive_attr { pub html_input_type: Option, pub select_list: Option, pub searchable: Option<()>, - pub textarea: Option<()> + pub textarea: Option<()>, + pub file_upload: Option<()>, + pub not_empty: Option<()> //pub inner_type: Option, // Anything that implements `syn::parse::Parse` is supported. diff --git a/actix_admin_macros/src/lib.rs b/actix_admin_macros/src/lib.rs index 22f216e..92dbd05 100644 --- a/actix_admin_macros/src/lib.rs +++ b/actix_admin_macros/src/lib.rs @@ -154,6 +154,7 @@ pub fn derive_actix_admin_model(input: proc_macro::TokenStream) -> proc_macro::T let fields_searchable = get_actix_admin_fields_searchable(&fields); let fields_type_path = get_actix_admin_fields_type_path_string(&fields); let fields_textarea = get_actix_admin_fields_textarea(&fields); + let fields_file_upload = get_actix_admin_fields_file_upload(&fields); let expanded = quote! { actix_admin::prelude::lazy_static! { @@ -187,7 +188,11 @@ pub fn derive_actix_admin_model(input: proc_macro::TokenStream) -> proc_macro::T #(#fields_textarea),* ]; - for (field_name, html_input_type, select_list, is_option_list, fields_type_path, is_textarea) in actix_admin::prelude::izip!(&field_names, &html_input_types, &field_select_lists, is_option_lists, fields_type_paths, fields_textareas) { + let fields_fileupload = [ + #(#fields_file_upload),* + ]; + + for (field_name, html_input_type, select_list, is_option_list, fields_type_path, is_textarea, is_file_upload) in actix_admin::prelude::izip!(&field_names, &html_input_types, &field_select_lists, is_option_lists, fields_type_paths, fields_textareas, fields_fileupload) { let select_list = select_list.replace('"', "").replace(' ', "").to_string(); let field_name = field_name.replace('"', "").replace(' ', "").to_string(); @@ -198,7 +203,7 @@ pub fn derive_actix_admin_model(input: proc_macro::TokenStream) -> proc_macro::T html_input_type: html_input_type, select_list: select_list.clone(), is_option: is_option_list, - field_type: ActixAdminViewModelFieldType::get_field_type(fields_type_path, select_list, is_textarea) + field_type: ActixAdminViewModelFieldType::get_field_type(fields_type_path, select_list, is_textarea, is_file_upload) }); } vec diff --git a/actix_admin_macros/src/model_fields.rs b/actix_admin_macros/src/model_fields.rs index 0fc4c0c..a52578a 100644 --- a/actix_admin_macros/src/model_fields.rs +++ b/actix_admin_macros/src/model_fields.rs @@ -7,13 +7,14 @@ pub struct ModelField { pub visibility: Visibility, pub ident: proc_macro2::Ident, pub ty: Type, - // struct field is option<> pub inner_type: Option, pub primary_key: bool, pub html_input_type: String, pub select_list: String, pub searchable: bool, - pub textarea: bool + pub textarea: bool, + pub file_upload: bool, + pub not_empty: bool } impl ModelField { diff --git a/actix_admin_macros/src/struct_fields.rs b/actix_admin_macros/src/struct_fields.rs index 848004b..2f81bc4 100644 --- a/actix_admin_macros/src/struct_fields.rs +++ b/actix_admin_macros/src/struct_fields.rs @@ -41,6 +41,12 @@ pub fn filter_fields(fields: &Fields) -> Vec { let is_textarea = actix_admin_attr .clone() .map_or(false, |attr| attr.textarea.is_some()); + let is_file_upload = actix_admin_attr + .clone() + .map_or(false, |attr| attr.file_upload.is_some()); + let is_not_empty = actix_admin_attr + .clone() + .map_or(false, |attr| attr.not_empty.is_some()); let select_list = actix_admin_attr.clone().map_or("".to_string(), |attr| { attr.select_list.map_or("".to_string(), |attr_field| { (LitStr::from(attr_field)).value() @@ -62,7 +68,9 @@ pub fn filter_fields(fields: &Fields) -> Vec { html_input_type: html_input_type, select_list: select_list, searchable: is_searchable, - textarea: is_textarea + textarea: is_textarea, + file_upload: is_file_upload, + not_empty: is_not_empty }; Some(model_field) } else { @@ -186,6 +194,20 @@ pub fn get_actix_admin_fields_textarea(fields: &Vec) -> Vec>() } +pub fn get_actix_admin_fields_file_upload(fields: &Vec) -> Vec { + fields + .iter() + .filter(|model_field| !model_field.primary_key) + .map(|model_field| { + let is_fileupload = model_field.file_upload; + + quote! { + #is_fileupload + } + }) + .collect::>() +} + pub fn get_actix_admin_fields_searchable(fields: &Vec) -> Vec { fields .iter() @@ -275,33 +297,34 @@ pub fn get_fields_for_validate_model(fields: &Vec) -> Vec { quote! { - model.get_datetime(#ident_name, #is_option_or_string).map_err(|err| errors.insert(#ident_name.to_string(), err)).ok() + model.get_datetime(#ident_name, #is_option_or_string, #is_allowed_to_be_empty).map_err(|err| errors.insert(#ident_name.to_string(), err)).ok() } }, (_, "Date") => { quote! { - model.get_date(#ident_name, #is_option_or_string).map_err(|err| errors.insert(#ident_name.to_string(), err)).ok() + model.get_date(#ident_name, #is_option_or_string, #is_allowed_to_be_empty).map_err(|err| errors.insert(#ident_name.to_string(), err)).ok() } }, (_, "bool") => { quote! { - model.get_bool(#ident_name, #is_option_or_string).map_err(|err| errors.insert(#ident_name.to_string(), err)).ok() + model.get_bool(#ident_name, #is_option_or_string, #is_allowed_to_be_empty).map_err(|err| errors.insert(#ident_name.to_string(), err)).ok() } }, // generic (true, _) => { let inner_ty = model_field.inner_type.to_owned().unwrap(); quote! { - model.get_value::<#inner_ty>(#ident_name, #is_option_or_string).map_err(|err| errors.insert(#ident_name.to_string(), err)).ok() + model.get_value::<#inner_ty>(#ident_name, #is_option_or_string, #is_allowed_to_be_empty).map_err(|err| errors.insert(#ident_name.to_string(), err)).ok() } }, (false, _) => { quote! { - model.get_value::<#ty>(#ident_name, #is_option_or_string).map_err(|err| errors.insert(#ident_name.to_string(), err)).ok() + model.get_value::<#ty>(#ident_name, #is_option_or_string, #is_allowed_to_be_empty).map_err(|err| errors.insert(#ident_name.to_string(), err)).ok() } } }; @@ -323,51 +346,52 @@ pub fn get_fields_for_create_model(fields: &Vec) -> Vec let type_path = model_field.get_type_path_string(); let is_option_or_string = model_field.is_option() || model_field.is_string(); + let is_allowed_to_be_empty = !model_field.not_empty; let res = match (model_field.is_option(), model_field.is_string(), type_path.as_str()) { // is DateTime (true , _, "DateTime") => { quote! { - #ident: Set(model.get_datetime(#ident_name, #is_option_or_string).unwrap()) + #ident: Set(model.get_datetime(#ident_name, #is_option_or_string, #is_allowed_to_be_empty).unwrap()) } }, (false , _, "DateTime") => { quote! { - #ident: Set(model.get_datetime(#ident_name, #is_option_or_string).unwrap().unwrap()) + #ident: Set(model.get_datetime(#ident_name, #is_option_or_string, #is_allowed_to_be_empty).unwrap().unwrap()) } }, (true , _, "Date") => { quote! { - #ident: Set(model.get_date(#ident_name, #is_option_or_string).unwrap()) + #ident: Set(model.get_date(#ident_name, #is_option_or_string, #is_allowed_to_be_empty).unwrap()) } }, (false , _, "Date") => { quote! { - #ident: Set(model.get_date(#ident_name, #is_option_or_string).unwrap().unwrap()) + #ident: Set(model.get_date(#ident_name, #is_option_or_string, #is_allowed_to_be_empty).unwrap().unwrap()) } }, (_ , _, "bool") => { quote! { - #ident: Set(model.get_bool(#ident_name, #is_option_or_string).unwrap().unwrap()) + #ident: Set(model.get_bool(#ident_name, #is_option_or_string, #is_allowed_to_be_empty).unwrap().unwrap()) } }, // Default fields (true, _, _) => { let inner_ty = model_field.inner_type.to_owned().unwrap(); quote! { - #ident: Set(model.get_value::<#inner_ty>(#ident_name, #is_option_or_string).unwrap()) + #ident: Set(model.get_value::<#inner_ty>(#ident_name, #is_option_or_string, #is_allowed_to_be_empty).unwrap()) } }, // is string which can be empty (false, true, _) => { quote! { - #ident: Set(model.get_value::<#ty>(#ident_name, #is_option_or_string).unwrap().unwrap_or(String::new())) + #ident: Set(model.get_value::<#ty>(#ident_name, #is_option_or_string, #is_allowed_to_be_empty).unwrap().unwrap_or(String::new())) } }, // no string (false, false, _) => { quote! { - #ident: Set(model.get_value::<#ty>(#ident_name, #is_option_or_string).unwrap().unwrap()) + #ident: Set(model.get_value::<#ty>(#ident_name, #is_option_or_string, #is_allowed_to_be_empty).unwrap().unwrap()) } } }; @@ -389,47 +413,48 @@ pub fn get_fields_for_edit_model(fields: &Vec) -> Vec { let type_path = model_field.get_type_path_string(); let is_option_or_string = model_field.is_option() || model_field.is_string(); + let is_allowed_to_be_empty = !model_field.not_empty; let res = match (model_field.is_option(), model_field.is_string(), type_path.as_str()) { (_, _, "bool") => { quote! { - entity.#ident = Set(model.get_bool(#ident_name, #is_option_or_string).unwrap().unwrap()) + entity.#ident = Set(model.get_bool(#ident_name, #is_option_or_string, #is_allowed_to_be_empty).unwrap().unwrap()) } }, (true , _, "DateTime") => { quote! { - entity.#ident = Set(model.get_datetime(#ident_name, #is_option_or_string).unwrap()) + entity.#ident = Set(model.get_datetime(#ident_name, #is_option_or_string, #is_allowed_to_be_empty).unwrap()) } }, (false , _, "DateTime") => { quote! { - entity.#ident = Set(model.get_datetime(#ident_name, #is_option_or_string).unwrap().unwrap()) + entity.#ident = Set(model.get_datetime(#ident_name, #is_option_or_string, #is_allowed_to_be_empty).unwrap().unwrap()) } }, (true , _, "Date") => { quote! { - entity.#ident = Set(model.get_date(#ident_name, #is_option_or_string).unwrap()) + entity.#ident = Set(model.get_date(#ident_name, #is_option_or_string, #is_allowed_to_be_empty).unwrap()) } }, (false , _, "Date") => { quote! { - entity.#ident = Set(model.get_date(#ident_name, #is_option_or_string).unwrap().unwrap()) + entity.#ident = Set(model.get_date(#ident_name, #is_option_or_string, #is_allowed_to_be_empty).unwrap().unwrap()) } }, (true, _, _) => { let inner_ty = model_field.inner_type.to_owned().unwrap(); quote! { - entity.#ident = Set(model.get_value::<#inner_ty>(#ident_name, #is_option_or_string).unwrap()) + entity.#ident = Set(model.get_value::<#inner_ty>(#ident_name, #is_option_or_string, #is_allowed_to_be_empty).unwrap()) } }, (false, true, _) => { quote! { - entity.#ident = Set(model.get_value::<#ty>(#ident_name, #is_option_or_string).unwrap().unwrap_or(String::new())) + entity.#ident = Set(model.get_value::<#ty>(#ident_name, #is_option_or_string, #is_allowed_to_be_empty).unwrap().unwrap_or(String::new())) } }, (false, false, _) => { quote! { - entity.#ident = Set(model.get_value::<#ty>(#ident_name, #is_option_or_string).unwrap().unwrap()) + entity.#ident = Set(model.get_value::<#ty>(#ident_name, #is_option_or_string, #is_allowed_to_be_empty).unwrap().unwrap()) } } }; diff --git a/examples/with_azure_auth/.env.example b/examples/azure_auth/.env.example similarity index 60% rename from examples/with_azure_auth/.env.example rename to examples/azure_auth/.env.example index 0dc0d1e..0bc55f8 100644 --- a/examples/with_azure_auth/.env.example +++ b/examples/azure_auth/.env.example @@ -1,3 +1,4 @@ +# Create a .env file in the same folder and add variables below with correct oauth credentials OAUTH2_CLIENT_SECRET= "TODO" OAUTH2_CLIENT_ID= "TODO" OAUTH2_SERVER= "login.microsoftonline.com/a5f5xxxx-xxxx-414a-8463-xxxxxxxxxxxxx(tenantId)" \ No newline at end of file diff --git a/examples/with_azure_auth/azure_auth/Cargo.toml b/examples/azure_auth/azure_auth/Cargo.toml similarity index 100% rename from examples/with_azure_auth/azure_auth/Cargo.toml rename to examples/azure_auth/azure_auth/Cargo.toml diff --git a/examples/with_azure_auth/azure_auth/src/lib.rs b/examples/azure_auth/azure_auth/src/lib.rs similarity index 100% rename from examples/with_azure_auth/azure_auth/src/lib.rs rename to examples/azure_auth/azure_auth/src/lib.rs diff --git a/examples/basic/src/entity/comment.rs b/examples/azure_auth/entity/comment.rs similarity index 100% rename from examples/basic/src/entity/comment.rs rename to examples/azure_auth/entity/comment.rs diff --git a/examples/basic/src/entity/mod.rs b/examples/azure_auth/entity/mod.rs similarity index 100% rename from examples/basic/src/entity/mod.rs rename to examples/azure_auth/entity/mod.rs diff --git a/examples/basic/src/entity/post.rs b/examples/azure_auth/entity/post.rs similarity index 100% rename from examples/basic/src/entity/post.rs rename to examples/azure_auth/entity/post.rs diff --git a/examples/with_azure_auth/src/main.rs b/examples/azure_auth/main.rs similarity index 96% rename from examples/with_azure_auth/src/main.rs rename to examples/azure_auth/main.rs index 9e6742d..4c15470 100644 --- a/examples/with_azure_auth/src/main.rs +++ b/examples/azure_auth/main.rs @@ -109,7 +109,8 @@ fn create_actix_admin_builder() -> ActixAdminBuilder { user_info.is_some() }), login_link: Some("/azure-auth/login".to_string()), - logout_link: Some("/azure-auth/logout".to_string()) + logout_link: Some("/azure-auth/logout".to_string()), + file_upload_directory: "./file_uploads" }; let mut admin_builder = ActixAdminBuilder::new(configuration); @@ -135,7 +136,8 @@ fn create_actix_admin_builder() -> ActixAdminBuilder { #[actix_rt::main] async fn main() { - dotenv::dotenv().ok(); + dotenv::from_filename("./examples/azure_auth/.env.example").ok(); + dotenv::from_filename("./examples/azure_auth/.env").ok(); let oauth2_client_id = env::var("OAUTH2_CLIENT_ID").expect("Missing the OAUTH2_CLIENT_ID environment variable."); let oauth2_client_secret = env::var("OAUTH2_CLIENT_SECRET").expect("Missing the OAUTH2_CLIENT_SECRET environment variable."); diff --git a/examples/with_azure_auth/templates/custom_handler.html b/examples/azure_auth/templates/custom_handler.html similarity index 100% rename from examples/with_azure_auth/templates/custom_handler.html rename to examples/azure_auth/templates/custom_handler.html diff --git a/examples/with_azure_auth/templates/custom_index.html b/examples/azure_auth/templates/custom_index.html similarity index 100% rename from examples/with_azure_auth/templates/custom_index.html rename to examples/azure_auth/templates/custom_index.html diff --git a/examples/with_azure_auth/templates/example_base.html b/examples/azure_auth/templates/example_base.html similarity index 100% rename from examples/with_azure_auth/templates/example_base.html rename to examples/azure_auth/templates/example_base.html diff --git a/examples/with_azure_auth/templates/index.html b/examples/azure_auth/templates/index.html similarity index 100% rename from examples/with_azure_auth/templates/index.html rename to examples/azure_auth/templates/index.html diff --git a/examples/basic/Cargo.toml b/examples/basic/Cargo.toml deleted file mode 100644 index 0067f30..0000000 --- a/examples/basic/Cargo.toml +++ /dev/null @@ -1,12 +0,0 @@ -[package] -name = "actix-admin-example" -version = "0.1.0" -edition = "2021" - -[dependencies] -actix-web = "4.0.1" -actix-rt = "2.7.0" -serde = "1.0.136" -serde_derive = "1.0.136" -sea-orm = { version = "^0.9.1", features = [ "sqlx-sqlite", "runtime-actix-native-tls", "macros" ], default-features = true } -actix-admin = { path = "../../" } \ No newline at end of file diff --git a/examples/with_azure_auth/src/entity/comment.rs b/examples/basic/entity/comment.rs similarity index 100% rename from examples/with_azure_auth/src/entity/comment.rs rename to examples/basic/entity/comment.rs diff --git a/examples/with_azure_auth/src/entity/mod.rs b/examples/basic/entity/mod.rs similarity index 97% rename from examples/with_azure_auth/src/entity/mod.rs rename to examples/basic/entity/mod.rs index 1f2bdef..4cfff87 100644 --- a/examples/with_azure_auth/src/entity/mod.rs +++ b/examples/basic/entity/mod.rs @@ -28,6 +28,7 @@ pub async fn create_post_table(db: &DbConn) -> Result { .col(ColumnDef::new(post::Column::TeaMandatory).string().not_null()) .col(ColumnDef::new(post::Column::TeaOptional).string()) .col(ColumnDef::new(post::Column::InsertDate).date()) + .col(ColumnDef::new(post::Column::Attachment).string()) .to_owned(); let _result = create_table(db, &stmt).await; diff --git a/examples/with_azure_auth/src/entity/post.rs b/examples/basic/entity/post.rs similarity index 95% rename from examples/with_azure_auth/src/entity/post.rs rename to examples/basic/entity/post.rs index 98f911e..ce0bf2d 100644 --- a/examples/with_azure_auth/src/entity/post.rs +++ b/examples/basic/entity/post.rs @@ -12,7 +12,7 @@ pub struct Model { #[serde(skip_deserializing)] #[actix_admin(primary_key)] pub id: i32, - #[actix_admin(searchable)] + #[actix_admin(searchable, not_empty)] pub title: String, #[sea_orm(column_type = "Text")] #[actix_admin(searchable, textarea)] @@ -22,6 +22,8 @@ pub struct Model { #[actix_admin(select_list="Tea")] pub tea_optional: Option, pub insert_date: Date, + #[actix_admin(file_upload)] + pub attachment: String } impl Display for Model { diff --git a/examples/basic/src/main.rs b/examples/basic/main.rs similarity index 96% rename from examples/basic/src/main.rs rename to examples/basic/main.rs index 66cff84..befa0a4 100644 --- a/examples/basic/src/main.rs +++ b/examples/basic/main.rs @@ -31,7 +31,8 @@ fn create_actix_admin_builder() -> ActixAdminBuilder { enable_auth: false, user_is_logged_in: None, login_link: None, - logout_link: None + logout_link: None, + file_upload_directory: "./file_uploads" }; let mut admin_builder = ActixAdminBuilder::new(configuration); diff --git a/examples/basic/templates/index.html b/examples/basic/templates/index.html deleted file mode 100644 index 8ba2199..0000000 --- a/examples/basic/templates/index.html +++ /dev/null @@ -1,15 +0,0 @@ - - - - - Actix Admin Example - - - - - - - - \ No newline at end of file diff --git a/examples/with_azure_auth/Cargo.toml b/examples/with_azure_auth/Cargo.toml deleted file mode 100644 index 9ee5420..0000000 --- a/examples/with_azure_auth/Cargo.toml +++ /dev/null @@ -1,17 +0,0 @@ -[package] -name = "actix-admin-example" -version = "0.1.0" -edition = "2021" - -[dependencies] -actix-web = "4.0.1" -actix-rt = "2.7.0" -actix-session = { version = "0.7.1", features = ["cookie-session"] } -tera = "1.15.0" -oauth2 = "4.1" -dotenv = "0.15" -serde = "1.0.136" -serde_derive = "1.0.136" -sea-orm = { version = "^0.9.1", features = [ "sqlx-sqlite", "runtime-actix-native-tls", "macros" ], default-features = true } -actix-admin = { path = "../../" } -azure_auth = { path = "./azure_auth" } \ No newline at end of file diff --git a/examples/with_azure_auth/README.md b/examples/with_azure_auth/README.md deleted file mode 100644 index 2b743ea..0000000 --- a/examples/with_azure_auth/README.md +++ /dev/null @@ -1 +0,0 @@ -Rename .env.example to .env and update the oauth client credentials \ No newline at end of file diff --git a/src/builder.rs b/src/builder.rs index 95a70a9..7a96474 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -1,9 +1,10 @@ -use crate::{prelude::*, ActixAdminMenuElement}; +use crate::{prelude::*, ActixAdminMenuElement, routes::delete_static_content}; use actix_web::{web, Route}; use std::collections::HashMap; +use std::fs; use crate::routes::{ - create_get, create_post, delete, delete_many, edit_get, edit_post, index, list, not_found, show, + create_get, create_post, delete, delete_many, edit_get, edit_post, index, list, not_found, show, download }; /// Represents a builder entity which helps generating the ActixAdmin configuration @@ -102,9 +103,13 @@ impl ActixAdminBuilderTrait for ActixAdminBuilder { .route("/delete", web::delete().to(delete_many::)) .route("/delete/{id}", web::delete().to(delete::)) .route("/show/{id}", web::get().to(show::)) + .route("/static_content/{id}/{column_name}", web::get().to(download::)) + .route("/static_content/{id}/{column_name}", web::delete().to(delete_static_content::)) .default_service(web::to(not_found)), ); + fs::create_dir_all(format!("{}/{}", &self.actix_admin.configuration.file_upload_directory, E::get_entity_name())).unwrap(); + let category = self.actix_admin.entity_names.get_mut(category_name); let menu_element = ActixAdminMenuElement { name: E::get_entity_name(), diff --git a/src/lib.rs b/src/lib.rs index 18a1079..cc58f70 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -96,6 +96,7 @@ pub fn get_html_input_type(value: &tera::Value, _: &HashMap "datetime-local", ActixAdminViewModelFieldType::Date => "date", ActixAdminViewModelFieldType::Checkbox => "checkbox", + ActixAdminViewModelFieldType::FileUpload => "file", _ => "text" }; @@ -120,7 +121,8 @@ pub struct ActixAdminConfiguration { pub enable_auth: bool, pub user_is_logged_in: Option fn(&'a Session) -> bool>, pub login_link: Option, - pub logout_link: Option + pub logout_link: Option, + pub file_upload_directory: &'static str } #[derive(Clone)] diff --git a/src/model.rs b/src/model.rs index 274826e..acfdcb6 100644 --- a/src/model.rs +++ b/src/model.rs @@ -1,11 +1,15 @@ -use crate::{ ActixAdminViewModelField, ActixAdminError}; +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 serde::Serialize; use std::collections::HashMap; -use actix_multipart:: {Multipart, MultipartError} ; -use futures_util::stream::StreamExt as _; -use chrono::{NaiveDateTime, NaiveDate}; +use std::fs::File; +use std::io::Write; +use std::time::{SystemTime, UNIX_EPOCH}; #[async_trait] pub trait ActixAdminModelTrait { @@ -13,9 +17,9 @@ pub trait ActixAdminModelTrait { db: &DatabaseConnection, page: usize, posts_per_page: usize, - search: &String + search: &String, ) -> Result<(usize, Vec), ActixAdminError>; - fn get_fields() -> &'static[ActixAdminViewModelField]; + fn get_fields() -> &'static [ActixAdminViewModelField]; fn validate_model(model: &mut ActixAdminModel); } @@ -30,10 +34,9 @@ pub struct ActixAdminModel { pub primary_key: Option, pub values: HashMap, pub errors: HashMap, - pub custom_errors: HashMap + pub custom_errors: HashMap, } - impl ActixAdminModel { pub fn create_empty() -> ActixAdminModel { ActixAdminModel { @@ -44,21 +47,47 @@ impl ActixAdminModel { } } - pub async fn create_from_payload(mut payload: Multipart) -> Result { + pub async fn create_from_payload( + mut payload: Multipart, file_upload_folder: &str + ) -> Result { let mut hashmap = HashMap::::new(); - + while let Some(item) = payload.next().await { let mut field = item?; - - // TODO: how to handle binary chunks? + + let mut binary_data: Vec = Vec::new(); while let Some(chunk) = field.next().await { + binary_data.push(chunk.unwrap()); //println!("-- CHUNK: \n{:?}", String::from_utf8(chunk.map_or(Vec::new(), |c| c.to_vec()))); - let res_string = String::from_utf8(chunk.map_or(Vec::new(), |c| c.to_vec())); + // let res_string = String::from_utf8(chunk.map_or(Vec::new(), |c| c.to_vec())); + } + let binary_data = binary_data.concat(); + if field.content_disposition().get_filename().is_some() { + let mut filename = field + .content_disposition() + .get_filename() + .unwrap() + .to_string(); + + let mut file_path = format!("{}/{}", file_upload_folder, filename); + let file_exists = std::path::Path::new(&file_path).exists(); + // Avoid overwriting existing files + if file_exists { + filename = format!("{}_{}", SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(), filename); + file_path = format!("{}/{}", file_upload_folder, filename); + } + + let file = File::create(file_path); + let _res = file.unwrap().write_all(&binary_data); + + hashmap.insert( + field.name().to_string(), + filename.clone() + ); + } else { + let res_string = String::from_utf8(binary_data); if res_string.is_ok() { - hashmap.insert( - field.name().to_string(), - res_string.unwrap() - ); + hashmap.insert(field.name().to_string(), res_string.unwrap()); } } } @@ -71,35 +100,68 @@ impl ActixAdminModel { }) } - pub fn get_value(&self, key: &str, is_option_or_string: bool) -> Result, String> { - self.get_value_by_closure(key, is_option_or_string, |val| val.parse::()) + pub fn get_value( + &self, + key: &str, + is_option_or_string: bool, + is_allowed_to_be_empty: bool + ) -> Result, String> { + self.get_value_by_closure(key, is_option_or_string, is_allowed_to_be_empty, |val| val.parse::()) } - pub fn get_datetime(&self, key: &str, is_option_or_string: bool) -> Result, String> { - self.get_value_by_closure(key, is_option_or_string, |val| NaiveDateTime::parse_from_str(val, "%Y-%m-%dT%H:%M")) + pub fn get_datetime( + &self, + key: &str, + is_option_or_string: bool, + is_allowed_to_be_empty: bool + ) -> Result, String> { + self.get_value_by_closure(key, is_option_or_string, is_allowed_to_be_empty, |val| { + NaiveDateTime::parse_from_str(val, "%Y-%m-%dT%H:%M") + }) } - pub fn get_date(&self, key: &str, is_option_or_string: bool) -> Result, String> { - self.get_value_by_closure(key, is_option_or_string, |val| NaiveDate::parse_from_str(val, "%Y-%m-%d")) + pub fn get_date( + &self, + key: &str, + is_option_or_string: bool, + is_allowed_to_be_empty: bool + ) -> Result, String> { + self.get_value_by_closure(key, is_option_or_string, is_allowed_to_be_empty, |val| { + NaiveDate::parse_from_str(val, "%Y-%m-%d") + }) } - pub fn get_bool(&self, key: &str, is_option_or_string: bool) -> Result, String> { - let val = self.get_value_by_closure(key, is_option_or_string, |val| if !val.is_empty() && (val == "true" || val == "yes") { Ok(true) } else { Ok(false) }); + pub fn get_bool(&self, key: &str, is_option_or_string: bool, is_allowed_to_be_empty: bool) -> Result, String> { + let val = self.get_value_by_closure(key, is_option_or_string, is_allowed_to_be_empty ,|val| { + if !val.is_empty() && (val == "true" || val == "yes") { + Ok(true) + } else { + Ok(false) + } + }); // not selected bool field equals to false and not to missing match val { Ok(val) => Ok(val), - Err(_) => Ok(Some(false)) + Err(_) => Ok(Some(false)), } } - fn get_value_by_closure(&self, key: &str, is_option_or_string: bool, f: impl Fn(&String) -> Result::Err>) -> Result, String> { + fn get_value_by_closure( + &self, + key: &str, + is_option_or_string: bool, + is_allowed_to_be_empty: bool, + f: impl Fn(&String) -> Result::Err>, + ) -> Result, String> { let value = self.values.get(key); let res: Result, String> = match value { Some(val) => { - if val.is_empty() && is_option_or_string { - return Ok(None); - } + match (val.is_empty(), is_option_or_string, is_allowed_to_be_empty) { + (true, true, true) => return Ok(None), + (true, true, false) => return Err("Cannot be empty".to_string()), + _ => {} + }; let parsed_val = f(val); @@ -109,11 +171,12 @@ impl ActixAdminModel { } } _ => { - match is_option_or_string { - true => Ok(None), - false => Err("Invalid Value".to_string()) // a missing value in the form for a non-optional value - } - } + match (is_option_or_string, is_allowed_to_be_empty) { + (true, true) => Ok(None), + (true, false) => Err("Cannot be empty".to_string()), + (false, _) => Err("Invalid Value".to_string()), // a missing value in the form for a non-optional value + } + } }; res diff --git a/src/routes/create_or_edit_get.rs b/src/routes/create_or_edit_get.rs index 0e0c242..41f5b00 100644 --- a/src/routes/create_or_edit_get.rs +++ b/src/routes/create_or_edit_get.rs @@ -70,7 +70,7 @@ async fn create_or_edit_get( session: Session, data: web::Data, payload: Multipart, ) -> Result { - let model = ActixAdminModel::create_from_payload(payload).await; - create_or_edit_post::(&session, &data, model, None).await + let actix_admin = data.get_actix_admin(); + let model = ActixAdminModel::create_from_payload( + payload, + &format!("{}/{}", actix_admin.configuration.file_upload_directory, E::get_entity_name()) + ) + .await; + create_or_edit_post::(&session, &data, model, None, actix_admin).await } pub async fn edit_post( @@ -26,8 +31,13 @@ pub async fn edit_post( payload: Multipart, id: web::Path, ) -> Result { - let model = ActixAdminModel::create_from_payload(payload).await; - create_or_edit_post::(&session, &data, model, Some(id.into_inner())).await + let actix_admin = data.get_actix_admin(); + let model = ActixAdminModel::create_from_payload( + payload, + &format!("{}/{}", actix_admin.configuration.file_upload_directory, E::get_entity_name()), + ) + .await; + create_or_edit_post::(&session, &data, model, Some(id.into_inner()), actix_admin).await } pub async fn create_or_edit_post( @@ -35,8 +45,8 @@ pub async fn create_or_edit_post, model_res: Result, id: Option, + actix_admin: &ActixAdmin, ) -> Result { - let actix_admin = data.get_actix_admin(); let entity_name = E::get_entity_name(); let view_model = actix_admin.view_models.get(&entity_name).unwrap(); @@ -62,23 +72,28 @@ pub async fn create_or_edit_post { - Ok(HttpResponse::SeeOther() + Ok(_) => Ok(HttpResponse::SeeOther() .append_header(( header::LOCATION, format!("/admin/{}/list", view_model.entity_name), )) - .finish()) - }, + .finish()), Err(e) => { errors.push(e); render_form::(actix_admin, view_model, db, entity_name, &model, errors).await - } - } + } + } } } -async fn render_form(actix_admin: &ActixAdmin, view_model: &ActixAdminViewModel, db: &&sea_orm::DatabaseConnection, entity_name: String, model: &ActixAdminModel, errors: Vec) -> Result { +async fn render_form( + actix_admin: &ActixAdmin, + view_model: &ActixAdminViewModel, + db: &&sea_orm::DatabaseConnection, + entity_name: String, + model: &ActixAdminModel, + errors: Vec, +) -> Result { let mut ctx = Context::new(); ctx.insert("entity_names", &actix_admin.entity_names); ctx.insert( @@ -86,13 +101,14 @@ async fn render_form(actix_admin: &ActixAdmin, view &ActixAdminViewModelSerializable::from(view_model.clone()), ); ctx.insert("select_lists", &E::get_select_lists(db).await?); - ctx.insert("list_link", &E::get_list_link(&entity_name)); + ctx.insert("base_path", &E::get_base_path(&entity_name)); ctx.insert("model", model); - - let notifications: Vec = errors.into_iter() + + let notifications: Vec = errors + .into_iter() .map(|err| ActixAdminNotification::from(err)) .collect(); - + ctx.insert("notifications", ¬ifications); let body = TERA .render("create_or_edit.html", &ctx) @@ -122,4 +138,4 @@ impl From for ActixAdminModel { custom_errors: HashMap::new(), } } -} \ No newline at end of file +} diff --git a/src/routes/delete.rs b/src/routes/delete.rs index a2b7da2..6fd71de 100644 --- a/src/routes/delete.rs +++ b/src/routes/delete.rs @@ -1,7 +1,7 @@ use actix_web::{web, Error, HttpRequest, HttpResponse}; use actix_web::http::header; use actix_session::{Session}; -use crate::prelude::*; +use crate::{prelude::*}; use tera::{Context}; use super::{ user_can_access_page, render_unauthorized}; @@ -24,11 +24,23 @@ pub async fn delete( } let db = &data.get_db(); - let result = E::delete_entity(db, id.into_inner()).await; + let id = id.into_inner(); + let model_result = E::get_entity(db, id).await; + let delete_result = E::delete_entity(db, id).await; - match result { - Ok(_) => Ok(HttpResponse::Ok().finish()), - Err(_) => Ok(HttpResponse::InternalServerError().finish()) + match (model_result, delete_result) { + (Ok(model), Ok(_)) => { + for field in view_model.fields { + if field.field_type == ActixAdminViewModelFieldType::FileUpload { + let file_name = model.get_value::(&field.field_name, true, true).unwrap_or_default(); + let file_path = format!("{}/{}/{}", actix_admin.configuration.file_upload_directory, E::get_entity_name(), file_name.unwrap_or_default()); + std::fs::remove_file(file_path)?; + } + } + + Ok(HttpResponse::Ok().finish()) + }, + (_,_) => Ok(HttpResponse::InternalServerError().finish()) } } @@ -60,10 +72,20 @@ pub async fn delete_many // TODO: implement delete_many for id in entity_ids { - let result = E::delete_entity(db, id).await; - match result { - Err(e) => errors.push(e), - _ => {} + let model_result = E::get_entity(db, id).await; + let delete_result = E::delete_entity(db, id).await; + match (delete_result, model_result) { + (Err(e), _) => errors.push(e), + (Ok(_), Ok(model)) => { + for field in view_model.fields { + if field.field_type == ActixAdminViewModelFieldType::FileUpload { + let file_name = model.get_value::(&field.field_name, true, true).unwrap_or_default(); + let file_path = format!("{}/{}/{}", actix_admin.configuration.file_upload_directory, E::get_entity_name(), file_name.unwrap_or_default()); + std::fs::remove_file(file_path)?; + } + } + }, + (Ok(_), Err(e)) => errors.push(e) } } diff --git a/src/routes/mod.rs b/src/routes/mod.rs index c06c0cb..eef2b9c 100644 --- a/src/routes/mod.rs +++ b/src/routes/mod.rs @@ -17,4 +17,7 @@ mod delete; pub use delete::{ delete, delete_many }; mod helpers; -pub use helpers::{ add_auth_context, user_can_access_page, render_unauthorized }; \ No newline at end of file +pub use helpers::{ add_auth_context, user_can_access_page, render_unauthorized }; + +mod static_content; +pub use static_content::{download, delete_static_content}; \ No newline at end of file diff --git a/src/routes/show.rs b/src/routes/show.rs index c599783..2f8ea87 100644 --- a/src/routes/show.rs +++ b/src/routes/show.rs @@ -6,17 +6,22 @@ use crate::ActixAdminNotification; use crate::prelude::*; use crate::TERA; - -use super::{ add_auth_context }; +use super::{ add_auth_context, user_can_access_page, render_unauthorized}; pub async fn show(session: Session, data: web::Data, id: web::Path) -> Result { let actix_admin = data.get_actix_admin(); let db = &data.get_db(); - let result = E::get_entity(db, id.into_inner()).await; - let mut errors: Vec = Vec::new(); - let model; + let mut ctx = Context::new(); + let entity_name = E::get_entity_name(); + let view_model: &ActixAdminViewModel = actix_admin.view_models.get(&entity_name).unwrap(); + if !user_can_access_page(&session, actix_admin, view_model) { + return render_unauthorized(&ctx); + } + let mut errors: Vec = Vec::new(); + let result = E::get_entity(db, id.into_inner()).await; + let model; match result { Ok(res) => { model = res; @@ -27,9 +32,6 @@ pub async fn show(sessio } } - let entity_name = E::get_entity_name(); - let view_model: &ActixAdminViewModel = actix_admin.view_models.get(&entity_name).unwrap(); - let mut http_response_code = match errors.is_empty() { false => HttpResponse::InternalServerError(), true => HttpResponse::Ok() @@ -38,10 +40,9 @@ pub async fn show(sessio .map(|err| ActixAdminNotification::from(err)) .collect(); - let mut ctx = Context::new(); ctx.insert("model", &model); ctx.insert("view_model", &ActixAdminViewModelSerializable::from(view_model.clone())); - ctx.insert("list_link", &E::get_list_link(&entity_name)); + ctx.insert("base_path", &E::get_base_path(&entity_name)); ctx.insert("entity_names", &actix_admin.entity_names); ctx.insert("notifications", ¬ifications); diff --git a/src/routes/static_content.rs b/src/routes/static_content.rs new file mode 100644 index 0000000..ff612ab --- /dev/null +++ b/src/routes/static_content.rs @@ -0,0 +1,85 @@ +use actix_web::{web, error, Error, HttpResponse, HttpRequest}; +use actix_session::{Session}; +use tera::{Context}; +use crate::prelude::*; + +use super::{ user_can_access_page, render_unauthorized}; + +pub async fn download(req: HttpRequest, session: Session, data: web::Data, params: web::Path<(i32, String)>) -> Result { + let actix_admin = data.get_actix_admin(); + let db = &data.get_db(); + + let ctx = Context::new(); + let entity_name = E::get_entity_name(); + let view_model: &ActixAdminViewModel = actix_admin.view_models.get(&entity_name).unwrap(); + if !user_can_access_page(&session, actix_admin, view_model) { + return render_unauthorized(&ctx); + } + + let (id, column_name) = params.into_inner(); + let mut errors: Vec = Vec::new(); + let result = E::get_entity(db, id).await; + let model; + match result { + Ok(res) => { + model = res; + }, + Err(e) => { + errors.push(e); + model = ActixAdminModel::create_empty(); + } + } + + let file_name = model.get_value::(&column_name, true, true).unwrap_or_default(); + let file_path = format!("{}/{}/{}", actix_admin.configuration.file_upload_directory, E::get_entity_name(), file_name.unwrap_or_default()); + let file = actix_files::NamedFile::open_async(file_path).await; + + match file { + Ok(file) => Ok(file.into_response(&req)), + Err(_e) => Ok(HttpResponse::NotFound().content_type("text/html").body("")) + } + +} + +pub async fn delete_static_content(session: Session, data: web::Data, params: web::Path<(i32, String)>) -> Result { + let actix_admin = data.get_actix_admin(); + let db = &data.get_db(); + + let mut ctx = Context::new(); + let entity_name = E::get_entity_name(); + let view_model: &ActixAdminViewModel = actix_admin.view_models.get(&entity_name).unwrap(); + if !user_can_access_page(&session, actix_admin, view_model) { + return render_unauthorized(&ctx); + } + + let (id, column_name) = params.into_inner(); + let mut errors: Vec = Vec::new(); + let result = E::get_entity(db, id).await; + let mut model; + match result { + Ok(res) => { + model = res; + }, + Err(e) => { + errors.push(e); + model = ActixAdminModel::create_empty(); + } + } + + let file_name = model.get_value::(&column_name, true, true).unwrap_or_default(); + let file_path = format!("{}/{}/{}", actix_admin.configuration.file_upload_directory, E::get_entity_name(), file_name.unwrap_or_default()); + std::fs::remove_file(file_path).unwrap(); + model.values.remove(&column_name); + let _edit_res = E::edit_entity(db, id, model.clone()).await; + + let view_model_field = &view_model.fields.iter().find(|field| field.field_name == column_name).unwrap(); + ctx.insert("model_field", view_model_field); + ctx.insert("base_path", &E::get_base_path(&entity_name)); + ctx.insert("model", &model); + + let body = TERA + .render("form_elements/input.html", &ctx) + .map_err(|err| error::ErrorInternalServerError(err))? ; + Ok(HttpResponse::Ok().content_type("text/html").body(body)) + +} \ No newline at end of file diff --git a/src/view_model.rs b/src/view_model.rs index 4c17962..52c1af9 100644 --- a/src/view_model.rs +++ b/src/view_model.rs @@ -26,8 +26,8 @@ pub trait ActixAdminViewModelTrait { fn get_entity_name() -> String; - fn get_list_link(entity_name: &String) -> String { - format!("/admin/{}/list", entity_name) + fn get_base_path(entity_name: &String) -> String { + format!("/admin/{}", entity_name) } } @@ -61,7 +61,7 @@ impl From for ActixAdminViewModelSerializable { } -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub enum ActixAdminViewModelFieldType { Number, Text, @@ -70,7 +70,8 @@ pub enum ActixAdminViewModelFieldType { Date, Time, DateTime, - SelectList + SelectList, + FileUpload } #[derive(Clone, Debug, Serialize, Deserialize)] @@ -83,13 +84,16 @@ pub struct ActixAdminViewModelField { } impl ActixAdminViewModelFieldType { - pub fn get_field_type(type_path: &str, select_list: String, is_textarea: bool) -> ActixAdminViewModelFieldType { + pub fn get_field_type(type_path: &str, select_list: String, is_textarea: bool, is_file_upload: bool) -> ActixAdminViewModelFieldType { if !select_list.is_empty() { return ActixAdminViewModelFieldType::SelectList; } if is_textarea { return ActixAdminViewModelFieldType::TextArea; } + if is_file_upload { + return ActixAdminViewModelFieldType::FileUpload; + } match type_path { "i32" => ActixAdminViewModelFieldType::Number, diff --git a/templates/create_or_edit.html b/templates/create_or_edit.html index f0ff5e1..d02937f 100644 --- a/templates/create_or_edit.html +++ b/templates/create_or_edit.html @@ -29,7 +29,7 @@ diff --git a/templates/form_elements/input.html b/templates/form_elements/input.html index aa17e9f..97c2801 100644 --- a/templates/form_elements/input.html +++ b/templates/form_elements/input.html @@ -1,6 +1,5 @@ {% if model_field.field_type == "TextArea" %} - + aria-label="{{ model_field.field_name }}">{{ model.values | get(key=model_field.field_name, default="") }} +{% elif model_field.field_type == "FileUpload" and model.values | get(key=model_field.field_name, default="") != "" %} + {% else %} - 0 or model.custom_errors | length > 0 %} {% if model.errors | get(key=model_field.field_name, default="" ) !="" @@ -24,11 +26,7 @@ model.custom_errors | get(key=model_field.field_name, default="" ) !="" %}is-danger{% else %}is-success{% endif %} {% endif %} - " - type="{{ model_field | get_html_input_type }}" - value="{{ model.values | get(key=model_field.field_name, default="") }}" - name="{{ model_field.field_name }}" - placeholder="{{ model_field.field_name }}" - aria-label="{{ model_field.field_name }}" -> + " type="{{ model_field | get_html_input_type }}" + value="{{ model.values | get(key=model_field.field_name, default="") }}" name="{{ model_field.field_name }}" + placeholder="{{ model_field.field_name }}" aria-label="{{ model_field.field_name }}"> {% endif %} \ No newline at end of file diff --git a/templates/list.html b/templates/list.html index d7da7f8..19b57bd 100644 --- a/templates/list.html +++ b/templates/list.html @@ -88,12 +88,14 @@ - {{ entity.primary_key }} + {{ entity.primary_key }} {% for model_field in view_model.fields -%} {% if model_field.field_type == "Checkbox" %} {{ entity.values | get(key=model_field.field_name) | get_icon | safe }} + {% elif model_field.field_type == "FileUpload" %} + {{ entity.values | get(key=model_field.field_name) }} {% else %} {{ entity.values | get(key=model_field.field_name) }} {% endif %} diff --git a/templates/show.html b/templates/show.html index ec9945b..05c0e83 100644 --- a/templates/show.html +++ b/templates/show.html @@ -8,6 +8,8 @@

{% if model_field.field_type == "Checkbox" %} {{ model.values | get(key=model_field.field_name) | get_icon | safe }} + {% elif model_field.field_type == "FileUpload" %} + {{ entity.values | get(key=model_field.field_name) }} {% else %} {{ model.values | get(key=model_field.field_name) }} {% endif %} @@ -19,7 +21,7 @@

- Back + Back
diff --git a/tests/test_setup/helper.rs b/tests/test_setup/helper.rs index 3239940..953d79b 100644 --- a/tests/test_setup/helper.rs +++ b/tests/test_setup/helper.rs @@ -41,6 +41,7 @@ pub fn create_actix_admin_builder() -> ActixAdminBuilder { user_is_logged_in: None, login_link: None, logout_link: None, + file_upload_directory: "./file_uploads" }; let mut admin_builder = ActixAdminBuilder::new(configuration); @@ -78,8 +79,9 @@ async fn create_post_from_plaintext< data: web::Data, text: String, ) -> Result { + let actix_admin = data.get_actix_admin(); let model = ActixAdminModel::from(text); - create_or_edit_post::(&session, &data, Ok(model), None).await + create_or_edit_post::(&session, &data, Ok(model), None, actix_admin).await } async fn edit_post_from_plaintext< @@ -91,6 +93,7 @@ async fn edit_post_from_plaintext< text: String, id: web::Path, ) -> Result { + let actix_admin = data.get_actix_admin(); let model = ActixAdminModel::from(text); - create_or_edit_post::(&session, &data, Ok(model), Some(id.into_inner())).await + create_or_edit_post::(&session, &data, Ok(model), Some(id.into_inner()), actix_admin).await } \ No newline at end of file