From d43ddfa8bd86b21b1a1ecea79c3e83b2329b801e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Thu, 11 Feb 2021 21:52:11 +0100 Subject: [PATCH] old flash in new prop form, prop edit form --- .../resources/templates/_macros.html.tera | 1 + .../templates/property_create.html.tera | 16 +- .../templates/property_update.html.tera | 40 ++++ yopa-web/src/main.rs | 25 +- yopa-web/src/routes/object_model.rs | 6 +- yopa-web/src/routes/property_model.rs | 218 +++++++++++++----- yopa-web/src/routes/relation_model.rs | 6 +- yopa/src/lib.rs | 57 +++-- yopa/src/model.rs | 2 +- 9 files changed, 274 insertions(+), 97 deletions(-) create mode 100644 yopa-web/resources/templates/property_update.html.tera diff --git a/yopa-web/resources/templates/_macros.html.tera b/yopa-web/resources/templates/_macros.html.tera index 53d5e13..fa34425 100644 --- a/yopa-web/resources/templates/_macros.html.tera +++ b/yopa-web/resources/templates/_macros.html.tera @@ -5,5 +5,6 @@ {%- endif -%} {%- if prop.optional %}, OPTIONAL{% endif %} {%- if prop.multiple %}, MULTIPLE{% endif %} + Edit property · Delete property {% endmacro input %} diff --git a/yopa-web/resources/templates/property_create.html.tera b/yopa-web/resources/templates/property_create.html.tera index b66db34..69ac221 100644 --- a/yopa-web/resources/templates/property_create.html.tera +++ b/yopa-web/resources/templates/property_create.html.tera @@ -19,25 +19,25 @@ Define property -
+
- +
-
+

-
+
diff --git a/yopa-web/resources/templates/property_update.html.tera b/yopa-web/resources/templates/property_update.html.tera new file mode 100644 index 0000000..12e7bd1 --- /dev/null +++ b/yopa-web/resources/templates/property_update.html.tera @@ -0,0 +1,40 @@ +{% extends "_layout" %} + +{% block title -%} +Edit property +{%- endblock %} + +{% block nav -%} +Home +{%- endblock %} + +{% block content -%} + +

Edit property {{model.name}}

+ +
+ +
+ + + +
+ + +
+ + +
+ + +
+ + +
+ +{%- endblock %} diff --git a/yopa-web/src/main.rs b/yopa-web/src/main.rs index aa52a4f..5247f48 100644 --- a/yopa-web/src/main.rs +++ b/yopa-web/src/main.rs @@ -56,6 +56,7 @@ pub(crate) static TERA: Lazy = Lazy::new(|| { Err(tera::Error::msg("Expected nonenmpty object")) }); + // opt(checked=foo.is_checked) tera.register_function("opt", |args: &HashMap| -> tera::Result { if args.len() != 1 { return Err("Expected 1 argument".into()); @@ -71,6 +72,20 @@ pub(crate) static TERA: Lazy = Lazy::new(|| { } }); + // selected(val=foo.color,opt="Red") + tera.register_function("selected", |args: &HashMap| -> tera::Result { + match (args.get("val"), args.get("opt")) { + (Some(v), Some(w)) => { + if v == w { + Ok(Value::String("selected".into())) + } else { + Ok(Value::Null) + } + }, + _ => Err("Expected val and opt args".into()), + } + }); + // TODO need to inject HttpRequest::url_for() into tera context, but it then can't be accessed by the functions. // tera.register_function("url_for", |args: HashMap| -> tera::Result { // match args.get("name") { @@ -131,6 +146,8 @@ async fn main() -> std::io::Result<()> { // .service(routes::property_model::create_form) .service(routes::property_model::create) + .service(routes::property_model::update_form) + .service(routes::property_model::update) .service(routes::property_model::delete) .service(static_files) .default_service(web::to(|| HttpResponse::NotFound().body("File or endpoint not found"))) @@ -169,7 +186,7 @@ fn init_yopa() -> YopaStoreWrapper { optional: false, multiple: true, data_type: DataType::String, - default: None, + default: TypedValue::String("".into()), }).unwrap(); store.define_property(model::PropertyModel { @@ -179,7 +196,7 @@ fn init_yopa() -> YopaStoreWrapper { optional: false, multiple: false, data_type: DataType::String, - default: None, + default: TypedValue::String("".into()), }).unwrap(); store.define_property(model::PropertyModel { @@ -189,7 +206,7 @@ fn init_yopa() -> YopaStoreWrapper { optional: true, multiple: true, data_type: DataType::String, - default: Some(TypedValue::String("Pepa Novák".into())), + default: TypedValue::String("Pepa Novák".into()), }).unwrap(); let rel_book_id = store.define_relation(model::RelationModel { @@ -209,7 +226,7 @@ fn init_yopa() -> YopaStoreWrapper { optional: true, multiple: false, data_type: DataType::Integer, - default: None, + default: TypedValue::Integer(0), }).unwrap(); store.define_relation(model::RelationModel { diff --git a/yopa-web/src/routes/object_model.rs b/yopa-web/src/routes/object_model.rs index ccb0940..d675278 100644 --- a/yopa-web/src/routes/object_model.rs +++ b/yopa-web/src/routes/object_model.rs @@ -58,7 +58,7 @@ pub(crate) async fn create( redirect("/") } Err(e) => { - warn!("Error creating model: {:?}", e); + warn!("Error creating model: {}", e); session.flash_error(e.to_string()); session.set("old", form); redirect("/model/object/create") @@ -112,7 +112,7 @@ pub(crate) async fn update( redirect("/") } Err(e) => { - warn!("Error updating model: {:?}", e); + warn!("Error updating model: {}", e); session.flash_error(e.to_string()); session.set("old", form); redirect(format!("/model/object/update/{}", id)) @@ -135,7 +135,7 @@ pub(crate) async fn delete( redirect("/") } Err(e) => { - warn!("Error deleting object model: {:?}", e); + warn!("Error deleting object model: {}", e); session.flash_error(e.to_string()); redirect("/") // back? } diff --git a/yopa-web/src/routes/property_model.rs b/yopa-web/src/routes/property_model.rs index 673a740..59e0450 100644 --- a/yopa-web/src/routes/property_model.rs +++ b/yopa-web/src/routes/property_model.rs @@ -22,6 +22,20 @@ pub(crate) async fn create_form( let rg = store.read().await; + // Re-fill old values + if let Ok(Some(form)) = session.take::("old") { + context.insert("old", &form); + } else { + context.insert("old", &PropertyModelCreateForm { + object: Default::default(), + name: "".to_string(), + optional: false, + multiple: false, + data_type: DataType::String, + default: "".to_string() + }); + } + debug!("ID = {}", object_id); let object = { @@ -46,8 +60,8 @@ pub(crate) async fn create_form( TERA.build_response("property_create", &context) } -#[derive(Deserialize)] -pub(crate) struct PropertyModelCreate { +#[derive(Serialize,Deserialize)] +pub(crate) struct PropertyModelCreateForm { pub object: ID, pub name: String, #[serde(default)] @@ -60,9 +74,49 @@ pub(crate) struct PropertyModelCreate { pub default: String, } +fn parse_default(data_type : DataType, default : String) -> Result { + Ok(match data_type { + DataType::String => { + TypedValue::String(default.into()) + } + DataType::Integer => { + if default.is_empty() { + TypedValue::Integer(0) + } else { + // TODO better error reporting + TypedValue::Integer(default.parse() + .map_err(|_| { + format!("Error parsing \"{}\" as integer", default) + })?) + } + } + DataType::Decimal => { + if default.is_empty() { + TypedValue::Decimal(0.0) + } else { + // TODO better error reporting + TypedValue::Decimal(default.parse() + .map_err(|_| { + format!("Error parsing \"{}\" as decimal", default) + })?) + } + } + DataType::Boolean => { + if default.is_empty() { + TypedValue::Boolean(false) + } else { + TypedValue::String(default.clone().into()) + .cast_to(DataType::Boolean).map_err(|_| { + format!("Error parsing \"{}\" as boolean", default) + })? + } + } + }) +} + #[post("/model/property/create")] pub(crate) async fn create( - form: web::Form, + form: web::Form, store: crate::YopaStoreWrapper, session: Session, ) -> actix_web::Result { @@ -72,6 +126,16 @@ pub(crate) async fn create( let optional = form.optional; let multiple = form.multiple; + let default = match parse_default(form.data_type, form.default.clone()) { + Ok(def) => def, + Err(msg) => { + warn!("{}", msg); + session.flash_error(msg); + session.set("old", &form); + return redirect(format!("/model/property/create/{}", form.object)); + } + }; + match wg.define_property(PropertyModel { id: Default::default(), object: form.object, @@ -79,61 +143,7 @@ pub(crate) async fn create( optional, multiple, data_type: form.data_type, - default: { - match form.data_type { - DataType::String => { - if form.default.is_empty() && optional { - None - } else { - Some(TypedValue::String(form.default.into())) - } - } - DataType::Integer => { - if form.default.is_empty() { - if optional { - None - } else { - Some(TypedValue::Integer(0)) - } - } else { - // TODO better error reporting - Some(TypedValue::Integer(form.default.parse() - .map_err(|_| { - actix_web::error::ErrorBadRequest(format!("Error parsing \"{}\" as integer", form.default)) - })?)) - } - } - DataType::Decimal => { - if form.default.is_empty() { - if optional { - None - } else { - Some(TypedValue::Decimal(0.0)) - } - } else { - // TODO better error reporting - Some(TypedValue::Decimal(form.default.parse() - .map_err(|_| { - actix_web::error::ErrorBadRequest(format!("Error parsing \"{}\" as decimal", form.default)) - })?)) - } - } - DataType::Boolean => { - if form.default.is_empty() { - if optional { - None - } else { - Some(TypedValue::Boolean(false)) - } - } else { - Some(TypedValue::String(form.default.clone().into()) - .cast_to(DataType::Boolean).map_err(|_| { - actix_web::error::ErrorBadRequest(format!("Error parsing \"{}\" as boolean", form.default)) - })?) - } - } - } - }, + default, }) { Ok(_id) => { debug!("Property created, redirecting to root"); @@ -141,8 +151,9 @@ pub(crate) async fn create( redirect("/") } Err(e) => { - warn!("Error creating property model: {:?}", e); + warn!("Error creating property model: {}", e); session.flash_error(e.to_string()); + session.set("old", &form); redirect(format!("/model/property/create/{}", form.object)) } } @@ -162,9 +173,96 @@ pub(crate) async fn delete( redirect("/") } Err(e) => { - warn!("Error deleting property: {:?}", e); + warn!("Error deleting property: {}", e); session.flash_error(e.to_string()); redirect("/") // back? } } } + +#[derive(Serialize, Deserialize)] +pub(crate) struct PropertyModelEditForm { + pub name: String, + #[serde(default)] + pub optional: bool, + #[serde(default)] + pub multiple: bool, + pub data_type: DataType, + /// Default value to be parsed to the data type + /// May be unused if empty and optional + pub default: String, +} + + +#[get("/model/property/update/{model_id}")] +pub(crate) async fn update_form( + model_id: web::Path, + store: crate::YopaStoreWrapper, + session: Session, +) -> actix_web::Result { + let mut context = tera::Context::new(); + session.render_flash(&mut context); + let rg = store.read().await; + + let model = rg.get_property_model(*model_id) + .ok_or_else(|| actix_web::error::ErrorNotFound("No such model"))?; + + // Re-fill old values + if let Ok(Some(form)) = session.take::("old") { + let mut model = model.clone(); + model.name = form.name; + model.data_type = form.data_type; + model.default = TypedValue::String(form.default.into()); + model.optional = form.optional; + model.multiple = form.multiple; + context.insert("model", &model); + } else { + context.insert("model", model); + } + + TERA.build_response("property_update", &context) +} + +#[post("/model/property/update/{model_id}")] +pub(crate) async fn update( + model_id: web::Path, + form: web::Form, + store: crate::YopaStoreWrapper, + session: Session, +) -> actix_web::Result { + let mut wg = store.write().await; + let form = form.into_inner(); + + let id = model_id.into_inner(); + let default = match parse_default(form.data_type, form.default.clone()) { + Ok(def) => def, + Err(msg) => { + warn!("{}", msg); + session.flash_error(msg); + session.set("old", form); + return redirect(format!("/model/property/update/{}", id)); + } + }; + + match wg.update_property(PropertyModel { + id, + object: Default::default(), // dummy + name: form.name.clone(), + optional: form.optional, + multiple: form.multiple, + data_type: form.data_type, + default, + }) { + Ok(_id) => { + debug!("Relation updated, redirecting to root"); + session.flash_success(format!("Property \"{}\" updated.", form.name)); + redirect("/") + } + Err(e) => { + warn!("Error updating model: {}", e); + session.flash_error(e.to_string()); + session.set("old", form); + redirect(format!("/model/relation/update/{}", id)) + } + } +} diff --git a/yopa-web/src/routes/relation_model.rs b/yopa-web/src/routes/relation_model.rs index f9d3846..8c20f64 100644 --- a/yopa-web/src/routes/relation_model.rs +++ b/yopa-web/src/routes/relation_model.rs @@ -78,7 +78,7 @@ pub(crate) async fn create( redirect("/") } Err(e) => { - warn!("Error creating relation model: {:?}", e); + warn!("Error creating relation model: {}", e); session.flash_error(e.to_string()); redirect(format!("/model/relation/create/{}", form.object)) } @@ -105,7 +105,7 @@ pub(crate) async fn delete( redirect("/") } Err(e) => { - warn!("Error deleting relation model: {:?}", e); + warn!("Error deleting relation model: {}", e); session.flash_error(e.to_string()); redirect("/") // back? } @@ -175,7 +175,7 @@ pub(crate) async fn update( redirect("/") } Err(e) => { - warn!("Error updating model: {:?}", e); + warn!("Error updating model: {}", e); session.flash_error(e.to_string()); session.set("old", form); redirect(format!("/model/relation/update/{}", id)) diff --git a/yopa/src/lib.rs b/yopa/src/lib.rs index 0d878b0..98d89c7 100644 --- a/yopa/src/lib.rs +++ b/yopa/src/lib.rs @@ -129,16 +129,14 @@ impl Storage { if self.prop_models.iter().find(|(_, t)| t.object == prop.object && t.name == prop.name).is_some() { return Err(StorageError::ConstraintViolation( - format!("property with the name \"{}\" already exists on model {}", prop.name, prop.object).into())); + format!("property with the name \"{}\" already exists on model {}", prop.name, self.describe_model(prop.object)).into())); } // Ensure the default type is compatible - if let Some(d) = prop.default { - prop.default = Some(match d.cast_to(prop.data_type) { - Ok(v) => v, - Err(d) => return Err(StorageError::NotExist(format!("default value {:?} has invalid type", d).into())) - }); - } + prop.default = match prop.default.clone().cast_to(prop.data_type) { + Ok(v) => v, + Err(d) => return Err(StorageError::NotExist(format!("default value {:?} has invalid type", prop.default).into())) + }; debug!("Define property model \"{}\" of {}", prop.name, self.describe_model(prop.object)); let id = next_id(); @@ -272,16 +270,12 @@ impl Storage { } } else { if !prop.optional { - if let Some(def) = &prop.default { - values_to_insert.push(data::Value { - id: next_id(), - object: parent_id, - model: prop.id, - value: def.clone(), - }); - } else { - return Err(StorageError::ConstraintViolation(format!("{} is required for {} and no default value is defined", prop, self.describe_model(parent_model_id)).into())); - } + values_to_insert.push(data::Value { + id: next_id(), + object: parent_id, + model: prop.id, + value: prop.default.clone(), + }); } } } @@ -357,7 +351,7 @@ impl Storage { self.rel_models.get(&id) } - pub fn get_prop_model(&self, id : ID) -> Option<&PropertyModel> { + pub fn get_property_model(&self, id : ID) -> Option<&PropertyModel> { self.prop_models.get(&id) } @@ -426,4 +420,31 @@ impl Storage { self.rel_models.insert(rel.id, rel); Ok(()) } + + pub fn update_property(&mut self, mut prop: PropertyModel) -> Result<(), StorageError> { + if prop.name.is_empty() { + return Err(StorageError::ConstraintViolation(format!("Property name must not be empty.").into())); + } + + // Object can't be changed, so we re-fill them from the existing model + if let Some(existing) = self.prop_models.get(&prop.id) { + prop.object = existing.object; + } else { + return Err(StorageError::NotExist(format!("Property model ID {} does not exist.", prop.id).into())); + } + + if self.prop_models.iter().find(|(_, t)| t.object == prop.object && t.name == prop.name && t.id != prop.id).is_some() { + return Err(StorageError::ConstraintViolation( + format!("property with the name \"{}\" already exists on {}", prop.name, self.describe_model(prop.object)).into())); + } + + // Ensure the default type is compatible + prop.default = match prop.default.clone().cast_to(prop.data_type) { + Ok(v) => v, + Err(d) => return Err(StorageError::NotExist(format!("default value {:?} has invalid type", prop.default).into())) + }; + + self.prop_models.insert(prop.id, prop); + Ok(()) + } } diff --git a/yopa/src/model.rs b/yopa/src/model.rs index 1182404..e124f9a 100644 --- a/yopa/src/model.rs +++ b/yopa/src/model.rs @@ -62,7 +62,7 @@ pub struct PropertyModel { /// Property data type pub data_type: DataType, /// Default value, used for newly created objects - pub default: Option, + pub default: TypedValue, } /// Value data type