include templates in library statically

This commit is contained in:
Manuel Gugger 2023-03-20 17:42:10 +01:00
parent c6118f52c4
commit e61b683baa
21 changed files with 610 additions and 264 deletions

View File

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

View File

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

View File

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

View File

@ -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<i32>,
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<super::post::Entity> for Entity {
fn to() -> RelationDef {
Relation::Post.def()
}
}
impl ActiveModelBehavior for ActiveModel {}
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
}
}

View File

@ -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<ExecResult, DbErr> {
let builder = db.get_database_backend();
db.execute(builder.build(stmt)).await
}
pub async fn create_post_table(db: &DbConn) -> Result<ExecResult, DbErr> {
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
}

View File

@ -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<Tea>,
#[sea_orm(column_type = "Date")]
#[actix_admin(list_sort_position="1")]
pub insert_date: Date,
#[actix_admin(file_upload)]
pub attachment: Option<String>
}
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<super::comment::Entity> 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<Tea, Self::Err> {
match input {
"EverydayTea" => Ok(Tea::EverydayTea),
"BreakfastTea" => Ok(Tea::BreakfastTea),
_ => Err(()),
}
}
}
impl ActixAdminModelValidationTrait<ActiveModel> for Entity {}

87
basictest/basic/main.rs Normal file
View File

@ -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::<AppState, Post>(&post_view_model);
let some_category = "Groupings";
let comment_view_model = ActixAdminViewModel::from(Comment);
admin_builder.add_entity_to_category::<AppState, Comment>(&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::<AppState>()
)
.wrap(middleware::Logger::default())
})
.bind("127.0.0.1:5000")
.expect("Can not bind to port 5000")
.run()
.await
.unwrap();
}

View File

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

View File

@ -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<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"), "*")).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::<T, E>))
.route("/file/{id}/{column_name}", web::get().to(download::<T, E>))
.route("/file/{id}/{column_name}", web::delete().to(delete_file::<T, E>))
.default_service(web::to(not_found))
.default_service(web::to(not_found::<T>))
);
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::<T>));
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()
}
}
}

View File

@ -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<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())
}
pub 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())
}
pub 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)
}
}
pub 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())
}
// AppDataTrait
pub trait ActixAdminAppDataTrait {
fn get_db(&self) -> &DatabaseConnection;
@ -214,6 +81,7 @@ pub struct ActixAdmin {
pub entity_names: HashMap<String, Vec<ActixAdminMenuElement>>,
pub view_models: HashMap<String, ActixAdminViewModel>,
pub configuration: ActixAdminConfiguration,
pub tera: Tera
}
#[derive(PartialEq, Eq, Clone, Serialize)]

View File

@ -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<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTra
let view_model = 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 model;
@ -93,7 +92,7 @@ async fn create_or_edit_get<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTra
ctx.insert("sort_order", &sort_order);
ctx.insert("page", &page);
let body = TERA
let body = actix_admin.tera
.render("create_or_edit.html", &ctx)
.map_err(|err| error::ErrorInternalServerError(err))?;
Ok(http_response_code.content_type("text/html").body(body))

View File

@ -3,7 +3,6 @@ use super::{Params, DEFAULT_ENTITIES_PER_PAGE};
use crate::prelude::*;
use crate::ActixAdminError;
use crate::ActixAdminNotification;
use crate::TERA;
use actix_multipart::Multipart;
use actix_multipart::MultipartError;
use actix_session::Session;
@ -75,7 +74,7 @@ pub async fn create_or_edit_post<T: ActixAdminAppDataTrait, E: ActixAdminViewMod
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();
@ -187,7 +186,7 @@ async fn render_form<E: ActixAdminViewModelTrait>(
.collect();
ctx.insert("notifications", &notifications);
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))

View File

@ -20,7 +20,7 @@ pub async fn delete<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>(
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<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>
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();

View File

@ -13,7 +13,7 @@ pub async fn download<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>(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<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>
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<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>
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))

View File

@ -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<HttpResponse, Error> {
let body = TERA
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))

View File

@ -4,8 +4,6 @@ use tera::{Context};
use crate::prelude::*;
use crate::TERA;
use super::{ add_auth_context };
pub fn get_admin_ctx<T: ActixAdminAppDataTrait>(session: Session, data: &web::Data<T>) -> Context {
@ -29,14 +27,14 @@ pub async fn index<T: ActixAdminAppDataTrait>(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<HttpResponse, Error> {
let body = TERA
pub async fn not_found<T: ActixAdminAppDataTrait>(data: web::Data<T>) -> Result<HttpResponse, Error> {
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))

View File

@ -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<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>(
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::<Params>::from_query(req.query_string()).unwrap();
@ -139,7 +138,7 @@ pub async fn list<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>(
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))

View File

@ -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<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>(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<crate::ActixAdminError> = Vec::new();
@ -67,7 +66,7 @@ pub async fn show<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>(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))

View File

@ -6,5 +6,89 @@
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.1/css/all.min.css">
<script src="https://unpkg.com/htmx.org@1.8.4"></script>
<link rel="stylesheet" href="/admin/static/css/default.css">
<script src="/admin/static/js/default.js"></script>
<script>
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",
"<div class=\"notification mb-4 is-light is-danger\"><button class=\"delete\" onclick=\"this.parentElement.remove()\"></button>An Error occurred</div>");
})
</script>
<style>
.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
}
</style>

View File

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

View File

@ -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",
"<div class=\"notification mb-4 is-light is-danger\"><button class=\"delete\" onclick=\"this.parentElement.remove()\"></button>An Error occurred</div>");
})