implement basic error handling

This commit is contained in:
Manuel Gugger 2022-07-27 00:37:49 +02:00
parent 38792b0fb5
commit 4d2d625d25
10 changed files with 114 additions and 58 deletions

View File

@ -78,7 +78,7 @@ pub fn derive_crud_fns(input: proc_macro::TokenStream) -> proc_macro::TokenStrea
values: hashmap![ values: hashmap![
#(#fields_for_from_model),* #(#fields_for_from_model),*
], ],
errors: Vec::new() errors: HashMap::new()
} }
} }
} }
@ -103,7 +103,8 @@ pub fn derive_crud_fns(input: proc_macro::TokenStream) -> proc_macro::TokenStrea
async fn create_entity(db: &DatabaseConnection, mut model: ActixAdminModel) -> ActixAdminModel { async fn create_entity(db: &DatabaseConnection, mut model: ActixAdminModel) -> ActixAdminModel {
let mut validation_errs = Entity::validate_model(&model); let mut validation_errs = Entity::validate_model(&model);
model.errors.append(&mut validation_errs); //model.errors.append(&mut validation_errs);
model.errors = validation_errs;
if !model.has_errors() { if !model.has_errors() {
let new_model = ActiveModel::from(model.clone()); let new_model = ActiveModel::from(model.clone());
@ -123,7 +124,8 @@ pub fn derive_crud_fns(input: proc_macro::TokenStream) -> proc_macro::TokenStrea
async fn edit_entity(db: &DatabaseConnection, id: i32, mut model: ActixAdminModel) -> ActixAdminModel { async fn edit_entity(db: &DatabaseConnection, id: i32, mut model: ActixAdminModel) -> ActixAdminModel {
let mut validation_errs = Entity::validate_model(&model); let mut validation_errs = Entity::validate_model(&model);
model.errors.append(&mut validation_errs); //model.errors.append(&mut validation_errs);
model.errors=validation_errs;
if !model.has_errors() { if !model.has_errors() {
let entity: Option<Model> = Entity::find_by_id(id).one(db).await.unwrap(); let entity: Option<Model> = Entity::find_by_id(id).one(db).await.unwrap();
@ -182,13 +184,13 @@ pub fn derive_crud_fns(input: proc_macro::TokenStream) -> proc_macro::TokenStrea
} }
fn validate_model(model: &ActixAdminModel) -> Vec<ActixAdminError> { fn validate_model(model: &ActixAdminModel) -> HashMap<String, String> {
let mut errors = Vec::<ActixAdminError>::new(); let mut errors = HashMap::<String, String>::new();
#(#fields_for_validate_model);*; #(#fields_for_validate_model);*;
let mut custom_errors = Entity.validate(); //let mut custom_errors = Entity.validate();
errors.append(&mut custom_errors); //errors.append(&mut custom_errors);
errors errors
} }

View File

@ -221,12 +221,12 @@ pub fn get_fields_for_validate_model(fields: &Vec<ModelField>) -> Vec<TokenStrea
true => { true => {
let inner_ty = model_field.inner_type.to_owned().unwrap(); let inner_ty = model_field.inner_type.to_owned().unwrap();
quote! { quote! {
model.get_value::<#inner_ty>(#ident_name).map_err(|err| errors.push(err)).ok(); model.get_value::<#inner_ty>(#ident_name).map_err(|err| errors.insert(#ident_name.to_string(), err)).ok();
} }
}, },
false => { false => {
quote! { quote! {
model.get_value::<#ty>(#ident_name).map_err(|err| errors.push(err)).ok(); model.get_value::<#ty>(#ident_name).map_err(|err| errors.insert(#ident_name.to_string(), err)).ok();
} }
} }
} }

View File

@ -14,7 +14,7 @@ pub mod prelude {
pub use crate::model::{ ActixAdminModel, ActixAdminModelTrait}; pub use crate::model::{ ActixAdminModel, ActixAdminModelTrait};
pub use crate::view_model::{ ActixAdminViewModel, ActixAdminViewModelTrait, ActixAdminViewModelField}; pub use crate::view_model::{ ActixAdminViewModel, ActixAdminViewModelTrait, ActixAdminViewModelField};
pub use actix_admin_macros::{ DeriveActixAdminModel, DeriveActixAdminSelectList }; pub use actix_admin_macros::{ DeriveActixAdminModel, DeriveActixAdminSelectList };
pub use crate::{ ActixAdminAppDataTrait, ActixAdmin, ActixAdminError }; pub use crate::{ ActixAdminAppDataTrait, ActixAdmin };
pub use crate::{ hashmap, ActixAdminSelectListTrait }; pub use crate::{ hashmap, ActixAdminSelectListTrait };
} }
@ -35,12 +35,6 @@ lazy_static! {
Tera::new(concat!(env!("CARGO_MANIFEST_DIR"), "/templates/**/*")).unwrap(); Tera::new(concat!(env!("CARGO_MANIFEST_DIR"), "/templates/**/*")).unwrap();
} }
#[derive(Clone, Debug, Serialize)]
pub struct ActixAdminError {
field_name: Option<String>,
error: String
}
// AppDataTrait // AppDataTrait
pub trait ActixAdminAppDataTrait { pub trait ActixAdminAppDataTrait {
fn get_db(&self) -> &DatabaseConnection; fn get_db(&self) -> &DatabaseConnection;

View File

@ -1,9 +1,8 @@
use crate::ActixAdminViewModelField;
use async_trait::async_trait; use async_trait::async_trait;
use sea_orm::DatabaseConnection; use sea_orm::DatabaseConnection;
use serde::{Serialize}; use serde::Serialize;
use std::collections::HashMap; use std::collections::HashMap;
use crate::ActixAdminViewModelField;
use crate::ActixAdminError;
#[async_trait] #[async_trait]
pub trait ActixAdminModelTrait { pub trait ActixAdminModelTrait {
@ -13,11 +12,10 @@ pub trait ActixAdminModelTrait {
posts_per_page: usize, posts_per_page: usize,
) -> (usize, Vec<ActixAdminModel>); ) -> (usize, Vec<ActixAdminModel>);
fn get_fields() -> Vec<ActixAdminViewModelField>; fn get_fields() -> Vec<ActixAdminViewModelField>;
fn validate_model(model: &ActixAdminModel) -> Vec<ActixAdminError>; fn validate_model(model: &ActixAdminModel) -> HashMap<String, String>;
// function to be overridable for custom error handling // function to be overridable for custom error handling
fn validate(&self) -> Vec<ActixAdminError> { fn validate(&self) -> HashMap<String, String> {
return Vec::new() return HashMap::new();
} }
} }
@ -25,7 +23,7 @@ pub trait ActixAdminModelTrait {
pub struct ActixAdminModel { pub struct ActixAdminModel {
pub primary_key: Option<String>, pub primary_key: Option<String>,
pub values: HashMap<String, String>, pub values: HashMap<String, String>,
pub errors: Vec<ActixAdminError>, pub errors: HashMap<String, String>,
} }
impl From<String> for ActixAdminModel { impl From<String> for ActixAdminModel {
@ -33,38 +31,49 @@ impl From<String> for ActixAdminModel {
let mut hashmap = HashMap::new(); let mut hashmap = HashMap::new();
let key_values: Vec<&str> = string.split('&').collect(); let key_values: Vec<&str> = string.split('&').collect();
for key_value in key_values { for key_value in key_values {
let mut iter = key_value.splitn(2, '='); if !key_value.is_empty() {
hashmap.insert( let mut iter = key_value.splitn(2, '=');
iter.next().unwrap().to_string(), hashmap.insert(
iter.next().unwrap().to_string(), iter.next().unwrap().to_string(),
); iter.next().unwrap().to_string(),
);
}
} }
ActixAdminModel { primary_key: None, values: hashmap, errors: Vec::new() } ActixAdminModel {
primary_key: None,
values: hashmap,
errors: HashMap::new(),
}
} }
} }
impl ActixAdminModel { impl ActixAdminModel {
pub fn get_value<T: std::str::FromStr>(&self, key: &str) -> Result<Option<T>, ActixAdminError> { pub fn get_value<T: std::str::FromStr>(&self, key: &str) -> Result<Option<T>, String> {
let value = self.values.get(key); let value = self.values.get(key);
let res: Result<Option<T>, ActixAdminError> = match value { println!("{:?}", key);
println!("{:?}", value);
let res: Result<Option<T>, String> = match value {
Some(val) => { Some(val) => {
let parsed_val = val.parse::<T>(); if val.is_empty() {
return Ok(None);
}
let parsed_val = val.parse::<T>();
match parsed_val { match parsed_val {
Ok(val) => Ok(Some(val)), Ok(val) => Ok(Some(val)),
Err(_) => Err(ActixAdminError { Err(_) => Err("Invalid Value".to_string()),
field_name: Some(key.to_string()),
error: "Invalid Value".to_string()
})
} }
}, }
_ => Ok(None) _ => Ok(None),
}; };
res res
} }
pub fn has_errors(&self) -> bool { pub fn has_errors(&self) -> bool {
return &self.errors.len() != &0 return &self.errors.len() != &0;
} }
} }

View File

@ -9,24 +9,26 @@ pub async fn create_get<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>(
_req: HttpRequest, _req: HttpRequest,
data: web::Data<T>, data: web::Data<T>,
_body: web::Payload, _body: web::Payload,
_text: String, text: String,
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, Error> {
let _db = &data.get_db(); let db = &data.get_db();
let entity_name = E::get_entity_name(); let entity_name = E::get_entity_name();
let entity_names = &data.get_actix_admin().entity_names; let entity_names = &data.get_actix_admin().entity_names;
let actix_admin = data.get_actix_admin(); let actix_admin = data.get_actix_admin();
let view_model = actix_admin.view_models.get(&entity_name).unwrap(); let view_model = actix_admin.view_models.get(&entity_name).unwrap();
let mut model = ActixAdminModel::from(text);
let mut ctx = Context::new(); let mut ctx = Context::new();
ctx.insert("entity_names", &entity_names); ctx.insert("entity_names", &entity_names);
ctx.insert("view_model", &view_model); ctx.insert("view_model", &view_model);
ctx.insert("select_lists", &E::get_select_lists(_db).await); ctx.insert("select_lists", &E::get_select_lists(db).await);
ctx.insert("list_link", &E::get_list_link(&entity_name)); ctx.insert("list_link", &E::get_list_link(&entity_name));
ctx.insert("model", &model);
let body = TERA let body = TERA
.render("create.html", &ctx) .render("edit.html", &ctx)
.map_err(|err| error::ErrorInternalServerError(err))?; .map_err(|err| error::ErrorInternalServerError(err))?;
Ok(HttpResponse::Ok().content_type("text/html").body(body)) Ok(HttpResponse::Ok().content_type("text/html").body(body))
} }

View File

@ -1,5 +1,7 @@
use actix_web::http::header; use actix_web::http::header;
use actix_web::{web, Error, HttpRequest, HttpResponse}; use actix_web::{web, error, Error, HttpRequest, HttpResponse};
use tera::{Context};
use crate::TERA;
use crate::prelude::*; use crate::prelude::*;
@ -10,15 +12,32 @@ pub async fn create_post<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, Error> {
let db = &data.get_db(); let db = &data.get_db();
let entity_name = E::get_entity_name(); let entity_name = E::get_entity_name();
let entity_names = &data.get_actix_admin().entity_names;
let actix_admin = data.get_actix_admin(); let actix_admin = data.get_actix_admin();
let view_model = actix_admin.view_models.get(&entity_name).unwrap(); let view_model = actix_admin.view_models.get(&entity_name).unwrap();
let mut _admin_model = ActixAdminModel::from(text); let mut model = ActixAdminModel::from(text);
_admin_model = E::create_entity(db, _admin_model).await; model = E::create_entity(db, model).await;
Ok(HttpResponse::Found() if model.has_errors() {
let mut ctx = Context::new();
ctx.insert("entity_names", &entity_names);
ctx.insert("view_model", &view_model);
ctx.insert("select_lists", &E::get_select_lists(db).await);
ctx.insert("list_link", &E::get_list_link(&entity_name));
ctx.insert("model", &model);
println!("{:?}", model.errors);
let body = TERA
.render("edit.html", &ctx)
.map_err(|err| error::ErrorInternalServerError(err))?;
Ok(HttpResponse::Ok().content_type("text/html").body(body))
}
else {
Ok(HttpResponse::Found()
.append_header(( .append_header((
header::LOCATION, header::LOCATION,
format!("/admin/{}/list", view_model.entity_name), format!("/admin/{}/list", view_model.entity_name),
)) ))
.finish()) .finish())
}
} }

View File

@ -8,7 +8,6 @@ use crate::TERA;
pub async fn edit_get<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>( pub async fn edit_get<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>(
_req: HttpRequest, _req: HttpRequest,
data: web::Data<T>, data: web::Data<T>,
_body: web::Payload,
_text: String, _text: String,
id: web::Path<i32> id: web::Path<i32>
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, Error> {

View File

@ -1,7 +1,10 @@
use actix_web::http::header; use actix_web::http::header;
use actix_web::{web, Error, HttpRequest, HttpResponse}; use actix_web::{web, error, Error, HttpRequest, HttpResponse};
use tera::{Context};
use crate::TERA;
use crate::prelude::*; use crate::prelude::*;
use super::edit_get::edit_get;
pub async fn edit_post<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>( pub async fn edit_post<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>(
_req: HttpRequest, _req: HttpRequest,
@ -12,14 +15,30 @@ pub async fn edit_post<T: ActixAdminAppDataTrait, E: ActixAdminViewModelTrait>(
let db = &data.get_db(); let db = &data.get_db();
let entity_name = E::get_entity_name(); let entity_name = E::get_entity_name();
let actix_admin = data.get_actix_admin(); let actix_admin = data.get_actix_admin();
let entity_names = &data.get_actix_admin().entity_names;
let view_model = actix_admin.view_models.get(&entity_name).unwrap(); let view_model = actix_admin.view_models.get(&entity_name).unwrap();
let mut _admin_model = ActixAdminModel::from(text); let mut model = ActixAdminModel::from(text);
_admin_model = E::edit_entity(db, id.into_inner(), _admin_model).await; model = E::edit_entity(db, id.into_inner(), model).await;
if model.has_errors() {
let mut ctx = Context::new();
ctx.insert("entity_names", &entity_names);
ctx.insert("view_model", &view_model);
ctx.insert("model", &model);
ctx.insert("select_lists", &E::get_select_lists(db).await);
ctx.insert("list_link", &E::get_list_link(&entity_name));
let body = TERA
.render("edit.html", &ctx)
.map_err(|err| error::ErrorInternalServerError(err))?;
Ok(HttpResponse::Ok().content_type("text/html").body(body))
}
else {
Ok(HttpResponse::Found() Ok(HttpResponse::Found()
.append_header(( .append_header((
header::LOCATION, header::LOCATION,
format!("/admin/{}/list", view_model.entity_name), format!("/admin/{}/list", view_model.entity_name),
)) ))
.finish()) .finish())
}
} }

View File

@ -2,26 +2,38 @@
{% block content %} {% block content %}
<article> <article>
<div>
<ul>
{{ model.errors }}
</ul>
</div>
<form method="post"> <form method="post">
<div> <div>
{% for model_field in view_model.fields -%} {% for model_field in view_model.fields -%}
<label for="{{ model_field.field_name }}"> <label for="{{ model_field.field_name }}">
{{ model_field.field_name }} {{ model_field.field_name }}
{% if model_field.select_list != "" %} {% if model_field.select_list != "" %}
<select name="{{ model_field.field_name }}"> <select name="{{ model_field.field_name }}"
{% if model.errors | get(key=model_field.field_name, default="" ) !="" %} placeholder="Invalid" aria-invalid="true" {% endif %}>
{% if model_field.is_option %} {% if model_field.is_option %}
<option value=""></option> <option value=""></option>
{% else %}
<option value="" selected disabled>Select</option>
{% endif %} {% endif %}
{% for select_list_item in select_lists[model_field.field_name] -%} {% for select_list_item in select_lists[model_field.field_name] -%}
<option {% if select_list_item[0]==model.values | get(key=model_field.field_name) %} selected {% <option {% if select_list_item[0]==model.values | get(key=model_field.field_name, default="" ) %}
endif %} value="{{ select_list_item[0] }}">{{ select_list_item[1] }}</option> selected {% endif %} value="{{ select_list_item[0] }}">{{ select_list_item[1] }}</option>
{%- endfor %} {%- endfor %}
</select> </select>
</label> </label>
{% else %} {% else %}
<input type="{{ model_field.html_input_type }}" value="{{ model.values | get(key=model_field.field_name) }}" <input type="{{ model_field.html_input_type }}"
value="{{ model.values | get(key=model_field.field_name, default="") }}"
name="{{ model_field.field_name }}" placeholder="{{ model_field.field_name }}" name="{{ model_field.field_name }}" placeholder="{{ model_field.field_name }}"
aria-label="{{ model_field.field_name }}"><!-- required="" --> aria-label="{{ model_field.field_name }}">
{% if model.errors | get(key=model_field.field_name, default="") != "" %}
placeholder="Invalid" aria-invalid="true"
{% endif %}
{% endif %} {% endif %}
</label> </label>
{%- endfor %} {%- endfor %}

Binary file not shown.