parse datetime and bool correctly

This commit is contained in:
Manuel Gugger 2022-08-05 18:23:51 +02:00
parent 5aa8bc809d
commit fb4cd9f7d7
13 changed files with 193 additions and 79 deletions

View File

@ -7,6 +7,7 @@ edition = "2021"
actix-web = "4.0.1" actix-web = "4.0.1"
actix-multipart = "0.4.0" actix-multipart = "0.4.0"
futures-util = "0.3.21" futures-util = "0.3.21"
chrono = "0.4.20"
tera = "1.16.0" tera = "1.16.0"
actix_admin_macros = { path = "actix_admin_macros" } actix_admin_macros = { path = "actix_admin_macros" }
async-trait = "0.1.53" async-trait = "0.1.53"

View File

@ -211,12 +211,17 @@ pub fn derive_crud_fns(input: proc_macro::TokenStream) -> proc_macro::TokenStrea
]; ];
for (field_name, html_input_type, select_list, is_option_list, fields_type_path) in izip!(&field_names, &html_input_types, &field_select_lists, is_option_lists, fields_type_paths) { for (field_name, html_input_type, select_list, is_option_list, fields_type_path) in izip!(&field_names, &html_input_types, &field_select_lists, is_option_lists, fields_type_paths) {
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();
vec.push(ActixAdminViewModelField { vec.push(ActixAdminViewModelField {
field_name: field_name.replace('"', "").replace(' ', "").to_string(), field_name: field_name,
html_input_type: html_input_type.replace('"', "").replace(' ', "").to_string(), html_input_type: html_input_type,
select_list: select_list.replace('"', "").replace(' ', "").to_string(), select_list: select_list.clone(),
is_option: is_option_list, is_option: is_option_list,
field_type: ActixAdminViewModelFieldType::from(fields_type_path) field_type: ActixAdminViewModelFieldType::get_field_type(fields_type_path, select_list)
}); });
} }
vec vec

View File

@ -254,22 +254,36 @@ pub fn get_fields_for_validate_model(fields: &Vec<ModelField>) -> Vec<TokenStrea
.map(|model_field| { .map(|model_field| {
let ident_name = model_field.ident.to_string(); let ident_name = model_field.ident.to_string();
let ty = model_field.ty.to_owned(); let ty = model_field.ty.to_owned();
let type_path = model_field.get_type_path_string();
let is_option_or_string = model_field.is_option() || model_field.is_string(); let is_option_or_string = model_field.is_option() || model_field.is_string();
match model_field.is_option() { let res = match (model_field.is_option(), type_path.as_str()) {
true => { (_, "DateTime") => {
quote! {
model.get_datetime(#ident_name, #is_option_or_string).map_err(|err| errors.insert(#ident_name.to_string(), err)).ok()
}
},
(_, "bool") => {
quote! {
model.get_bool(#ident_name, #is_option_or_string).map_err(|err| errors.insert(#ident_name.to_string(), err)).ok()
}
},
// generic
(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, #is_option_or_string).map_err(|err| errors.insert(#ident_name.to_string(), err)).ok() model.get_value::<#inner_ty>(#ident_name, #is_option_or_string).map_err(|err| errors.insert(#ident_name.to_string(), err)).ok()
} }
}, },
false => { (false, _) => {
quote! { quote! {
model.get_value::<#ty>(#ident_name, #is_option_or_string).map_err(|err| errors.insert(#ident_name.to_string(), err)).ok() model.get_value::<#ty>(#ident_name, #is_option_or_string).map_err(|err| errors.insert(#ident_name.to_string(), err)).ok()
} }
} }
} };
res
}) })
.collect::<Vec<_>>() .collect::<Vec<_>>()
} }
@ -283,30 +297,49 @@ pub fn get_fields_for_create_model(fields: &Vec<ModelField>) -> Vec<TokenStream>
let ident_name = model_field.ident.to_string(); let ident_name = model_field.ident.to_string();
let ident = model_field.ident.to_owned(); let ident = model_field.ident.to_owned();
let ty = model_field.ty.to_owned(); let ty = model_field.ty.to_owned();
let type_path = model_field.get_type_path_string();
let is_option_or_string = model_field.is_option() || model_field.is_string(); let is_option_or_string = model_field.is_option() || model_field.is_string();
match model_field.is_option() { let res = match (model_field.is_option(), model_field.is_string(), type_path.as_str()) {
true => { // is DateTime
(true , _, "DateTime") => {
quote! {
#ident: Set(model.get_datetime(#ident_name, #is_option_or_string).unwrap())
}
},
(false , _, "DateTime") => {
quote! {
#ident: Set(model.get_datetime(#ident_name, #is_option_or_string).unwrap().unwrap())
}
},
(_ , _, "bool") => {
quote! {
#ident: Set(model.get_bool(#ident_name, #is_option_or_string).unwrap().unwrap())
}
},
// Default fields
(true, _, _) => {
let inner_ty = model_field.inner_type.to_owned().unwrap(); let inner_ty = model_field.inner_type.to_owned().unwrap();
quote! { quote! {
#ident: Set(model.get_value::<#inner_ty>(#ident_name, #is_option_or_string).unwrap()) #ident: Set(model.get_value::<#inner_ty>(#ident_name, #is_option_or_string).unwrap())
} }
} },
// TODO: this fails for empty string as it returns None which then cannot be unwrapped // is string which can be empty
// Should do maybe typecheck and if it is string return "" instead of None (false, true, _) => {
false => { quote! {
if model_field.is_string() { #ident: Set(model.get_value::<#ty>(#ident_name, #is_option_or_string).unwrap().unwrap_or(String::new()))
quote! { }
#ident: Set(model.get_value::<#ty>(#ident_name, #is_option_or_string).unwrap().unwrap_or(String::new())) },
} // no string
} else { (false, false, _) => {
quote! { quote! {
#ident: Set(model.get_value::<#ty>(#ident_name, #is_option_or_string).unwrap().unwrap()) #ident: Set(model.get_value::<#ty>(#ident_name, #is_option_or_string).unwrap().unwrap())
}
} }
} }
} };
res
}) })
.collect::<Vec<_>>() .collect::<Vec<_>>()
} }
@ -320,28 +353,45 @@ pub fn get_fields_for_edit_model(fields: &Vec<ModelField>) -> Vec<TokenStream> {
let ident_name = model_field.ident.to_string(); let ident_name = model_field.ident.to_string();
let ident = model_field.ident.to_owned(); let ident = model_field.ident.to_owned();
let ty = model_field.ty.to_owned(); let ty = model_field.ty.to_owned();
let type_path = model_field.get_type_path_string();
let is_option_or_string = model_field.is_option() || model_field.is_string(); let is_option_or_string = model_field.is_option() || model_field.is_string();
match model_field.is_option() { let res = match (model_field.is_option(), model_field.is_string(), type_path.as_str()) {
true => { (_, _, "bool") => {
quote! {
entity.#ident = Set(model.get_bool(#ident_name, #is_option_or_string).unwrap().unwrap())
}
},
(true , _, "DateTime") => {
quote! {
entity.#ident = Set(model.get_datetime(#ident_name, #is_option_or_string).unwrap())
}
},
(false , _, "DateTime") => {
quote! {
entity.#ident = Set(model.get_datetime(#ident_name, #is_option_or_string).unwrap().unwrap())
}
},
(true, _, _) => {
let inner_ty = model_field.inner_type.to_owned().unwrap(); let inner_ty = model_field.inner_type.to_owned().unwrap();
quote! { quote! {
entity.#ident = Set(model.get_value::<#inner_ty>(#ident_name, #is_option_or_string).unwrap()) entity.#ident = Set(model.get_value::<#inner_ty>(#ident_name, #is_option_or_string).unwrap())
} }
} },
false => { (false, true, _) => {
if model_field.is_string() { quote! {
quote! { entity.#ident = Set(model.get_value::<#ty>(#ident_name, #is_option_or_string).unwrap().unwrap_or(String::new()))
entity.#ident = Set(model.get_value::<#ty>(#ident_name, #is_option_or_string).unwrap().unwrap_or(String::new()))
}
} }
else { },
quote! { (false, false, _) => {
entity.#ident = Set(model.get_value::<#ty>(#ident_name, #is_option_or_string).unwrap().unwrap()) quote! {
} entity.#ident = Set(model.get_value::<#ty>(#ident_name, #is_option_or_string).unwrap().unwrap())
} }
} }
} };
res
}) })
.collect::<Vec<_>>() .collect::<Vec<_>>()
} }

View File

@ -1,7 +1,7 @@
use lazy_static::lazy_static; use lazy_static::lazy_static;
use sea_orm::DatabaseConnection; use sea_orm::DatabaseConnection;
use std::collections::HashMap; use std::collections::HashMap;
use tera::{Tera, Result, Value, to_value, try_get_value }; use tera::{Tera, Result, to_value, try_get_value };
use std::{ hash::BuildHasher}; use std::{ hash::BuildHasher};
pub mod view_model; pub mod view_model;
@ -35,6 +35,7 @@ lazy_static! {
let mut tera = Tera::new(concat!(env!("CARGO_MANIFEST_DIR"), "/templates/**/*")).unwrap(); 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_type", get_html_input_type);
tera.register_filter("get_html_input_class", get_html_input_class); tera.register_filter("get_html_input_class", get_html_input_class);
tera.register_filter("get_icon", get_icon);
tera tera
}; };
} }
@ -49,6 +50,17 @@ pub fn get_html_input_class<S: BuildHasher>(value: &tera::Value, _: &HashMap<Str
Ok(to_value(html_input_type).unwrap()) 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_html_input_type<S: BuildHasher>(value: &tera::Value, _: &HashMap<String, tera::Value, S>) -> Result<tera::Value> { 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); let field = try_get_value!("get_html_input_type", "value", ActixAdminViewModelField, value);
@ -60,6 +72,7 @@ pub fn get_html_input_type<S: BuildHasher>(value: &tera::Value, _: &HashMap<Stri
let html_input_type = match field.field_type { let html_input_type = match field.field_type {
ActixAdminViewModelFieldType::Text => "text", ActixAdminViewModelFieldType::Text => "text",
ActixAdminViewModelFieldType::DateTime => "datetime-local", ActixAdminViewModelFieldType::DateTime => "datetime-local",
ActixAdminViewModelFieldType::Date => "date",
ActixAdminViewModelFieldType::Checkbox => "checkbox", ActixAdminViewModelFieldType::Checkbox => "checkbox",
_ => "text" _ => "text"
}; };

View File

@ -5,6 +5,10 @@ use serde::Serialize;
use std::collections::HashMap; use std::collections::HashMap;
use actix_multipart:: {Multipart, MultipartError} ; use actix_multipart:: {Multipart, MultipartError} ;
use futures_util::stream::StreamExt as _; use futures_util::stream::StreamExt as _;
use chrono::{NaiveDateTime, NaiveDate};
use sea_orm::prelude::*;
use std::str::FromStr;
#[async_trait] #[async_trait]
pub trait ActixAdminModelTrait { pub trait ActixAdminModelTrait {
@ -29,6 +33,7 @@ pub struct ActixAdminModel {
pub errors: HashMap<String, String>, pub errors: HashMap<String, String>,
} }
impl ActixAdminModel { impl ActixAdminModel {
pub fn create_empty() -> ActixAdminModel { pub fn create_empty() -> ActixAdminModel {
ActixAdminModel { ActixAdminModel {
@ -65,6 +70,23 @@ impl ActixAdminModel {
} }
pub fn get_value<T: std::str::FromStr>(&self, key: &str, is_option_or_string: bool) -> Result<Option<T>, String> { pub fn get_value<T: std::str::FromStr>(&self, key: &str, is_option_or_string: bool) -> Result<Option<T>, String> {
self.get_value_by_closure(key, is_option_or_string, |val| val.parse::<T>())
}
pub fn get_datetime(&self, key: &str, is_option_or_string: bool) -> Result<Option<DateTime>, String> {
self.get_value_by_closure(key, is_option_or_string, |val| NaiveDateTime::parse_from_str(val, "%Y-%m-%dT%H:%M"))
}
pub fn get_bool(&self, key: &str, is_option_or_string: bool) -> Result<Option<bool>, String> {
let val = self.get_value_by_closure(key, is_option_or_string, |val| if !val.is_empty() { Ok(true) } else { Ok(false) });
// not selected bool field equals to false and not to missing
match val {
Ok(val) => Ok(val),
Err(_) => Ok(Some(false))
}
}
fn get_value_by_closure<T: std::str::FromStr>(&self, key: &str, is_option_or_string: bool, f: impl Fn(&String) -> Result<T, <T as std::str::FromStr>::Err>) -> Result<Option<T>, String> {
let value = self.values.get(key); let value = self.values.get(key);
let res: Result<Option<T>, String> = match value { let res: Result<Option<T>, String> = match value {
@ -73,7 +95,7 @@ impl ActixAdminModel {
return Ok(None); return Ok(None);
} }
let parsed_val = val.parse::<T>(); let parsed_val = f(val);
println!("{:?}", val); println!("{:?}", val);
match parsed_val { match parsed_val {

View File

@ -47,20 +47,6 @@ pub enum ActixAdminViewModelFieldType {
SelectList SelectList
} }
impl From<&str> for ActixAdminViewModelFieldType {
fn from(input: &str) -> ActixAdminViewModelFieldType {
match input {
"i32" => ActixAdminViewModelFieldType::Number,
"i64" => ActixAdminViewModelFieldType::Number,
"usize" => ActixAdminViewModelFieldType::Number,
"String" => ActixAdminViewModelFieldType::Text,
"bool" => ActixAdminViewModelFieldType::Checkbox,
"DateTimeWithTimeZone" => ActixAdminViewModelFieldType::DateTime,
_ => ActixAdminViewModelFieldType::Text
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ActixAdminViewModelField { pub struct ActixAdminViewModelField {
pub field_name: String, pub field_name: String,
@ -68,4 +54,25 @@ pub struct ActixAdminViewModelField {
pub select_list: String, pub select_list: String,
pub is_option: bool, pub is_option: bool,
pub field_type: ActixAdminViewModelFieldType pub field_type: ActixAdminViewModelFieldType
}
impl ActixAdminViewModelFieldType {
pub fn get_field_type(type_path: &str, select_list: String) -> ActixAdminViewModelFieldType {
if !select_list.is_empty() {
println!("field_type {} {}", type_path, select_list);
return ActixAdminViewModelFieldType::SelectList;
}
match type_path {
"i32" => ActixAdminViewModelFieldType::Number,
"i64" => ActixAdminViewModelFieldType::Number,
"usize" => ActixAdminViewModelFieldType::Number,
"String" => ActixAdminViewModelFieldType::Text,
"bool" => ActixAdminViewModelFieldType::Checkbox,
"DateTimeWithTimeZone" => ActixAdminViewModelFieldType::DateTime,
"DateTime" => ActixAdminViewModelFieldType::DateTime,
"Date" => ActixAdminViewModelFieldType::Date,
_ => ActixAdminViewModelFieldType::Text
}
}
} }

View File

@ -7,34 +7,15 @@
<label class="{{ model_field | get_html_input_type }}" for="{{ model_field.field_name }}"> <label class="{{ model_field | get_html_input_type }}" for="{{ model_field.field_name }}">
{{ model_field.field_name | split(pat="_") | join(sep=" ") | title }} {{ model_field.field_name | split(pat="_") | join(sep=" ") | title }}
</label> </label>
{% if model_field.select_list != "" %}
<div class="control"> <div class="control">
<div class="select {% if model.errors | get(key=model_field.field_name, default="" ) !="" %}is-danger{% endif %}"> {% if model_field.field_type == "SelectList" %}
<select name="{{ model_field.field_name }}"> {% include "form_elements/selectlist.html" %}
{% if model_field.is_option %} {% elif model_field.field_type == "Checkbox" %}
<option value=""></option> {% include "form_elements/checkbox.html" %}
{% else %}
<option value="" selected disabled>Select</option>
{% endif %}
{% 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, default="" ) %}
selected {% endif %} value="{{ select_list_item[0] }}">{{ select_list_item[1] | split(pat="_") |
join(sep=" ") | title }}</option>
{%- endfor %}
</select>
</div>
</div>
{% else %} {% else %}
<div class="control"> {% include "form_elements/input.html" %}
<input class="{{ model_field | get_html_input_class }} {% if model.errors | get(key=model_field.field_name,default="" ) !="" %}is-danger{% endif %}"
type="{{ model_field | get_html_input_type }}"
value="{{ model.values | get(key=model_field.field_name, default="") | split(pat=" _") | join(sep=" " ) | title }}"
name="{{ model_field.field_name }}"
placeholder="{{ model_field.field_name }}"
aria-label="{{ model_field.field_name }}"
>
</div>
{% endif %} {% endif %}
</div>
</div> </div>
{%- endfor %} {%- endfor %}
<div class="field is-grouped"> <div class="field is-grouped">

View File

@ -0,0 +1,9 @@
<input
class="{{ model_field | get_html_input_class }}"
type="{{ model_field | get_html_input_type }}"
value="true"
name="{{ model_field.field_name }}"
placeholder="{{ model_field.field_name }}"
aria-label="{{ model_field.field_name }}"
{% if model.values | get(key=model_field.field_name) == "true" %}checked{% endif %}
>

View File

@ -0,0 +1,8 @@
<input
class="{{ model_field | get_html_input_class }} {% if model.errors | get(key=model_field.field_name,default="" ) !="" %}is-danger{% endif %}"
type="{{ model_field | get_html_input_type }}"
value="{{ model.values | get(key=model_field.field_name, default="") | split(pat=" _") | join(sep=" " ) | title }}"
name="{{ model_field.field_name }}"
placeholder="{{ model_field.field_name }}"
aria-label="{{ model_field.field_name }}"
>

View File

@ -0,0 +1,14 @@
<div class="select {% if model.errors | get(key=model_field.field_name, default="" ) !="" %}is-danger{% endif %}">
<select name="{{ model_field.field_name }}">
{% if model_field.is_option %}
<option value=""></option>
{% else %}
<option value="" selected disabled>Select</option>
{% endif %}
{% 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, default="" ) %} selected {%
endif %} value="{{ select_list_item[0] }}">{{ select_list_item[1] | split(pat="_") |
join(sep=" ") | title }}</option>
{%- endfor %}
</select>
</div>

View File

@ -86,7 +86,11 @@
<td><input type="checkbox" name="ids" value="{{ entity.primary_key }}"></td> <td><input type="checkbox" name="ids" value="{{ entity.primary_key }}"></td>
<td>{{ entity.primary_key }}</td> <td>{{ entity.primary_key }}</td>
{% for model_field in view_model.fields -%} {% for model_field in view_model.fields -%}
<td>{{ entity.values | get(key=model_field.field_name) }}</td> {% if model_field.field_type == "Checkbox" %}
<td>{{ entity.values | get(key=model_field.field_name) | get_icon | safe }}</td>
{% else %}
<td>{{ entity.values | get(key=model_field.field_name) }}</td>
{% endif %}
{%- endfor %} {%- endfor %}
<td> <td>
<a href="edit/{{ entity.primary_key }}"><i class="fa-solid fa-pen-to-square"></i></a> <a href="edit/{{ entity.primary_key }}"><i class="fa-solid fa-pen-to-square"></i></a>

Binary file not shown.

View File

@ -14,7 +14,7 @@ pub struct Model {
#[actix_admin(html_input_type = "email")] #[actix_admin(html_input_type = "email")]
pub user: String, pub user: String,
#[sea_orm(column_type = "DateTime")] #[sea_orm(column_type = "DateTime")]
pub insert_date: DateTimeWithTimeZone, pub insert_date: DateTime,
pub is_visible: bool pub is_visible: bool
} }