diff --git a/Cargo.toml b/Cargo.toml index 9f292b0..f65f654 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "actix-admin" description = "An admin interface for actix-web" license = "MIT OR Apache-2.0" -version = "0.3.0" +version = "0.4.0" repository = "https://github.com/mgugger/actix-admin" edition = "2021" exclude = [ @@ -19,26 +19,26 @@ name = "actix_admin" path = "src/lib.rs" [dependencies] -actix-web = "^4.2.1" +actix-web = "^4.3.1" actix-session = { version = "^0.7.2", features = [] } -actix-multipart = "^0.4.0" +actix-multipart = "^0.6.0" actix-files = "^0.6.2" -futures-util = "0.3.25" -chrono = "0.4.23" -tera = "^1.17.1" -async-trait = "^0.1.60" +futures-util = "0.3.27" +chrono = "0.4.24" +tera = "^1.18.1" +async-trait = "^0.1.67" lazy_static = "^1.4.0" itertools = "^0.10.5" -serde = "^1.0.152" -serde_derive = "^1.0.152" -sea-orm = { version = "^0.10.6", features = [], default-features = false } -actix-admin-macros = { version = "0.3.0", path = "actix_admin_macros" } +serde = "^1.0.158" +serde_derive = "^1.0.158" +sea-orm = { version = "^0.11.1", features = [], default-features = false } +actix-admin-macros = { version = "0.4.0", path = "actix_admin_macros" } derive_more = "0.99.17" regex = "1.7.1" [dev-dependencies] -sea-orm = { version = "^0.10.6", features = [ "sqlx-sqlite", "runtime-actix-native-tls", "macros" ], default-features = true } -actix-rt = "2.7.0" +sea-orm = { version = "^0.11.1", 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.3" dotenv = "0.15" diff --git a/actix_admin_macros/Cargo.toml b/actix_admin_macros/Cargo.toml index 1684faa..eead9a8 100644 --- a/actix_admin_macros/Cargo.toml +++ b/actix_admin_macros/Cargo.toml @@ -3,7 +3,7 @@ name = "actix-admin-macros" description = "macros to be used with actix-admin crate" license = "MIT OR Apache-2.0" repository = "https://github.com/mgugger/actix-admin" -version = "0.3.0" +version = "0.4.0" edition = "2021" exclude = [ "tests/*" @@ -14,7 +14,7 @@ proc-macro = true [dependencies] bae = "0.1.7" -quote = "1.0" -syn = { version = "1.0", features = ["full", "extra-traits"] } -proc-macro2 = { version = "1.0.36", default-features = false } +quote = "1.0.26" +syn = { version = "1.0.109", features = ["full", "extra-traits"] } +proc-macro2 = { version = "1.0.52", default-features = false } regex = "1.7.1" \ No newline at end of file diff --git a/basictest/basic/Cargo.toml b/basictest/basic/Cargo.toml new file mode 100644 index 0000000..4b68cd1 --- /dev/null +++ b/basictest/basic/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "actix-admin-example" +description = "An admin interface for actix-web" +version = "0.4.0" +edition = "2021" + +[[bin]] +name = "actix-admin-example" +path = "main.rs" + +[dependencies] +actix-web = "^4.2.1" +actix-rt = "2.7.0" +actix-multipart = "^0.4.0" +sea-orm = { version = "^0.11.1", features = [ "sqlx-sqlite", "runtime-actix-native-tls", "macros" ], default-features = true } +chrono = "0.4.23" +tera = "^1.17.1" +serde = "^1.0.152" +serde_derive = "^1.0.152" +actix-admin = { version = "0.4.0", path = "../../" } +regex = "1.7.1" \ No newline at end of file diff --git a/basictest/basic/entity/comment.rs b/basictest/basic/entity/comment.rs new file mode 100644 index 0000000..33149b8 --- /dev/null +++ b/basictest/basic/entity/comment.rs @@ -0,0 +1,52 @@ +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)] +#[sea_orm(table_name = "comment")] +pub struct Model { + #[sea_orm(primary_key)] + #[serde(skip_deserializing)] + #[actix_admin(primary_key)] + pub id: i32, + pub comment: String, + #[sea_orm(column_type = "Text")] + #[actix_admin(html_input_type = "email", list_regex_mask= "^([a-zA-Z]*)")] + pub user: String, + #[sea_orm(column_type = "DateTime")] + pub insert_date: DateTime, + pub is_visible: bool, + #[actix_admin(select_list="Post")] + pub post_id: Option, + pub my_decimal: Decimal +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::post::Entity", + from = "Column::PostId", + to = "super::post::Column::Id" + )] + Post, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Post.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} + +impl ActixAdminModelValidationTrait for Entity { + fn validate(model: &ActiveModel) -> HashMap { + 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 + } +} \ No newline at end of file diff --git a/basictest/basic/entity/mod.rs b/basictest/basic/entity/mod.rs new file mode 100644 index 0000000..ae6ea04 --- /dev/null +++ b/basictest/basic/entity/mod.rs @@ -0,0 +1,91 @@ +// setup +use sea_orm::sea_query::{ForeignKeyCreateStatement, ColumnDef, TableCreateStatement}; +use sea_orm::{Set, EntityTrait, error::*, sea_query, ConnectionTrait, DbConn, ExecResult}; +pub mod comment; +pub mod post; +pub use comment::Entity as Comment; +pub use post::Entity as Post; +use chrono::{Local, Duration, DurationRound}; +use sea_orm::prelude::Decimal; + +// setup +async fn create_table(db: &DbConn, stmt: &TableCreateStatement) -> Result { + let builder = db.get_database_backend(); + db.execute(builder.build(stmt)).await +} + +pub async fn create_post_table(db: &DbConn) -> Result { + let stmt = sea_query::Table::create() + .table(post::Entity) + .if_not_exists() + .col( + ColumnDef::new(post::Column::Id) + .integer() + .not_null() + .auto_increment() + .primary_key(), + ) + .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::TeaOptional).string()) + .col(ColumnDef::new(post::Column::InsertDate).date().not_null()) + .col(ColumnDef::new(post::Column::Attachment).string()) + .to_owned(); + + let _result = create_table(db, &stmt).await; + + let stmt = sea_query::Table::create() + .table(comment::Entity) + .if_not_exists() + .col( + ColumnDef::new(post::Column::Id) + .integer() + .not_null() + .auto_increment() + .primary_key(), + ) + .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::PostId).integer()) + .foreign_key( + ForeignKeyCreateStatement::new() + .name("fk-comment-post") + .from_tbl(Comment) + .from_col(comment::Column::PostId) + .to_tbl(Post) + .to_col(post::Column::Id), + ) + .to_owned(); + + let res = create_table(db, &stmt).await; + + for i in 1..1000 { + let row = post::ActiveModel { + title: Set(format!("Test {}", i)), + text: Set("some content".to_string()), + tea_mandatory: Set(post::Tea::EverydayTea), + tea_optional: Set(None), + insert_date: Set(Local::now().date_naive()), + ..Default::default() + }; + let _res = Post::insert(row).exec(db).await; + } + + for i in 1..1000 { + let row = comment::ActiveModel { + comment: Set(format!("Test {}", i)), + user: Set("me@home.com".to_string()), + my_decimal: Set(Decimal::new(105, 0)), + insert_date: Set(Local::now().naive_utc().duration_round(Duration::minutes(1)).unwrap()), + is_visible: Set(i%2 == 0), + ..Default::default() + }; + let _res = Comment::insert(row).exec(db).await; + } + + res +} diff --git a/basictest/basic/entity/post.rs b/basictest/basic/entity/post.rs new file mode 100644 index 0000000..e23f4de --- /dev/null +++ b/basictest/basic/entity/post.rs @@ -0,0 +1,74 @@ +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)] +#[sea_orm(table_name = "post")] +pub struct Model { + #[sea_orm(primary_key)] + #[serde(skip_deserializing)] + #[actix_admin(primary_key)] + pub id: i32, + #[actix_admin(searchable, not_empty)] + pub title: String, + #[sea_orm(column_type = "Text")] + #[actix_admin(searchable, textarea, list_hide_column)] + pub text: String, + #[actix_admin(select_list="Tea")] + pub tea_mandatory: Tea, + #[actix_admin(select_list="Tea")] + pub tea_optional: Option, + #[sea_orm(column_type = "Date")] + #[actix_admin(list_sort_position="1")] + pub insert_date: Date, + #[actix_admin(file_upload)] + pub attachment: Option +} + +impl Display for Model { + fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + match &*self { + _ => write!(formatter, "{} {}", &self.title, ""/* &self.insert_date*/), + } + } +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm(has_many = "super::comment::Entity")] + Comment, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Comment.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} + +#[derive(Debug, Clone, PartialEq, EnumIter, DeriveActiveEnum, Deserialize, Serialize, DeriveActixAdminEnumSelectList)] +#[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "tea")] +pub enum Tea { + #[sea_orm(string_value = "EverydayTea")] + EverydayTea, + #[sea_orm(string_value = "BreakfastTea")] + BreakfastTea, +} + +impl FromStr for Tea { + type Err = (); + + fn from_str(input: &str) -> Result { + match input { + "EverydayTea" => Ok(Tea::EverydayTea), + "BreakfastTea" => Ok(Tea::BreakfastTea), + _ => Err(()), + } + } +} + +impl ActixAdminModelValidationTrait for Entity {} \ No newline at end of file diff --git a/basictest/basic/main.rs b/basictest/basic/main.rs new file mode 100644 index 0000000..1174f62 --- /dev/null +++ b/basictest/basic/main.rs @@ -0,0 +1,87 @@ +extern crate serde_derive; + +use actix_admin::prelude::*; +use actix_web::{web, App, HttpServer, middleware}; +use sea_orm::{ConnectOptions, DatabaseConnection}; +use std::time::Duration; +mod entity; +use entity::{Post, Comment}; + +#[derive(Clone)] +pub struct AppState { + pub db: DatabaseConnection, + pub actix_admin: ActixAdmin, +} + +impl ActixAdminAppDataTrait for AppState { + fn get_db(&self) -> &DatabaseConnection { + &self.db + } + fn get_actix_admin(&self) -> &ActixAdmin { + &self.actix_admin + } +} + +fn create_actix_admin_builder() -> ActixAdminBuilder { + let configuration = ActixAdminConfiguration { + enable_auth: false, + user_is_logged_in: None, + login_link: None, + logout_link: None, + file_upload_directory: "./file_uploads" + }; + + let mut admin_builder = ActixAdminBuilder::new(configuration); + + + let post_view_model = ActixAdminViewModel::from(Post); + admin_builder.add_entity::(&post_view_model); + + let some_category = "Groupings"; + let comment_view_model = ActixAdminViewModel::from(Comment); + admin_builder.add_entity_to_category::(&comment_view_model, some_category); + + admin_builder +} + +fn get_db_options() -> ConnectOptions { + let db_url = "sqlite::memory:".to_string(); + let mut opt = ConnectOptions::new(db_url); + opt.max_connections(100) + .min_connections(5) + .connect_timeout(Duration::from_secs(8)) + .idle_timeout(Duration::from_secs(8)) + .sqlx_logging(true); + opt +} + +#[actix_rt::main] +async fn main() { + let opt = get_db_options(); + let conn = sea_orm::Database::connect(opt).await.unwrap(); + let _ = entity::create_post_table(&conn).await; + + println!("The admin interface is available at http://localhost:5000/admin/"); + + HttpServer::new(move || { + + let actix_admin_builder = create_actix_admin_builder(); + + let app_state = AppState { + db: conn.clone(), + actix_admin: actix_admin_builder.get_actix_admin(), + }; + + App::new() + .app_data(web::Data::new(app_state)) + .service( + actix_admin_builder.get_scope::() + ) + .wrap(middleware::Logger::default()) + }) + .bind("127.0.0.1:5000") + .expect("Can not bind to port 5000") + .run() + .await + .unwrap(); +} \ No newline at end of file diff --git a/docs/content/_index.md b/docs/content/_index.md index f692da6..357ecc3 100644 --- a/docs/content/_index.md +++ b/docs/content/_index.md @@ -16,4 +16,30 @@ The actix-admin crate aims at creating a web admin interface similar to other ad ## Example -Check the [examples](https://github.com/mgugger/actix-admin/tree/main/examples) and run ```cargo run --example basic``` from the root folder for a basic in-memory sqlite version. The admin interface is accessible under ```localhost:5000/admin/```. \ No newline at end of file +Check the [examples](https://github.com/mgugger/actix-admin/tree/main/examples) and run ```cargo run --example basic``` from the root folder for a basic in-memory sqlite version. The admin interface is accessible under ```localhost:5000/admin/```. + +## Minimal Cargo.toml + +```toml +[package] +name = "actix-admin-example" +description = "An admin interface for actix-web" +version = "0.4.0" +edition = "2021" + +[[bin]] +name = "actix-admin-example" +path = "main.rs" + +[dependencies] +actix-web = "^4.2.1" +actix-rt = "2.7.0" +actix-multipart = "^0.4.0" +sea-orm = { version = "^0.11.1", features = [ "sqlx-sqlite", "runtime-actix-native-tls", "macros" ], default-features = true } +chrono = "0.4.23" +tera = "^1.17.1" +serde = "^1.0.152" +serde_derive = "^1.0.152" +actix-admin = { version = "0.4.0", path = "../../" } +regex = "1.7.1" +``` \ No newline at end of file diff --git a/src/builder.rs b/src/builder.rs index f81e353..ca29282 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -1,10 +1,13 @@ use crate::{prelude::*, ActixAdminMenuElement, routes::delete_file}; -use actix_web::{web, Route}; +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}; /// Represents a builder entity which helps generating the ActixAdmin configuration pub struct ActixAdminBuilder { @@ -62,6 +65,133 @@ pub trait ActixAdminBuilderTrait { fn get_actix_admin(&self) -> ActixAdmin; } +fn get_html_input_class( + value: &tera::Value, + _: &HashMap, +) -> Result { + 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( + value: &tera::Value, + _: &HashMap, +) -> Result { + let field = try_get_value!("get_icon", "value", String, value); + let font_awesome_icon = match field.as_str() { + "true" => "", + "false" => "", + _ => panic!("not implemented icon"), + }; + + Ok(to_value(font_awesome_icon).unwrap()) +} + +fn get_regex_val( + value: &tera::Value, + args: &HashMap, +) -> Result { + 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( + value: &tera::Value, + _: &HashMap, +) -> Result { + 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"), "*")).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 { @@ -69,6 +199,7 @@ impl ActixAdminBuilderTrait for ActixAdminBuilder { entity_names: HashMap::new(), view_models: HashMap::new(), configuration: configuration, + tera: get_tera() }, custom_routes: Vec::new(), scopes: HashMap::new(), @@ -104,7 +235,7 @@ impl ActixAdminBuilderTrait for ActixAdminBuilder { .route("/show/{id}", web::get().to(show::)) .route("/file/{id}/{column_name}", web::get().to(download::)) .route("/file/{id}/{column_name}", web::delete().to(delete_file::)) - .default_service(web::to(not_found)) + .default_service(web::to(not_found::)) ); fs::create_dir_all(format!("{}/{}", &self.actix_admin.configuration.file_upload_directory, E::get_entity_name())).unwrap(); @@ -232,8 +363,7 @@ impl ActixAdminBuilderTrait for ActixAdminBuilder { }; let mut admin_scope = web::scope("/admin") .route("/", index_handler) - .service(actix_files::Files::new("/static", "./static").show_files_listing()) - .default_service(web::to(not_found)); + .default_service(web::to(not_found::)); for (_entity, scope) in self.scopes { admin_scope = admin_scope.service(scope); @@ -249,4 +379,4 @@ impl ActixAdminBuilderTrait for ActixAdminBuilder { fn get_actix_admin(&self) -> ActixAdmin { self.actix_admin.clone() } -} +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 09ee58d..03304ca 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,12 +12,10 @@ use actix_web::{ }; use async_trait::async_trait; use derive_more::{Display, Error}; -use lazy_static::lazy_static; use sea_orm::DatabaseConnection; use serde::Serialize; +use tera::Tera; use std::collections::HashMap; -use std::hash::BuildHasher; -use tera::{to_value, try_get_value, Result, Tera}; pub mod builder; pub mod model; @@ -32,7 +30,6 @@ pub mod prelude { ActixAdminViewModel, ActixAdminViewModelField, ActixAdminViewModelFieldType, ActixAdminViewModelSerializable, ActixAdminViewModelTrait, }; - pub use crate::TERA; pub use crate::{hashmap, ActixAdminSelectListTrait}; pub use crate::{ActixAdmin, ActixAdminAppDataTrait, ActixAdminConfiguration, ActixAdminError}; pub use actix_admin_macros::{ @@ -56,136 +53,6 @@ macro_rules! hashmap { }} } -// globals -lazy_static! { - pub static ref TERA: Tera = { - let mut tera = Tera::new(concat!(env!("CARGO_MANIFEST_DIR"), "/templates/**/*")).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 - }; -} - -pub fn get_html_input_class( - value: &tera::Value, - _: &HashMap, -) -> Result { - 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()) -} - -pub fn get_icon( - value: &tera::Value, - _: &HashMap, -) -> Result { - let field = try_get_value!("get_icon", "value", String, value); - let font_awesome_icon = match field.as_str() { - "true" => "", - "false" => "", - _ => panic!("not implemented icon"), - }; - - Ok(to_value(font_awesome_icon).unwrap()) -} - -pub fn get_regex_val( - value: &tera::Value, - args: &HashMap, -) -> Result { - 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) - } -} - -pub fn get_html_input_type( - value: &tera::Value, - _: &HashMap, -) -> Result { - 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()) -} - // AppDataTrait pub trait ActixAdminAppDataTrait { fn get_db(&self) -> &DatabaseConnection; @@ -214,6 +81,7 @@ pub struct ActixAdmin { pub entity_names: HashMap>, pub view_models: HashMap, pub configuration: ActixAdminConfiguration, + pub tera: Tera } #[derive(PartialEq, Eq, Clone, Serialize)] diff --git a/src/routes/create_or_edit_get.rs b/src/routes/create_or_edit_get.rs index bbd7123..03fe5f5 100644 --- a/src/routes/create_or_edit_get.rs +++ b/src/routes/create_or_edit_get.rs @@ -5,7 +5,6 @@ use crate::ActixAdminError; use crate::ActixAdminNotification; use crate::prelude::*; -use crate::TERA; use super::DEFAULT_ENTITIES_PER_PAGE; use super::Params; use super::{ add_auth_context, user_can_access_page, render_unauthorized}; @@ -48,7 +47,7 @@ async fn create_or_edit_get( .collect(); ctx.insert("notifications", ¬ifications); - let body = TERA + let body = actix_admin.tera .render("create_or_edit.html", &ctx) .map_err(|err| error::ErrorInternalServerError(err))?; Ok(HttpResponse::Ok().content_type("text/html").body(body)) diff --git a/src/routes/delete.rs b/src/routes/delete.rs index ad658a2..b374ed5 100644 --- a/src/routes/delete.rs +++ b/src/routes/delete.rs @@ -20,7 +20,7 @@ pub async fn delete( if !user_can_access_page(&session, actix_admin, view_model) { let mut ctx = Context::new(); ctx.insert("render_partial", &true); - return render_unauthorized(&ctx); + return render_unauthorized(&ctx, &actix_admin); } let db = &data.get_db(); @@ -68,7 +68,7 @@ pub async fn delete_many if !user_can_access_page(&session, actix_admin, view_model) { let mut ctx = Context::new(); ctx.insert("render_partial", &true); - return render_unauthorized(&ctx); + return render_unauthorized(&ctx, &actix_admin); } let db = &data.get_db(); diff --git a/src/routes/file.rs b/src/routes/file.rs index d7318df..e907f68 100644 --- a/src/routes/file.rs +++ b/src/routes/file.rs @@ -13,7 +13,7 @@ pub async fn download(re 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); + return render_unauthorized(&ctx, &actix_admin); } let (id, column_name) = params.into_inner(); @@ -49,7 +49,7 @@ pub async fn delete_file 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); + return render_unauthorized(&ctx, &actix_admin); } let (id, column_name) = params.into_inner(); @@ -77,7 +77,7 @@ pub async fn delete_file ctx.insert("base_path", &E::get_base_path(&entity_name)); ctx.insert("model", &model); - let body = TERA + let body = actix_admin.tera .render("form_elements/input.html", &ctx) .map_err(|err| error::ErrorInternalServerError(err))? ; Ok(HttpResponse::Ok().content_type("text/html").body(body)) diff --git a/src/routes/helpers.rs b/src/routes/helpers.rs index 1467d95..3ff5385 100644 --- a/src/routes/helpers.rs +++ b/src/routes/helpers.rs @@ -2,7 +2,6 @@ use actix_session::{Session}; use tera::{Context}; use crate::prelude::*; -use crate::TERA; use actix_web::{error, Error, HttpResponse}; @@ -29,8 +28,8 @@ pub fn user_can_access_page(session: &Session, actix_admin: &ActixAdmin, view_mo } } -pub fn render_unauthorized(ctx: &Context) -> Result { - let body = TERA +pub fn render_unauthorized(ctx: &Context, actix_admin: &ActixAdmin) -> Result { + let body = actix_admin.tera .render("unauthorized.html", &ctx) .map_err(|err| error::ErrorInternalServerError(err))?; Ok(HttpResponse::Unauthorized().content_type("text/html").body(body)) diff --git a/src/routes/index.rs b/src/routes/index.rs index e00aebe..b87176f 100644 --- a/src/routes/index.rs +++ b/src/routes/index.rs @@ -4,8 +4,6 @@ use tera::{Context}; use crate::prelude::*; -use crate::TERA; - use super::{ add_auth_context }; pub fn get_admin_ctx(session: Session, data: &web::Data) -> Context { @@ -29,14 +27,14 @@ pub async fn index(session: Session, data: web::Data< add_auth_context(&session, actix_admin, &mut ctx); - let body = TERA + let body = actix_admin.tera .render("index.html", &ctx) .map_err(|_| error::ErrorInternalServerError("Template error"))?; Ok(HttpResponse::Ok().content_type("text/html").body(body)) } -pub async fn not_found() -> Result { - let body = TERA +pub async fn not_found(data: web::Data) -> Result { + let body = data.get_actix_admin().tera .render("not_found.html", &Context::new()) .map_err(|_| error::ErrorInternalServerError("Template error"))?; Ok(HttpResponse::NotFound().content_type("text/html").body(body)) diff --git a/src/routes/list.rs b/src/routes/list.rs index e62608f..23eaf10 100644 --- a/src/routes/list.rs +++ b/src/routes/list.rs @@ -13,7 +13,6 @@ use crate::ActixAdminModel; use crate::ActixAdminNotification; use crate::ActixAdminViewModel; use crate::ActixAdminViewModelTrait; -use crate::TERA; use actix_session::Session; #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] @@ -63,7 +62,7 @@ pub async fn list( ctx.insert("entity_names", &actix_admin.entity_names); if !user_can_access_page(&session, actix_admin, view_model) { - return render_unauthorized(&ctx); + return render_unauthorized(&ctx, actix_admin); } let params = web::Query::::from_query(req.query_string()).unwrap(); @@ -139,7 +138,7 @@ pub async fn list( ctx.insert("sort_by", &sort_by); ctx.insert("sort_order", &sort_order); - let body = TERA + let body = actix_admin.tera .render("list.html", &ctx) .map_err(|err| error::ErrorInternalServerError(err))?; Ok(http_response_code.content_type("text/html").body(body)) diff --git a/src/routes/show.rs b/src/routes/show.rs index 1455815..36ef033 100644 --- a/src/routes/show.rs +++ b/src/routes/show.rs @@ -6,7 +6,6 @@ use tera::{Context}; use crate::ActixAdminNotification; use crate::prelude::*; -use crate::TERA; use super::{Params, DEFAULT_ENTITIES_PER_PAGE}; use super::{ add_auth_context, user_can_access_page, render_unauthorized}; @@ -18,7 +17,7 @@ pub async fn show(sessio 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); + return render_unauthorized(&ctx, &actix_admin); } let mut errors: Vec = Vec::new(); @@ -67,7 +66,7 @@ pub async fn show(sessio add_auth_context(&session, actix_admin, &mut ctx); - let body = TERA + let body = actix_admin.tera .render("show.html", &ctx) .map_err(|err| error::ErrorInternalServerError(format!("{:?}", err)))?; Ok(http_response_code.content_type("text/html").body(body)) diff --git a/src/templates/head.html b/src/templates/head.html index f9dbc61..cb09590 100644 --- a/src/templates/head.html +++ b/src/templates/head.html @@ -6,5 +6,89 @@ - - \ No newline at end of file + + + + diff --git a/static/css/default.css b/static/css/default.css deleted file mode 100644 index e46f995..0000000 --- a/static/css/default.css +++ /dev/null @@ -1,12 +0,0 @@ -.loader-wrapper { - position: absolute; - height: 100%; - width: 100%; - display: flex; - background: rgba(255, 255, 255, 0.3); - justify-content: center; - border-radius: 6px; - align-items: center; - z-index: 6; - pointer-events: none -} \ No newline at end of file diff --git a/static/js/default.js b/static/js/default.js deleted file mode 100644 index 2b5f172..0000000 --- a/static/js/default.js +++ /dev/null @@ -1,68 +0,0 @@ -document.onkeydown = function (e) { - switch (e.which) { - case 37: // left - let left_el = document.getElementsByClassName('left-arrow-click').item(0); - if (left_el) { left_el.click(); }; - break; - - //case 38: // up - // break; - - case 39: // right - let right_el = document.getElementsByClassName('right-arrow-click').item(0); - if (right_el) { right_el.click(); }; - break; - - //case 40: // down - // break; - - default: return; // exit this handler for other keys - } - e.preventDefault(); // prevent the default action (scroll / move caret) -}; - -function checkAll(bx) { - var cbs = document.getElementsByTagName('input'); - for (var i = 0; i < cbs.length; i++) { - if (cbs[i].type == 'checkbox') { - cbs[i].checked = bx.checked; - } - } -} - -function sort_by(column) { - current_sort_order = document.getElementsByName("sort_order")[0].value; - if (current_sort_order == "Asc") { - document.getElementsByName("sort_order").forEach((e) => e.value = "Desc"); - } else { - document.getElementsByName("sort_order").forEach((e) => e.value = "Asc"); - } - document.getElementsByName("sort_by").forEach((e) => e.value = column); - document.getElementById('table_form').requestSubmit(); -} - -document.addEventListener('DOMContentLoaded', () => { - // Get all "navbar-burger" elements - const $navbarBurgers = Array.prototype.slice.call(document.querySelectorAll('.navbar-burger'), 0); - - // Add a click event on each of them - $navbarBurgers.forEach(el => { - el.addEventListener('click', () => { - - // Get the target from the "data-target" attribute - const target = el.dataset.target; - const $target = document.getElementById(target); - - // Toggle the "is-active" class on both the "navbar-burger" and the "navbar-menu" - el.classList.toggle('is-active'); - $target.classList.toggle('is-active'); - - }); - }); -}); - -htmx.on("htmx:responseError", function () { - document.getElementById("notifications").insertAdjacentHTML( - "afterend", - "
An Error occurred
"); -}) \ No newline at end of file