From c6118f52c4c55595b4b5c654a81db2a13cc3ee60 Mon Sep 17 00:00:00 2001 From: Manuel Gugger Date: Wed, 15 Feb 2023 12:18:24 +0100 Subject: [PATCH] add regex attr for field masking in list --- Cargo.toml | 1 + actix_admin_macros/Cargo.toml | 3 +- actix_admin_macros/src/attributes.rs | 3 +- actix_admin_macros/src/lib.rs | 15 ++++- actix_admin_macros/src/model_fields.rs | 3 +- actix_admin_macros/src/struct_fields.rs | 9 ++- examples/basic/entity/comment.rs | 2 +- src/lib.rs | 24 ++++++++ src/routes/list.rs | 73 +++++++++++++++++++------ src/view_model.rs | 5 +- 10 files changed, 112 insertions(+), 26 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4ca4072..9f292b0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 } diff --git a/actix_admin_macros/Cargo.toml b/actix_admin_macros/Cargo.toml index 6c24a84..1684faa 100644 --- a/actix_admin_macros/Cargo.toml +++ b/actix_admin_macros/Cargo.toml @@ -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 } \ No newline at end of file +proc-macro2 = { version = "1.0.36", default-features = false } +regex = "1.7.1" \ No newline at end of file diff --git a/actix_admin_macros/src/attributes.rs b/actix_admin_macros/src/attributes.rs index 92d9e69..075eff2 100644 --- a/actix_admin_macros/src/attributes.rs +++ b/actix_admin_macros/src/attributes.rs @@ -18,7 +18,8 @@ pub mod derive_attr { pub file_upload: Option<()>, pub not_empty: Option<()>, pub list_sort_position: Option, - pub list_hide_column: Option<()> + pub list_hide_column: Option<()>, + pub list_regex_mask: Option //pub inner_type: Option, // Anything that implements `syn::parse::Parse` is supported. diff --git a/actix_admin_macros/src/lib.rs b/actix_admin_macros/src/lib.rs index 3a2820d..71f32ee 100644 --- a/actix_admin_macros/src/lib.rs +++ b/actix_admin_macros/src/lib.rs @@ -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 diff --git a/actix_admin_macros/src/model_fields.rs b/actix_admin_macros/src/model_fields.rs index 9528f21..1dbad5d 100644 --- a/actix_admin_macros/src/model_fields.rs +++ b/actix_admin_macros/src/model_fields.rs @@ -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 { diff --git a/actix_admin_macros/src/struct_fields.rs b/actix_admin_macros/src/struct_fields.rs index 9c481a8..0ffadd9 100644 --- a/actix_admin_macros/src/struct_fields.rs +++ b/actix_admin_macros/src/struct_fields.rs @@ -56,6 +56,12 @@ pub fn filter_fields(fields: &Fields) -> Vec { 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::(); @@ -90,7 +96,8 @@ pub fn filter_fields(fields: &Fields) -> Vec { 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 { diff --git a/examples/basic/entity/comment.rs b/examples/basic/entity/comment.rs index 0a97799..33149b8 100644 --- a/examples/basic/entity/comment.rs +++ b/examples/basic/entity/comment.rs @@ -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, diff --git a/src/lib.rs b/src/lib.rs index 3284990..09ee58d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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( 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, diff --git a/src/routes/list.rs b/src/routes/list.rs index 4768eb2..e62608f 100644 --- a/src/routes/list.rs +++ b/src/routes/list.rs @@ -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) { + 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( session: Session, req: HttpRequest, @@ -38,7 +56,7 @@ pub async fn list( let entity_name = E::get_entity_name(); let view_model: &ActixAdminViewModel = actix_admin.view_models.get(&entity_name).unwrap(); let mut errors: Vec = Vec::new(); - + let mut ctx = Context::new(); add_auth_context(&session, actix_admin, &mut ctx); @@ -58,23 +76,38 @@ pub async fn list( 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::::new()); ctx.insert("num_pages", &0); @@ -87,9 +120,10 @@ pub async fn list( let mut http_response_code = match errors.is_empty() { false => HttpResponse::InternalServerError(), - true => HttpResponse::Ok() - }; - let notifications: Vec = errors.into_iter() + true => HttpResponse::Ok(), + }; + let notifications: Vec = errors + .into_iter() .map(|err| ActixAdminNotification::from(err)) .collect(); @@ -97,7 +131,10 @@ pub async fn list( ctx.insert("notifications", ¬ifications); 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( .render("list.html", &ctx) .map_err(|err| error::ErrorInternalServerError(err))?; Ok(http_response_code.content_type("text/html").body(body)) -} \ No newline at end of file +} diff --git a/src/view_model.rs b/src/view_model.rs index b94a7db..5c83db3 100644 --- a/src/view_model.rs +++ b/src/view_model.rs @@ -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 } impl ActixAdminViewModelFieldType {