implement file_upload field

This commit is contained in:
Manuel Gugger 2022-12-29 19:56:20 +01:00
parent ce964220b7
commit a600ff5c9c
40 changed files with 399 additions and 187 deletions

5
.gitignore vendored
View File

@ -26,4 +26,7 @@ _site/
.jekyll-metadata .jekyll-metadata
# Ignore folders generated by Bundler # Ignore folders generated by Bundler
.bundle/ .bundle/
vendor/ vendor/
# Ignore File Uploads
file_uploads/

View File

@ -6,18 +6,21 @@ version = "0.2.0"
repository = "https://github.com/mgugger/actix-admin" repository = "https://github.com/mgugger/actix-admin"
edition = "2021" edition = "2021"
exclude = [ exclude = [
"example/*", "examples/*",
"actix_admin_macros/*", "actix_admin_macros/*",
"tests/*", "tests/*",
"README.md", "README.md"
"static/*",
"azure_auth/*",
] ]
[lib]
name = "actix_admin"
path = "src/lib.rs"
[dependencies] [dependencies]
actix-web = "^4.0.1" actix-web = "^4.0.1"
actix-session = { version = "^0.7.1", features = [] } actix-session = { version = "^0.7.1", features = [] }
actix-multipart = "^0.4.0" actix-multipart = "^0.4.0"
actix-files = "^0.6.2"
futures-util = "0.3.21" futures-util = "0.3.21"
chrono = "0.4.20" chrono = "0.4.20"
tera = "^1.16.0" tera = "^1.16.0"
@ -31,4 +34,9 @@ actix-admin-macros = { version = "0.2.0", path = "actix_admin_macros" }
derive_more = "0.99.17" derive_more = "0.99.17"
[dev-dependencies] [dev-dependencies]
sea-orm = { version = "^0.9.1", features = [ "sqlx-sqlite", "runtime-actix-native-tls", "macros" ], default-features = true } 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"] }

View File

@ -14,7 +14,9 @@ pub mod derive_attr {
pub html_input_type: Option<syn::LitStr>, pub html_input_type: Option<syn::LitStr>,
pub select_list: Option<syn::LitStr>, pub select_list: Option<syn::LitStr>,
pub searchable: Option<()>, pub searchable: Option<()>,
pub textarea: Option<()> pub textarea: Option<()>,
pub file_upload: Option<()>,
pub not_empty: Option<()>
//pub inner_type: Option<syn::Type>, //pub inner_type: Option<syn::Type>,
// Anything that implements `syn::parse::Parse` is supported. // Anything that implements `syn::parse::Parse` is supported.

View File

@ -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_searchable = get_actix_admin_fields_searchable(&fields);
let fields_type_path = get_actix_admin_fields_type_path_string(&fields); let fields_type_path = get_actix_admin_fields_type_path_string(&fields);
let fields_textarea = get_actix_admin_fields_textarea(&fields); let fields_textarea = get_actix_admin_fields_textarea(&fields);
let fields_file_upload = get_actix_admin_fields_file_upload(&fields);
let expanded = quote! { let expanded = quote! {
actix_admin::prelude::lazy_static! { actix_admin::prelude::lazy_static! {
@ -187,7 +188,11 @@ pub fn derive_actix_admin_model(input: proc_macro::TokenStream) -> proc_macro::T
#(#fields_textarea),* #(#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 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();
@ -198,7 +203,7 @@ pub fn derive_actix_admin_model(input: proc_macro::TokenStream) -> proc_macro::T
html_input_type: html_input_type, html_input_type: html_input_type,
select_list: select_list.clone(), select_list: select_list.clone(),
is_option: is_option_list, 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 vec

View File

@ -7,13 +7,14 @@ pub struct ModelField {
pub visibility: Visibility, pub visibility: Visibility,
pub ident: proc_macro2::Ident, pub ident: proc_macro2::Ident,
pub ty: Type, pub ty: Type,
// struct field is option<>
pub inner_type: Option<Type>, pub inner_type: Option<Type>,
pub primary_key: bool, pub primary_key: bool,
pub html_input_type: String, pub html_input_type: String,
pub select_list: String, pub select_list: String,
pub searchable: bool, pub searchable: bool,
pub textarea: bool pub textarea: bool,
pub file_upload: bool,
pub not_empty: bool
} }
impl ModelField { impl ModelField {

View File

@ -41,6 +41,12 @@ pub fn filter_fields(fields: &Fields) -> Vec<ModelField> {
let is_textarea = actix_admin_attr let is_textarea = actix_admin_attr
.clone() .clone()
.map_or(false, |attr| attr.textarea.is_some()); .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| { let select_list = actix_admin_attr.clone().map_or("".to_string(), |attr| {
attr.select_list.map_or("".to_string(), |attr_field| { attr.select_list.map_or("".to_string(), |attr_field| {
(LitStr::from(attr_field)).value() (LitStr::from(attr_field)).value()
@ -62,7 +68,9 @@ pub fn filter_fields(fields: &Fields) -> Vec<ModelField> {
html_input_type: html_input_type, html_input_type: html_input_type,
select_list: select_list, select_list: select_list,
searchable: is_searchable, searchable: is_searchable,
textarea: is_textarea textarea: is_textarea,
file_upload: is_file_upload,
not_empty: is_not_empty
}; };
Some(model_field) Some(model_field)
} else { } else {
@ -186,6 +194,20 @@ pub fn get_actix_admin_fields_textarea(fields: &Vec<ModelField>) -> Vec<TokenStr
.collect::<Vec<_>>() .collect::<Vec<_>>()
} }
pub fn get_actix_admin_fields_file_upload(fields: &Vec<ModelField>) -> Vec<TokenStream> {
fields
.iter()
.filter(|model_field| !model_field.primary_key)
.map(|model_field| {
let is_fileupload = model_field.file_upload;
quote! {
#is_fileupload
}
})
.collect::<Vec<_>>()
}
pub fn get_actix_admin_fields_searchable(fields: &Vec<ModelField>) -> Vec<TokenStream> { pub fn get_actix_admin_fields_searchable(fields: &Vec<ModelField>) -> Vec<TokenStream> {
fields fields
.iter() .iter()
@ -275,33 +297,34 @@ pub fn get_fields_for_validate_model(fields: &Vec<ModelField>) -> Vec<TokenStrea
let type_path = model_field.get_type_path_string(); let type_path = model_field.get_type_path_string();
let is_option_or_string = model_field.is_option() || model_field.is_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(), type_path.as_str()) { let res = match (model_field.is_option(), type_path.as_str()) {
(_, "DateTime") => { (_, "DateTime") => {
quote! { 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") => { (_, "Date") => {
quote! { 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") => { (_, "bool") => {
quote! { 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 // generic
(true, _) => { (true, _) => {
let inner_ty = model_field.inner_type.to_owned().unwrap(); let inner_ty = model_field.inner_type.to_owned().unwrap();
quote! { 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, _) => { (false, _) => {
quote! { 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<ModelField>) -> Vec<TokenStream>
let type_path = model_field.get_type_path_string(); let type_path = model_field.get_type_path_string();
let is_option_or_string = model_field.is_option() || model_field.is_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()) { let res = match (model_field.is_option(), model_field.is_string(), type_path.as_str()) {
// is DateTime // is DateTime
(true , _, "DateTime") => { (true , _, "DateTime") => {
quote! { 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") => { (false , _, "DateTime") => {
quote! { 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") => { (true , _, "Date") => {
quote! { 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") => { (false , _, "Date") => {
quote! { 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") => { (_ , _, "bool") => {
quote! { 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 // Default fields
(true, _, _) => { (true, _, _) => {
let inner_ty = model_field.inner_type.to_owned().unwrap(); let inner_ty = model_field.inner_type.to_owned().unwrap();
quote! { 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 // is string which can be empty
(false, true, _) => { (false, true, _) => {
quote! { 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 // no string
(false, false, _) => { (false, false, _) => {
quote! { 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<ModelField>) -> Vec<TokenStream> {
let type_path = model_field.get_type_path_string(); let type_path = model_field.get_type_path_string();
let is_option_or_string = model_field.is_option() || model_field.is_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()) { let res = match (model_field.is_option(), model_field.is_string(), type_path.as_str()) {
(_, _, "bool") => { (_, _, "bool") => {
quote! { 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") => { (true , _, "DateTime") => {
quote! { 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") => { (false , _, "DateTime") => {
quote! { 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") => { (true , _, "Date") => {
quote! { 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") => { (false , _, "Date") => {
quote! { 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, _, _) => { (true, _, _) => {
let inner_ty = model_field.inner_type.to_owned().unwrap(); let inner_ty = model_field.inner_type.to_owned().unwrap();
quote! { 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, _) => { (false, true, _) => {
quote! { 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, _) => { (false, false, _) => {
quote! { 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())
} }
} }
}; };

View File

@ -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_SECRET= "TODO"
OAUTH2_CLIENT_ID= "TODO" OAUTH2_CLIENT_ID= "TODO"
OAUTH2_SERVER= "login.microsoftonline.com/a5f5xxxx-xxxx-414a-8463-xxxxxxxxxxxxx(tenantId)" OAUTH2_SERVER= "login.microsoftonline.com/a5f5xxxx-xxxx-414a-8463-xxxxxxxxxxxxx(tenantId)"

View File

@ -109,7 +109,8 @@ fn create_actix_admin_builder() -> ActixAdminBuilder {
user_info.is_some() user_info.is_some()
}), }),
login_link: Some("/azure-auth/login".to_string()), 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); let mut admin_builder = ActixAdminBuilder::new(configuration);
@ -135,7 +136,8 @@ fn create_actix_admin_builder() -> ActixAdminBuilder {
#[actix_rt::main] #[actix_rt::main]
async fn 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_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."); let oauth2_client_secret = env::var("OAUTH2_CLIENT_SECRET").expect("Missing the OAUTH2_CLIENT_SECRET environment variable.");

View File

@ -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 = "../../" }

View File

@ -28,6 +28,7 @@ pub async fn create_post_table(db: &DbConn) -> Result<ExecResult, DbErr> {
.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::TeaOptional).string())
.col(ColumnDef::new(post::Column::InsertDate).date()) .col(ColumnDef::new(post::Column::InsertDate).date())
.col(ColumnDef::new(post::Column::Attachment).string())
.to_owned(); .to_owned();
let _result = create_table(db, &stmt).await; let _result = create_table(db, &stmt).await;

View File

@ -12,7 +12,7 @@ pub struct Model {
#[serde(skip_deserializing)] #[serde(skip_deserializing)]
#[actix_admin(primary_key)] #[actix_admin(primary_key)]
pub id: i32, pub id: i32,
#[actix_admin(searchable)] #[actix_admin(searchable, not_empty)]
pub title: String, pub title: String,
#[sea_orm(column_type = "Text")] #[sea_orm(column_type = "Text")]
#[actix_admin(searchable, textarea)] #[actix_admin(searchable, textarea)]
@ -22,6 +22,8 @@ pub struct Model {
#[actix_admin(select_list="Tea")] #[actix_admin(select_list="Tea")]
pub tea_optional: Option<Tea>, pub tea_optional: Option<Tea>,
pub insert_date: Date, pub insert_date: Date,
#[actix_admin(file_upload)]
pub attachment: String
} }
impl Display for Model { impl Display for Model {

View File

@ -31,7 +31,8 @@ fn create_actix_admin_builder() -> ActixAdminBuilder {
enable_auth: false, enable_auth: false,
user_is_logged_in: None, user_is_logged_in: None,
login_link: None, login_link: None,
logout_link: None logout_link: None,
file_upload_directory: "./file_uploads"
}; };
let mut admin_builder = ActixAdminBuilder::new(configuration); let mut admin_builder = ActixAdminBuilder::new(configuration);

View File

@ -1,15 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Actix Admin Example</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
</head>
<body>
<ul>
<li><a href="/admin/">Go to Actix-Admin</a></li>
</ul>
</body>
</html>

View File

@ -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" }

View File

@ -1 +0,0 @@
Rename .env.example to .env and update the oauth client credentials

View File

@ -1,9 +1,10 @@
use crate::{prelude::*, ActixAdminMenuElement}; use crate::{prelude::*, ActixAdminMenuElement, routes::delete_static_content};
use actix_web::{web, Route}; use actix_web::{web, Route};
use std::collections::HashMap; use std::collections::HashMap;
use std::fs;
use crate::routes::{ 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 /// 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::<T, E>)) .route("/delete", web::delete().to(delete_many::<T, E>))
.route("/delete/{id}", web::delete().to(delete::<T, E>)) .route("/delete/{id}", web::delete().to(delete::<T, E>))
.route("/show/{id}", web::get().to(show::<T, E>)) .route("/show/{id}", web::get().to(show::<T, E>))
.route("/static_content/{id}/{column_name}", web::get().to(download::<T, E>))
.route("/static_content/{id}/{column_name}", web::delete().to(delete_static_content::<T, E>))
.default_service(web::to(not_found)), .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 category = self.actix_admin.entity_names.get_mut(category_name);
let menu_element = ActixAdminMenuElement { let menu_element = ActixAdminMenuElement {
name: E::get_entity_name(), name: E::get_entity_name(),

View File

@ -96,6 +96,7 @@ pub fn get_html_input_type<S: BuildHasher>(value: &tera::Value, _: &HashMap<Stri
ActixAdminViewModelFieldType::DateTime => "datetime-local", ActixAdminViewModelFieldType::DateTime => "datetime-local",
ActixAdminViewModelFieldType::Date => "date", ActixAdminViewModelFieldType::Date => "date",
ActixAdminViewModelFieldType::Checkbox => "checkbox", ActixAdminViewModelFieldType::Checkbox => "checkbox",
ActixAdminViewModelFieldType::FileUpload => "file",
_ => "text" _ => "text"
}; };
@ -120,7 +121,8 @@ pub struct ActixAdminConfiguration {
pub enable_auth: bool, pub enable_auth: bool,
pub user_is_logged_in: Option<for<'a> fn(&'a Session) -> bool>, pub user_is_logged_in: Option<for<'a> fn(&'a Session) -> bool>,
pub login_link: Option<String>, pub login_link: Option<String>,
pub logout_link: Option<String> pub logout_link: Option<String>,
pub file_upload_directory: &'static str
} }
#[derive(Clone)] #[derive(Clone)]

View File

@ -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 async_trait::async_trait;
use chrono::{NaiveDate, NaiveDateTime};
use futures_util::stream::StreamExt as _;
use sea_orm::DatabaseConnection; use sea_orm::DatabaseConnection;
use serde::Serialize; use serde::Serialize;
use std::collections::HashMap; use std::collections::HashMap;
use actix_multipart:: {Multipart, MultipartError} ; use std::fs::File;
use futures_util::stream::StreamExt as _; use std::io::Write;
use chrono::{NaiveDateTime, NaiveDate}; use std::time::{SystemTime, UNIX_EPOCH};
#[async_trait] #[async_trait]
pub trait ActixAdminModelTrait { pub trait ActixAdminModelTrait {
@ -13,9 +17,9 @@ pub trait ActixAdminModelTrait {
db: &DatabaseConnection, db: &DatabaseConnection,
page: usize, page: usize,
posts_per_page: usize, posts_per_page: usize,
search: &String search: &String,
) -> Result<(usize, Vec<ActixAdminModel>), ActixAdminError>; ) -> Result<(usize, Vec<ActixAdminModel>), ActixAdminError>;
fn get_fields() -> &'static[ActixAdminViewModelField]; fn get_fields() -> &'static [ActixAdminViewModelField];
fn validate_model(model: &mut ActixAdminModel); fn validate_model(model: &mut ActixAdminModel);
} }
@ -30,10 +34,9 @@ pub struct ActixAdminModel {
pub primary_key: Option<String>, pub primary_key: Option<String>,
pub values: HashMap<String, String>, pub values: HashMap<String, String>,
pub errors: HashMap<String, String>, pub errors: HashMap<String, String>,
pub custom_errors: HashMap<String, String> pub custom_errors: HashMap<String, String>,
} }
impl ActixAdminModel { impl ActixAdminModel {
pub fn create_empty() -> ActixAdminModel { pub fn create_empty() -> ActixAdminModel {
ActixAdminModel { ActixAdminModel {
@ -44,21 +47,47 @@ impl ActixAdminModel {
} }
} }
pub async fn create_from_payload(mut payload: Multipart) -> Result<ActixAdminModel, MultipartError> { pub async fn create_from_payload(
mut payload: Multipart, file_upload_folder: &str
) -> Result<ActixAdminModel, MultipartError> {
let mut hashmap = HashMap::<String, String>::new(); let mut hashmap = HashMap::<String, String>::new();
while let Some(item) = payload.next().await { while let Some(item) = payload.next().await {
let mut field = item?; let mut field = item?;
// TODO: how to handle binary chunks? let mut binary_data: Vec<Bytes> = Vec::new();
while let Some(chunk) = field.next().await { 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()))); //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() { if res_string.is_ok() {
hashmap.insert( hashmap.insert(field.name().to_string(), res_string.unwrap());
field.name().to_string(),
res_string.unwrap()
);
} }
} }
} }
@ -71,35 +100,68 @@ impl ActixAdminModel {
}) })
} }
pub fn get_value<T: std::str::FromStr>(&self, key: &str, is_option_or_string: bool) -> Result<Option<T>, String> { pub fn get_value<T: std::str::FromStr>(
self.get_value_by_closure(key, is_option_or_string, |val| val.parse::<T>()) &self,
key: &str,
is_option_or_string: bool,
is_allowed_to_be_empty: bool
) -> Result<Option<T>, String> {
self.get_value_by_closure(key, is_option_or_string, is_allowed_to_be_empty, |val| val.parse::<T>())
} }
pub fn get_datetime(&self, key: &str, is_option_or_string: bool) -> Result<Option<NaiveDateTime>, String> { pub fn get_datetime(
self.get_value_by_closure(key, is_option_or_string, |val| NaiveDateTime::parse_from_str(val, "%Y-%m-%dT%H:%M")) &self,
key: &str,
is_option_or_string: bool,
is_allowed_to_be_empty: bool
) -> Result<Option<NaiveDateTime>, 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<Option<NaiveDate>, String> { pub fn get_date(
self.get_value_by_closure(key, is_option_or_string, |val| NaiveDate::parse_from_str(val, "%Y-%m-%d")) &self,
key: &str,
is_option_or_string: bool,
is_allowed_to_be_empty: bool
) -> Result<Option<NaiveDate>, 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<Option<bool>, String> { pub fn get_bool(&self, key: &str, is_option_or_string: bool, is_allowed_to_be_empty: bool) -> Result<Option<bool>, 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) }); 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 // not selected bool field equals to false and not to missing
match val { match val {
Ok(val) => Ok(val), Ok(val) => Ok(val),
Err(_) => Ok(Some(false)) Err(_) => Ok(Some(false)),
} }
} }
fn get_value_by_closure<T: std::str::FromStr>(&self, key: &str, is_option_or_string: bool, f: impl Fn(&String) -> Result<T, <T as std::str::FromStr>::Err>) -> Result<Option<T>, String> { fn get_value_by_closure<T: std::str::FromStr>(
&self,
key: &str,
is_option_or_string: bool,
is_allowed_to_be_empty: bool,
f: impl Fn(&String) -> Result<T, <T as std::str::FromStr>::Err>,
) -> Result<Option<T>, String> {
let value = self.values.get(key); let value = self.values.get(key);
let res: Result<Option<T>, String> = match value { let res: Result<Option<T>, String> = match value {
Some(val) => { Some(val) => {
if val.is_empty() && is_option_or_string { match (val.is_empty(), is_option_or_string, is_allowed_to_be_empty) {
return Ok(None); (true, true, true) => return Ok(None),
} (true, true, false) => return Err("Cannot be empty".to_string()),
_ => {}
};
let parsed_val = f(val); let parsed_val = f(val);
@ -109,11 +171,12 @@ impl ActixAdminModel {
} }
} }
_ => { _ => {
match is_option_or_string { match (is_option_or_string, is_allowed_to_be_empty) {
true => Ok(None), (true, true) => Ok(None),
false => Err("Invalid Value".to_string()) // a missing value in the form for a non-optional value (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 res

View File

@ -70,7 +70,7 @@ async fn create_or_edit_get<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTra
ctx.insert("view_model", &ActixAdminViewModelSerializable::from(view_model.clone())); ctx.insert("view_model", &ActixAdminViewModelSerializable::from(view_model.clone()));
ctx.insert("select_lists", &E::get_select_lists(db).await?); 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); ctx.insert("model", &model);
ctx.insert("notifications", &notifications); ctx.insert("notifications", &notifications);

View File

@ -1,23 +1,28 @@
use super::{render_unauthorized, user_can_access_page}; use super::{render_unauthorized, user_can_access_page};
use crate::prelude::*;
use crate::ActixAdminError; use crate::ActixAdminError;
use crate::ActixAdminNotification; use crate::ActixAdminNotification;
use crate::prelude::*;
use crate::TERA; use crate::TERA;
use actix_multipart::Multipart;
use actix_multipart::MultipartError; use actix_multipart::MultipartError;
use actix_session::Session; use actix_session::Session;
use actix_web::http::header; use actix_web::http::header;
use actix_web::{error, web, Error, HttpResponse}; use actix_web::{error, web, Error, HttpResponse};
use tera::Context;
use actix_multipart::Multipart;
use std::collections::HashMap; use std::collections::HashMap;
use tera::Context;
pub async fn create_post<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>( pub async fn create_post<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>(
session: Session, session: Session,
data: web::Data<T>, data: web::Data<T>,
payload: Multipart, payload: Multipart,
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, Error> {
let model = ActixAdminModel::create_from_payload(payload).await; let actix_admin = data.get_actix_admin();
create_or_edit_post::<T, E>(&session, &data, model, None).await let model = ActixAdminModel::create_from_payload(
payload,
&format!("{}/{}", actix_admin.configuration.file_upload_directory, E::get_entity_name())
)
.await;
create_or_edit_post::<T, E>(&session, &data, model, None, actix_admin).await
} }
pub async fn edit_post<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>( pub async fn edit_post<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>(
@ -26,8 +31,13 @@ pub async fn edit_post<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>(
payload: Multipart, payload: Multipart,
id: web::Path<i32>, id: web::Path<i32>,
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, Error> {
let model = ActixAdminModel::create_from_payload(payload).await; let actix_admin = data.get_actix_admin();
create_or_edit_post::<T, E>(&session, &data, model, Some(id.into_inner())).await let model = ActixAdminModel::create_from_payload(
payload,
&format!("{}/{}", actix_admin.configuration.file_upload_directory, E::get_entity_name()),
)
.await;
create_or_edit_post::<T, E>(&session, &data, model, Some(id.into_inner()), actix_admin).await
} }
pub async fn create_or_edit_post<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>( pub async fn create_or_edit_post<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>(
@ -35,8 +45,8 @@ pub async fn create_or_edit_post<T: ActixAdminAppDataTrait, E: ActixAdminViewMod
data: &web::Data<T>, data: &web::Data<T>,
model_res: Result<ActixAdminModel, MultipartError>, model_res: Result<ActixAdminModel, MultipartError>,
id: Option<i32>, id: Option<i32>,
actix_admin: &ActixAdmin,
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, Error> {
let actix_admin = data.get_actix_admin();
let entity_name = E::get_entity_name(); let entity_name = E::get_entity_name();
let view_model = actix_admin.view_models.get(&entity_name).unwrap(); let view_model = actix_admin.view_models.get(&entity_name).unwrap();
@ -62,23 +72,28 @@ pub async fn create_or_edit_post<T: ActixAdminAppDataTrait, E: ActixAdminViewMod
}; };
match res { match res {
Ok(_) => { Ok(_) => Ok(HttpResponse::SeeOther()
Ok(HttpResponse::SeeOther()
.append_header(( .append_header((
header::LOCATION, header::LOCATION,
format!("/admin/{}/list", view_model.entity_name), format!("/admin/{}/list", view_model.entity_name),
)) ))
.finish()) .finish()),
},
Err(e) => { Err(e) => {
errors.push(e); errors.push(e);
render_form::<E>(actix_admin, view_model, db, entity_name, &model, errors).await render_form::<E>(actix_admin, view_model, db, entity_name, &model, errors).await
} }
} }
} }
} }
async fn render_form<E: ActixAdminViewModelTrait>(actix_admin: &ActixAdmin, view_model: &ActixAdminViewModel, db: &&sea_orm::DatabaseConnection, entity_name: String, model: &ActixAdminModel, errors: Vec<ActixAdminError>) -> Result<HttpResponse, Error> { async fn render_form<E: ActixAdminViewModelTrait>(
actix_admin: &ActixAdmin,
view_model: &ActixAdminViewModel,
db: &&sea_orm::DatabaseConnection,
entity_name: String,
model: &ActixAdminModel,
errors: Vec<ActixAdminError>,
) -> Result<HttpResponse, Error> {
let mut ctx = Context::new(); let mut ctx = Context::new();
ctx.insert("entity_names", &actix_admin.entity_names); ctx.insert("entity_names", &actix_admin.entity_names);
ctx.insert( ctx.insert(
@ -86,13 +101,14 @@ async fn render_form<E: ActixAdminViewModelTrait>(actix_admin: &ActixAdmin, view
&ActixAdminViewModelSerializable::from(view_model.clone()), &ActixAdminViewModelSerializable::from(view_model.clone()),
); );
ctx.insert("select_lists", &E::get_select_lists(db).await?); 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); ctx.insert("model", model);
let notifications: Vec<ActixAdminNotification> = errors.into_iter() let notifications: Vec<ActixAdminNotification> = errors
.into_iter()
.map(|err| ActixAdminNotification::from(err)) .map(|err| ActixAdminNotification::from(err))
.collect(); .collect();
ctx.insert("notifications", &notifications); ctx.insert("notifications", &notifications);
let body = TERA let body = TERA
.render("create_or_edit.html", &ctx) .render("create_or_edit.html", &ctx)
@ -122,4 +138,4 @@ impl From<String> for ActixAdminModel {
custom_errors: HashMap::new(), custom_errors: HashMap::new(),
} }
} }
} }

View File

@ -1,7 +1,7 @@
use actix_web::{web, Error, HttpRequest, HttpResponse}; use actix_web::{web, Error, HttpRequest, HttpResponse};
use actix_web::http::header; use actix_web::http::header;
use actix_session::{Session}; use actix_session::{Session};
use crate::prelude::*; use crate::{prelude::*};
use tera::{Context}; use tera::{Context};
use super::{ user_can_access_page, render_unauthorized}; use super::{ user_can_access_page, render_unauthorized};
@ -24,11 +24,23 @@ pub async fn delete<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>(
} }
let db = &data.get_db(); 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 { match (model_result, delete_result) {
Ok(_) => Ok(HttpResponse::Ok().finish()), (Ok(model), Ok(_)) => {
Err(_) => Ok(HttpResponse::InternalServerError().finish()) for field in view_model.fields {
if field.field_type == ActixAdminViewModelFieldType::FileUpload {
let file_name = model.get_value::<String>(&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<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>
// TODO: implement delete_many // TODO: implement delete_many
for id in entity_ids { for id in entity_ids {
let result = E::delete_entity(db, id).await; let model_result = E::get_entity(db, id).await;
match result { let delete_result = E::delete_entity(db, id).await;
Err(e) => errors.push(e), 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::<String>(&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)
} }
} }

View File

@ -17,4 +17,7 @@ mod delete;
pub use delete::{ delete, delete_many }; pub use delete::{ delete, delete_many };
mod helpers; mod helpers;
pub use helpers::{ add_auth_context, user_can_access_page, render_unauthorized }; pub use helpers::{ add_auth_context, user_can_access_page, render_unauthorized };
mod static_content;
pub use static_content::{download, delete_static_content};

View File

@ -6,17 +6,22 @@ use crate::ActixAdminNotification;
use crate::prelude::*; use crate::prelude::*;
use crate::TERA; use crate::TERA;
use super::{ add_auth_context, user_can_access_page, render_unauthorized};
use super::{ add_auth_context };
pub async fn show<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>(session: Session, data: web::Data<T>, id: web::Path<i32>) -> Result<HttpResponse, Error> { pub async fn show<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>(session: Session, data: web::Data<T>, id: web::Path<i32>) -> Result<HttpResponse, Error> {
let actix_admin = data.get_actix_admin(); let actix_admin = data.get_actix_admin();
let db = &data.get_db(); let db = &data.get_db();
let result = E::get_entity(db, id.into_inner()).await;
let mut errors: Vec<crate::ActixAdminError> = 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<crate::ActixAdminError> = Vec::new();
let result = E::get_entity(db, id.into_inner()).await;
let model;
match result { match result {
Ok(res) => { Ok(res) => {
model = res; model = res;
@ -27,9 +32,6 @@ pub async fn show<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>(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() { let mut http_response_code = match errors.is_empty() {
false => HttpResponse::InternalServerError(), false => HttpResponse::InternalServerError(),
true => HttpResponse::Ok() true => HttpResponse::Ok()
@ -38,10 +40,9 @@ pub async fn show<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>(sessio
.map(|err| ActixAdminNotification::from(err)) .map(|err| ActixAdminNotification::from(err))
.collect(); .collect();
let mut ctx = Context::new();
ctx.insert("model", &model); ctx.insert("model", &model);
ctx.insert("view_model", &ActixAdminViewModelSerializable::from(view_model.clone())); 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("entity_names", &actix_admin.entity_names);
ctx.insert("notifications", &notifications); ctx.insert("notifications", &notifications);

View File

@ -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<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>(req: HttpRequest, session: Session, data: web::Data<T>, params: web::Path<(i32, String)>) -> Result<HttpResponse, Error> {
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<crate::ActixAdminError> = 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::<String>(&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<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>(session: Session, data: web::Data<T>, params: web::Path<(i32, String)>) -> Result<HttpResponse, Error> {
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<crate::ActixAdminError> = 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::<String>(&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))
}

View File

@ -26,8 +26,8 @@ pub trait ActixAdminViewModelTrait {
fn get_entity_name() -> String; fn get_entity_name() -> String;
fn get_list_link(entity_name: &String) -> String { fn get_base_path(entity_name: &String) -> String {
format!("/admin/{}/list", entity_name) format!("/admin/{}", entity_name)
} }
} }
@ -61,7 +61,7 @@ impl From<ActixAdminViewModel> for ActixAdminViewModelSerializable {
} }
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub enum ActixAdminViewModelFieldType { pub enum ActixAdminViewModelFieldType {
Number, Number,
Text, Text,
@ -70,7 +70,8 @@ pub enum ActixAdminViewModelFieldType {
Date, Date,
Time, Time,
DateTime, DateTime,
SelectList SelectList,
FileUpload
} }
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
@ -83,13 +84,16 @@ pub struct ActixAdminViewModelField {
} }
impl ActixAdminViewModelFieldType { 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() { if !select_list.is_empty() {
return ActixAdminViewModelFieldType::SelectList; return ActixAdminViewModelFieldType::SelectList;
} }
if is_textarea { if is_textarea {
return ActixAdminViewModelFieldType::TextArea; return ActixAdminViewModelFieldType::TextArea;
} }
if is_file_upload {
return ActixAdminViewModelFieldType::FileUpload;
}
match type_path { match type_path {
"i32" => ActixAdminViewModelFieldType::Number, "i32" => ActixAdminViewModelFieldType::Number,

View File

@ -29,7 +29,7 @@
<button class="button is-link" type="submit">Save</i></button> <button class="button is-link" type="submit">Save</i></button>
</div> </div>
<div class="control"> <div class="control">
<a class="button is-link is-light" href="{{ list_link }}">Cancel</a> <a class="button is-link is-light" href="{{ base_path }}/list">Cancel</a>
</div> </div>
</div> </div>
</form> </form>

View File

@ -1,6 +1,5 @@
{% if model_field.field_type == "TextArea" %} {% if model_field.field_type == "TextArea" %}
<textarea <textarea class="{{ model_field | get_html_input_class }}
class="{{ model_field | get_html_input_class }}
{% if model.errors | length > 0 or model.custom_errors | length > 0 %} {% if model.errors | length > 0 or model.custom_errors | length > 0 %}
{% if {% if
model.errors | get(key=model_field.field_name, default="" ) !="" model.errors | get(key=model_field.field_name, default="" ) !=""
@ -8,15 +7,18 @@
model.custom_errors | get(key=model_field.field_name, default="" ) !="" model.custom_errors | get(key=model_field.field_name, default="" ) !=""
%}is-danger{% else %}is-success{% endif %} %}is-danger{% else %}is-success{% endif %}
{% endif %} {% endif %}
" " type="{{ model_field | get_html_input_type }}" name="{{ model_field.field_name }}"
type="{{ model_field | get_html_input_type }}"
name="{{ model_field.field_name }}"
placeholder="{{ model_field.field_name }}" placeholder="{{ model_field.field_name }}"
aria-label="{{ model_field.field_name }}" aria-label="{{ model_field.field_name }}">{{ model.values | get(key=model_field.field_name, default="") }}</textarea>
>{{ model.values | get(key=model_field.field_name, default="") }}</textarea> {% elif model_field.field_type == "FileUpload" and model.values | get(key=model_field.field_name, default="") != "" %}
<div>
<a hx-disable href="{{ base_path }}/static_content/{{ model.primary_key }}/{{ model_field.field_name }}">{{ model.values |
get(key=model_field.field_name, default="") }}</a>
<a class="is-pulled-right" hx-target="closest div" hx-push-url="false" hx-delete="{{ base_path }}/static_content/{{ model.primary_key }}/{{ model_field.field_name }}"
hx-confirm="Are you sure?"><i class="fa-solid fa-trash"></i></a>
</div>
{% else %} {% else %}
<input <input class="{{ model_field | get_html_input_class }}
class="{{ model_field | get_html_input_class }}
{% if model.errors | length > 0 or model.custom_errors | length > 0 %} {% if model.errors | length > 0 or model.custom_errors | length > 0 %}
{% if {% if
model.errors | get(key=model_field.field_name, default="" ) !="" model.errors | get(key=model_field.field_name, default="" ) !=""
@ -24,11 +26,7 @@
model.custom_errors | get(key=model_field.field_name, default="" ) !="" model.custom_errors | get(key=model_field.field_name, default="" ) !=""
%}is-danger{% else %}is-success{% endif %} %}is-danger{% else %}is-success{% endif %}
{% endif %} {% endif %}
" " type="{{ model_field | get_html_input_type }}"
type="{{ model_field | get_html_input_type }}" value="{{ model.values | get(key=model_field.field_name, default="") }}" name="{{ model_field.field_name }}"
value="{{ model.values | get(key=model_field.field_name, default="") }}" placeholder="{{ model_field.field_name }}" aria-label="{{ model_field.field_name }}">
name="{{ model_field.field_name }}"
placeholder="{{ model_field.field_name }}"
aria-label="{{ model_field.field_name }}"
>
{% endif %} {% endif %}

View File

@ -88,12 +88,14 @@
<td><input type="checkbox" name="ids" value="{{ entity.primary_key }}"></td> <td><input type="checkbox" name="ids" value="{{ entity.primary_key }}"></td>
<td> <td>
<a href="show/{{ entity.primary_key }}"> <a href="show/{{ entity.primary_key }}">
<i class="fa-solid fa-magnifying-glass"></i> {{ entity.primary_key }} <i class="fa-solid fa-magnifying-glass"></i> {{ entity.primary_key }}
</a> </a>
</td> </td>
{% for model_field in view_model.fields -%} {% for model_field in view_model.fields -%}
{% if model_field.field_type == "Checkbox" %} {% if model_field.field_type == "Checkbox" %}
<td>{{ entity.values | get(key=model_field.field_name) | get_icon | safe }}</td> <td>{{ entity.values | get(key=model_field.field_name) | get_icon | safe }}</td>
{% elif model_field.field_type == "FileUpload" %}
<td><a href="static_content/{{ entity.primary_key }}/{{ model_field.field_name }}">{{ entity.values | get(key=model_field.field_name) }}</a></td>
{% else %} {% else %}
<td>{{ entity.values | get(key=model_field.field_name) }}</td> <td>{{ entity.values | get(key=model_field.field_name) }}</td>
{% endif %} {% endif %}

View File

@ -8,6 +8,8 @@
<p> <p>
{% if model_field.field_type == "Checkbox" %} {% if model_field.field_type == "Checkbox" %}
<td>{{ model.values | get(key=model_field.field_name) | get_icon | safe }}</td> <td>{{ model.values | get(key=model_field.field_name) | get_icon | safe }}</td>
{% elif model_field.field_type == "FileUpload" %}
<td><a href="static_content/{{ entity.primary_key }}/{{ model_field.field_name }}">{{ entity.values | get(key=model_field.field_name) }}</a></td>
{% else %} {% else %}
<td>{{ model.values | get(key=model_field.field_name) }}</td> <td>{{ model.values | get(key=model_field.field_name) }}</td>
{% endif %} {% endif %}
@ -19,7 +21,7 @@
<div class="column"> <div class="column">
<div class="field is-grouped"> <div class="field is-grouped">
<div class="control"> <div class="control">
<a class="button is-link is-light" href="{{ list_link }}">Back</a> <a class="button is-link is-light" href="{{ base_path }}/list">Back</a>
</div> </div>
</div> </div>
</div> </div>

View File

@ -41,6 +41,7 @@ pub fn create_actix_admin_builder() -> ActixAdminBuilder {
user_is_logged_in: None, user_is_logged_in: None,
login_link: None, login_link: None,
logout_link: None, logout_link: None,
file_upload_directory: "./file_uploads"
}; };
let mut admin_builder = ActixAdminBuilder::new(configuration); let mut admin_builder = ActixAdminBuilder::new(configuration);
@ -78,8 +79,9 @@ async fn create_post_from_plaintext<
data: web::Data<T>, data: web::Data<T>,
text: String, text: String,
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, Error> {
let actix_admin = data.get_actix_admin();
let model = ActixAdminModel::from(text); let model = ActixAdminModel::from(text);
create_or_edit_post::<T, E>(&session, &data, Ok(model), None).await create_or_edit_post::<T, E>(&session, &data, Ok(model), None, actix_admin).await
} }
async fn edit_post_from_plaintext< async fn edit_post_from_plaintext<
@ -91,6 +93,7 @@ async fn edit_post_from_plaintext<
text: String, text: String,
id: web::Path<i32>, id: web::Path<i32>,
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, Error> {
let actix_admin = data.get_actix_admin();
let model = ActixAdminModel::from(text); let model = ActixAdminModel::from(text);
create_or_edit_post::<T, E>(&session, &data, Ok(model), Some(id.into_inner())).await create_or_edit_post::<T, E>(&session, &data, Ok(model), Some(id.into_inner()), actix_admin).await
} }