refactor, add model edit form with "old"

master
Ondřej Hruška 4 years ago
parent d8bfc19986
commit 2a71f827b0
Signed by: MightyPork
GPG Key ID: 2C5FD5035250423D
  1. 1
      yopa-web/resources/templates/index.html.tera
  2. 2
      yopa-web/resources/templates/model_create.html.tera
  3. 22
      yopa-web/resources/templates/model_update.html.tera
  4. 10
      yopa-web/resources/templates/property_create.html.tera
  5. 10
      yopa-web/resources/templates/relation_create.html.tera
  6. 77
      yopa-web/src/main.rs
  7. 390
      yopa-web/src/routes.rs
  8. 135
      yopa-web/src/routes/object_model.rs
  9. 168
      yopa-web/src/routes/property_model.rs
  10. 111
      yopa-web/src/routes/relation_model.rs
  11. 25
      yopa-web/src/scratch.rs
  12. 14
      yopa-web/src/session_ext.rs
  13. 16
      yopa-web/src/tera_ext.rs
  14. 39
      yopa-web/src/utils.rs
  15. 14
      yopa/src/lib.rs

@ -24,6 +24,7 @@
<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}}">Delete 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>
<br> <br>

@ -16,7 +16,7 @@ Define object
<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"><br> <input type="text" id="name" name="name" autocomplete="off"><br>
<input type="submit" value="Save"> <input type="submit" value="Save">
</form> </form>

@ -0,0 +1,22 @@
{% extends "_layout" %}
{% block title -%}
Edit object model
{%- endblock %}
{% block nav -%}
<a href="/">Home</a>
{%- endblock %}
{% block content -%}
<h1>Edit object model {{ model.name }}</h1>
<form action="/model/object/update/{{ model.id }}" method="POST">
<label for="name">Name:</label>
<input type="text" id="name" name="name" value="{{ model.name }}" autocomplete="off"><br>
<input type="submit" value="Save">
</form>
{%- endblock %}

@ -19,17 +19,17 @@ 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"><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"> <input type="checkbox" name="optional" id="optional" value="1" autocomplete="off">
<br> <br>
<label for="multiple">Multiple:</label> <label for="multiple">Multiple:</label>
<input type="checkbox" name="multiple" id="multiple" value="1"><br> <input type="checkbox" name="multiple" id="multiple" value="1" autocomplete="off"><br>
<label for="data_type">Type:</label> <label for="data_type">Type:</label>
<select name="data_type" id="data_type"> <select name="data_type" id="data_type" autocomplete="off">
<option value="String" selected>String</option> <option value="String" selected>String</option>
<option value="Integer">Integer</option> <option value="Integer">Integer</option>
<option value="Decimal">Decimal</option> <option value="Decimal">Decimal</option>
@ -37,7 +37,7 @@ Define property
</select><br> </select><br>
<label for="default">Default:</label> <label for="default">Default:</label>
<input type="text" id="default" name="default"><br> <input type="text" id="default" name="default" autocomplete="off"><br>
<input type="submit" value="Save"> <input type="submit" value="Save">
</form> </form>

@ -18,20 +18,20 @@ Define relation
<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"><br> <input type="text" id="name" name="name" autocomplete="off"><br>
<label for="reciprocal_name">Reciprocal name:</label> <label for="reciprocal_name">Reciprocal name:</label>
<input type="text" id="reciprocal_name" name="reciprocal_name"><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"> <input type="checkbox" name="optional" id="optional" value="1" autocomplete="off">
<br> <br>
<label for="multiple">Multiple:</label> <label for="multiple">Multiple:</label>
<input type="checkbox" name="multiple" id="multiple" value="1"><br> <input type="checkbox" name="multiple" id="multiple" value="1" autocomplete="off"><br>
<label for="related">Related object:</label> <label for="related">Related object:</label>
<select name="related" id="related"> <select name="related" id="related" autocomplete="off">
{% for m in models %} {% for m in models %}
<option value="{{ m.id }}">{{ m.name }}</option> <option value="{{ m.id }}">{{ m.name }}</option>
{% endfor %} {% endfor %}

@ -1,27 +1,33 @@
#[macro_use] extern crate log; #[macro_use]
#[macro_use] extern crate actix_web; extern crate actix_web;
#[macro_use]
extern crate log;
use actix_web::{get, post, web, App, HttpResponse, HttpServer, Responder, HttpRequest}; use std::borrow::Borrow;
use parking_lot::Mutex; use std::collections::HashMap;
use actix_web::web::{service, scope}; use std::ops::Deref;
use actix_web::http::StatusCode; use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::atomic::{AtomicUsize, Ordering};
use std::path::{PathBuf, Path};
use actix_session::CookieSession;
use actix_web::{App, get, HttpRequest, HttpResponse, HttpServer, post, Responder, web};
use actix_web::http::StatusCode;
use actix_web::web::{scope, service};
use actix_web_static_files; use actix_web_static_files;
use tera::Tera; use actix_web_static_files::ResourceFiles as StaticFiles;
use include_dir::Dir; use include_dir::Dir;
use std::sync::Arc;
use log::LevelFilter; use log::LevelFilter;
use crate::tera_ext::TeraExt;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use std::borrow::Borrow; use parking_lot::Mutex;
use std::ops::Deref;
use yopa::{Storage, TypedValue};
use std::collections::HashMap;
use actix_session::CookieSession;
use rand::Rng; use rand::Rng;
use actix_web_static_files::ResourceFiles as StaticFiles; use tera::Tera;
use yopa::{Storage, TypedValue};
use crate::tera_ext::TeraExt;
mod utils;
mod tera_ext; mod tera_ext;
mod routes; mod routes;
mod session_ext; mod session_ext;
@ -32,13 +38,13 @@ include!(concat!(env!("OUT_DIR"), "/static_files.rs"));
// Embed templates // Embed templates
static TEMPLATES: include_dir::Dir = include_dir::include_dir!("./resources/templates"); static TEMPLATES: include_dir::Dir = include_dir::include_dir!("./resources/templates");
pub(crate) static TERA : Lazy<Tera> = Lazy::new(|| { pub(crate) static TERA: Lazy<Tera> = Lazy::new(|| {
let mut tera = Tera::default(); let mut tera = Tera::default();
tera.add_include_dir_templates(&TEMPLATES).unwrap(); tera.add_include_dir_templates(&TEMPLATES).unwrap();
// Special filter for the TypedValue map // Special filter for the TypedValue map
use serde_json::Value; use serde_json::Value;
tera.register_filter("print_typed_value", |v : &Value, _ : &HashMap<String, Value>| -> tera::Result<Value> { tera.register_filter("print_typed_value", |v: &Value, _: &HashMap<String, Value>| -> tera::Result<Value> {
if v.is_null() { if v.is_null() {
return Ok(v.clone()); return Ok(v.clone());
} }
@ -95,15 +101,20 @@ async fn main() -> std::io::Result<()> {
/* Routes */ /* Routes */
.service(routes::index) .service(routes::index)
.service(routes::object_model_create_form) //
.service(routes::object_model_create) .service(routes::object_model::create_form)
.service(routes::relation_model_create_form) .service(routes::object_model::create)
.service(routes::relation_model_create) .service(routes::object_model::update_form)
.service(routes::property_model_create_form) .service(routes::object_model::update)
.service(routes::property_model_create) .service(routes::object_model::delete)
.service(routes::object_model_delete) //
.service(routes::relation_model_delete) .service(routes::relation_model::create_form)
.service(routes::property_model_delete) .service(routes::relation_model::create)
.service(routes::relation_model::delete)
//
.service(routes::property_model::create_form)
.service(routes::property_model::create)
.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")))
}) })
@ -141,7 +152,7 @@ fn init_yopa() -> YopaStoreWrapper {
optional: false, optional: false,
multiple: true, multiple: true,
data_type: DataType::String, data_type: DataType::String,
default: None default: None,
}).unwrap(); }).unwrap();
store.define_property(model::PropertyModel { store.define_property(model::PropertyModel {
@ -151,7 +162,7 @@ fn init_yopa() -> YopaStoreWrapper {
optional: false, optional: false,
multiple: false, multiple: false,
data_type: DataType::String, data_type: DataType::String,
default: None default: None,
}).unwrap(); }).unwrap();
store.define_property(model::PropertyModel { store.define_property(model::PropertyModel {
@ -161,7 +172,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: Some(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 {
@ -171,7 +182,7 @@ fn init_yopa() -> YopaStoreWrapper {
reciprocal_name: "recipes".to_string(), reciprocal_name: "recipes".to_string(),
optional: true, optional: true,
multiple: true, multiple: true,
related: id_book related: id_book,
}).unwrap(); }).unwrap();
store.define_property(model::PropertyModel { store.define_property(model::PropertyModel {
@ -181,7 +192,7 @@ fn init_yopa() -> YopaStoreWrapper {
optional: true, optional: true,
multiple: false, multiple: false,
data_type: DataType::Integer, data_type: DataType::Integer,
default: None default: None,
}).unwrap(); }).unwrap();
store.define_relation(model::RelationModel { store.define_relation(model::RelationModel {
@ -191,7 +202,7 @@ fn init_yopa() -> YopaStoreWrapper {
reciprocal_name: "related recipe".to_string(), reciprocal_name: "related recipe".to_string(),
optional: true, optional: true,
multiple: true, multiple: true,
related: id_recipe related: id_recipe,
}).unwrap(); }).unwrap();
web::Data::new(tokio::sync::RwLock::new(store)) web::Data::new(tokio::sync::RwLock::new(store))

@ -1,71 +1,27 @@
use actix_web::{web, HttpRequest, Responder, HttpResponse}; use std::fmt::{Debug, Display};
use crate::TERA;
use crate::tera_ext::TeraExt;
use yopa::{Storage, StorageError, ID, DataType, TypedValue};
use serde::{Deserialize, Serialize};
use yopa::model::{PropertyModel, RelationModel, ObjectModel};
use std::ops::DerefMut; use std::ops::DerefMut;
use actix_session::Session;
use crate::session_ext::SessionExt;
use std::str::FromStr; use std::str::FromStr;
use std::fmt::{Debug, Display};
use actix_web::http::header::IntoHeaderValue;
#[derive(Serialize, Debug)]
struct ObjectModelDisplay<'a> {
id : yopa::ID,
name : &'a str,
properties: Vec<&'a PropertyModel>,
relations: Vec<RelationModelDisplay<'a>>,
reciprocal_relations: Vec<RelationModelDisplay<'a>>,
}
#[derive(Serialize, Debug)]
struct RelationModelDisplay<'a> {
model : &'a RelationModel,
related_name : &'a str,
properties: Vec<&'a PropertyModel>,
}
fn redirect(path : impl IntoHeaderValue) -> actix_web::Result<HttpResponse> {
Ok(HttpResponse::SeeOther()
.header("location", path) // back - to where?
.finish())
}
trait ParseOrBadReq { use actix_session::Session;
fn parse_or_bad_request<T, E>(&self) -> actix_web::Result<T> use actix_web::{HttpRequest, HttpResponse, Responder, web};
where T: FromStr<Err=E>, use actix_web::http::header::IntoHeaderValue;
E: Display + Debug + 'static; use serde::{Deserialize, Serialize};
}
impl ParseOrBadReq for &str { use yopa::{DataType, ID, Storage, StorageError, TypedValue};
fn parse_or_bad_request<T, E>(&self) -> actix_web::Result<T> use yopa::model::{ObjectModel, PropertyModel, RelationModel};
where T: FromStr<Err=E>,
E: Display + Debug + 'static
{
self.parse::<T>()
.map_err(|e| {
error!("Parse error for \"{}\"", self);
actix_web::error::ErrorBadRequest(e)
})
}
}
impl ParseOrBadReq for String { use crate::session_ext::SessionExt;
fn parse_or_bad_request<T, E>(&self) -> actix_web::Result<T> use crate::TERA;
where T: FromStr<Err=E>, use crate::tera_ext::TeraExt;
E: Display + Debug + 'static use crate::routes::relation_model::RelationModelDisplay;
{ use crate::routes::object_model::ObjectModelDisplay;
self.as_str()
.parse_or_bad_request()
}
}
pub(crate) mod object_model;
pub(crate) mod relation_model;
pub(crate) mod property_model;
#[get("/")] #[get("/")]
pub(crate) async fn index(session : Session, store : crate::YopaStoreWrapper) -> actix_web::Result<impl Responder> { pub(crate) async fn index(session: Session, store: crate::YopaStoreWrapper) -> actix_web::Result<impl Responder> {
let rg = store.read().await; let rg = store.read().await;
let models_iter = rg.get_object_models(); let models_iter = rg.get_object_models();
@ -86,7 +42,7 @@ pub(crate) async fn index(session : Session, store : crate::YopaStoreWrapper) ->
RelationModelDisplay { RelationModelDisplay {
model: rm, model: rm,
related_name: rg.get_model_name(rm.related), related_name: rg.get_model_name(rm.related),
properties: rprops properties: rprops,
} }
}).collect::<Vec<_>>(); }).collect::<Vec<_>>();
relations.sort_by_key(|d| &d.model.name); relations.sort_by_key(|d| &d.model.name);
@ -100,7 +56,7 @@ pub(crate) async fn index(session : Session, store : crate::YopaStoreWrapper) ->
RelationModelDisplay { RelationModelDisplay {
model: rm, model: rm,
related_name: rg.get_model_name(rm.object), related_name: rg.get_model_name(rm.object),
properties: rprops properties: rprops,
} }
}).collect::<Vec<_>>(); }).collect::<Vec<_>>();
reciprocal_relations.sort_by_key(|d| &d.model.reciprocal_name); reciprocal_relations.sort_by_key(|d| &d.model.reciprocal_name);
@ -113,7 +69,7 @@ pub(crate) async fn index(session : Session, store : crate::YopaStoreWrapper) ->
name: &om.name, name: &om.name,
properties, properties,
relations, relations,
reciprocal_relations reciprocal_relations,
}) })
} }
@ -125,311 +81,3 @@ pub(crate) async fn index(session : Session, store : crate::YopaStoreWrapper) ->
TERA.build_response("index", &ctx) TERA.build_response("index", &ctx)
} }
#[get("/model/object/create")]
pub(crate) async fn object_model_create_form(session : Session) -> actix_web::Result<impl Responder> {
let mut context = tera::Context::new();
session.render_flash(&mut context);
TERA.build_response("model_create", &context)
}
#[derive(Deserialize)]
pub(crate) struct ObjectModelCreate {
pub name : String,
}
#[post("/model/object/create")]
pub(crate) async fn object_model_create(
form : web::Form<ObjectModelCreate>,
store : crate::YopaStoreWrapper,
session : Session
) -> actix_web::Result<impl Responder> {
let mut wg = store.write().await;
let form = form.into_inner();
match wg.define_object(ObjectModel {
id: Default::default(),
name: form.name.clone()
}) {
Ok(_id) => {
debug!("Object created, redirecting to root");
session.flash_success(format!("Object model \"{}\" created.", form.name));
redirect("/")
}
Err(e) => {
warn!("Error creating model: {:?}", e);
session.flash_error(e.to_string());
redirect("/model/object/create")
}
}
}
#[get("/model/relation/create/{object_id}")]
pub(crate) async fn relation_model_create_form(
object_id : web::Path<String>,
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;
debug!("ID = {}", object_id);
let object = rg.get_object_model(object_id.parse_or_bad_request()?)
.ok_or_else(|| actix_web::error::ErrorNotFound("No such source object"))?;
let mut models: Vec<_> = rg.get_object_models().collect();
models.sort_by_key(|m| &m.name);
context.insert("models", &models);
context.insert("object", &object);
TERA.build_response("relation_create", &context)
}
#[derive(Deserialize)]
pub(crate) struct RelationModelCreate {
pub object : ID,
pub name : String,
pub reciprocal_name : String,
pub optional : Option<i32>,
pub multiple : Option<i32>,
pub related : ID,
}
#[post("/model/relation/create")]
pub(crate) async fn relation_model_create(
form : web::Form<RelationModelCreate>,
store : crate::YopaStoreWrapper,
session : Session
) -> actix_web::Result<impl Responder> {
let mut wg = store.write().await;
let form = form.into_inner();
match wg.define_relation(RelationModel {
id: Default::default(),
object: form.object,
name: form.name.clone(),
reciprocal_name: form.reciprocal_name.clone(),
optional: form.optional.unwrap_or_default() != 0,
multiple: form.multiple.unwrap_or_default() != 0,
related: form.related
}) {
Ok(_id) => {
debug!("Relation created, redirecting to root");
session.flash_success(format!("Relation model \"{}\" created.", form.name));
redirect("/")
}
Err(e) => {
warn!("Error creating relation model: {:?}", e);
session.flash_error(e.to_string());
redirect(format!("/model/relation/create/{}", form.object))
}
}
}
#[derive(Serialize, Debug)]
struct ObjectOrRelationModelDisplay {
id : ID,
describe : String,
}
#[get("/model/property/create/{object_id}")]
pub(crate) async fn property_model_create_form(
object_id : web::Path<String>,
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;
debug!("ID = {}", object_id);
let object = {
let id = object_id.parse_or_bad_request()?;
debug!("Create property for ID={}", id);
if let Some(om) = rg.get_object_model(id) {
ObjectOrRelationModelDisplay {
id: om.id,
describe: format!("object model \"{}\"", om.name),
}
} else if let Some(rm) = rg.get_relation_model(id) {
ObjectOrRelationModelDisplay {
id: rm.id,
describe: format!("relation model \"{}\"", rm.name),
}
} else {
return Err(actix_web::error::ErrorNotFound("No such source object"));
}
};
context.insert("object", &object);
TERA.build_response("property_create", &context)
}
#[derive(Deserialize)]
pub(crate) struct PropertyModelCreate {
pub object : ID,
pub name : String,
pub optional : Option<i32>,
pub multiple : Option<i32>,
pub data_type : DataType,
/// Default value to be parsed to the data type
/// May be unused if empty and optional
pub default : String,
}
#[post("/model/property/create")]
pub(crate) async fn property_model_create(
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.unwrap_or_default() != 0;
let multiple = form.multiple.unwrap_or_default() != 0;
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 => {
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))
})?)
}
}
}
}
}) {
Ok(_id) => {
debug!("Property created, redirecting to root");
session.flash_success(format!("Property model \"{}\" created.", form.name));
redirect("/")
}
Err(e) => {
warn!("Error creating property model: {:?}", e);
session.flash_error(e.to_string());
redirect(format!("/model/property/create/{}", form.object))
}
}
}
#[get("/model/object/delete/{id}")]
pub(crate) async fn object_model_delete(
id : web::Path<String>,
store : crate::YopaStoreWrapper,
session : Session
) -> actix_web::Result<impl Responder> {
let mut wg = store.write().await;
match wg.undefine_object(id.parse().map_err(|e| actix_web::error::ErrorBadRequest(e))?) {
Ok(om) => {
debug!("Object model deleted, redirecting to root");
session.flash_success(format!("Object model \"{}\" deleted.", om.name));
redirect("/")
}
Err(e) => {
warn!("Error deleting object model: {:?}", e);
session.flash_error(e.to_string());
redirect("/") // back?
}
}
}
#[get("/model/relation/delete/{id}")]
pub(crate) async fn relation_model_delete(
id : web::Path<String>,
store : crate::YopaStoreWrapper,
session : Session
) -> actix_web::Result<impl Responder> {
let mut wg = store.write().await;
match wg.undefine_relation(id.parse_or_bad_request()?) {
Ok(rm) => {
debug!("Relation deleted, redirecting to root");
session.flash_success(format!("Relation model \"{}\" deleted.", rm.name));
redirect("/")
}
Err(e) => {
warn!("Error deleting relation model: {:?}", e);
session.flash_error(e.to_string());
redirect("/") // back?
}
}
}
#[get("/model/property/delete/{id}")]
pub(crate) async fn property_model_delete(
id : web::Path<String>,
store : crate::YopaStoreWrapper,
session : Session
) -> actix_web::Result<impl Responder> {
let mut wg = store.write().await;
match wg.undefine_property(id.parse_or_bad_request()?) {
Ok(rm) => {
debug!("Property deleted, redirecting to root");
session.flash_success(format!("Property \"{}\" deleted.", rm.name));
redirect("/")
}
Err(e) => {
warn!("Error deleting property: {:?}", e);
session.flash_error(e.to_string());
redirect("/") // back?
}
}
}

@ -0,0 +1,135 @@
use actix_session::Session;
use actix_web::{Responder, web};
use serde::{Deserialize, Serialize};
use yopa::ID;
use yopa::model::{ObjectModel, PropertyModel};
use crate::routes::relation_model::RelationModelDisplay;
use crate::session_ext::SessionExt;
use crate::TERA;
use crate::tera_ext::TeraExt;
use crate::utils::redirect;
#[derive(Serialize, Debug)]
pub(crate) struct ObjectModelDisplay<'a> {
pub(crate) id: yopa::ID,
pub(crate) name: &'a str,
pub(crate) properties: Vec<&'a PropertyModel>,
pub(crate) relations: Vec<RelationModelDisplay<'a>>,
pub(crate) reciprocal_relations: Vec<RelationModelDisplay<'a>>,
}
#[get("/model/object/create")]
pub(crate) async fn create_form(session: Session) -> actix_web::Result<impl Responder> {
let mut context = tera::Context::new();
session.render_flash(&mut context);
TERA.build_response("model_create", &context)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct ObjectModelForm {
pub name: String,
}
#[post("/model/object/create")]
pub(crate) async fn create(
form: web::Form<ObjectModelForm>,
store: crate::YopaStoreWrapper,
session: Session,
) -> actix_web::Result<impl Responder> {
let mut wg = store.write().await;
let form = form.into_inner();
match wg.define_object(ObjectModel {
id: Default::default(),
name: form.name.clone(),
}) {
Ok(_id) => {
debug!("Object created, redirecting to root");
session.flash_success(format!("Object model \"{}\" created.", form.name));
redirect("/")
}
Err(e) => {
warn!("Error creating model: {:?}", e);
session.flash_error(e.to_string());
redirect("/model/object/create")
}
}
}
#[get("/model/object/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_object_model(*model_id)
.ok_or_else(|| actix_web::error::ErrorNotFound("No such model"))?;
// Re-fill old values
if let Ok(Some(form)) = session.take::<ObjectModelForm>("old") {
let mut model = model.clone();
model.name = form.name;
context.insert("model", &model);
} else {
context.insert("model", model);
}
TERA.build_response("model_update", &context)
}
#[post("/model/object/update/{model_id}")]
pub(crate) async fn update(
model_id: web::Path<ID>,
form: web::Form<ObjectModelForm>,
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_object(ObjectModel {
id,
name: form.name.clone(),
}) {
Ok(_id) => {
debug!("Object updated, redirecting to root");
session.flash_success(format!("Object 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/object/update/{}", id))
}
}
}
#[get("/model/object/delete/{id}")]
pub(crate) async fn delete(
id: web::Path<String>,
store: crate::YopaStoreWrapper,
session: Session,
) -> actix_web::Result<impl Responder> {
let mut wg = store.write().await;
match wg.undefine_object(id.parse().map_err(|e| actix_web::error::ErrorBadRequest(e))?) {
Ok(om) => {
debug!("Object model deleted, redirecting to root");
session.flash_success(format!("Object model \"{}\" deleted.", om.name));
redirect("/")
}
Err(e) => {
warn!("Error deleting object model: {:?}", e);
session.flash_error(e.to_string());
redirect("/") // back?
}
}
}

@ -0,0 +1,168 @@
use actix_session::Session;
use actix_web::{Responder, web};
use serde::{Serialize, Deserialize};
use yopa::{DataType, ID, TypedValue};
use yopa::model::{PropertyModel, RelationModel};
use crate::session_ext::SessionExt;
use crate::TERA;
use crate::tera_ext::TeraExt;
use crate::utils::{ParseOrBadReq, redirect};
use crate::routes::relation_model::ObjectOrRelationModelDisplay;
#[get("/model/property/create/{object_id}")]
pub(crate) async fn create_form(
object_id: web::Path<String>,
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;
debug!("ID = {}", object_id);
let object = {
let id = object_id.parse_or_bad_request()?;
debug!("Create property for ID={}", id);
if let Some(om) = rg.get_object_model(id) {
ObjectOrRelationModelDisplay {
id: om.id,
describe: format!("object model \"{}\"", om.name),
}
} else if let Some(rm) = rg.get_relation_model(id) {
ObjectOrRelationModelDisplay {
id: rm.id,
describe: format!("relation model \"{}\"", rm.name),
}
} else {
return Err(actix_web::error::ErrorNotFound("No such source object"));
}
};
context.insert("object", &object);
TERA.build_response("property_create", &context)
}
#[derive(Deserialize)]
pub(crate) struct PropertyModelCreate {
pub object: ID,
pub name: String,
pub optional: Option<i32>,
pub multiple: Option<i32>,
pub data_type: DataType,
/// Default value to be parsed to the data type
/// May be unused if empty and optional
pub default: String,
}
#[post("/model/property/create")]
pub(crate) async fn create(
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.unwrap_or_default() != 0;
let multiple = form.multiple.unwrap_or_default() != 0;
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 => {
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))
})?)
}
}
}
},
}) {
Ok(_id) => {
debug!("Property created, redirecting to root");
session.flash_success(format!("Property model \"{}\" created.", form.name));
redirect("/")
}
Err(e) => {
warn!("Error creating property model: {:?}", e);
session.flash_error(e.to_string());
redirect(format!("/model/property/create/{}", form.object))
}
}
}
#[get("/model/property/delete/{id}")]
pub(crate) async fn delete(
id: web::Path<String>,
store: crate::YopaStoreWrapper,
session: Session,
) -> actix_web::Result<impl Responder> {
let mut wg = store.write().await;
match wg.undefine_property(id.parse_or_bad_request()?) {
Ok(rm) => {
debug!("Property deleted, redirecting to root");
session.flash_success(format!("Property \"{}\" deleted.", rm.name));
redirect("/")
}
Err(e) => {
warn!("Error deleting property: {:?}", e);
session.flash_error(e.to_string());
redirect("/") // back?
}
}
}

@ -0,0 +1,111 @@
use actix_session::Session;
use actix_web::{Responder, web};
use serde::{Serialize, Deserialize};
use yopa::ID;
use yopa::model::{PropertyModel, RelationModel};
use crate::session_ext::SessionExt;
use crate::TERA;
use crate::tera_ext::TeraExt;
use crate::utils::{ParseOrBadReq, redirect};
#[derive(Serialize, Debug)]
pub(crate) struct RelationModelDisplay<'a> {
pub(crate) model: &'a RelationModel,
pub(crate) related_name: &'a str,
pub(crate) properties: Vec<&'a PropertyModel>,
}
#[get("/model/relation/create/{object_id}")]
pub(crate) async fn create_form(
object_id: web::Path<String>,
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;
debug!("ID = {}", object_id);
let object = rg.get_object_model(object_id.parse_or_bad_request()?)
.ok_or_else(|| actix_web::error::ErrorNotFound("No such source object"))?;
let mut models: Vec<_> = rg.get_object_models().collect();
models.sort_by_key(|m| &m.name);
context.insert("models", &models);
context.insert("object", &object);
TERA.build_response("relation_create", &context)
}
#[derive(Deserialize)]
pub(crate) struct RelationModelCreate {
pub object: ID,
pub name: String,
pub reciprocal_name: String,
pub optional: Option<i32>,
pub multiple: Option<i32>,
pub related: ID,
}
#[post("/model/relation/create")]
pub(crate) async fn create(
form: web::Form<RelationModelCreate>,
store: crate::YopaStoreWrapper,
session: Session,
) -> actix_web::Result<impl Responder> {
let mut wg = store.write().await;
let form = form.into_inner();
match wg.define_relation(RelationModel {
id: Default::default(),
object: form.object,
name: form.name.clone(),
reciprocal_name: form.reciprocal_name.clone(),
optional: form.optional.unwrap_or_default() != 0,
multiple: form.multiple.unwrap_or_default() != 0,
related: form.related,
}) {
Ok(_id) => {
debug!("Relation created, redirecting to root");
session.flash_success(format!("Relation model \"{}\" created.", form.name));
redirect("/")
}
Err(e) => {
warn!("Error creating relation model: {:?}", e);
session.flash_error(e.to_string());
redirect(format!("/model/relation/create/{}", form.object))
}
}
}
#[derive(Serialize, Debug)]
pub(crate) struct ObjectOrRelationModelDisplay {
pub(crate) id: ID,
pub(crate) describe: String,
}
#[get("/model/relation/delete/{id}")]
pub(crate) async fn delete(
id: web::Path<String>,
store: crate::YopaStoreWrapper,
session: Session,
) -> actix_web::Result<impl Responder> {
let mut wg = store.write().await;
match wg.undefine_relation(id.parse_or_bad_request()?) {
Ok(rm) => {
debug!("Relation deleted, redirecting to root");
session.flash_success(format!("Relation model \"{}\" deleted.", rm.name));
redirect("/")
}
Err(e) => {
warn!("Error deleting relation model: {:?}", e);
session.flash_error(e.to_string());
redirect("/") // back?
}
}
}

@ -1,25 +0,0 @@
// #[get("/")]
// async fn hello(state : web::Data<VisitCounter>) -> impl Responder {
// HttpResponse::Ok().body(format!("Hello world! {}", state.count_visit()))
// }
//
// #[post("/echo")]
// async fn echo(req_body: String) -> impl Responder {
// HttpResponse::Ok().body(req_body)
// }
// async fn static_files(req: HttpRequest) -> actix_web::Result<NamedFile> {
// let path: PathBuf = req.match_info().query("filename").parse().unwrap();
//
//
//
// Ok(NamedFile::open(path)?)
// }
//
// #[get("/users/{user_id}/{friend}")] // <- define path parameters
// async fn user_friend(web::Path((user_id, friend)): web::Path<(u32, String)>, state : web::Data<VisitCounter>) -> impl Responder {
// HttpResponse::Ok()
// .header("Content-Type", "text/html")
// .body(format!("<h1>Welcome {}, user_id {}!</h1>Visitor nr {}\n", friend, user_id, state.count_visit()))
// }

@ -1,15 +1,15 @@
use serde::de::DeserializeOwned;
use actix_session::Session; use actix_session::Session;
use serde::de::DeserializeOwned;
pub trait SessionExt { pub trait SessionExt {
/// Get a `value` from the session. /// Get a `value` from the session.
fn take<T: DeserializeOwned>(&self, key: &str) -> Result<Option<T>, actix_web::error::Error>; fn take<T: DeserializeOwned>(&self, key: &str) -> Result<Option<T>, actix_web::error::Error>;
fn render_flash(&self, context : &mut tera::Context); fn render_flash(&self, context: &mut tera::Context);
fn flash_error(&self, msg : impl AsRef<str>); fn flash_error(&self, msg: impl AsRef<str>);
fn flash_success(&self, msg : impl AsRef<str>); fn flash_success(&self, msg: impl AsRef<str>);
} }
impl SessionExt for Session { impl SessionExt for Session {
@ -19,7 +19,7 @@ impl SessionExt for Session {
Ok(val?) Ok(val?)
} }
fn render_flash(&self, context : &mut tera::Context) { fn render_flash(&self, context: &mut tera::Context) {
if let Ok(Some(msg)) = self.take::<String>("flash_error") { if let Ok(Some(msg)) = self.take::<String>("flash_error") {
context.insert("flash_error", &msg); context.insert("flash_error", &msg);
} }
@ -29,11 +29,11 @@ impl SessionExt for Session {
} }
} }
fn flash_error(&self, msg : impl AsRef<str>) { fn flash_error(&self, msg: impl AsRef<str>) {
self.set("flash_error", msg.as_ref()).unwrap(); self.set("flash_error", msg.as_ref()).unwrap();
} }
fn flash_success(&self, msg : impl AsRef<str>) { fn flash_success(&self, msg: impl AsRef<str>) {
self.set("flash_success", msg.as_ref()).unwrap(); self.set("flash_success", msg.as_ref()).unwrap();
} }
} }

@ -1,11 +1,11 @@
use tera::Tera;
use include_dir::Dir;
use actix_web::HttpResponse;
use actix_web::http::StatusCode; use actix_web::http::StatusCode;
use actix_web::HttpResponse;
use include_dir::Dir;
use tera::Tera;
fn tera_includedir_walk_folder_inner(collected : &mut Vec<(String, String)>, tera : &mut Tera, dir: &Dir) { fn tera_includedir_walk_folder_inner(collected: &mut Vec<(String, String)>, tera: &mut Tera, dir: &Dir) {
for f in dir.files() { for f in dir.files() {
let halves : Vec<_> = f.path().file_name().unwrap().to_str().unwrap().split('.').collect(); let halves: Vec<_> = f.path().file_name().unwrap().to_str().unwrap().split('.').collect();
if halves.last().unwrap() != &"tera" { if halves.last().unwrap() != &"tera" {
debug!("Bad file: {:?}", f); debug!("Bad file: {:?}", f);
@ -30,11 +30,11 @@ fn tera_includedir_walk_folder_inner(collected : &mut Vec<(String, String)>, ter
pub(crate) trait TeraExt { pub(crate) trait TeraExt {
fn add_include_dir_templates(&mut self, dir: &Dir) -> tera::Result<()>; fn add_include_dir_templates(&mut self, dir: &Dir) -> tera::Result<()>;
fn build_response(&self, template: &str, context : &tera::Context) -> actix_web::Result<HttpResponse> { fn build_response(&self, template: &str, context: &tera::Context) -> actix_web::Result<HttpResponse> {
self.build_err_response(StatusCode::OK, template, context) self.build_err_response(StatusCode::OK, template, context)
} }
fn build_err_response(&self, code : StatusCode, template: &str, context : &tera::Context) -> actix_web::Result<HttpResponse>; fn build_err_response(&self, code: StatusCode, template: &str, context: &tera::Context) -> actix_web::Result<HttpResponse>;
} }
impl TeraExt for Tera { impl TeraExt for Tera {
@ -45,7 +45,7 @@ impl TeraExt for Tera {
self.add_raw_templates(templates) self.add_raw_templates(templates)
} }
fn build_err_response(&self, code : StatusCode, template: &str, context : &tera::Context) -> actix_web::Result<HttpResponse> { fn build_err_response(&self, code: StatusCode, template: &str, context: &tera::Context) -> actix_web::Result<HttpResponse> {
let html = self.render(template, context).map_err(|e| { let html = self.render(template, context).map_err(|e| {
actix_web::error::ErrorInternalServerError(e) actix_web::error::ErrorInternalServerError(e)
})?; })?;

@ -0,0 +1,39 @@
use actix_web::http::header::IntoHeaderValue;
use actix_web::HttpResponse;
use std::str::FromStr;
use std::fmt::{Display, Debug};
pub fn redirect(path: impl IntoHeaderValue) -> actix_web::Result<HttpResponse> {
Ok(HttpResponse::SeeOther()
.header("location", path) // back - to where?
.finish())
}
pub trait ParseOrBadReq {
fn parse_or_bad_request<T, E>(&self) -> actix_web::Result<T>
where T: FromStr<Err=E>,
E: Display + Debug + 'static;
}
impl ParseOrBadReq for &str {
fn parse_or_bad_request<T, E>(&self) -> actix_web::Result<T>
where T: FromStr<Err=E>,
E: Display + Debug + 'static
{
self.parse::<T>()
.map_err(|e| {
error!("Parse error for \"{}\"", self);
actix_web::error::ErrorBadRequest(e)
})
}
}
impl ParseOrBadReq for String {
fn parse_or_bad_request<T, E>(&self) -> actix_web::Result<T>
where T: FromStr<Err=E>,
E: Display + Debug + 'static
{
self.as_str()
.parse_or_bad_request()
}
}

@ -375,4 +375,18 @@ impl Storage {
self.rel_models.values() self.rel_models.values()
.into_group_map_by(|model| model.related) .into_group_map_by(|model| model.related)
} }
// Updates
pub fn update_object(&mut self, model : ObjectModel) -> Result<(), StorageError> {
if !self.obj_models.contains_key(&model.id) {
return Err(StorageError::NotExist(format!("Object model ID {} does not exist.", model.id).into()));
}
if let Some(conflict) = self.obj_models.values().find(|m| m.id != model.id && m.name == model.name) {
return Err(StorageError::ConstraintViolation(format!("Object {} already has the name {}", conflict.id, model.name).into()));
}
self.obj_models.insert(model.id, model);
Ok(())
}
} }

Loading…
Cancel
Save