a small relational database with user-editable schema for manual data entry
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
yopa/yopa-web/src/routes/objects.rs

477 lines
15 KiB

use actix_session::Session;
use actix_web::{Responder, web, HttpResponse};
use crate::session_ext::SessionExt;
use crate::routes::models::object::ObjectModelForm;
use crate::TERA;
use crate::tera_ext::TeraExt;
use yopa::{ID, model, Storage, data, TypedValue};
use yopa::data::Object;
use serde::{Serialize,Deserialize};
use yopa::insert::{InsertObj, InsertValue};
use crate::utils::redirect;
use actix_web::web::Json;
use serde_json::Value;
use itertools::Itertools;
use yopa::model::{ObjectModel, PropertyModel, RelationModel};
use heck::TitleCase;
use std::collections::HashMap;
use json_dotpath::DotPaths;
use yopa::update::UpdateObj;
// we only need references here, Context serializes everything to Value.
// cloning would be a waste of cycles
#[derive(Debug, Default, Serialize, Clone)]
pub struct Schema<'a> {
pub obj_models: Vec<&'a model::ObjectModel>,
pub rel_models: Vec<&'a model::RelationModel>,
pub prop_models: Vec<&'a model::PropertyModel>,
}
#[derive(Serialize,Debug,Clone)]
pub struct ObjectCreateData<'a> {
pub model_id: ID,
pub schema: Schema<'a>,
pub objects: Vec<&'a Object>,
}
#[get("/object/create/{model_id}")]
pub(crate) async fn create_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"))?;
context.insert("model", model);
let form_data = prepare_object_create_data(&rg, model.id)?;
context.insert("form_data", &form_data);
TERA.build_response("objects/object_create", &context)
}
fn prepare_object_create_data(rg : &Storage, model_id : ID) -> actix_web::Result<ObjectCreateData> {
let model = rg.get_object_model(model_id)
.ok_or_else(|| actix_web::error::ErrorNotFound("No such model"))?;
let relations : Vec<_> = rg.get_relation_models_for_object_model(model.id).collect();
let mut prop_object_ids : Vec<ID> = relations.iter().map(|r| r.id).collect();
prop_object_ids.push(model.id);
prop_object_ids.sort();
prop_object_ids.dedup();
let mut related_ids : Vec<_> = relations.iter().map(|r| r.related).collect();
related_ids.sort();
related_ids.dedup();
Ok(ObjectCreateData {
model_id: model.id,
schema: Schema {
obj_models: rg.get_object_models().collect(), // TODO get only the ones that matter here
rel_models: relations,
prop_models: rg.get_property_models_for_parents(prop_object_ids).collect()
},
objects: rg.get_objects_of_type(related_ids).collect()
})
}
#[post("/object/create")]
pub(crate) async fn create(
form: web::Json<InsertObj>,
store: crate::YopaStoreWrapper,
session: Session,
) -> actix_web::Result<impl Responder> {
warn!("{:?}", form);
// let des : InsertObj = serde_json::from_value(form.into_inner()).unwrap();
//
// Ok(HttpResponse::Ok().finish())
//
let mut wg = store.write().await;
let form = form.into_inner();
let name = form.name.clone();
let model_name = wg.get_model_name(form.model).to_owned().to_title_case();
match wg.insert_object(form) {
Ok(_id) => {
debug!("Object created, redirecting to root");
session.flash_success(format!("{} \"{}\" created.", model_name, name));
Ok(HttpResponse::Ok().finish())
}
Err(e) => {
warn!("Error creating model: {}", e);
Ok(HttpResponse::BadRequest().body(e.to_string()))
}
}
}
#[derive(Debug, Serialize, Clone)]
struct ModelWithObjects<'a> {
model : &'a ObjectModel,
objects: Vec<&'a Object>
}
#[get("/objects")]
pub(crate) async fn list(session: Session, store: crate::YopaStoreWrapper) -> actix_web::Result<impl Responder> {
list_inner(session, store).await
}
pub(crate) async fn list_inner(session: Session, store: crate::YopaStoreWrapper) -> actix_web::Result<HttpResponse> {
let rg = store.read().await;
let mut objects_by_model = rg.get_grouped_objects();
let mut models : Vec<_> = rg.get_object_models()
.sorted_by_key(|m| &m.name)
.map(|model| {
let mut objects = objects_by_model.remove(&model.id).unwrap_or_default();
objects.sort_by_key(|o| &o.name);
ModelWithObjects {
model,
objects
}
}).collect();
let mut ctx = tera::Context::new();
ctx.insert("models", &models);
session.render_flash(&mut ctx);
TERA.build_response("objects/index", &ctx)
}
#[derive(Debug,Serialize)]
struct PropertyView<'a> {
model : &'a PropertyModel,
values: Vec<&'a yopa::data::Value>
}
#[derive(Debug,Serialize)]
struct RelationView<'a> {
model : &'a RelationModel,
related_name: &'a str,
instances: Vec<RelationInstanceView<'a>>
}
#[derive(Debug,Serialize)]
struct RelationInstanceView<'a> {
related: &'a Object,
properties: Vec<PropertyView<'a>>
}
#[get("/object/detail/{id}")]
pub(crate) async fn detail(
id: web::Path<ID>,
store: crate::YopaStoreWrapper,
session: Session
) -> actix_web::Result<impl Responder> {
let object_id = *id;
let mut context = tera::Context::new();
session.render_flash(&mut context);
let rg = store.read().await;
let object = rg.get_object(object_id)
.ok_or_else(|| actix_web::error::ErrorNotFound("No such object"))?;
let model = rg.get_object_model(object.model)
.ok_or_else(|| actix_web::error::ErrorNotFound("No such model"))?;
context.insert("object", object);
context.insert("model", model);
context.insert("kind", &rg.get_model_name(object.model));
let mut relations = rg.get_relations_for_object(object_id).collect_vec();
let reci_relations = rg.get_reciprocal_relations_for_object(object_id).collect_vec();
// values by parent ID
let mut ids_to_get_values_for = relations.iter().map(|r| r.id).collect_vec();
ids_to_get_values_for.extend(reci_relations.iter().map(|r| r.id));
ids_to_get_values_for.push(object_id);
let mut grouped_values = rg.get_grouped_values_for_objects(ids_to_get_values_for);
// object's own properties
{
let mut object_values_by_model = grouped_values
.remove(&object_id).unwrap_or_default().into_iter()
.into_group_map_by(|value| value.model);
let mut view_object_properties = vec![];
for (prop_model_id, values) in object_values_by_model {
view_object_properties.push(PropertyView {
model: rg.get_property_model(prop_model_id).unwrap(),
values
})
}
view_object_properties.sort_by_key(|p| &p.model.name);
context.insert("properties", &view_object_properties);
}
// go through relations
{
let grouped_relations = relations.iter()
.into_group_map_by(|relation| relation.model);
let mut relation_views = vec![];
for (model_id, relations) in grouped_relations {
let mut instances = vec![];
for rel in relations {
let related_obj = match rg.get_object(rel.related) {
None => continue,
Some(obj) => obj
};
let mut rel_values_by_model = grouped_values
.remove(&rel.id).unwrap_or_default().into_iter()
.into_group_map_by(|value| value.model);
let mut view_rel_properties = vec![];
for (prop_model_id, values) in rel_values_by_model {
view_rel_properties.push(PropertyView {
model: rg.get_property_model(prop_model_id).unwrap(),
values
})
}
view_rel_properties.sort_by_key(|p| &p.model.name);
instances.push(RelationInstanceView {
related: related_obj,
properties: view_rel_properties
})
}
instances.sort_by_key(|r| &r.related.name);
relation_views.push(RelationView {
model: rg.get_relation_model(model_id).unwrap(),
related_name: rg.get_model_name(model_id),
instances
})
}
relation_views.sort_by_key(|r| &r.model.name);
context.insert("relations", &relation_views);
}
// near-copypasta for reciprocal
{
let grouped_relations = reci_relations.iter()
.into_group_map_by(|relation| relation.model);
let mut relation_views = vec![];
for (model_id, relations) in grouped_relations {
let mut instances = vec![];
for rel in relations {
let related_obj = match rg.get_object(rel.object) {
None => continue,
Some(obj) => obj
};
let mut rel_values_by_model = grouped_values
.remove(&rel.id).unwrap_or_default().into_iter()
.into_group_map_by(|value| value.model);
let mut view_rel_properties = vec![];
for (prop_model_id, values) in rel_values_by_model {
view_rel_properties.push(PropertyView {
model: rg.get_property_model(prop_model_id).unwrap(),
values
})
}
view_rel_properties.sort_by_key(|p| &p.model.name);
instances.push(RelationInstanceView {
related: related_obj,
properties: view_rel_properties
})
}
instances.sort_by_key(|r| &r.related.name);
relation_views.push(RelationView {
model: rg.get_relation_model(model_id).unwrap(),
related_name: rg.get_model_name(model_id),
instances
})
}
relation_views.sort_by_key(|r| &r.model.reciprocal_name);
context.insert("reciprocal_relations", &relation_views);
}
TERA.build_response("objects/object_detail", &context)
}
#[derive(Serialize,Debug,Clone)]
struct EnrichedObject<'a> {
id: ID,
model: ID,
name: String,
values: HashMap<String /* ID but as string so serde will stop exploding */, Vec<&'a data::Value>>,
relations: HashMap<String /* ID */, Vec<EnrichedRelation<'a>>>,
}
#[derive(Serialize,Debug,Clone)]
struct EnrichedRelation<'a> {
id: ID,
object: ID,
model: ID,
related: ID,
values: HashMap<String /* ID */, Vec<&'a data::Value>>,
}
#[get("/object/update/{id}")]
pub(crate) async fn update_form(
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 object = rg.get_object(*id)
.ok_or_else(|| actix_web::error::ErrorNotFound("No such object"))?;
let model = rg.get_object_model(object.model)
.ok_or_else(|| actix_web::error::ErrorNotFound("Object has no model"))?;
// maybe its useful,idk
context.insert("model", &model);
context.insert("object", &object);
let create_data = prepare_object_create_data(&rg, model.id)?;
let mut value_map = HashMap::new();
let mut relation_map = HashMap::new();
// Some properties may have no values, so we first check what IDs to expect
let prop_ids = create_data.schema.prop_models.iter()
.filter(|p| p.object == model.id).map(|p| p.id).collect_vec();
let mut values_grouped = rg.get_values_for_object(*id)
.into_group_map_by(|v| v.model);
prop_ids.into_iter().for_each(|id| {
value_map.insert(id.to_string(), values_grouped.remove(&id).unwrap_or_default());
});
{
// Some properties may have no values, so we first check what IDs to expect
4 years ago
let relation_model_ids = create_data.schema.rel_models.iter()
.filter(|p| p.object == model.id)
.map(|p| p.id).collect_vec();
let relations = rg.get_relations_for_object(*id).collect_vec();
4 years ago
let relation_ids = relations.iter().map(|r| r.id).collect_vec();
let mut relations_grouped_by_model = relations.iter()
.into_group_map_by(|relation| relation.model);
4 years ago
let mut property_models_grouped_by_parent = rg.get_grouped_prop_models_for_parents(relation_model_ids.clone());
let mut relation_values_grouped_by_instance = rg.get_grouped_values_for_objects(relation_ids);
4 years ago
for rel_model_id in relation_model_ids {
let relations = relations_grouped_by_model.remove(&rel_model_id).unwrap_or_default();
let mut instances = vec![];
4 years ago
let prop_models_for_relation = property_models_grouped_by_parent.remove(&rel_model_id).unwrap_or_default();
for rel in relations {
let mut relation_values_map = HashMap::new();
// values keyed by model
4 years ago
let mut rel_values = relation_values_grouped_by_instance.remove(&rel.id).unwrap_or_default().into_iter()
.into_group_map_by(|relation| relation.model);
4 years ago
prop_models_for_relation.iter().for_each(|prop_model| {
relation_values_map.insert(prop_model.id.to_string(), rel_values.remove(&prop_model.id).unwrap_or_default());
});
4 years ago
instances.push(EnrichedRelation {
id: rel.id,
object: rel.object,
model: rel.model,
related: rel.related,
values: relation_values_map
4 years ago
});
}
relation_map.insert(rel_model_id.to_string(), instances);
}
}
let mut form = serde_json::to_value(create_data)?;
let object = EnrichedObject {
4 years ago
id: object.id,
model: object.model,
name: object.name.clone(),
values: value_map,
relations: relation_map
};
4 years ago
let _ = form.dot_remove("model_id");
form.dot_set("object", object);
context.insert("form_data", &form);
TERA.build_response("objects/object_update", &context)
}
#[post("/object/update/{id}")]
pub(crate) async fn update(
id: web::Path<ID>,
form: web::Json<UpdateObj>,
store: crate::YopaStoreWrapper,
session: Session,
) -> actix_web::Result<HttpResponse> {
warn!("{:?}", form);
let mut wg = store.write().await;
let form = form.into_inner();
let name = form.name.clone();
let object = wg.get_object(form.id)
.ok_or_else(|| actix_web::error::ErrorNotFound("No such object"))?;
let model_name = wg.get_model_name(object.model).to_owned().to_title_case();
match wg.update_object(form) {
Ok(_id) => {
debug!("Object created, redirecting to root");
session.flash_success(format!("{} \"{}\" updated.", model_name, name));
Ok(HttpResponse::Ok().finish())
}
Err(e) => {
warn!("Error updating model: {}", e);
Ok(HttpResponse::BadRequest().body(e.to_string()))
}
}
}