old flash in new prop form, prop edit form

master
Ondřej Hruška 4 years ago
parent 0af8b89140
commit d43ddfa8bd
Signed by: MightyPork
GPG Key ID: 2C5FD5035250423D
  1. 1
      yopa-web/resources/templates/_macros.html.tera
  2. 16
      yopa-web/resources/templates/property_create.html.tera
  3. 40
      yopa-web/resources/templates/property_update.html.tera
  4. 25
      yopa-web/src/main.rs
  5. 6
      yopa-web/src/routes/object_model.rs
  6. 214
      yopa-web/src/routes/property_model.rs
  7. 6
      yopa-web/src/routes/relation_model.rs
  8. 45
      yopa/src/lib.rs
  9. 2
      yopa/src/model.rs

@ -5,5 +5,6 @@
{%- endif -%} {%- endif -%}
{%- if prop.optional %}, OPTIONAL{% endif %} {%- if prop.optional %}, OPTIONAL{% endif %}
{%- if prop.multiple %}, MULTIPLE{% endif %} {%- if prop.multiple %}, MULTIPLE{% endif %}
<a href="/model/property/update/{{prop.id}}">Edit property</a> &middot;
<a href="/model/property/delete/{{prop.id}}" onclick="return confirm('Delete property?')">Delete property</a> <a href="/model/property/delete/{{prop.id}}" onclick="return confirm('Delete property?')">Delete property</a>
{% endmacro input %} {% endmacro input %}

@ -19,25 +19,25 @@ Define property
<input type="hidden" name="object" value="{{object.id}}"> <input type="hidden" name="object" value="{{object.id}}">
<label for="name">Name:</label> <label for="name">Name:</label>
<input type="text" id="name" name="name" autocomplete="off"><br> <input type="text" id="name" name="name" value="{{old.name}}" autocomplete="off"><br>
<label for="optional">Optional:</label> <label for="optional">Optional:</label>
<input type="checkbox" name="optional" id="optional" value="true" autocomplete="off"> <input type="checkbox" name="optional" id="optional" value="true" {{opt(checked=old.optional)}} autocomplete="off">
<br> <br>
<label for="multiple">Multiple:</label> <label for="multiple">Multiple:</label>
<input type="checkbox" name="multiple" id="multiple" value="true" autocomplete="off"><br> <input type="checkbox" name="multiple" id="multiple" value="true" {{opt(checked=old.multiple)}} autocomplete="off"><br>
<label for="data_type">Type:</label> <label for="data_type">Type:</label>
<select name="data_type" id="data_type" autocomplete="off"> <select name="data_type" id="data_type" autocomplete="off">
<option value="String" selected>String</option> <option value="String" {{selected(opt="String",val=old.data_type)}}>String</option>
<option value="Integer">Integer</option> <option value="Integer" {{selected(opt="Integer",val=old.data_type)}}>Integer</option>
<option value="Decimal">Decimal</option> <option value="Decimal" {{selected(opt="Decimal",val=old.data_type)}}>Decimal</option>
<option value="Boolean">Boolean</option> <option value="Boolean" {{selected(opt="Boolean",val=old.data_type)}}>Boolean</option>
</select><br> </select><br>
<label for="default">Default:</label> <label for="default">Default:</label>
<input type="text" id="default" name="default" autocomplete="off"><br> <input type="text" id="default" name="default" value="{{old.default}}" autocomplete="off"><br>
<input type="submit" value="Save"> <input type="submit" value="Save">
</form> </form>

@ -0,0 +1,40 @@
{% extends "_layout" %}
{% block title -%}
Edit property
{%- endblock %}
{% block nav -%}
<a href="/">Home</a>
{%- endblock %}
{% block content -%}
<h1>Edit property {{model.name}}</h1>
<form action="/model/property/update/{{model.id}}" method="POST">
<label for="name">Name:</label>
<input type="text" id="name" name="name" value="{{model.name}}" autocomplete="off"><br>
<label for="optional">Optional:</label>
<input type="checkbox" name="optional" id="optional" value="true" {{opt(checked=model.optional)}} autocomplete="off">
<br>
<label for="multiple">Multiple:</label>
<input type="checkbox" name="multiple" id="multiple" value="true" {{opt(checked=model.multiple)}} autocomplete="off"><br>
<label for="data_type">Type:</label>
<select name="data_type" id="data_type" autocomplete="off">
<option value="String" {{selected(val=model.data_type, opt="String")}}>String</option>
<option value="Integer" {{selected(val=model.data_type, opt="Integer")}}>Integer</option>
<option value="Decimal" {{selected(val=model.data_type, opt="Decimal")}}>Decimal</option>
<option value="Boolean" {{selected(val=model.data_type, opt="Boolean")}}>Boolean</option>
</select><br>
<label for="default">Default:</label>
<input type="text" id="default" name="default" value="{{model.default | print_typed_value}}" autocomplete="off"><br>
<input type="submit" value="Save">
</form>
{%- endblock %}

@ -56,6 +56,7 @@ pub(crate) static TERA: Lazy<Tera> = Lazy::new(|| {
Err(tera::Error::msg("Expected nonenmpty object")) Err(tera::Error::msg("Expected nonenmpty object"))
}); });
// opt(checked=foo.is_checked)
tera.register_function("opt", |args: &HashMap<String, Value>| -> tera::Result<Value> { tera.register_function("opt", |args: &HashMap<String, Value>| -> tera::Result<Value> {
if args.len() != 1 { if args.len() != 1 {
return Err("Expected 1 argument".into()); return Err("Expected 1 argument".into());
@ -71,6 +72,20 @@ pub(crate) static TERA: Lazy<Tera> = Lazy::new(|| {
} }
}); });
// selected(val=foo.color,opt="Red")
tera.register_function("selected", |args: &HashMap<String, Value>| -> tera::Result<Value> {
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. // 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<String, Value>| -> tera::Result<Value> { // tera.register_function("url_for", |args: HashMap<String, Value>| -> tera::Result<Value> {
// match args.get("name") { // 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_form)
.service(routes::property_model::create) .service(routes::property_model::create)
.service(routes::property_model::update_form)
.service(routes::property_model::update)
.service(routes::property_model::delete) .service(routes::property_model::delete)
.service(static_files) .service(static_files)
.default_service(web::to(|| HttpResponse::NotFound().body("File or endpoint not found"))) .default_service(web::to(|| HttpResponse::NotFound().body("File or endpoint not found")))
@ -169,7 +186,7 @@ fn init_yopa() -> YopaStoreWrapper {
optional: false, optional: false,
multiple: true, multiple: true,
data_type: DataType::String, data_type: DataType::String,
default: None, default: TypedValue::String("".into()),
}).unwrap(); }).unwrap();
store.define_property(model::PropertyModel { store.define_property(model::PropertyModel {
@ -179,7 +196,7 @@ fn init_yopa() -> YopaStoreWrapper {
optional: false, optional: false,
multiple: false, multiple: false,
data_type: DataType::String, data_type: DataType::String,
default: None, default: TypedValue::String("".into()),
}).unwrap(); }).unwrap();
store.define_property(model::PropertyModel { store.define_property(model::PropertyModel {
@ -189,7 +206,7 @@ fn init_yopa() -> YopaStoreWrapper {
optional: true, optional: true,
multiple: true, multiple: true,
data_type: DataType::String, data_type: DataType::String,
default: Some(TypedValue::String("Pepa Novák".into())), default: TypedValue::String("Pepa Novák".into()),
}).unwrap(); }).unwrap();
let rel_book_id = store.define_relation(model::RelationModel { let rel_book_id = store.define_relation(model::RelationModel {
@ -209,7 +226,7 @@ fn init_yopa() -> YopaStoreWrapper {
optional: true, optional: true,
multiple: false, multiple: false,
data_type: DataType::Integer, data_type: DataType::Integer,
default: None, default: TypedValue::Integer(0),
}).unwrap(); }).unwrap();
store.define_relation(model::RelationModel { store.define_relation(model::RelationModel {

@ -58,7 +58,7 @@ pub(crate) async fn create(
redirect("/") redirect("/")
} }
Err(e) => { Err(e) => {
warn!("Error creating model: {:?}", e); warn!("Error creating model: {}", e);
session.flash_error(e.to_string()); session.flash_error(e.to_string());
session.set("old", form); session.set("old", form);
redirect("/model/object/create") redirect("/model/object/create")
@ -112,7 +112,7 @@ pub(crate) async fn update(
redirect("/") redirect("/")
} }
Err(e) => { Err(e) => {
warn!("Error updating model: {:?}", e); warn!("Error updating model: {}", e);
session.flash_error(e.to_string()); session.flash_error(e.to_string());
session.set("old", form); session.set("old", form);
redirect(format!("/model/object/update/{}", id)) redirect(format!("/model/object/update/{}", id))
@ -135,7 +135,7 @@ pub(crate) async fn delete(
redirect("/") redirect("/")
} }
Err(e) => { Err(e) => {
warn!("Error deleting object model: {:?}", e); warn!("Error deleting object model: {}", e);
session.flash_error(e.to_string()); session.flash_error(e.to_string());
redirect("/") // back? redirect("/") // back?
} }

@ -22,6 +22,20 @@ pub(crate) async fn create_form(
let rg = store.read().await; let rg = store.read().await;
// Re-fill old values
if let Ok(Some(form)) = session.take::<PropertyModelCreateForm>("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); debug!("ID = {}", object_id);
let object = { let object = {
@ -46,8 +60,8 @@ pub(crate) async fn create_form(
TERA.build_response("property_create", &context) TERA.build_response("property_create", &context)
} }
#[derive(Deserialize)] #[derive(Serialize,Deserialize)]
pub(crate) struct PropertyModelCreate { pub(crate) struct PropertyModelCreateForm {
pub object: ID, pub object: ID,
pub name: String, pub name: String,
#[serde(default)] #[serde(default)]
@ -60,80 +74,76 @@ pub(crate) struct PropertyModelCreate {
pub default: String, pub default: String,
} }
#[post("/model/property/create")] fn parse_default(data_type : DataType, default : String) -> Result<TypedValue, String> {
pub(crate) async fn create( Ok(match data_type {
form: web::Form<PropertyModelCreate>,
store: crate::YopaStoreWrapper,
session: Session,
) -> actix_web::Result<impl Responder> {
let mut wg = store.write().await;
let form = form.into_inner();
let optional = form.optional;
let multiple = form.multiple;
match wg.define_property(PropertyModel {
id: Default::default(),
object: form.object,
name: form.name.clone(),
optional,
multiple,
data_type: form.data_type,
default: {
match form.data_type {
DataType::String => { DataType::String => {
if form.default.is_empty() && optional { TypedValue::String(default.into())
None
} else {
Some(TypedValue::String(form.default.into()))
}
} }
DataType::Integer => { DataType::Integer => {
if form.default.is_empty() { if default.is_empty() {
if optional { TypedValue::Integer(0)
None
} else {
Some(TypedValue::Integer(0))
}
} else { } else {
// TODO better error reporting // TODO better error reporting
Some(TypedValue::Integer(form.default.parse() TypedValue::Integer(default.parse()
.map_err(|_| { .map_err(|_| {
actix_web::error::ErrorBadRequest(format!("Error parsing \"{}\" as integer", form.default)) format!("Error parsing \"{}\" as integer", default)
})?)) })?)
} }
} }
DataType::Decimal => { DataType::Decimal => {
if form.default.is_empty() { if default.is_empty() {
if optional { TypedValue::Decimal(0.0)
None
} else {
Some(TypedValue::Decimal(0.0))
}
} else { } else {
// TODO better error reporting // TODO better error reporting
Some(TypedValue::Decimal(form.default.parse() TypedValue::Decimal(default.parse()
.map_err(|_| { .map_err(|_| {
actix_web::error::ErrorBadRequest(format!("Error parsing \"{}\" as decimal", form.default)) format!("Error parsing \"{}\" as decimal", default)
})?)) })?)
} }
} }
DataType::Boolean => { DataType::Boolean => {
if form.default.is_empty() { if default.is_empty() {
if optional { TypedValue::Boolean(false)
None
} else { } else {
Some(TypedValue::Boolean(false)) TypedValue::String(default.clone().into())
}
} else {
Some(TypedValue::String(form.default.clone().into())
.cast_to(DataType::Boolean).map_err(|_| { .cast_to(DataType::Boolean).map_err(|_| {
actix_web::error::ErrorBadRequest(format!("Error parsing \"{}\" as boolean", form.default)) format!("Error parsing \"{}\" as boolean", default)
})?) })?
} }
} }
})
} }
},
#[post("/model/property/create")]
pub(crate) async fn create(
form: web::Form<PropertyModelCreateForm>,
store: crate::YopaStoreWrapper,
session: Session,
) -> actix_web::Result<impl Responder> {
let mut wg = store.write().await;
let form = form.into_inner();
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,
name: form.name.clone(),
optional,
multiple,
data_type: form.data_type,
default,
}) { }) {
Ok(_id) => { Ok(_id) => {
debug!("Property created, redirecting to root"); debug!("Property created, redirecting to root");
@ -141,8 +151,9 @@ pub(crate) async fn create(
redirect("/") redirect("/")
} }
Err(e) => { Err(e) => {
warn!("Error creating property model: {:?}", e); warn!("Error creating property model: {}", e);
session.flash_error(e.to_string()); session.flash_error(e.to_string());
session.set("old", &form);
redirect(format!("/model/property/create/{}", form.object)) redirect(format!("/model/property/create/{}", form.object))
} }
} }
@ -162,9 +173,96 @@ pub(crate) async fn delete(
redirect("/") redirect("/")
} }
Err(e) => { Err(e) => {
warn!("Error deleting property: {:?}", e); warn!("Error deleting property: {}", e);
session.flash_error(e.to_string()); session.flash_error(e.to_string());
redirect("/") // back? 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<ID>,
store: crate::YopaStoreWrapper,
session: Session,
) -> actix_web::Result<impl Responder> {
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::<PropertyModelEditForm>("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<ID>,
form: web::Form<PropertyModelEditForm>,
store: crate::YopaStoreWrapper,
session: Session,
) -> actix_web::Result<impl Responder> {
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))
}
}
}

@ -78,7 +78,7 @@ pub(crate) async fn create(
redirect("/") redirect("/")
} }
Err(e) => { Err(e) => {
warn!("Error creating relation model: {:?}", e); warn!("Error creating relation model: {}", e);
session.flash_error(e.to_string()); session.flash_error(e.to_string());
redirect(format!("/model/relation/create/{}", form.object)) redirect(format!("/model/relation/create/{}", form.object))
} }
@ -105,7 +105,7 @@ pub(crate) async fn delete(
redirect("/") redirect("/")
} }
Err(e) => { Err(e) => {
warn!("Error deleting relation model: {:?}", e); warn!("Error deleting relation model: {}", e);
session.flash_error(e.to_string()); session.flash_error(e.to_string());
redirect("/") // back? redirect("/") // back?
} }
@ -175,7 +175,7 @@ pub(crate) async fn update(
redirect("/") redirect("/")
} }
Err(e) => { Err(e) => {
warn!("Error updating model: {:?}", e); warn!("Error updating model: {}", e);
session.flash_error(e.to_string()); session.flash_error(e.to_string());
session.set("old", form); session.set("old", form);
redirect(format!("/model/relation/update/{}", id)) redirect(format!("/model/relation/update/{}", id))

@ -129,16 +129,14 @@ impl Storage {
if self.prop_models.iter().find(|(_, t)| t.object == prop.object && t.name == prop.name).is_some() { if self.prop_models.iter().find(|(_, t)| t.object == prop.object && t.name == prop.name).is_some() {
return Err(StorageError::ConstraintViolation( 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 // Ensure the default type is compatible
if let Some(d) = prop.default { prop.default = match prop.default.clone().cast_to(prop.data_type) {
prop.default = Some(match d.cast_to(prop.data_type) {
Ok(v) => v, Ok(v) => v,
Err(d) => return Err(StorageError::NotExist(format!("default value {:?} has invalid type", d).into())) 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)); debug!("Define property model \"{}\" of {}", prop.name, self.describe_model(prop.object));
let id = next_id(); let id = next_id();
@ -272,16 +270,12 @@ impl Storage {
} }
} else { } else {
if !prop.optional { if !prop.optional {
if let Some(def) = &prop.default {
values_to_insert.push(data::Value { values_to_insert.push(data::Value {
id: next_id(), id: next_id(),
object: parent_id, object: parent_id,
model: prop.id, model: prop.id,
value: def.clone(), value: prop.default.clone(),
}); });
} else {
return Err(StorageError::ConstraintViolation(format!("{} is required for {} and no default value is defined", prop, self.describe_model(parent_model_id)).into()));
}
} }
} }
} }
@ -357,7 +351,7 @@ impl Storage {
self.rel_models.get(&id) 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) self.prop_models.get(&id)
} }
@ -426,4 +420,31 @@ impl Storage {
self.rel_models.insert(rel.id, rel); self.rel_models.insert(rel.id, rel);
Ok(()) 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(())
}
} }

@ -62,7 +62,7 @@ pub struct PropertyModel {
/// Property data type /// Property data type
pub data_type: DataType, pub data_type: DataType,
/// Default value, used for newly created objects /// Default value, used for newly created objects
pub default: Option<TypedValue>, pub default: TypedValue,
} }
/// Value data type /// Value data type

Loading…
Cancel
Save