implement file_upload field
This commit is contained in:
parent
ce964220b7
commit
a600ff5c9c
3
.gitignore
vendored
3
.gitignore
vendored
@ -27,3 +27,6 @@ _site/
|
||||
# Ignore folders generated by Bundler
|
||||
.bundle/
|
||||
vendor/
|
||||
|
||||
# Ignore File Uploads
|
||||
file_uploads/
|
16
Cargo.toml
16
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"
|
||||
@ -32,3 +35,8 @@ derive_more = "0.99.17"
|
||||
|
||||
[dev-dependencies]
|
||||
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"] }
|
@ -14,7 +14,9 @@ pub mod derive_attr {
|
||||
pub html_input_type: Option<syn::LitStr>,
|
||||
pub select_list: Option<syn::LitStr>,
|
||||
pub searchable: Option<()>,
|
||||
pub textarea: Option<()>
|
||||
pub textarea: Option<()>,
|
||||
pub file_upload: Option<()>,
|
||||
pub not_empty: Option<()>
|
||||
//pub inner_type: Option<syn::Type>,
|
||||
|
||||
// Anything that implements `syn::parse::Parse` is supported.
|
||||
|
@ -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
|
||||
|
@ -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<Type>,
|
||||
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 {
|
||||
|
@ -41,6 +41,12 @@ pub fn filter_fields(fields: &Fields) -> Vec<ModelField> {
|
||||
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<ModelField> {
|
||||
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<ModelField>) -> Vec<TokenStr
|
||||
.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> {
|
||||
fields
|
||||
.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 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()) {
|
||||
(_, "DateTime") => {
|
||||
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<ModelField>) -> Vec<TokenStream>
|
||||
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<ModelField>) -> Vec<TokenStream> {
|
||||
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())
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -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)"
|
@ -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.");
|
@ -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 = "../../" }
|
@ -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::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;
|
@ -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<Tea>,
|
||||
pub insert_date: Date,
|
||||
#[actix_admin(file_upload)]
|
||||
pub attachment: String
|
||||
}
|
||||
|
||||
impl Display for Model {
|
@ -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);
|
@ -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>
|
@ -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" }
|
@ -1 +0,0 @@
|
||||
Rename .env.example to .env and update the oauth client credentials
|
@ -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::<T, E>))
|
||||
.route("/delete/{id}", web::delete().to(delete::<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)),
|
||||
);
|
||||
|
||||
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(),
|
||||
|
@ -96,6 +96,7 @@ pub fn get_html_input_type<S: BuildHasher>(value: &tera::Value, _: &HashMap<Stri
|
||||
ActixAdminViewModelFieldType::DateTime => "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<for<'a> fn(&'a Session) -> bool>,
|
||||
pub login_link: Option<String>,
|
||||
pub logout_link: Option<String>
|
||||
pub logout_link: Option<String>,
|
||||
pub file_upload_directory: &'static str
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
|
125
src/model.rs
125
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<ActixAdminModel>), 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<String>,
|
||||
pub values: HashMap<String, String>,
|
||||
pub errors: HashMap<String, String>,
|
||||
pub custom_errors: HashMap<String, String>
|
||||
pub custom_errors: HashMap<String, String>,
|
||||
}
|
||||
|
||||
|
||||
impl ActixAdminModel {
|
||||
pub fn create_empty() -> 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();
|
||||
|
||||
while let Some(item) = payload.next().await {
|
||||
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 {
|
||||
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<T: std::str::FromStr>(&self, key: &str, is_option_or_string: bool) -> Result<Option<T>, String> {
|
||||
self.get_value_by_closure(key, is_option_or_string, |val| val.parse::<T>())
|
||||
pub fn get_value<T: std::str::FromStr>(
|
||||
&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> {
|
||||
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<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> {
|
||||
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<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> {
|
||||
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<Option<bool>, 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<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 res: Result<Option<T>, 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,9 +171,10 @@ 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
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -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("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("notifications", ¬ifications);
|
||||
|
||||
|
@ -1,23 +1,28 @@
|
||||
use super::{render_unauthorized, user_can_access_page};
|
||||
use crate::prelude::*;
|
||||
use crate::ActixAdminError;
|
||||
use crate::ActixAdminNotification;
|
||||
use crate::prelude::*;
|
||||
use crate::TERA;
|
||||
use actix_multipart::Multipart;
|
||||
use actix_multipart::MultipartError;
|
||||
use actix_session::Session;
|
||||
use actix_web::http::header;
|
||||
use actix_web::{error, web, Error, HttpResponse};
|
||||
use tera::Context;
|
||||
use actix_multipart::Multipart;
|
||||
use std::collections::HashMap;
|
||||
use tera::Context;
|
||||
|
||||
pub async fn create_post<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>(
|
||||
session: Session,
|
||||
data: web::Data<T>,
|
||||
payload: Multipart,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let model = ActixAdminModel::create_from_payload(payload).await;
|
||||
create_or_edit_post::<T, E>(&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::<T, E>(&session, &data, model, None, actix_admin).await
|
||||
}
|
||||
|
||||
pub async fn edit_post<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>(
|
||||
@ -26,8 +31,13 @@ pub async fn edit_post<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>(
|
||||
payload: Multipart,
|
||||
id: web::Path<i32>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let model = ActixAdminModel::create_from_payload(payload).await;
|
||||
create_or_edit_post::<T, E>(&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::<T, E>(&session, &data, model, Some(id.into_inner()), actix_admin).await
|
||||
}
|
||||
|
||||
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>,
|
||||
model_res: Result<ActixAdminModel, MultipartError>,
|
||||
id: Option<i32>,
|
||||
actix_admin: &ActixAdmin,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
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,14 +72,12 @@ pub async fn create_or_edit_post<T: ActixAdminAppDataTrait, E: ActixAdminViewMod
|
||||
};
|
||||
|
||||
match res {
|
||||
Ok(_) => {
|
||||
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::<E>(actix_admin, view_model, db, entity_name, &model, errors).await
|
||||
@ -78,7 +86,14 @@ pub async fn create_or_edit_post<T: ActixAdminAppDataTrait, E: ActixAdminViewMod
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
ctx.insert("entity_names", &actix_admin.entity_names);
|
||||
ctx.insert(
|
||||
@ -86,10 +101,11 @@ async fn render_form<E: ActixAdminViewModelTrait>(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<ActixAdminNotification> = errors.into_iter()
|
||||
let notifications: Vec<ActixAdminNotification> = errors
|
||||
.into_iter()
|
||||
.map(|err| ActixAdminNotification::from(err))
|
||||
.collect();
|
||||
|
||||
|
@ -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<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>(
|
||||
}
|
||||
|
||||
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::<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
|
||||
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::<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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -18,3 +18,6 @@ pub use delete::{ delete, delete_many };
|
||||
|
||||
mod helpers;
|
||||
pub use helpers::{ add_auth_context, user_can_access_page, render_unauthorized };
|
||||
|
||||
mod static_content;
|
||||
pub use static_content::{download, delete_static_content};
|
@ -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<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 db = &data.get_db();
|
||||
let result = E::get_entity(db, id.into_inner()).await;
|
||||
|
||||
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 {
|
||||
Ok(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() {
|
||||
false => HttpResponse::InternalServerError(),
|
||||
true => HttpResponse::Ok()
|
||||
@ -38,10 +40,9 @@ pub async fn show<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>(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);
|
||||
|
||||
|
85
src/routes/static_content.rs
Normal file
85
src/routes/static_content.rs
Normal 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))
|
||||
|
||||
}
|
@ -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<ActixAdminViewModel> 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,
|
||||
|
@ -29,7 +29,7 @@
|
||||
<button class="button is-link" type="submit">Save</i></button>
|
||||
</div>
|
||||
<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>
|
||||
</form>
|
||||
|
@ -1,6 +1,5 @@
|
||||
{% if model_field.field_type == "TextArea" %}
|
||||
<textarea
|
||||
class="{{ model_field | get_html_input_class }}
|
||||
<textarea class="{{ model_field | get_html_input_class }}
|
||||
{% if model.errors | length > 0 or model.custom_errors | length > 0 %}
|
||||
{% if
|
||||
model.errors | get(key=model_field.field_name, default="" ) !=""
|
||||
@ -8,15 +7,18 @@
|
||||
model.custom_errors | get(key=model_field.field_name, default="" ) !=""
|
||||
%}is-danger{% else %}is-success{% 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 }}"
|
||||
aria-label="{{ model_field.field_name }}"
|
||||
>{{ model.values | get(key=model_field.field_name, default="") }}</textarea>
|
||||
aria-label="{{ model_field.field_name }}">{{ 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 %}
|
||||
<input
|
||||
class="{{ model_field | get_html_input_class }}
|
||||
<input class="{{ model_field | get_html_input_class }}
|
||||
{% if model.errors | length > 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 %}
|
@ -94,6 +94,8 @@
|
||||
{% for model_field in view_model.fields -%}
|
||||
{% if model_field.field_type == "Checkbox" %}
|
||||
<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 %}
|
||||
<td>{{ entity.values | get(key=model_field.field_name) }}</td>
|
||||
{% endif %}
|
||||
|
@ -8,6 +8,8 @@
|
||||
<p>
|
||||
{% if model_field.field_type == "Checkbox" %}
|
||||
<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 %}
|
||||
<td>{{ model.values | get(key=model_field.field_name) }}</td>
|
||||
{% endif %}
|
||||
@ -19,7 +21,7 @@
|
||||
<div class="column">
|
||||
<div class="field is-grouped">
|
||||
<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>
|
||||
|
@ -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<T>,
|
||||
text: String,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let actix_admin = data.get_actix_admin();
|
||||
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<
|
||||
@ -91,6 +93,7 @@ async fn edit_post_from_plaintext<
|
||||
text: String,
|
||||
id: web::Path<i32>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let actix_admin = data.get_actix_admin();
|
||||
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
|
||||
}
|
Loading…
Reference in New Issue
Block a user