refactor, add model edit form with "old"
This commit is contained in:
+48
-37
@@ -1,27 +1,33 @@
|
||||
#[macro_use] extern crate log;
|
||||
#[macro_use] extern crate actix_web;
|
||||
#[macro_use]
|
||||
extern crate actix_web;
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
|
||||
use actix_web::{get, post, web, App, HttpResponse, HttpServer, Responder, HttpRequest};
|
||||
use parking_lot::Mutex;
|
||||
use actix_web::web::{service, scope};
|
||||
use actix_web::http::StatusCode;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::path::{PathBuf, Path};
|
||||
use actix_web_static_files;
|
||||
use tera::Tera;
|
||||
use include_dir::Dir;
|
||||
use std::sync::Arc;
|
||||
use log::LevelFilter;
|
||||
use crate::tera_ext::TeraExt;
|
||||
use once_cell::sync::Lazy;
|
||||
use std::borrow::Borrow;
|
||||
use std::ops::Deref;
|
||||
use yopa::{Storage, TypedValue};
|
||||
use std::collections::HashMap;
|
||||
use actix_session::CookieSession;
|
||||
use rand::Rng;
|
||||
use actix_web_static_files::ResourceFiles as StaticFiles;
|
||||
use std::ops::Deref;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
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::ResourceFiles as StaticFiles;
|
||||
use include_dir::Dir;
|
||||
use log::LevelFilter;
|
||||
use once_cell::sync::Lazy;
|
||||
use parking_lot::Mutex;
|
||||
use rand::Rng;
|
||||
use tera::Tera;
|
||||
|
||||
use yopa::{Storage, TypedValue};
|
||||
|
||||
use crate::tera_ext::TeraExt;
|
||||
|
||||
mod utils;
|
||||
mod tera_ext;
|
||||
mod routes;
|
||||
mod session_ext;
|
||||
@@ -32,13 +38,13 @@ include!(concat!(env!("OUT_DIR"), "/static_files.rs"));
|
||||
// Embed 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();
|
||||
tera.add_include_dir_templates(&TEMPLATES).unwrap();
|
||||
|
||||
// Special filter for the TypedValue map
|
||||
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() {
|
||||
return Ok(v.clone());
|
||||
}
|
||||
@@ -95,15 +101,20 @@ async fn main() -> std::io::Result<()> {
|
||||
|
||||
/* Routes */
|
||||
.service(routes::index)
|
||||
.service(routes::object_model_create_form)
|
||||
.service(routes::object_model_create)
|
||||
.service(routes::relation_model_create_form)
|
||||
.service(routes::relation_model_create)
|
||||
.service(routes::property_model_create_form)
|
||||
.service(routes::property_model_create)
|
||||
.service(routes::object_model_delete)
|
||||
.service(routes::relation_model_delete)
|
||||
.service(routes::property_model_delete)
|
||||
//
|
||||
.service(routes::object_model::create_form)
|
||||
.service(routes::object_model::create)
|
||||
.service(routes::object_model::update_form)
|
||||
.service(routes::object_model::update)
|
||||
.service(routes::object_model::delete)
|
||||
//
|
||||
.service(routes::relation_model::create_form)
|
||||
.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)
|
||||
.default_service(web::to(|| HttpResponse::NotFound().body("File or endpoint not found")))
|
||||
})
|
||||
@@ -141,7 +152,7 @@ fn init_yopa() -> YopaStoreWrapper {
|
||||
optional: false,
|
||||
multiple: true,
|
||||
data_type: DataType::String,
|
||||
default: None
|
||||
default: None,
|
||||
}).unwrap();
|
||||
|
||||
store.define_property(model::PropertyModel {
|
||||
@@ -151,7 +162,7 @@ fn init_yopa() -> YopaStoreWrapper {
|
||||
optional: false,
|
||||
multiple: false,
|
||||
data_type: DataType::String,
|
||||
default: None
|
||||
default: None,
|
||||
}).unwrap();
|
||||
|
||||
store.define_property(model::PropertyModel {
|
||||
@@ -161,7 +172,7 @@ fn init_yopa() -> YopaStoreWrapper {
|
||||
optional: true,
|
||||
multiple: true,
|
||||
data_type: DataType::String,
|
||||
default: Some(TypedValue::String("Pepa Novák".into()))
|
||||
default: Some(TypedValue::String("Pepa Novák".into())),
|
||||
}).unwrap();
|
||||
|
||||
let rel_book_id = store.define_relation(model::RelationModel {
|
||||
@@ -171,7 +182,7 @@ fn init_yopa() -> YopaStoreWrapper {
|
||||
reciprocal_name: "recipes".to_string(),
|
||||
optional: true,
|
||||
multiple: true,
|
||||
related: id_book
|
||||
related: id_book,
|
||||
}).unwrap();
|
||||
|
||||
store.define_property(model::PropertyModel {
|
||||
@@ -181,7 +192,7 @@ fn init_yopa() -> YopaStoreWrapper {
|
||||
optional: true,
|
||||
multiple: false,
|
||||
data_type: DataType::Integer,
|
||||
default: None
|
||||
default: None,
|
||||
}).unwrap();
|
||||
|
||||
store.define_relation(model::RelationModel {
|
||||
@@ -191,7 +202,7 @@ fn init_yopa() -> YopaStoreWrapper {
|
||||
reciprocal_name: "related recipe".to_string(),
|
||||
optional: true,
|
||||
multiple: true,
|
||||
related: id_recipe
|
||||
related: id_recipe,
|
||||
}).unwrap();
|
||||
|
||||
web::Data::new(tokio::sync::RwLock::new(store))
|
||||
|
||||
+22
-374
@@ -1,71 +1,27 @@
|
||||
use actix_web::{web, HttpRequest, Responder, HttpResponse};
|
||||
use std::fmt::{Debug, Display};
|
||||
use std::ops::DerefMut;
|
||||
use std::str::FromStr;
|
||||
|
||||
use actix_session::Session;
|
||||
use actix_web::{HttpRequest, HttpResponse, Responder, web};
|
||||
use actix_web::http::header::IntoHeaderValue;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use yopa::{DataType, ID, Storage, StorageError, TypedValue};
|
||||
use yopa::model::{ObjectModel, PropertyModel, RelationModel};
|
||||
|
||||
use crate::session_ext::SessionExt;
|
||||
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 actix_session::Session;
|
||||
use crate::session_ext::SessionExt;
|
||||
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 {
|
||||
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()
|
||||
}
|
||||
}
|
||||
use crate::routes::relation_model::RelationModelDisplay;
|
||||
use crate::routes::object_model::ObjectModelDisplay;
|
||||
|
||||
pub(crate) mod object_model;
|
||||
pub(crate) mod relation_model;
|
||||
pub(crate) mod property_model;
|
||||
|
||||
#[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 models_iter = rg.get_object_models();
|
||||
@@ -86,7 +42,7 @@ pub(crate) async fn index(session : Session, store : crate::YopaStoreWrapper) ->
|
||||
RelationModelDisplay {
|
||||
model: rm,
|
||||
related_name: rg.get_model_name(rm.related),
|
||||
properties: rprops
|
||||
properties: rprops,
|
||||
}
|
||||
}).collect::<Vec<_>>();
|
||||
relations.sort_by_key(|d| &d.model.name);
|
||||
@@ -100,7 +56,7 @@ pub(crate) async fn index(session : Session, store : crate::YopaStoreWrapper) ->
|
||||
RelationModelDisplay {
|
||||
model: rm,
|
||||
related_name: rg.get_model_name(rm.object),
|
||||
properties: rprops
|
||||
properties: rprops,
|
||||
}
|
||||
}).collect::<Vec<_>>();
|
||||
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,
|
||||
properties,
|
||||
relations,
|
||||
reciprocal_relations
|
||||
reciprocal_relations,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -125,311 +81,3 @@ pub(crate) async fn index(session : Session, store : crate::YopaStoreWrapper) ->
|
||||
|
||||
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 serde::de::DeserializeOwned;
|
||||
|
||||
pub trait SessionExt {
|
||||
/// Get a `value` from the session.
|
||||
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 {
|
||||
@@ -19,7 +19,7 @@ impl SessionExt for Session {
|
||||
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") {
|
||||
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();
|
||||
}
|
||||
|
||||
fn flash_success(&self, msg : impl AsRef<str>) {
|
||||
fn flash_success(&self, msg: impl AsRef<str>) {
|
||||
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::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() {
|
||||
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" {
|
||||
debug!("Bad file: {:?}", f);
|
||||
@@ -30,11 +30,11 @@ fn tera_includedir_walk_folder_inner(collected : &mut Vec<(String, String)>, ter
|
||||
pub(crate) trait TeraExt {
|
||||
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)
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -45,7 +45,7 @@ impl TeraExt for Tera {
|
||||
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| {
|
||||
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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user