Compare commits

...

22 Commits

Author SHA1 Message Date
c2fc9cda9b Rebase to master 2023-09-14 15:34:34 +02:00
c473e35566 Fix field name 2023-09-14 15:26:29 +02:00
ae072d1812 Trim input name 2023-09-14 15:25:44 +02:00
40fd0863fc Trim input name 2023-09-14 15:25:42 +02:00
ad79a3852a trim html 2023-09-14 15:22:50 +02:00
69f294ecf4 trim html 2023-09-14 15:22:50 +02:00
f79b952f8a Model 2023-09-14 15:22:50 +02:00
eb35037bca Display validation errors 2023-09-14 15:22:49 +02:00
0a85d78a9b Add missing param 2023-09-14 15:22:19 +02:00
75b3a112a5 Fix feature 2023-09-14 15:22:19 +02:00
02a7a36ac7 Update lib.rs 2023-09-14 15:22:19 +02:00
2f6ca86aa3 Update index.rs 2023-09-14 15:22:19 +02:00
f19bbe39e8 Update create_or_edit_get.rs 2023-09-14 15:22:19 +02:00
d6795a3725 Update create_or_edit_post.rs 2023-09-14 15:22:19 +02:00
270fe7fee1 Update create_or_edit_post.rs 2023-09-14 15:22:19 +02:00
d411355bec Update show.rs 2023-09-14 15:22:19 +02:00
da466306e1 Update create_or_edit_post.rs 2023-09-14 15:22:19 +02:00
27b6889a8b Update index.rs 2023-09-14 15:22:19 +02:00
4f01634b0e Update Cargo.toml 2023-09-14 15:22:17 +02:00
Manuel Gugger
f82b331f98 move tera template into cfg feature to allow for other templates 2023-08-24 17:46:30 +02:00
Manuel Gugger
defe9cdd90
Merge pull request #5 from keithamus/upgrade-to-sea-orm-0-12-and-fix-code
upgrade to sea-orm 0.12 and fix code
2023-08-24 14:26:58 +02:00
Keith Cirkel
81cb448eb8 upgrade to sea-orm 0.12 and fix code 2023-08-23 20:46:45 +01:00
39 changed files with 787 additions and 470 deletions

View File

@ -18,6 +18,11 @@ exclude = [
name = "actix_admin"
path = "src/lib.rs"
[features]
default = ["bulma_css", 'enable-tracing']
bulma_css = []
enable-tracing = ['tracing']
[dependencies]
actix-web = "^4.3.1"
actix-session = { version = "^0.7.2", features = [] }
@ -28,19 +33,20 @@ chrono = "0.4.26"
tera = "^1.19.0"
async-trait = "^0.1.68"
lazy_static = "^1.4.0"
itertools = "^0.10.5"
itertools = "^0.11.0"
serde = "^1.0.164"
serde_derive = "^1.0.164"
sea-orm = { version = "^0.11.3", features = [], default-features = false }
sea-orm = { version = "^0.12.2", features = [], default-features = false }
actix-admin-macros = { version = "0.5.0", path = "actix_admin_macros" }
derive_more = "0.99.17"
regex = "1.8.4"
urlencoding = "2.1.2"
tracing = { version = "0.1", optional = true }
[dev-dependencies]
sea-orm = { version = "^0.11.3", features = [ "sqlx-sqlite", "runtime-actix-native-tls", "macros" ], default-features = true }
sea-orm = { version = "^0.12.2", features = [ "sqlx-sqlite", "runtime-actix-native-tls", "macros" ], default-features = true }
actix-rt = "2.8.0"
azure_auth = { path = "./examples/azure_auth/azure_auth" }
oauth2 = "4.4.1"
dotenv = "0.15"
actix-session = { version = "0.7.2", features = ["cookie-session"] }
actix-session = { version = "0.7.2", features = ["cookie-session"] }

View File

@ -253,7 +253,7 @@ pub fn derive_actix_admin_model(input: proc_macro::TokenStream) -> proc_macro::T
for (field_name, html_input_type, select_list, is_option_list, fields_type_path, is_textarea, is_file_upload, list_sort_position, list_hide_column, list_regex_mask) in actix_admin::prelude::izip!(&field_names, &html_input_types, &field_select_lists, is_option_lists, fields_type_paths, fields_textareas, fields_fileupload, list_sort_positions, list_hide_columns, list_regex_masks) {
let select_list = select_list.replace('"', "").replace(' ', "").to_string();
let field_name = field_name.replace('"', "").replace(' ', "").to_string();
let field_name = field_name.replace('"', "").replace(' ', "").trim().to_string();
let html_input_type = html_input_type.replace('"', "").replace(' ', "").to_string();
let mut list_regex_mask_regex = None;
if list_regex_mask != "" {
@ -261,8 +261,8 @@ pub fn derive_actix_admin_model(input: proc_macro::TokenStream) -> proc_macro::T
};
vec.push(ActixAdminViewModelField {
field_name: field_name,
html_input_type: html_input_type,
field_name,
html_input_type,
select_list: select_list.clone(),
is_option: is_option_list,
list_sort_position: list_sort_position,

View File

@ -37,7 +37,7 @@ impl ModelField {
pub fn get_type_path_string(&self) -> String {
let type_path_string: String;
if self.is_option() {
match &self.inner_type.clone().unwrap() {
match &self.inner_type.as_ref().unwrap() {
Type::Path(type_path) => type_path_string = type_path.clone().into_token_stream().to_string(),
_ => panic!("not a type path")
}
@ -50,4 +50,4 @@ impl ModelField {
type_path_string
}
}
}

View File

@ -87,17 +87,17 @@ pub fn filter_fields(fields: &Fields) -> Vec<ModelField> {
visibility: field_vis,
ident: field_ident,
ty: field_ty,
inner_type: inner_type,
inner_type,
primary_key: is_primary_key,
html_input_type: html_input_type,
select_list: select_list,
html_input_type,
select_list,
searchable: is_searchable,
textarea: is_textarea,
file_upload: is_file_upload,
not_empty: is_not_empty,
list_sort_position: list_sort_position,
list_sort_position,
list_hide_column: is_list_hide_column,
list_regex_mask: list_regex_mask
list_regex_mask,
};
Some(model_field)
} else {
@ -187,7 +187,7 @@ pub fn get_actix_admin_fields_searchable(fields: &Vec<ModelField>) -> Vec<TokenS
let column_name = capitalize_first_letter(&model_field.ident.to_string());
let column_ident = Ident::new(&column_name, Span::call_site());
quote! {
.add(Column::#column_ident.contains(&search))
.add(Column::#column_ident.contains(search))
}
})
.collect::<Vec<_>>()

View File

@ -46,7 +46,7 @@ impl Related<super::comment::Entity> for Entity {
impl ActiveModelBehavior for ActiveModel {}
#[derive(Debug, Clone, PartialEq, EnumIter, DeriveActiveEnum, Deserialize, Serialize, DeriveActixAdminEnumSelectList)]
#[derive(Debug, Clone, PartialEq, EnumIter, DeriveDisplay, DeriveActiveEnum, Deserialize, Serialize, DeriveActixAdminEnumSelectList)]
#[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "tea")]
pub enum Tea {
#[sea_orm(string_value = "EverydayTea")]
@ -69,4 +69,4 @@ impl FromStr for Tea {
impl ActixAdminModelValidationTrait<ActiveModel> for Entity {}
impl ActixAdminModelFilterTrait<Entity> for Entity {}
impl ActixAdminModelFilterTrait<Entity> for Entity {}

View File

@ -51,7 +51,7 @@ impl Related<super::comment::Entity> for Entity {
impl ActiveModelBehavior for ActiveModel {}
#[derive(Debug, Clone, PartialEq, EnumIter, DeriveActiveEnum, Deserialize, Serialize, DeriveActixAdminEnumSelectList)]
#[derive(Debug, Clone, PartialEq, EnumIter, DeriveDisplay, DeriveActiveEnum, Deserialize, Serialize, DeriveActixAdminEnumSelectList)]
#[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "tea")]
pub enum Tea {
#[sea_orm(string_value = "EverydayTea")]
@ -74,4 +74,4 @@ impl FromStr for Tea {
impl ActixAdminModelValidationTrait<ActiveModel> for Entity {}
impl ActixAdminModelFilterTrait<Entity> for Entity {}
impl ActixAdminModelFilterTrait<Entity> for Entity {}

View File

@ -1,13 +1,8 @@
use crate::{prelude::*, ActixAdminMenuElement, routes::delete_file};
use crate::{prelude::*, ActixAdminMenuElement};
use actix_web::{web, Route };
use tera::Tera;
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, download
};
use std::hash::BuildHasher;
use tera::{to_value, try_get_value, Result};
use crate::routes::*;
/// Represents a builder entity which helps generating the ActixAdmin configuration
pub struct ActixAdminBuilder {
@ -24,9 +19,7 @@ pub trait ActixAdminBuilderTrait {
&mut self,
view_model: &ActixAdminViewModel,
);
fn add_entity_to_category<
E: ActixAdminViewModelTrait + 'static,
>(
fn add_entity_to_category<E: ActixAdminViewModelTrait + 'static>(
&mut self,
view_model: &ActixAdminViewModel,
category_name: &str,
@ -44,20 +37,16 @@ pub trait ActixAdminBuilderTrait {
path: &str,
route: Route,
add_to_menu: bool,
category: &str
category: &str,
);
fn add_custom_handler_for_entity<
E: ActixAdminViewModelTrait + 'static,
>(
fn add_custom_handler_for_entity<E: ActixAdminViewModelTrait + 'static>(
&mut self,
menu_element_name: &str,
path: &str,
route: Route,
add_to_menu: bool
add_to_menu: bool,
);
fn add_custom_handler_for_entity_in_category<
E: ActixAdminViewModelTrait + 'static,
>(
fn add_custom_handler_for_entity_in_category<E: ActixAdminViewModelTrait + 'static>(
&mut self,
menu_element_name: &str,
path: &str,
@ -70,141 +59,14 @@ pub trait ActixAdminBuilderTrait {
fn get_actix_admin(&self) -> ActixAdmin;
}
fn get_html_input_class<S: BuildHasher>(
value: &tera::Value,
_: &HashMap<String, tera::Value, S>,
) -> Result<tera::Value> {
let field = try_get_value!(
"get_html_input_class",
"value",
ActixAdminViewModelField,
value
);
let html_input_type = match field.field_type {
ActixAdminViewModelFieldType::TextArea => "textarea",
ActixAdminViewModelFieldType::Checkbox => "checkbox",
_ => "input",
};
Ok(to_value(html_input_type).unwrap())
}
fn get_icon<S: BuildHasher>(
value: &tera::Value,
_: &HashMap<String, tera::Value, S>,
) -> Result<tera::Value> {
let field = try_get_value!("get_icon", "value", String, value);
let font_awesome_icon = match field.as_str() {
"true" => "<i class=\"fa-solid fa-check\"></i>",
"false" => "<i class=\"fa-solid fa-xmark\"></i>",
_ => panic!("not implemented icon"),
};
Ok(to_value(font_awesome_icon).unwrap())
}
fn get_regex_val<S: BuildHasher>(
value: &tera::Value,
args: &HashMap<String, tera::Value, S>,
) -> Result<tera::Value> {
let field = try_get_value!("get_regex_val", "value", ActixAdminViewModelField, value);
let s = args.get("values");
let field_val = s.unwrap().get(&field.field_name);
println!("field {} regex {:?}", field.field_name, field.list_regex_mask);
match (field_val, field.list_regex_mask) {
(Some(val), Some(r)) => {
let val_str = val.to_string();
let is_match = r.is_match(&val_str);
println!("is match: {}, regex {}", is_match, r.to_string());
let result_str = r.replace_all(&val_str, "*");
return Ok(to_value(result_str).unwrap());
},
(Some(val), None) => { return Ok(to_value(val).unwrap()); },
(_, _) => panic!("key {} not found in model values", &field.field_name)
}
}
fn get_html_input_type<S: BuildHasher>(
value: &tera::Value,
_: &HashMap<String, tera::Value, S>,
) -> Result<tera::Value> {
let field = try_get_value!(
"get_html_input_type",
"value",
ActixAdminViewModelField,
value
);
// TODO: convert to option
if field.html_input_type != "" {
return Ok(to_value(field.html_input_type).unwrap());
}
let html_input_type = match field.field_type {
ActixAdminViewModelFieldType::Text => "text",
ActixAdminViewModelFieldType::DateTime => "datetime-local",
ActixAdminViewModelFieldType::Date => "date",
ActixAdminViewModelFieldType::Checkbox => "checkbox",
ActixAdminViewModelFieldType::FileUpload => "file",
_ => "text",
};
Ok(to_value(html_input_type).unwrap())
}
fn get_tera() -> Tera {
let mut tera = Tera::new(concat!(env!("CARGO_MANIFEST_DIR"), "/src/templates/*.html")).unwrap();
tera.register_filter("get_html_input_type", get_html_input_type);
tera.register_filter("get_html_input_class", get_html_input_class);
tera.register_filter("get_icon", get_icon);
tera.register_filter("get_regex_val", get_regex_val);
let list_html = include_str!("templates/list.html");
let create_or_edit_html = include_str!("templates/create_or_edit.html");
let base_html = include_str!("templates/base.html");
let head_html = include_str!("templates/head.html");
let index_html = include_str!("templates/index.html");
let loader_html = include_str!("templates/loader.html");
let navbar_html = include_str!("templates/navbar.html");
let not_found_html = include_str!("templates/not_found.html");
let show_html = include_str!("templates/show.html");
let unauthorized_html = include_str!("templates/unauthorized.html");
// form elements
let checkbox_html = include_str!("templates/form_elements/checkbox.html");
let input_html = include_str!("templates/form_elements/input.html");
let selectlist_html = include_str!("templates/form_elements/selectlist.html");
let _res = tera.add_raw_templates(vec![
("base.html", base_html),
("list.html", list_html),
("create_or_edit.html", create_or_edit_html),
("head.html", head_html),
("index.html", index_html),
("loader.html", loader_html),
("navbar.html", navbar_html),
("not_found.html", not_found_html),
("show.html",show_html),
("unauthorized.html", unauthorized_html),
// form elements
("form_elements/checkbox.html", checkbox_html),
("form_elements/input.html", input_html),
("form_elements/selectlist.html", selectlist_html),
]);
tera
}
impl ActixAdminBuilderTrait for ActixAdminBuilder {
fn new(configuration: ActixAdminConfiguration) -> Self {
ActixAdminBuilder {
actix_admin: ActixAdmin {
entity_names: HashMap::new(),
view_models: HashMap::new(),
configuration: configuration,
tera: get_tera()
tera: crate::tera_templates::get_tera(),
configuration,
},
custom_routes: Vec::new(),
scopes: HashMap::new(),
@ -219,9 +81,7 @@ impl ActixAdminBuilderTrait for ActixAdminBuilder {
let _ = &self.add_entity_to_category::<E>(view_model, "");
}
fn add_entity_to_category<
E: ActixAdminViewModelTrait + 'static,
>(
fn add_entity_to_category<E: ActixAdminViewModelTrait + 'static>(
&mut self,
view_model: &ActixAdminViewModel,
category_name: &str,
@ -238,11 +98,19 @@ impl ActixAdminBuilderTrait for ActixAdminBuilder {
.route("/delete/{id}", web::delete().to(delete::<E>))
.route("/show/{id}", web::get().to(show::<E>))
.route("/file/{id}/{column_name}", web::get().to(download::<E>))
.route("/file/{id}/{column_name}", web::delete().to(delete_file::<E>))
.default_service(web::to(not_found))
);
.route(
"/file/{id}/{column_name}",
web::delete().to(delete_file::<E>),
)
.default_service(web::to(not_found)),
);
fs::create_dir_all(format!("{}/{}", &self.actix_admin.configuration.file_upload_directory, E::get_entity_name())).unwrap();
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 {
@ -253,11 +121,9 @@ impl ActixAdminBuilderTrait for ActixAdminBuilder {
match category {
Some(entity_list) => entity_list.push(menu_element),
None => {
let mut entity_list = Vec::new();
entity_list.push(menu_element);
self.actix_admin
.entity_names
.insert(category_name.to_string(), entity_list);
.insert(category_name.to_string(), vec![menu_element]);
}
}
@ -275,7 +141,7 @@ impl ActixAdminBuilderTrait for ActixAdminBuilder {
path: &str,
route: Route,
add_to_menu: bool,
category_name: &str
category_name: &str,
) {
self.custom_routes.push((path.to_string(), route));
@ -298,7 +164,7 @@ impl ActixAdminBuilderTrait for ActixAdminBuilder {
self.actix_admin
.entity_names
.insert(category_name.to_string(), entity_list);
},
}
}
}
}
@ -308,14 +174,12 @@ impl ActixAdminBuilderTrait for ActixAdminBuilder {
menu_element_name: &str,
path: &str,
route: Route,
add_to_menu: bool
add_to_menu: bool,
) {
self.add_custom_handler_to_category(menu_element_name, path, route, add_to_menu, "");
}
fn add_custom_handler_for_entity<
E: ActixAdminViewModelTrait + 'static,
>(
fn add_custom_handler_for_entity<E: ActixAdminViewModelTrait + 'static>(
&mut self,
menu_element_name: &str,
path: &str,
@ -331,9 +195,7 @@ impl ActixAdminBuilderTrait for ActixAdminBuilder {
);
}
fn add_custom_handler_for_entity_in_category<
E: ActixAdminViewModelTrait + 'static,
>(
fn add_custom_handler_for_entity_in_category<E: ActixAdminViewModelTrait + 'static>(
&mut self,
menu_element_name: &str,
path: &str,
@ -352,8 +214,7 @@ impl ActixAdminBuilderTrait for ActixAdminBuilder {
match existing_scope {
Some(scope) => {
let existing_scope = scope.route(path, route);
self.scopes
.insert(E::get_entity_name(), existing_scope);
self.scopes.insert(E::get_entity_name(), existing_scope);
}
_ => {
let new_scope =
@ -398,4 +259,4 @@ impl ActixAdminBuilderTrait for ActixAdminBuilder {
fn get_actix_admin(&self) -> ActixAdmin {
self.actix_admin.clone()
}
}
}

View File

@ -13,22 +13,26 @@ use actix_web::{
use async_trait::async_trait;
use derive_more::{Display, Error};
use sea_orm::DatabaseConnection;
use serde_derive::{Serialize};
use tera::Tera;
use serde_derive::Serialize;
use std::collections::HashMap;
use tera::Tera;
pub mod builder;
pub mod model;
pub mod routes;
pub mod view_model;
pub mod tera_templates;
pub mod prelude {
pub use crate::builder::{ActixAdminBuilder, ActixAdminBuilderTrait};
pub use crate::model::{ActixAdminModel, ActixAdminModelTrait, ActixAdminModelValidationTrait, ActixAdminModelFilter, ActixAdminModelFilterTrait, ActixAdminModelFilterType};
pub use crate::model::{
ActixAdminModel, ActixAdminModelFilter, ActixAdminModelFilterTrait,
ActixAdminModelFilterType, ActixAdminModelTrait, ActixAdminModelValidationTrait,
};
pub use crate::routes::{create_or_edit_post, get_admin_ctx, SortOrder};
pub use crate::view_model::{
ActixAdminViewModel, ActixAdminViewModelField, ActixAdminViewModelFieldType,
ActixAdminViewModelSerializable, ActixAdminViewModelTrait, ActixAdminViewModelFilter
ActixAdminViewModelFilter, ActixAdminViewModelSerializable, ActixAdminViewModelTrait,
};
pub use crate::{hashmap, ActixAdminSelectListTrait};
pub use crate::{ActixAdmin, ActixAdminConfiguration, ActixAdminError};
@ -68,7 +72,7 @@ pub struct ActixAdminConfiguration {
pub login_link: Option<String>,
pub logout_link: Option<String>,
pub file_upload_directory: &'static str,
pub navbar_title: &'static str
pub navbar_title: &'static str,
}
#[derive(Clone)]
@ -76,7 +80,7 @@ pub struct ActixAdmin {
pub entity_names: HashMap<String, Vec<ActixAdminMenuElement>>,
pub view_models: HashMap<String, ActixAdminViewModel>,
pub configuration: ActixAdminConfiguration,
pub tera: Tera
pub tera: Tera,
}
#[derive(PartialEq, Eq, Clone, Serialize)]
@ -116,6 +120,8 @@ pub enum ActixAdminError {
impl error::ResponseError for ActixAdminError {
fn error_response(&self) -> HttpResponse {
#[cfg(feature = "enable-tracing")]
tracing::debug!("{self:#?}");
HttpResponse::build(self.status_code())
.insert_header(ContentType::html())
.body(self.to_string())
@ -123,6 +129,7 @@ impl error::ResponseError for ActixAdminError {
fn status_code(&self) -> StatusCode {
match *self {
Self::ValidationErrors => StatusCode::BAD_REQUEST,
_ => StatusCode::INTERNAL_SERVER_ERROR,
}
}

View File

@ -22,7 +22,7 @@ pub trait ActixAdminModelTrait {
filter_values: HashMap<String, Option<String>>,
search: &str,
sort_by: &str,
sort_order: &SortOrder
sort_order: &SortOrder,
) -> Result<(u64, Vec<ActixAdminModel>), ActixAdminError>;
fn get_fields() -> &'static [ActixAdminViewModelField];
fn validate_model(model: &mut ActixAdminModel);
@ -38,7 +38,7 @@ pub struct ActixAdminModelFilter<E: EntityTrait> {
pub name: String,
pub filter_type: ActixAdminModelFilterType,
pub filter: fn(sea_orm::Select<E>, Option<String>) -> sea_orm::Select<E>,
pub values: Option<Vec<(String, String)>>
pub values: Option<Vec<(String, String)>>,
}
#[derive(Clone, Debug, Serialize)]
@ -47,7 +47,7 @@ pub enum ActixAdminModelFilterType {
SelectList,
Date,
DateTime,
Checkbox
Checkbox,
}
#[async_trait]
@ -55,7 +55,10 @@ pub trait ActixAdminModelFilterTrait<E: EntityTrait> {
fn get_filter() -> Vec<ActixAdminModelFilter<E>> {
Vec::new()
}
async fn get_filter_values(_filter: &ActixAdminModelFilter<E>, _db: &DatabaseConnection)-> Option<Vec<(String, String)>> {
async fn get_filter_values(
_filter: &ActixAdminModelFilter<E>,
_db: &DatabaseConnection,
) -> Option<Vec<(String, String)>> {
None
}
}
@ -66,7 +69,7 @@ impl<T: EntityTrait> From<ActixAdminModelFilter<T>> for ActixAdminViewModelFilte
name: filter.name,
value: None,
values: None,
filter_type: Some(filter.filter_type)
filter_type: Some(filter.filter_type),
}
}
}
@ -90,13 +93,20 @@ impl ActixAdminModel {
}
pub async fn create_from_payload(
mut payload: Multipart, file_upload_folder: &str
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?;
eprintln!(
"FIELD NAME {:?}, FILENAME {:?}",
field.name(),
field.content_disposition().get_filename()
);
let mut binary_data: Vec<Bytes> = Vec::new();
while let Some(chunk) = field.next().await {
binary_data.push(chunk.unwrap());
@ -115,21 +125,28 @@ impl ActixAdminModel {
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);
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()
);
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().trim().to_string(),
res_string.unwrap(),
);
}
}
}
@ -146,16 +163,18 @@ impl ActixAdminModel {
&self,
key: &str,
is_option_or_string: bool,
is_allowed_to_be_empty: 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>())
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,
is_allowed_to_be_empty: 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")
@ -166,21 +185,27 @@ impl ActixAdminModel {
&self,
key: &str,
is_option_or_string: bool,
is_allowed_to_be_empty: 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, 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)
}
});
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),

View File

@ -1,14 +1,14 @@
use actix_web::{error, web, Error, HttpRequest, HttpResponse};
use sea_orm::DatabaseConnection;
use tera::{Context};
use actix_session::{Session};
use crate::prelude::*;
use crate::ActixAdminError;
use crate::ActixAdminNotification;
use crate::prelude::*;
use actix_session::Session;
use actix_web::{error, web, Error, HttpRequest, HttpResponse};
use sea_orm::DatabaseConnection;
use tera::Context;
use super::DEFAULT_ENTITIES_PER_PAGE;
use super::Params;
use super::{ add_auth_context, user_can_access_page, render_unauthorized};
use super::DEFAULT_ENTITIES_PER_PAGE;
use super::{add_auth_context, render_unauthorized, user_can_access_page};
pub async fn create_get<E: ActixAdminViewModelTrait>(
session: Session,
@ -20,7 +20,7 @@ pub async fn create_get<E: ActixAdminViewModelTrait>(
) -> Result<HttpResponse, Error> {
let db = db.get_ref();
let model = ActixAdminModel::create_empty();
create_or_edit_get::<E>(&session, req, &data, db, Ok(model)).await
}
@ -30,7 +30,7 @@ pub async fn edit_get<E: ActixAdminViewModelTrait>(
data: web::Data<ActixAdmin>,
db: web::Data<DatabaseConnection>,
_text: String,
id: web::Path<i32>
id: web::Path<i32>,
) -> Result<HttpResponse, Error> {
let db = db.get_ref();
let model = E::get_entity(db, id.into_inner()).await;
@ -38,7 +38,13 @@ pub async fn edit_get<E: ActixAdminViewModelTrait>(
create_or_edit_get::<E>(&session, req, &data, db, model).await
}
async fn create_or_edit_get<E: ActixAdminViewModelTrait>(session: &Session, req: HttpRequest, data: &web::Data<ActixAdmin>, db: &sea_orm::DatabaseConnection, model_result: Result<ActixAdminModel, ActixAdminError>) -> Result<HttpResponse, Error>{
async fn create_or_edit_get<E: ActixAdminViewModelTrait>(
session: &Session,
req: HttpRequest,
data: &web::Data<ActixAdmin>,
db: &sea_orm::DatabaseConnection,
model_result: Result<ActixAdminModel, ActixAdminError>,
) -> Result<HttpResponse, Error> {
let actix_admin = &data.get_ref();
let mut ctx = Context::new();
add_auth_context(&session, actix_admin, &mut ctx);
@ -57,8 +63,10 @@ async fn create_or_edit_get<E: ActixAdminViewModelTrait>(session: &Session, req:
match model_result {
Ok(res) => {
model = res;
},
}
Err(e) => {
#[cfg(feature = "enable-tracing")]
tracing::error!("{e}");
errors.push(e);
model = ActixAdminModel::create_empty();
}
@ -67,9 +75,10 @@ async fn create_or_edit_get<E: ActixAdminViewModelTrait>(session: &Session, req:
let mut http_response_code = match errors.is_empty() {
true => HttpResponse::Ok(),
false => HttpResponse::InternalServerError(),
};
let notifications: Vec<ActixAdminNotification> = errors.into_iter()
.map(|err| ActixAdminNotification::from(err))
};
let notifications: Vec<ActixAdminNotification> = errors
.into_iter()
.map(ActixAdminNotification::from)
.collect();
let params = web::Query::<Params>::from_query(req.query_string()).unwrap();
@ -80,10 +89,16 @@ async fn create_or_edit_get<E: ActixAdminViewModelTrait>(session: &Session, req:
.unwrap_or(DEFAULT_ENTITIES_PER_PAGE);
let render_partial = req.headers().contains_key("HX-Target");
let search = params.search.clone().unwrap_or(String::new());
let sort_by = params.sort_by.clone().unwrap_or(view_model.primary_key.to_string());
let sort_by = params
.sort_by
.clone()
.unwrap_or(view_model.primary_key.to_string());
let sort_order = params.sort_order.as_ref().unwrap_or(&SortOrder::Asc);
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("base_path", &E::get_base_path(&entity_name));
ctx.insert("model", &model);
@ -94,9 +109,14 @@ async fn create_or_edit_get<E: ActixAdminViewModelTrait>(session: &Session, req:
ctx.insert("sort_by", &sort_by);
ctx.insert("sort_order", &sort_order);
ctx.insert("page", &page);
let body = actix_admin.tera
let body = actix_admin
.tera
.render("create_or_edit.html", &ctx)
.map_err(|err| error::ErrorInternalServerError(err))?;
.map_err(|err| {
#[cfg(feature = "enable-tracing")]
tracing::error!("{err}");
error::ErrorInternalServerError(err)
})?;
Ok(http_response_code.content_type("text/html").body(body))
}
}

View File

@ -50,15 +50,7 @@ pub async fn edit_post<E: ActixAdminViewModelTrait>(
),
)
.await;
create_or_edit_post::<E>(
&session,
req,
db,
model,
Some(id.into_inner()),
actix_admin,
)
.await
create_or_edit_post::<E>(&session, req, db, model, Some(id.into_inner()), actix_admin).await
}
pub async fn create_or_edit_post<E: ActixAdminViewModelTrait>(
@ -82,10 +74,19 @@ pub async fn create_or_edit_post<E: ActixAdminViewModelTrait>(
let db = db.get_ref();
let mut model = model_res.unwrap();
#[cfg(feature = "enable-tracing")]
{
tracing::debug!("Entity model: {:#?}", model);
}
E::validate_entity(&mut model);
if model.has_errors() {
errors.push(ActixAdminError::ValidationErrors);
#[cfg(feature = "enable-tracing")]
{
tracing::error!("OP errors: {errors:#?}");
tracing::debug!("Model errors: {:#?}", model.errors);
}
render_form::<E>(
req,
actix_admin,
@ -125,6 +126,8 @@ pub async fn create_or_edit_post<E: ActixAdminViewModelTrait>(
.finish())
}
Err(e) => {
#[cfg(feature = "enable-tracing")]
tracing::error!("{e}");
errors.push(e);
render_form::<E>(
req,
@ -173,6 +176,7 @@ async fn render_form<E: ActixAdminViewModelTrait>(
ctx.insert("sort_order", &sort_order);
ctx.insert("page", &page);
ctx.insert("navbar_title", &actix_admin.configuration.navbar_title);
ctx.insert("entity_names", &actix_admin.entity_names);
ctx.insert(
"view_model",
@ -184,13 +188,22 @@ async fn render_form<E: ActixAdminViewModelTrait>(
let notifications: Vec<ActixAdminNotification> = errors
.into_iter()
.map(|err| ActixAdminNotification::from(err))
.map(|err| {
#[cfg(feature = "enable-tracing")]
tracing::error!("{err}");
ActixAdminNotification::from(err)
})
.collect();
ctx.insert("notifications", &notifications);
let body = actix_admin.tera
let body = actix_admin
.tera
.render("create_or_edit.html", &ctx)
.map_err(|err| error::ErrorInternalServerError(err))?;
.map_err(|err| {
#[cfg(feature = "enable-tracing")]
tracing::error!("{err}");
error::ErrorInternalServerError(err)
})?;
Ok(HttpResponse::Ok().content_type("text/html").body(body))
}

View File

@ -35,7 +35,7 @@ pub async fn delete<E: ActixAdminViewModelTrait>(
for field in view_model.fields {
if field.field_type == ActixAdminViewModelFieldType::FileUpload {
let file_name = model
.get_value::<String>(&field.field_name, true, true)
.get_value::<String>(field.field_name(), true, true)
.unwrap_or_default();
if file_name.is_some() {
let file_path = format!(
@ -77,7 +77,11 @@ pub async fn delete_many<E: ActixAdminViewModelTrait>(
let db = &db.get_ref();
let entity_name = E::get_entity_name();
let ids: Vec<i32> = form.iter().filter(|el| el.0 == "ids").map(|el| el.1.parse::<i32>().unwrap()).collect();
let ids: Vec<i32> = form
.iter()
.filter(|el| el.0 == "ids")
.map(|el| el.1.parse::<i32>().unwrap())
.collect();
// TODO: implement delete_many
for id in ids {
@ -89,7 +93,7 @@ pub async fn delete_many<E: ActixAdminViewModelTrait>(
for field in view_model.fields {
if field.field_type == ActixAdminViewModelFieldType::FileUpload {
let file_name = model
.get_value::<String>(&field.field_name, true, true)
.get_value::<String>(field.field_name(), true, true)
.unwrap_or_default();
if file_name.is_some() {
let file_path = format!(
@ -107,23 +111,28 @@ pub async fn delete_many<E: ActixAdminViewModelTrait>(
}
}
let entities_per_page = form.iter()
let entities_per_page = form
.iter()
.find(|el| el.0 == "entities_per_page")
.map(|e| e.1.to_string())
.unwrap_or("10".to_string());
let search = form.iter()
let search = form
.iter()
.find(|el| el.0 == "search")
.map(|e| e.1.to_string())
.unwrap_or_default();
let sort_by = form.iter()
let sort_by = form
.iter()
.find(|el| el.0 == "sort_by")
.map(|e| e.1.to_string())
.unwrap_or("id".to_string());
let sort_order = form.iter()
let sort_order = form
.iter()
.find(|el| el.0 == "sort_order")
.map(|e| e.1.to_string())
.unwrap_or("Asc".to_string());
let page = form.iter()
let page = form
.iter()
.find(|el| el.0 == "page")
.map(|e| e.1.to_string())
.unwrap_or("1".to_string());

View File

@ -1,12 +1,18 @@
use actix_web::{web, error, Error, HttpResponse, HttpRequest};
use actix_session::{Session};
use sea_orm::DatabaseConnection;
use tera::{Context};
use crate::prelude::*;
use actix_session::Session;
use actix_web::{error, web, Error, HttpRequest, HttpResponse};
use sea_orm::DatabaseConnection;
use tera::Context;
use super::{ user_can_access_page, render_unauthorized};
use super::{render_unauthorized, user_can_access_page};
pub async fn download<E: ActixAdminViewModelTrait>(req: HttpRequest, session: Session, data: web::Data<ActixAdmin>, db: web::Data<DatabaseConnection>, params: web::Path<(i32, String)>) -> Result<HttpResponse, Error> {
pub async fn download<E: ActixAdminViewModelTrait>(
req: HttpRequest,
session: Session,
data: web::Data<ActixAdmin>,
db: web::Data<DatabaseConnection>,
params: web::Path<(i32, String)>,
) -> Result<HttpResponse, Error> {
let actix_admin = &data.into_inner();
let db = &db.into_inner();
@ -16,7 +22,7 @@ pub async fn download<E: ActixAdminViewModelTrait>(req: HttpRequest, session: Se
if !user_can_access_page(&session, actix_admin, view_model) {
return render_unauthorized(&ctx, &actix_admin);
}
let (id, column_name) = params.into_inner();
let mut errors: Vec<crate::ActixAdminError> = Vec::new();
let result = E::get_entity(db, id).await;
@ -24,25 +30,36 @@ pub async fn download<E: ActixAdminViewModelTrait>(req: HttpRequest, session: Se
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_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(""))
Err(_e) => Ok(HttpResponse::NotFound().content_type("text/html").body("")),
}
}
pub async fn delete_file<E: ActixAdminViewModelTrait>(session: Session, data: web::Data<ActixAdmin>, db: web::Data<DatabaseConnection>, params: web::Path<(i32, String)>) -> Result<HttpResponse, Error> {
pub async fn delete_file<E: ActixAdminViewModelTrait>(
session: Session,
data: web::Data<ActixAdmin>,
db: web::Data<DatabaseConnection>,
params: web::Path<(i32, String)>,
) -> Result<HttpResponse, Error> {
let actix_admin = &data.into_inner();
let mut ctx = Context::new();
@ -51,7 +68,7 @@ pub async fn delete_file<E: ActixAdminViewModelTrait>(session: Session, data: we
if !user_can_access_page(&session, actix_admin, view_model) {
return render_unauthorized(&ctx, &actix_admin);
}
let (id, column_name) = params.into_inner();
let mut errors: Vec<crate::ActixAdminError> = Vec::new();
let result = E::get_entity(db.get_ref(), id).await;
@ -59,27 +76,38 @@ pub async fn delete_file<E: ActixAdminViewModelTrait>(session: Session, data: we
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_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.get_ref(), id, model.clone()).await;
let view_model_field = &view_model.fields.iter().find(|field| field.field_name == column_name).unwrap();
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 = actix_admin.tera
let body = actix_admin
.tera
.render("form_elements/input.html", &ctx)
.map_err(|err| error::ErrorInternalServerError(err))? ;
.map_err(|err| error::ErrorInternalServerError(err))?;
Ok(HttpResponse::Ok().content_type("text/html").body(body))
}
}

View File

@ -1,10 +1,9 @@
use actix_session::{Session};
use tera::{Context};
use actix_session::Session;
use tera::Context;
use crate::prelude::*;
use actix_web::{error, Error, HttpResponse};
pub fn add_auth_context(session: &Session, actix_admin: &ActixAdmin, ctx: &mut Context) {
let enable_auth = &actix_admin.configuration.enable_auth;
ctx.insert("enable_auth", &enable_auth);
@ -12,26 +11,53 @@ pub fn add_auth_context(session: &Session, actix_admin: &ActixAdmin, ctx: &mut C
if *enable_auth {
let func = &actix_admin.configuration.user_is_logged_in.unwrap();
ctx.insert("user_is_logged_in", &func(session));
ctx.insert("login_link", &actix_admin.configuration.login_link.as_ref().unwrap_or(&String::new()));
ctx.insert("logout_link", &actix_admin.configuration.logout_link.as_ref().unwrap_or(&String::new()));
ctx.insert(
"login_link",
&actix_admin
.configuration
.login_link
.as_ref()
.unwrap_or(&String::new()),
);
ctx.insert(
"logout_link",
&actix_admin
.configuration
.logout_link
.as_ref()
.unwrap_or(&String::new()),
);
}
}
pub fn user_can_access_page(session: &Session, actix_admin: &ActixAdmin, view_model: &ActixAdminViewModel) -> bool {
pub fn user_can_access_page(
session: &Session,
actix_admin: &ActixAdmin,
view_model: &ActixAdminViewModel,
) -> bool {
let auth_is_enabled = &actix_admin.configuration.enable_auth;
let user_is_logged_in = &actix_admin.configuration.user_is_logged_in;
let user_can_access_view_model = &view_model.user_can_access;
match (auth_is_enabled, user_is_logged_in, user_can_access_view_model) {
(true, Some(auth_func), Some(view_model_access_func)) => auth_func(session) && view_model_access_func(session),
match (
auth_is_enabled,
user_is_logged_in,
user_can_access_view_model,
) {
(true, Some(auth_func), Some(view_model_access_func)) => {
auth_func(session) && view_model_access_func(session)
}
(true, Some(auth_func), _) => auth_func(session),
(_, _, _) => !auth_is_enabled,
}
}
pub fn render_unauthorized(ctx: &Context, actix_admin: &ActixAdmin) -> Result<HttpResponse, Error> {
let body = actix_admin.tera
.render("unauthorized.html", &ctx)
.map_err(|err| error::ErrorInternalServerError(err))?;
Ok(HttpResponse::Unauthorized().content_type("text/html").body(body))
}
let body = actix_admin
.tera
.render("unauthorized.html", &ctx)
.map_err(|err| error::ErrorInternalServerError(err))?;
Ok(HttpResponse::Unauthorized()
.content_type("text/html")
.body(body))
}

View File

@ -1,10 +1,10 @@
use actix_session::Session;
use actix_web::{error, web, Error, HttpResponse};
use actix_session::{Session};
use tera::{Context};
use tera::Context;
use crate::prelude::*;
use super::{ add_auth_context };
use super::add_auth_context;
pub fn get_admin_ctx(session: Session, data: &web::Data<ActixAdmin>) -> Context {
let actix_admin = data.get_ref();
@ -23,20 +23,29 @@ pub async fn index(session: Session, data: web::Data<ActixAdmin>) -> Result<Http
let mut ctx = Context::new();
ctx.insert("entity_names", &actix_admin.entity_names);
ctx.insert("notifications", &notifications);
ctx.insert("notifications", &notifications);
add_auth_context(&session, actix_admin, &mut ctx);
let body = actix_admin.tera
.render("index.html", &ctx)
.map_err(|_| error::ErrorInternalServerError("Template error"))?;
let body = actix_admin.tera.render("index.html", &ctx).map_err(|e| {
#[cfg(feature = "enable-tracing")]
tracing::error!("{}", e);
error::ErrorInternalServerError("Template error")
})?;
Ok(HttpResponse::Ok().content_type("text/html").body(body))
}
pub async fn not_found(data: web::Data<ActixAdmin>) -> Result<HttpResponse, Error> {
let body = data.get_ref().tera
let body = data
.get_ref()
.tera
.render("not_found.html", &Context::new())
.map_err(|_| error::ErrorInternalServerError("Template error"))?;
Ok(HttpResponse::NotFound().content_type("text/html").body(body))
.map_err(|e| {
#[cfg(feature = "enable-tracing")]
tracing::error!("{}", e);
error::ErrorInternalServerError("Template error")
})?;
Ok(HttpResponse::NotFound()
.content_type("text/html")
.body(body))
}

View File

@ -1,10 +1,10 @@
use std::fmt;
use sea_orm::DatabaseConnection;
use urlencoding::decode;
use crate::prelude::*;
use actix_web::{error, web, Error, HttpRequest, HttpResponse};
use serde_derive::{Serialize, Deserialize};
use sea_orm::DatabaseConnection;
use serde_derive::{Deserialize, Serialize};
use std::fmt;
use tera::Context;
use urlencoding::decode;
use super::{
add_auth_context, render_unauthorized, user_can_access_page, Params, DEFAULT_ENTITIES_PER_PAGE,
@ -39,8 +39,8 @@ pub fn replace_regex(view_model: &ActixAdminViewModel, models: &mut Vec<ActixAdm
models.into_iter().for_each(|m| {
let regex = f.list_regex_mask.as_ref().unwrap();
let field = f;
let vals = &mut m.values;
vals.entry(field.field_name.to_string())
m.values
.entry(field.field_name().to_string())
.and_modify(|f| *f = regex.replace_all(f, "****").to_string());
})
});
@ -50,7 +50,7 @@ pub async fn list<E: ActixAdminViewModelTrait>(
session: Session,
req: HttpRequest,
data: web::Data<ActixAdmin>,
db: web::Data<DatabaseConnection>
db: web::Data<DatabaseConnection>,
) -> Result<HttpResponse, Error> {
let actix_admin = &data.into_inner();
let entity_name = E::get_entity_name();
@ -88,15 +88,30 @@ pub async fn list<E: ActixAdminViewModelTrait>(
.map(|f| {
let mut kv = f.split("=");
let af = ActixAdminViewModelFilter {
name: kv.next().unwrap().strip_prefix("filter_").unwrap_or_default().to_string(),
name: kv
.next()
.unwrap()
.strip_prefix("filter_")
.unwrap_or_default()
.to_string(),
value: kv.next().map(|s| s.to_string()).filter(|f| !f.is_empty()),
values: None,
filter_type: None
filter_type: None,
};
af
}).collect();
})
.collect();
let result = E::list(&db, page, entities_per_page, actixadminfilters, &search, &sort_by, &sort_order).await;
let result = E::list(
&db,
page,
entities_per_page,
actixadminfilters,
&search,
&sort_by,
&sort_order,
)
.await;
match result {
Ok(res) => {

View File

@ -2,34 +2,34 @@ mod create_or_edit_get;
pub use create_or_edit_get::{create_get, edit_get};
mod create_or_edit_post;
pub use create_or_edit_post::{ create_post, edit_post, create_or_edit_post };
pub use create_or_edit_post::{create_or_edit_post, create_post, edit_post};
mod index;
pub use index::{ index, not_found, get_admin_ctx };
pub use index::{get_admin_ctx, index, not_found};
mod list;
pub use list::{ list, SortOrder };
pub use list::{list, SortOrder};
mod show;
pub use show::show;
mod delete;
pub use delete::{ delete, delete_many };
pub use delete::{delete, delete_many};
mod helpers;
pub use helpers::{ add_auth_context, user_can_access_page, render_unauthorized };
pub use helpers::{add_auth_context, render_unauthorized, user_can_access_page};
mod file;
pub use file::{download, delete_file};
pub use file::{delete_file, download};
use serde_derive::{Deserialize};
use serde_derive::Deserialize;
#[derive(Debug, Deserialize)]
pub struct Params {
page: Option<u64>,
entities_per_page: Option<u64>,
search: Option<String>,
sort_by: Option<String>,
sort_order: Option<SortOrder>
sort_order: Option<SortOrder>,
}
const DEFAULT_ENTITIES_PER_PAGE: u64 = 10;
const DEFAULT_ENTITIES_PER_PAGE: u64 = 10;

View File

@ -1,17 +1,21 @@
use actix_session::Session;
use actix_web::HttpRequest;
use actix_web::{error, web, Error, HttpResponse};
use actix_session::{Session};
use sea_orm::DatabaseConnection;
use tera::{Context};
use tera::Context;
use crate::ActixAdminNotification;
use crate::prelude::*;
use crate::ActixAdminNotification;
use super::{add_auth_context, render_unauthorized, user_can_access_page};
use super::{Params, DEFAULT_ENTITIES_PER_PAGE};
use super::{ add_auth_context, user_can_access_page, render_unauthorized};
pub async fn show<E: ActixAdminViewModelTrait>(
session: Session, req: HttpRequest, data: web::Data<ActixAdmin>, id: web::Path<i32>, db: web::Data<DatabaseConnection>
session: Session,
req: HttpRequest,
data: web::Data<ActixAdmin>,
id: web::Path<i32>,
db: web::Data<DatabaseConnection>,
) -> Result<HttpResponse, Error> {
let actix_admin = &data.into_inner();
@ -21,14 +25,14 @@ pub async fn show<E: ActixAdminViewModelTrait>(
if !user_can_access_page(&session, actix_admin, view_model) {
return render_unauthorized(&ctx, &actix_admin);
}
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;
},
}
Err(e) => {
errors.push(e);
model = ActixAdminModel::create_empty();
@ -37,9 +41,10 @@ pub async fn show<E: ActixAdminViewModelTrait>(
let mut http_response_code = match errors.is_empty() {
false => HttpResponse::InternalServerError(),
true => HttpResponse::Ok()
};
let notifications: Vec<ActixAdminNotification> = errors.into_iter()
true => HttpResponse::Ok(),
};
let notifications: Vec<ActixAdminNotification> = errors
.into_iter()
.map(|err| ActixAdminNotification::from(err))
.collect();
@ -51,11 +56,17 @@ pub async fn show<E: ActixAdminViewModelTrait>(
.unwrap_or(DEFAULT_ENTITIES_PER_PAGE);
let render_partial = req.headers().contains_key("HX-Target");
let search = params.search.clone().unwrap_or(String::new());
let sort_by = params.sort_by.clone().unwrap_or(view_model.primary_key.to_string());
let sort_by = params
.sort_by
.clone()
.unwrap_or(view_model.primary_key.to_string());
let sort_order = params.sort_order.as_ref().unwrap_or(&SortOrder::Asc);
ctx.insert("model", &model);
ctx.insert("view_model", &ActixAdminViewModelSerializable::from(view_model.clone()));
ctx.insert(
"view_model",
&ActixAdminViewModelSerializable::from(view_model.clone()),
);
ctx.insert("base_path", &E::get_base_path(&entity_name));
ctx.insert("entity_names", &actix_admin.entity_names);
ctx.insert("notifications", &notifications);
@ -68,8 +79,10 @@ pub async fn show<E: ActixAdminViewModelTrait>(
add_auth_context(&session, actix_admin, &mut ctx);
let body = actix_admin.tera
.render("show.html", &ctx)
.map_err(|err| error::ErrorInternalServerError(format!("{:?}", err)))?;
let body = actix_admin.tera.render("show.html", &ctx).map_err(|err| {
#[cfg(enable_tracing)]
tracing::error!("{err}");
error::ErrorInternalServerError(format!("{:?}", err))
})?;
Ok(http_response_code.content_type("text/html").body(body))
}
}

View File

@ -7,26 +7,31 @@
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 | trim }}"
placeholder="{{ model_field.field_name }}"
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 }}/file/{{ model.primary_key }}/{{ model_field.field_name }}">{{ model.values |
get(key=model_field.field_name, default="") }}</a>
<a hx-disable href="{{ base_path }}/file/{{ 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 }}/file/{{ 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 }}
{% if model.errors | length > 0 or model.custom_errors | length > 0 %}
{% if
model.errors | get(key=model_field.field_name, default="" ) !=""
or
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 }}">
{% endif %}
{% if model.errors | length > 0 or model.custom_errors | length > 0 %}
{% if model.errors | get(key=model_field.field_name, default="" ) !="" or 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 | trim }}"
placeholder="{{ model_field.field_name }}"
aria-label="{{ model_field.field_name }}"
>
{% endif %}

View File

@ -3,8 +3,8 @@
<title>{{ navbar_title }}</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<script src="https://unpkg.com/htmx.org@1.9.2"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
<script src="https://unpkg.com/htmx.org@1.9.4"></script>
<script>
document.onkeydown = function (e) {

View File

@ -268,4 +268,4 @@
</nav>
</div>
{% endblock content %}
{% endblock content %}

176
src/tera_templates.rs Normal file
View File

@ -0,0 +1,176 @@
use std::{collections::HashMap, hash::BuildHasher};
use tera::Tera;
use tera::{to_value, try_get_value, Result};
use crate::view_model::{ActixAdminViewModelField, ActixAdminViewModelFieldType};
struct TeraTemplate {
// Pages
list_html: &'static str,
create_or_edit_html: &'static str,
base_html: &'static str,
head_html: &'static str,
index_html: &'static str,
loader_html: &'static str,
navbar_html: &'static str,
not_found_html: &'static str,
show_html: &'static str,
unauthorized_html: &'static str,
// Form Elements
checkbox_html: &'static str,
input_html: &'static str,
selectlist_html: &'static str,
}
pub fn get_tera() -> Tera {
let mut tera = load_templates();
tera.register_filter("get_html_input_type", get_html_input_type);
tera.register_filter("get_html_input_class", get_html_input_class);
tera.register_filter("get_icon", get_icon);
tera.register_filter("get_regex_val", get_regex_val);
tera
}
fn get_html_input_class<S: BuildHasher>(
value: &tera::Value,
_: &HashMap<String, tera::Value, S>,
) -> Result<tera::Value> {
let field = try_get_value!(
"get_html_input_class",
"value",
ActixAdminViewModelField,
value
);
let html_input_type = match field.field_type {
ActixAdminViewModelFieldType::TextArea => "textarea",
ActixAdminViewModelFieldType::Checkbox => "checkbox",
_ => "input",
};
Ok(to_value(html_input_type).unwrap())
}
fn get_icon<S: BuildHasher>(
value: &tera::Value,
_: &HashMap<String, tera::Value, S>,
) -> Result<tera::Value> {
let field = try_get_value!("get_icon", "value", String, value);
let font_awesome_icon = match field.as_str() {
"true" => "<i class=\"fa-solid fa-check\"></i>",
"false" => "<i class=\"fa-solid fa-xmark\"></i>",
_ => panic!("not implemented icon"),
};
Ok(to_value(font_awesome_icon).unwrap())
}
fn get_regex_val<S: BuildHasher>(
value: &tera::Value,
args: &HashMap<String, tera::Value, S>,
) -> Result<tera::Value> {
let field = try_get_value!("get_regex_val", "value", ActixAdminViewModelField, value);
let s = args.get("values");
let field_val = s.unwrap().get(&field.field_name);
println!(
"field {} regex {:?}",
field.field_name, field.list_regex_mask
);
match (field_val, field.list_regex_mask) {
(Some(val), Some(r)) => {
let val_str = val.to_string();
let is_match = r.is_match(&val_str);
println!("is match: {}, regex {}", is_match, r.to_string());
let result_str = r.replace_all(&val_str, "*");
return Ok(to_value(result_str).unwrap());
}
(Some(val), None) => {
return Ok(to_value(val).unwrap());
}
(_, _) => panic!("key {} not found in model values", &field.field_name),
}
}
fn get_html_input_type<S: BuildHasher>(
value: &tera::Value,
_: &HashMap<String, tera::Value, S>,
) -> Result<tera::Value> {
let field = try_get_value!(
"get_html_input_type",
"value",
ActixAdminViewModelField,
value
);
// TODO: convert to option
if field.html_input_type != "" {
return Ok(to_value(field.html_input_type).unwrap());
}
let html_input_type = match field.field_type {
ActixAdminViewModelFieldType::Text => "text",
ActixAdminViewModelFieldType::DateTime => "datetime-local",
ActixAdminViewModelFieldType::Date => "date",
ActixAdminViewModelFieldType::Checkbox => "checkbox",
ActixAdminViewModelFieldType::FileUpload => "file",
_ => "text",
};
Ok(to_value(html_input_type).unwrap())
}
fn add_templates_to_tera(tera: &mut Tera, tera_template: TeraTemplate) {
let _res = tera.add_raw_templates(vec![
("base.html", tera_template.base_html),
("list.html", tera_template.list_html),
("create_or_edit.html", tera_template.create_or_edit_html),
("head.html", tera_template.head_html),
("index.html", tera_template.index_html),
("loader.html", tera_template.loader_html),
("navbar.html", tera_template.navbar_html),
("not_found.html", tera_template.not_found_html),
("show.html", tera_template.show_html),
("unauthorized.html", tera_template.unauthorized_html),
// form elements
("form_elements/checkbox.html", tera_template.checkbox_html),
("form_elements/input.html", tera_template.input_html),
(
"form_elements/selectlist.html",
tera_template.selectlist_html,
),
]);
}
// Cargo Features
#[cfg(feature = "bulma_css")]
fn load_templates() -> Tera {
let mut tera = Tera::new(concat!(
env!("CARGO_MANIFEST_DIR"),
"/src/templates/bulma/*.html"
))
.unwrap();
let tera_template = TeraTemplate {
list_html: include_str!("templates/bulma/list.html"),
create_or_edit_html: include_str!("templates/bulma/create_or_edit.html"),
base_html: include_str!("templates/bulma/base.html"),
head_html: include_str!("templates/bulma/head.html"),
index_html: include_str!("templates/bulma/index.html"),
loader_html: include_str!("templates/bulma/loader.html"),
navbar_html: include_str!("templates/bulma/navbar.html"),
not_found_html: include_str!("templates/bulma/not_found.html"),
show_html: include_str!("templates/bulma/show.html"),
unauthorized_html: include_str!("templates/bulma/unauthorized.html"),
// form elements
checkbox_html: include_str!("templates/bulma/form_elements/checkbox.html"),
input_html: include_str!("templates/bulma/form_elements/input.html"),
selectlist_html: include_str!("templates/bulma/form_elements/selectlist.html"),
};
add_templates_to_tera(&mut tera, tera_template);
tera
}

View File

@ -1,12 +1,12 @@
use crate::ActixAdminError;
use crate::{model::ActixAdminModelFilterType, ActixAdminModel, SortOrder};
use actix_session::Session;
use async_trait::async_trait;
use regex::Regex;
use sea_orm::DatabaseConnection;
use serde_derive::{Serialize, Deserialize};
use serde_derive::{Deserialize, Serialize};
use std::collections::HashMap;
use crate::{ActixAdminModel, SortOrder, model::ActixAdminModelFilterType};
use actix_session::{Session};
use std::convert::From;
use crate::ActixAdminError;
#[async_trait(?Send)]
pub trait ActixAdminViewModelTrait {
@ -17,16 +17,30 @@ pub trait ActixAdminViewModelTrait {
viewmodel_filter: Vec<ActixAdminViewModelFilter>,
search: &str,
sort_by: &str,
sort_order: &SortOrder
sort_order: &SortOrder,
) -> Result<(u64, Vec<ActixAdminModel>), ActixAdminError>;
// TODO: Replace return value with proper Result Type containing Ok or Err
async fn create_entity(db: &DatabaseConnection, model: ActixAdminModel) -> Result<ActixAdminModel, ActixAdminError>;
async fn create_entity(
db: &DatabaseConnection,
model: ActixAdminModel,
) -> Result<ActixAdminModel, ActixAdminError>;
async fn delete_entity(db: &DatabaseConnection, id: i32) -> Result<bool, ActixAdminError>;
async fn get_entity(db: &DatabaseConnection, id: i32) -> Result<ActixAdminModel, ActixAdminError>;
async fn edit_entity(db: &DatabaseConnection, id: i32, model: ActixAdminModel) -> Result<ActixAdminModel, ActixAdminError>;
async fn get_select_lists(db: &DatabaseConnection) -> Result<HashMap<String, Vec<(String, String)>>, ActixAdminError>;
async fn get_viewmodel_filter(db: &DatabaseConnection) -> HashMap<String, ActixAdminViewModelFilter>;
async fn get_entity(
db: &DatabaseConnection,
id: i32,
) -> Result<ActixAdminModel, ActixAdminError>;
async fn edit_entity(
db: &DatabaseConnection,
id: i32,
model: ActixAdminModel,
) -> Result<ActixAdminModel, ActixAdminError>;
async fn get_select_lists(
db: &DatabaseConnection,
) -> Result<HashMap<String, Vec<(String, String)>>, ActixAdminError>;
async fn get_viewmodel_filter(
db: &DatabaseConnection,
) -> HashMap<String, ActixAdminViewModelFilter>;
fn validate_entity(model: &mut ActixAdminModel);
fn get_entity_name() -> String;
@ -40,10 +54,10 @@ pub trait ActixAdminViewModelTrait {
pub struct ActixAdminViewModel {
pub entity_name: String,
pub primary_key: String,
pub fields: &'static[ActixAdminViewModelField],
pub fields: &'static [ActixAdminViewModelField],
pub show_search: bool,
pub user_can_access: Option<fn(&Session) -> bool>,
pub default_show_aside: bool
pub default_show_aside: bool,
}
#[derive(Clone, Debug, Serialize)]
@ -52,7 +66,7 @@ pub struct ActixAdminViewModelSerializable {
pub primary_key: String,
pub fields: &'static [ActixAdminViewModelField],
pub show_search: bool,
pub default_show_aside: bool
pub default_show_aside: bool,
}
#[derive(Clone, Debug, Serialize)]
@ -60,7 +74,7 @@ pub struct ActixAdminViewModelFilter {
pub name: String,
pub value: Option<String>,
pub values: Option<Vec<(String, String)>>,
pub filter_type: Option<ActixAdminModelFilterType>
pub filter_type: Option<ActixAdminModelFilterType>,
}
// TODO: better alternative to serialize only specific fields for ActixAdminViewModel
@ -71,7 +85,7 @@ impl From<ActixAdminViewModel> for ActixAdminViewModelSerializable {
primary_key: entity.primary_key,
fields: entity.fields,
show_search: entity.show_search,
default_show_aside: entity.default_show_aside
default_show_aside: entity.default_show_aside,
}
}
}
@ -86,7 +100,7 @@ pub enum ActixAdminViewModelFieldType {
Time,
DateTime,
SelectList,
FileUpload
FileUpload,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
@ -99,11 +113,22 @@ pub struct ActixAdminViewModelField {
pub list_sort_position: usize,
pub list_hide_column: bool,
#[serde(skip_serializing, skip_deserializing)]
pub list_regex_mask: Option<Regex>
pub list_regex_mask: Option<Regex>,
}
impl ActixAdminViewModelField {
pub fn field_name(&self) -> &str {
self.field_name.trim()
}
}
impl ActixAdminViewModelFieldType {
pub fn get_field_type(type_path: &str, select_list: String, is_textarea: bool, is_file_upload: 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;
}
@ -118,12 +143,12 @@ impl ActixAdminViewModelFieldType {
"i32" => ActixAdminViewModelFieldType::Number,
"i64" => ActixAdminViewModelFieldType::Number,
"usize" => ActixAdminViewModelFieldType::Number,
"String" => ActixAdminViewModelFieldType::Text,
"bool" => ActixAdminViewModelFieldType::Checkbox,
"String" => ActixAdminViewModelFieldType::Text,
"bool" => ActixAdminViewModelFieldType::Checkbox,
"DateTimeWithTimeZone" => ActixAdminViewModelFieldType::DateTime,
"DateTime" => ActixAdminViewModelFieldType::DateTime,
"Date" => ActixAdminViewModelFieldType::Date,
_ => ActixAdminViewModelFieldType::Text
_ => ActixAdminViewModelFieldType::Text,
}
}
}
}

View File

@ -4,6 +4,8 @@ use test_setup::prelude::*;
#[cfg(test)]
mod get_request_is_success {
extern crate serde_derive;
use super::create_app;
use super::BodyTest;
use actix_admin::prelude::*;
use actix_web::body::to_bytes;
use actix_web::test;
@ -12,8 +14,6 @@ mod get_request_is_success {
use sea_orm::EntityTrait;
use sea_orm::PaginatorTrait;
use sea_orm::QueryOrder;
use super::create_app;
use super::BodyTest;
#[actix_web::test]
async fn get_admin_index() {
@ -33,12 +33,17 @@ mod get_request_is_success {
let db = super::setup_db(true).await;
let page = 5;
let page_size = 50; // Verify with default size in list.rs
let url = format!("/admin/{}/list?page={}&entities_per_page={}", crate::Post::get_entity_name(), page, page_size);
let url = format!(
"/admin/{}/list?page={}&entities_per_page={}",
crate::Post::get_entity_name(),
page,
page_size
);
let entities = crate::Post::find()
let entities = crate::Post::find()
.order_by_asc(crate::post::Column::Id)
.paginate(&db, page_size)
.fetch_page(page-1)
.fetch_page(page - 1)
.await
.unwrap();
@ -49,9 +54,12 @@ mod get_request_is_success {
#[actix_web::test]
async fn get_post_list_search() {
let db = super::setup_db(true).await;
let url = format!("/admin/{}/list?search=Test%20155", crate::Post::get_entity_name());
let url = format!(
"/admin/{}/list?search=Test%20155",
crate::Post::get_entity_name()
);
test_response_contains(url.as_str(), &db, vec!("Test 155".to_string())).await
test_response_contains(url.as_str(), &db, vec!["Test 155".to_string()]).await
}
#[actix_web::test]
@ -59,7 +67,12 @@ mod get_request_is_success {
let db = super::setup_db(true).await;
let search_string_encoded = "Test%2015";
let entities_per_page = 11;
let url = format!("/admin/{}/list?search={}&entities_per_page={}", crate::Comment::get_entity_name(), search_string_encoded, entities_per_page);
let url = format!(
"/admin/{}/list?search={}&entities_per_page={}",
crate::Comment::get_entity_name(),
search_string_encoded,
entities_per_page
);
let mut elements_to_verify = Vec::new();
elements_to_verify.push("Test 15".to_string());
@ -76,17 +89,22 @@ mod get_request_is_success {
let page = 17;
let page_size = 20; // Verify with default size in list.rs
let url = format!("/admin/{}/list?page={}&entities_per_page={}", crate::Comment::get_entity_name(), page, page_size);
let url = format!(
"/admin/{}/list?page={}&entities_per_page={}",
crate::Comment::get_entity_name(),
page,
page_size
);
let query = if page_size == 5 {
crate::Comment::find().order_by_asc(crate::comment::Column::Id)
} else {
crate::Comment::find().order_by_asc(crate::comment::Column::Id)
};
let entities = query
.paginate(&db, page_size)
.fetch_page(page-1)
.fetch_page(page - 1)
.await
.unwrap();
@ -119,11 +137,7 @@ mod get_request_is_success {
async fn get_comment_show() {
let db = super::setup_db(true).await;
let url = format!(
"/admin/{}/show/{}",
crate::Comment::get_entity_name(),
1
);
let url = format!("/admin/{}/show/{}", crate::Comment::get_entity_name(), 1);
test_get_is_success(url.as_str(), &db).await
}
@ -131,39 +145,38 @@ mod get_request_is_success {
async fn get_post_show() {
let db = super::setup_db(true).await;
let url = format!(
"/admin/{}/show/{}",
crate::Comment::get_entity_name(),
1
);
let url = format!("/admin/{}/show/{}", crate::Comment::get_entity_name(), 1);
test_get_is_success(url.as_str(), &db).await
}
async fn test_response_contains(url: &str, db: &DatabaseConnection, elements_to_verify: Vec<String>) {
let app = create_app!(db);
async fn test_response_contains(
url: &str,
db: &DatabaseConnection,
elements_to_verify: Vec<String>,
) {
let app = create_app!(db);
let req = test::TestRequest::get()
.uri(url)
.to_request();
let req = test::TestRequest::get().uri(url).to_request();
let resp = test::call_service(&app, req).await;
let body = to_bytes(resp.into_body()).await.unwrap();
let body = body.as_str();
for element in elements_to_verify {
assert!(body.contains(&element), "Body did not contain element {}: \n{}", element, body);
assert!(
body.contains(&element),
"Body did not contain element {}: \n{}",
element,
body
);
}
}
async fn test_get_is_success(url: &str, db: &DatabaseConnection) {
let app = create_app!(db);
let app = create_app!(db);
let req = test::TestRequest::get()
.uri(url)
.to_request();
let req = test::TestRequest::get().uri(url).to_request();
let resp = test::call_service(&app, req).await;
assert!(resp.status().is_success());
}
}

View File

@ -4,16 +4,12 @@ use test_setup::prelude::*;
#[cfg(test)]
mod post_create_and_edit_is_success {
use actix_admin::prelude::*;
use actix_web::{
test,
App,
http::header::ContentType
};
use chrono::{ NaiveDateTime, NaiveDate };
use serde::{Serialize};
use sea_orm::{ PaginatorTrait, EntityTrait, prelude::Decimal};
use crate::{create_app};
use actix_web::{http::header::ContentType, test, App};
use chrono::{NaiveDate, NaiveDateTime};
use sea_orm::{prelude::Decimal, EntityTrait, PaginatorTrait};
use serde::Serialize;
use crate::create_app;
#[derive(Serialize, Clone)]
pub struct CommentModel {
@ -23,7 +19,7 @@ mod post_create_and_edit_is_success {
user: &'static str,
is_visible: &'static str,
post_id: Option<&'static str>,
my_decimal: &'static str
my_decimal: &'static str,
}
#[derive(Serialize, Clone)]
@ -48,9 +44,9 @@ mod post_create_and_edit_is_success {
user: "test",
is_visible: "true",
post_id: None,
my_decimal: "113.141" // must be larger than 100
my_decimal: "113.141", // must be larger than 100
};
let req = test::TestRequest::post()
.insert_header(ContentType::form_url_encoded())
.uri("/admin/comment/create_post_from_plaintext")
@ -69,12 +65,15 @@ mod post_create_and_edit_is_success {
assert_eq!(entities.len(), 1, "After post, db does not contain 1 model");
let entity = entities.first().unwrap();
assert_eq!(entity.id, 1);
assert_eq!(entity.comment,"test");
assert_eq!(entity.comment, "test");
assert_eq!(entity.user, "test");
assert!(entity.is_visible);
assert!(entity.post_id.is_none());
assert_eq!(entity.my_decimal, Decimal::new(113141, 3));
assert_eq!(entity.insert_date, NaiveDateTime::parse_from_str("1977-04-01T14:00", "%Y-%m-%dT%H:%M").unwrap());
assert_eq!(
entity.insert_date,
NaiveDateTime::parse_from_str("1977-04-01T14:00", "%Y-%m-%dT%H:%M").unwrap()
);
// update entity
model.my_decimal = "213.141";
@ -98,7 +97,11 @@ mod post_create_and_edit_is_success {
.await
.expect("could not retrieve entities");
assert_eq!(entities.len(), 1, "After edit post, db does not contain 1 model");
assert_eq!(
entities.len(),
1,
"After edit post, db does not contain 1 model"
);
let entity = entities.first().unwrap();
assert_eq!(entity.id, 1);
assert_eq!(entity.comment, "updated");
@ -106,9 +109,12 @@ mod post_create_and_edit_is_success {
assert!(!entity.is_visible);
assert!(entity.post_id.is_none());
assert_eq!(entity.my_decimal, Decimal::new(213141, 3));
assert_eq!(entity.insert_date, NaiveDateTime::parse_from_str("1987-04-01T14:00", "%Y-%m-%dT%H:%M").unwrap());
assert_eq!(
entity.insert_date,
NaiveDateTime::parse_from_str("1987-04-01T14:00", "%Y-%m-%dT%H:%M").unwrap()
);
}
#[actix_web::test]
async fn post_create_and_edit() {
let db = super::setup_db(false).await;
@ -119,7 +125,7 @@ mod post_create_and_edit_is_success {
insert_date: "1977-04-01",
title: "test",
text: "test",
tea_mandatory: "EverydayTea"
tea_mandatory: "EverydayTea",
};
let req = test::TestRequest::post()
@ -140,10 +146,16 @@ mod post_create_and_edit_is_success {
assert_eq!(entities.len(), 1, "After post, db does not contain 1 model");
let entity = entities.first().unwrap();
assert_eq!(entity.id, 1);
assert_eq!(entity.tea_mandatory, super::test_setup::post::Tea::EverydayTea);
assert_eq!(
entity.tea_mandatory,
super::test_setup::post::Tea::EverydayTea
);
assert_eq!(entity.title, model.title);
assert_eq!(entity.text, model.text);
assert_eq!(entity.insert_date, NaiveDate::parse_from_str("1977-04-01", "%Y-%m-%d").unwrap());
assert_eq!(
entity.insert_date,
NaiveDate::parse_from_str("1977-04-01", "%Y-%m-%d").unwrap()
);
// update entity
model.tea_mandatory = "BreakfastTea";
@ -166,11 +178,18 @@ mod post_create_and_edit_is_success {
.await
.expect("could not retrieve entities");
assert_eq!(entities.len(), 1, "After edit post, db does not contain 1 model");
assert_eq!(
entities.len(),
1,
"After edit post, db does not contain 1 model"
);
let entity = entities.first().unwrap();
assert_eq!(entity.id, 1);
assert_eq!(entity.text, "updated");
assert_eq!(entity.title, "updated");
assert_eq!(entity.insert_date, NaiveDate::parse_from_str("1987-04-01", "%Y-%m-%d").unwrap());
assert_eq!(
entity.insert_date,
NaiveDate::parse_from_str("1987-04-01", "%Y-%m-%d").unwrap()
);
}
}

View File

@ -1,10 +1,18 @@
use super::Post;
use actix_admin::prelude::*;
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
use actix_admin::prelude::*;
use super::Post;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Deserialize, Serialize,
DeriveActixAdmin, DeriveActixAdminModel, DeriveActixAdminViewModel
#[derive(
Clone,
Debug,
PartialEq,
DeriveEntityModel,
Deserialize,
Serialize,
DeriveActixAdmin,
DeriveActixAdminModel,
DeriveActixAdminViewModel,
)]
#[sea_orm(table_name = "comment")]
pub struct Model {
@ -20,9 +28,9 @@ pub struct Model {
#[sea_orm(column_type = "DateTime")]
pub insert_date: DateTime,
pub is_visible: bool,
#[actix_admin(select_list="Post")]
#[actix_admin(select_list = "Post")]
pub post_id: Option<i32>,
pub my_decimal: Decimal
pub my_decimal: Decimal,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
@ -47,10 +55,13 @@ impl ActixAdminModelValidationTrait<ActiveModel> for Entity {
fn validate(model: &ActiveModel) -> HashMap<String, String> {
let mut errors = HashMap::new();
if model.my_decimal.clone().unwrap() < Decimal::from(100 as i16) {
errors.insert("my_decimal".to_string(), "Must be larger than 100".to_string());
errors.insert(
"my_decimal".to_string(),
"Must be larger than 100".to_string(),
);
}
errors
}
}
impl ActixAdminModelFilterTrait<Entity> for Entity {}
impl ActixAdminModelFilterTrait<Entity> for Entity {}

View File

@ -1,10 +1,10 @@
use actix_admin::prelude::*;
use actix_session::Session;
use actix_web::HttpRequest;
use actix_web::web;
use actix_web::Error;
use actix_web::HttpResponse;
use actix_web::web::Bytes;
use actix_web::Error;
use actix_web::HttpRequest;
use actix_web::HttpResponse;
use chrono::Local;
use sea_orm::prelude::Decimal;
use sea_orm::{ConnectOptions, DatabaseConnection, EntityTrait, Set};
@ -78,7 +78,7 @@ pub fn create_actix_admin_builder() -> ActixAdminBuilder {
login_link: None,
logout_link: None,
file_upload_directory: "./file_uploads",
navbar_title: "test"
navbar_title: "test",
};
let mut admin_builder = ActixAdminBuilder::new(configuration);
@ -157,4 +157,4 @@ impl BodyTest for Bytes {
fn as_str(&self) -> &str {
std::str::from_utf8(self).unwrap()
}
}
}

View File

@ -1,22 +1,18 @@
// setup
use sea_orm::sea_query::{ForeignKeyCreateStatement, ColumnDef, TableCreateStatement};
use sea_orm::sea_query::{ColumnDef, ForeignKeyCreateStatement, TableCreateStatement};
use sea_orm::{error::*, sea_query, ConnectionTrait, DbConn, ExecResult};
pub mod comment;
pub mod post;
pub mod helper;
pub mod post;
pub use comment::Entity as Comment;
pub use post::Entity as Post;
pub mod prelude {
pub use crate::test_setup::helper::{
create_actix_admin_builder,
setup_db,
BodyTest
};
pub use super::comment;
pub use super::post;
pub use super::Comment;
pub use super::Post;
pub use crate::test_setup::helper::{create_actix_admin_builder, setup_db, BodyTest};
}
// setup
@ -38,7 +34,11 @@ pub async fn create_tables(db: &DbConn) -> Result<ExecResult, DbErr> {
)
.col(ColumnDef::new(post::Column::Title).string().not_null())
.col(ColumnDef::new(post::Column::Text).string().not_null())
.col(ColumnDef::new(post::Column::TeaMandatory).string().not_null())
.col(
ColumnDef::new(post::Column::TeaMandatory)
.string()
.not_null(),
)
.col(ColumnDef::new(post::Column::TeaOptional).string())
.col(ColumnDef::new(post::Column::InsertDate).date())
.to_owned();
@ -57,9 +57,21 @@ pub async fn create_tables(db: &DbConn) -> Result<ExecResult, DbErr> {
)
.col(ColumnDef::new(comment::Column::Comment).string().not_null())
.col(ColumnDef::new(comment::Column::User).string().not_null())
.col(ColumnDef::new(comment::Column::InsertDate).date_time().not_null())
.col(ColumnDef::new(comment::Column::IsVisible).boolean().not_null())
.col(ColumnDef::new(comment::Column::MyDecimal).decimal().not_null())
.col(
ColumnDef::new(comment::Column::InsertDate)
.date_time()
.not_null(),
)
.col(
ColumnDef::new(comment::Column::IsVisible)
.boolean()
.not_null(),
)
.col(
ColumnDef::new(comment::Column::MyDecimal)
.decimal()
.not_null(),
)
.col(ColumnDef::new(comment::Column::PostId).integer())
.foreign_key(
ForeignKeyCreateStatement::new()

View File

@ -1,11 +1,22 @@
use actix_admin::prelude::*;
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
use actix_admin::prelude::*;
use std::fmt;
use std::fmt::Display;
use std::str::FromStr;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Deserialize, Serialize, DeriveActixAdmin, DeriveActixAdminViewModel, DeriveActixAdminModel, DeriveActixAdminModelSelectList)]
#[derive(
Clone,
Debug,
PartialEq,
DeriveEntityModel,
Deserialize,
Serialize,
DeriveActixAdmin,
DeriveActixAdminViewModel,
DeriveActixAdminModel,
DeriveActixAdminModelSelectList,
)]
#[sea_orm(table_name = "post")]
pub struct Model {
#[sea_orm(primary_key)]
@ -17,9 +28,9 @@ pub struct Model {
#[sea_orm(column_type = "Text")]
#[actix_admin(searchable, textarea)]
pub text: String,
#[actix_admin(select_list="Tea")]
#[actix_admin(select_list = "Tea")]
pub tea_mandatory: Tea,
#[actix_admin(select_list="Tea")]
#[actix_admin(select_list = "Tea")]
pub tea_optional: Option<Tea>,
pub insert_date: Date,
}
@ -27,7 +38,7 @@ pub struct Model {
impl Display for Model {
fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
match &*self {
_ => write!(formatter, "{} {}", &self.title, &self.insert_date),
_ => write!(formatter, "{} {}", &self.title, &self.insert_date),
}
}
}
@ -46,7 +57,20 @@ impl Related<super::comment::Entity> for Entity {
impl ActiveModelBehavior for ActiveModel {}
#[derive(Debug, Clone, PartialEq, EnumIter, DeriveActiveEnum, Deserialize, Serialize, DeriveActixAdminEnumSelectList)]
<<<<<<< HEAD
#[derive(Debug, Clone, PartialEq, EnumIter, DeriveDisplay, DeriveActiveEnum, Deserialize, Serialize, DeriveActixAdminEnumSelectList)]
=======
#[derive(
Debug,
Clone,
PartialEq,
EnumIter,
DeriveActiveEnum,
Deserialize,
Serialize,
DeriveActixAdminEnumSelectList,
)]
>>>>>>> 7db2971 (Trim input name)
#[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "tea")]
pub enum Tea {
#[sea_orm(string_value = "EverydayTea")]
@ -60,13 +84,13 @@ impl FromStr for Tea {
fn from_str(input: &str) -> Result<Tea, Self::Err> {
match input {
"EverydayTea" => Ok(Tea::EverydayTea),
"BreakfastTea" => Ok(Tea::BreakfastTea),
_ => Err(()),
"EverydayTea" => Ok(Tea::EverydayTea),
"BreakfastTea" => Ok(Tea::BreakfastTea),
_ => Err(()),
}
}
}
impl ActixAdminModelValidationTrait<ActiveModel> for Entity {}
impl ActixAdminModelFilterTrait<Entity> for Entity {}
impl ActixAdminModelFilterTrait<Entity> for Entity {}