relation edit

master
Ondřej Hruška 4 years ago
parent 2a71f827b0
commit bf7e9a1871
Signed by: MightyPork
GPG Key ID: 2C5FD5035250423D
  1. 2
      yopa-web/resources/templates/_macros.html.tera
  2. 5
      yopa-web/resources/templates/index.html.tera
  3. 4
      yopa-web/resources/templates/model_create.html.tera
  4. 4
      yopa-web/resources/templates/property_create.html.tera
  5. 4
      yopa-web/resources/templates/relation_create.html.tera
  6. 35
      yopa-web/resources/templates/relation_update.html.tera
  7. 17
      yopa-web/src/main.rs
  8. 10
      yopa-web/src/routes/object_model.rs
  9. 10
      yopa-web/src/routes/property_model.rs
  10. 85
      yopa-web/src/routes/relation_model.rs
  11. 29
      yopa/src/lib.rs

@ -5,5 +5,5 @@
{%- 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/delete/{{prop.id}}">Delete property</a> <a href="/model/property/delete/{{prop.id}}" onclick="return confirm('Delete property?')">Delete property</a>
{% endmacro input %} {% endmacro input %}

@ -23,7 +23,7 @@
<li> <li>
<b title="{{model.id}}">{{model.name}}</b><br> <b title="{{model.id}}">{{model.name}}</b><br>
<a href="/model/object/delete/{{model.id}}">Delete model</a> &middot; <a href="/model/object/delete/{{model.id}}" onclick="return confirm('Delete model?')">Delete model</a> &middot;
<a href="/model/object/update/{{model.id}}">Edit model</a> &middot; <a href="/model/object/update/{{model.id}}">Edit model</a> &middot;
<a href="/model/relation/create/{{model.id}}">New relation</a> &middot; <a href="/model/relation/create/{{model.id}}">New relation</a> &middot;
<a href="/model/property/create/{{model.id}}">New property</a> <a href="/model/property/create/{{model.id}}">New property</a>
@ -50,7 +50,8 @@
{%- if rel.model.multiple %}, MULTIPLE{% endif %} {%- if rel.model.multiple %}, MULTIPLE{% endif %}
<br> <br>
<a href="/model/relation/delete/{{rel.model.id}}">Delete relation</a> &middot; <a href="/model/relation/update/{{rel.model.id}}">Edit relation</a> &middot;
<a href="/model/relation/delete/{{rel.model.id}}" onclick="return confirm('Delete relation?')">Delete relation</a> &middot;
<a href="/model/property/create/{{rel.model.id}}">New property</a> <a href="/model/property/create/{{rel.model.id}}">New property</a>
<br> <br>

@ -12,11 +12,9 @@ Define object
<h1>Define new object model</h1> <h1>Define new object model</h1>
{# TODO implement value restoration on error #}
<form action="/model/object/create" method="POST"> <form action="/model/object/create" method="POST">
<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>
<input type="submit" value="Save"> <input type="submit" value="Save">
</form> </form>

@ -22,11 +22,11 @@ Define property
<input type="text" id="name" name="name" autocomplete="off"><br> <input type="text" id="name" name="name" autocomplete="off"><br>
<label for="optional">Optional:</label> <label for="optional">Optional:</label>
<input type="checkbox" name="optional" id="optional" value="1" autocomplete="off"> <input type="checkbox" name="optional" id="optional" value="true" autocomplete="off">
<br> <br>
<label for="multiple">Multiple:</label> <label for="multiple">Multiple:</label>
<input type="checkbox" name="multiple" id="multiple" value="1" autocomplete="off"><br> <input type="checkbox" name="multiple" id="multiple" value="true" 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">

@ -24,11 +24,11 @@ Define relation
<input type="text" id="reciprocal_name" name="reciprocal_name" autocomplete="off"><br> <input type="text" id="reciprocal_name" name="reciprocal_name" autocomplete="off"><br>
<label for="optional">Optional:</label> <label for="optional">Optional:</label>
<input type="checkbox" name="optional" id="optional" value="1" autocomplete="off"> <input type="checkbox" name="optional" id="optional" value="true" autocomplete="off">
<br> <br>
<label for="multiple">Multiple:</label> <label for="multiple">Multiple:</label>
<input type="checkbox" name="multiple" id="multiple" value="1" autocomplete="off"><br> <input type="checkbox" name="multiple" id="multiple" value="true" autocomplete="off"><br>
<label for="related">Related object:</label> <label for="related">Related object:</label>
<select name="related" id="related" autocomplete="off"> <select name="related" id="related" autocomplete="off">

@ -0,0 +1,35 @@
{% extends "_layout" %}
{% block title -%}
Edit relation
{%- endblock %}
{% block nav -%}
<a href="/">Home</a>
{%- endblock %}
{% block content -%}
<h1>Edit relation model "{{model.name}}"</h1>
<form action="/model/relation/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="reciprocal_name">Reciprocal name:</label>
<input type="text" id="reciprocal_name" name="reciprocal_name" value="{{model.reciprocal_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>
<p>The related object cannot be changed. Create a new relation if needed.</p>
<input type="submit" value="Save">
</form>
{%- endblock %}

@ -56,6 +56,21 @@ pub(crate) static TERA: Lazy<Tera> = Lazy::new(|| {
Err(tera::Error::msg("Expected nonenmpty object")) Err(tera::Error::msg("Expected nonenmpty object"))
}); });
tera.register_function("opt", |args: &HashMap<String, Value>| -> tera::Result<Value> {
if args.len() != 1 {
return Err("Expected 1 argument".into());
}
match args.iter().nth(0) {
Some((name, &Value::Bool(true))) => {
Ok(Value::String(name.clone()))
},
Some((_, &Value::Bool(false))) => {
Ok(Value::Null)
},
_ => Err("Expected bool argument".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") {
@ -110,6 +125,8 @@ async fn main() -> std::io::Result<()> {
// //
.service(routes::relation_model::create_form) .service(routes::relation_model::create_form)
.service(routes::relation_model::create) .service(routes::relation_model::create)
.service(routes::relation_model::update_form)
.service(routes::relation_model::update)
.service(routes::relation_model::delete) .service(routes::relation_model::delete)
// //
.service(routes::property_model::create_form) .service(routes::property_model::create_form)

@ -25,10 +25,17 @@ pub(crate) async fn create_form(session: Session) -> actix_web::Result<impl Resp
let mut context = tera::Context::new(); let mut context = tera::Context::new();
session.render_flash(&mut context); session.render_flash(&mut context);
// Re-fill old values
if let Ok(Some(form)) = session.take::<ObjectModelForm>("old") {
context.insert("old", &form);
} else {
context.insert("old", &ObjectModelForm::default());
}
TERA.build_response("model_create", &context) TERA.build_response("model_create", &context)
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Default, Debug, Clone, Serialize, Deserialize)]
pub(crate) struct ObjectModelForm { pub(crate) struct ObjectModelForm {
pub name: String, pub name: String,
} }
@ -53,6 +60,7 @@ pub(crate) async fn create(
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);
redirect("/model/object/create") redirect("/model/object/create")
} }
} }

@ -50,8 +50,10 @@ pub(crate) async fn create_form(
pub(crate) struct PropertyModelCreate { pub(crate) struct PropertyModelCreate {
pub object: ID, pub object: ID,
pub name: String, pub name: String,
pub optional: Option<i32>, #[serde(default)]
pub multiple: Option<i32>, pub optional: bool,
#[serde(default)]
pub multiple: bool,
pub data_type: DataType, pub data_type: DataType,
/// Default value to be parsed to the data type /// Default value to be parsed to the data type
/// May be unused if empty and optional /// May be unused if empty and optional
@ -67,8 +69,8 @@ pub(crate) async fn create(
let mut wg = store.write().await; let mut wg = store.write().await;
let form = form.into_inner(); let form = form.into_inner();
let optional = form.optional.unwrap_or_default() != 0; let optional = form.optional;
let multiple = form.multiple.unwrap_or_default() != 0; let multiple = form.multiple;
match wg.define_property(PropertyModel { match wg.define_property(PropertyModel {
id: Default::default(), id: Default::default(),

@ -44,18 +44,20 @@ pub(crate) async fn create_form(
} }
#[derive(Deserialize)] #[derive(Deserialize)]
pub(crate) struct RelationModelCreate { pub(crate) struct RelationModelCreateForm {
pub object: ID, pub object: ID,
pub name: String, pub name: String,
pub reciprocal_name: String, pub reciprocal_name: String,
pub optional: Option<i32>, #[serde(default)]
pub multiple: Option<i32>, pub optional: bool,
#[serde(default)]
pub multiple: bool,
pub related: ID, pub related: ID,
} }
#[post("/model/relation/create")] #[post("/model/relation/create")]
pub(crate) async fn create( pub(crate) async fn create(
form: web::Form<RelationModelCreate>, form: web::Form<RelationModelCreateForm>,
store: crate::YopaStoreWrapper, store: crate::YopaStoreWrapper,
session: Session, session: Session,
) -> actix_web::Result<impl Responder> { ) -> actix_web::Result<impl Responder> {
@ -66,8 +68,8 @@ pub(crate) async fn create(
object: form.object, object: form.object,
name: form.name.clone(), name: form.name.clone(),
reciprocal_name: form.reciprocal_name.clone(), reciprocal_name: form.reciprocal_name.clone(),
optional: form.optional.unwrap_or_default() != 0, optional: form.optional,
multiple: form.multiple.unwrap_or_default() != 0, multiple: form.multiple,
related: form.related, related: form.related,
}) { }) {
Ok(_id) => { Ok(_id) => {
@ -109,3 +111,74 @@ pub(crate) async fn delete(
} }
} }
} }
#[derive(Serialize,Deserialize)]
pub(crate) struct RelationModelEditForm {
pub name: String,
pub reciprocal_name: String,
#[serde(default)]
pub optional: bool,
#[serde(default)]
pub multiple: bool,
}
#[get("/model/relation/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_relation_model(*model_id)
.ok_or_else(|| actix_web::error::ErrorNotFound("No such model"))?;
// Re-fill old values
if let Ok(Some(form)) = session.take::<RelationModelEditForm>("old") {
let mut model = model.clone();
model.name = form.name;
model.optional = form.optional; // TODO try if regular bools cant be used
model.multiple = form.multiple;
context.insert("model", &model);
} else {
context.insert("model", model);
}
TERA.build_response("relation_update", &context)
}
#[post("/model/relation/update/{model_id}")]
pub(crate) async fn update(
model_id: web::Path<ID>,
form: web::Form<RelationModelEditForm>,
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();
match wg.update_relation(RelationModel {
id,
object: Default::default(), // dummy
name: form.name.clone(),
reciprocal_name: form.reciprocal_name.to_string(),
optional: form.optional,
multiple: form.multiple,
related: Default::default() // dummy
}) {
Ok(_id) => {
debug!("Relation updated, redirecting to root");
session.flash_success(format!("Relation model \"{}\" 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))
}
}
}

@ -389,4 +389,33 @@ impl Storage {
self.obj_models.insert(model.id, model); self.obj_models.insert(model.id, model);
Ok(()) Ok(())
} }
pub fn update_relation(&mut self, mut rel : RelationModel) -> Result<(), StorageError> {
// Object and Related can't be changed, so we re-fill them from the existing model
if let Some(existing) = self.rel_models.get(&rel.id) {
rel.object = existing.object;
rel.related = existing.related;
} else {
return Err(StorageError::NotExist(format!("Relation model ID {} does not exist.", rel.id).into()));
}
// Difficult checks ...
// yes this is stupid and inefficient and slow and
if let Some((_, colliding)) = self.rel_models.iter().find(|(_, other)| {
(other.name == rel.name && other.object == rel.object && rel.id != other.id) // Exact match
|| (other.name == rel.reciprocal_name && other.object == rel.related && rel.id != other.id) // Our reciprocal name collides with related's own relation name
|| (other.reciprocal_name == rel.name && other.related == rel.object && rel.id != other.id) // Our name name collides with a reciprocal name on the other relation
|| (other.reciprocal_name == rel.reciprocal_name && other.related == rel.related && rel.id != other.id) // Reciprocal names collide for the same destination
}) {
return Err(StorageError::ConstraintViolation(
format!("name collision (\"{}\" / \"{}\") with existing relation (\"{}\" / \"{}\")",
rel.name, rel.reciprocal_name,
colliding.name, colliding.reciprocal_name
).into()));
}
self.rel_models.insert(rel.id, rel);
Ok(())
}
} }

Loading…
Cancel
Save