add regex attr for field masking in list

This commit is contained in:
Manuel Gugger 2023-02-15 12:18:24 +01:00
parent a9b6dd01df
commit c6118f52c4
10 changed files with 112 additions and 26 deletions

View File

@ -34,6 +34,7 @@ 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" }
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 }

View File

@ -16,4 +16,5 @@ proc-macro = true
bae = "0.1.7"
quote = "1.0"
syn = { version = "1.0", features = ["full", "extra-traits"] }
proc-macro2 = { version = "1.0.36", default-features = false }
proc-macro2 = { version = "1.0.36", default-features = false }
regex = "1.7.1"

View File

@ -18,7 +18,8 @@ pub mod derive_attr {
pub file_upload: Option<()>,
pub not_empty: Option<()>,
pub list_sort_position: Option<syn::LitStr>,
pub list_hide_column: Option<()>
pub list_hide_column: Option<()>,
pub list_regex_mask: Option<syn::LitStr>
//pub inner_type: Option<syn::Type>,
// Anything that implements `syn::parse::Parse` is supported.

View File

@ -38,6 +38,7 @@ pub fn derive_actix_admin(_input: proc_macro::TokenStream) -> proc_macro::TokenS
EntityTrait
};
use std::collections::HashMap;
use regex::Regex;
};
proc_macro::TokenStream::from(expanded)
}
@ -145,6 +146,7 @@ pub fn derive_actix_admin_model(input: proc_macro::TokenStream) -> proc_macro::T
let field_names = get_fields_as_tokenstream(&fields, |model_field| -> String { model_field.ident.to_string() });
let field_html_input_type = get_fields_as_tokenstream(&fields, |model_field| -> String { model_field.html_input_type.to_string() });
let field_list_regex_mask = get_fields_as_tokenstream(&fields, |model_field| -> String { model_field.list_regex_mask.to_string() });
let field_select_list = get_fields_as_tokenstream(&fields, |model_field| -> String { model_field.select_list.to_string() });
let is_option_list = get_fields_as_tokenstream(&fields, |model_field| -> bool { model_field.is_option() });
let fields_for_create_model = get_fields_for_create_model(&fields);
@ -202,12 +204,20 @@ pub fn derive_actix_admin_model(input: proc_macro::TokenStream) -> proc_macro::T
let list_hide_columns = [
#(#fields_list_hide_column),*
];
let list_regex_masks = [
#(#field_list_regex_mask),*
];
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) 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) {
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 html_input_type = html_input_type.replace('"', "").replace(' ', "").to_string();
let mut list_regex_mask_regex = None;
if list_regex_mask != "" {
list_regex_mask_regex = Some(Regex::new(list_regex_mask).unwrap());
};
vec.push(ActixAdminViewModelField {
field_name: field_name,
@ -216,7 +226,8 @@ pub fn derive_actix_admin_model(input: proc_macro::TokenStream) -> proc_macro::T
is_option: is_option_list,
list_sort_position: list_sort_position,
field_type: ActixAdminViewModelFieldType::get_field_type(fields_type_path, select_list, is_textarea, is_file_upload),
list_hide_column: list_hide_column
list_hide_column: list_hide_column,
list_regex_mask: list_regex_mask_regex
});
}
vec

View File

@ -16,7 +16,8 @@ pub struct ModelField {
pub file_upload: bool,
pub not_empty: bool,
pub list_sort_position: usize,
pub list_hide_column: bool
pub list_hide_column: bool,
pub list_regex_mask: String
}
impl ModelField {

View File

@ -56,6 +56,12 @@ pub fn filter_fields(fields: &Fields) -> Vec<ModelField> {
let is_not_empty = actix_admin_attr
.clone()
.map_or(false, |attr| attr.not_empty.is_some());
let list_regex_mask = actix_admin_attr.clone().map_or("".to_string(), |attr| {
attr.list_regex_mask
.map_or("".to_string(), |attr_field| {
(LitStr::from(attr_field)).value()
})
});
let list_sort_position: usize = actix_admin_attr.clone().map_or(99, |attr| {
attr.list_sort_position.map_or( 99, |attr_field| {
let sort_pos = LitStr::from(attr_field).value().parse::<usize>();
@ -90,7 +96,8 @@ pub fn filter_fields(fields: &Fields) -> Vec<ModelField> {
file_upload: is_file_upload,
not_empty: is_not_empty,
list_sort_position: list_sort_position,
list_hide_column: is_list_hide_column
list_hide_column: is_list_hide_column,
list_regex_mask: list_regex_mask
};
Some(model_field)
} else {

View File

@ -12,7 +12,7 @@ pub struct Model {
pub id: i32,
pub comment: String,
#[sea_orm(column_type = "Text")]
#[actix_admin(html_input_type = "email")]
#[actix_admin(html_input_type = "email", list_regex_mask= "^([a-zA-Z]*)")]
pub user: String,
#[sea_orm(column_type = "DateTime")]
pub insert_date: DateTime,

View File

@ -63,6 +63,7 @@ lazy_static! {
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");
@ -134,6 +135,29 @@ pub fn get_icon<S: BuildHasher>(
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>,

View File

@ -1,18 +1,20 @@
use std::fmt;
use crate::prelude::*;
use actix_web::{error, web, Error, HttpRequest, HttpResponse};
use serde::Deserialize;
use serde::Serialize;
use serde::{Deserialize};
use tera::{Context};
use crate::{prelude::*};
use tera::Context;
use crate::ActixAdminViewModelTrait;
use crate::ActixAdminViewModel;
use super::{
add_auth_context, render_unauthorized, user_can_access_page, Params, DEFAULT_ENTITIES_PER_PAGE,
};
use crate::ActixAdminModel;
use crate::ActixAdminNotification;
use crate::ActixAdminViewModel;
use crate::ActixAdminViewModelTrait;
use crate::TERA;
use actix_session::{Session};
use super::{ add_auth_context, user_can_access_page, render_unauthorized, Params, DEFAULT_ENTITIES_PER_PAGE};
use actix_session::Session;
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
pub enum SortOrder {
@ -29,6 +31,22 @@ impl fmt::Display for SortOrder {
}
}
pub fn replace_regex(view_model: &ActixAdminViewModel, models: &mut Vec<ActixAdminModel>) {
view_model
.fields
.iter()
.filter(|f| f.list_regex_mask.is_some())
.for_each(|f| {
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())
.and_modify(|f| *f = regex.replace_all(f, "****").to_string());
})
});
}
pub async fn list<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>(
session: Session,
req: HttpRequest,
@ -38,7 +56,7 @@ pub async fn list<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>(
let entity_name = E::get_entity_name();
let view_model: &ActixAdminViewModel = actix_admin.view_models.get(&entity_name).unwrap();
let mut errors: Vec<ActixAdminError> = Vec::new();
let mut ctx = Context::new();
add_auth_context(&session, actix_admin, &mut ctx);
@ -58,23 +76,38 @@ pub async fn list<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>(
let search = params.search.clone().unwrap_or(String::new());
let db = data.get_db();
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);
let result = E::list(db, page, entities_per_page, &search, &sort_by, &sort_order).await;
match result {
Ok(res) => {
let entities = res.1;
let mut entities = res.1;
replace_regex(view_model, &mut entities);
let num_pages = std::cmp::max(res.0, 1);
ctx.insert("entities", &entities);
ctx.insert("num_pages", &num_pages);
ctx.insert("page", &std::cmp::min(num_pages, page));
page = std::cmp::min(page, num_pages);
let min_show_page = if &page < &5 { 1 } else { let max_page = &page - &5; max_page };
let max_show_page = if &page >= &num_pages { std::cmp::max(1, num_pages - 1) } else { let max_page = &page + &5; std::cmp::min(num_pages - 1, max_page) };
let min_show_page = if &page < &5 {
1
} else {
let max_page = &page - &5;
max_page
};
let max_show_page = if &page >= &num_pages {
std::cmp::max(1, num_pages - 1)
} else {
let max_page = &page + &5;
std::cmp::min(num_pages - 1, max_page)
};
ctx.insert("min_show_page", &min_show_page);
ctx.insert("max_show_page", &max_show_page);
},
}
Err(e) => {
ctx.insert("entities", &Vec::<ActixAdminModel>::new());
ctx.insert("num_pages", &0);
@ -87,9 +120,10 @@ pub async fn list<T: ActixAdminAppDataTrait, 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();
@ -97,7 +131,10 @@ pub async fn list<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>(
ctx.insert("notifications", &notifications);
ctx.insert("entities_per_page", &entities_per_page);
ctx.insert("render_partial", &render_partial);
ctx.insert("view_model", &ActixAdminViewModelSerializable::from(view_model.clone()));
ctx.insert(
"view_model",
&ActixAdminViewModelSerializable::from(view_model.clone()),
);
ctx.insert("search", &search);
ctx.insert("sort_by", &sort_by);
ctx.insert("sort_order", &sort_order);
@ -106,4 +143,4 @@ pub async fn list<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>(
.render("list.html", &ctx)
.map_err(|err| error::ErrorInternalServerError(err))?;
Ok(http_response_code.content_type("text/html").body(body))
}
}

View File

@ -1,4 +1,5 @@
use async_trait::async_trait;
use regex::Regex;
use sea_orm::DatabaseConnection;
use serde::{Serialize, Deserialize};
use std::collections::HashMap;
@ -84,7 +85,9 @@ pub struct ActixAdminViewModelField {
pub is_option: bool,
pub field_type: ActixAdminViewModelFieldType,
pub list_sort_position: usize,
pub list_hide_column: bool
pub list_hide_column: bool,
#[serde(skip_serializing, skip_deserializing)]
pub list_regex_mask: Option<Regex>
}
impl ActixAdminViewModelFieldType {