parent
d8bfc19986
commit
2a71f827b0
@ -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 %} |
@ -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()))
|
|
||||||
// }
|
|
@ -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() |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue