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