use std::borrow::Cow; use std::collections::HashMap; use actix_session::Session; use actix_web::{web, HttpResponse, Responder}; use heck::TitleCase; use itertools::Itertools; use json_dotpath::DotPaths; use serde::Serialize; use yopa::{data, model, Storage, ID}; use yopa::insert::InsertObj; use yopa::model::{ObjectModel, PropertyModel, RelationModel}; use yopa::update::UpdateObj; use crate::session_ext::SessionExt; use crate::tera_ext::TeraExt; use crate::utils::{redirect, StorageErrorIntoResponseError}; use crate::TERA; use yopa::helpers::GroupByModel; // 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(Debug, Clone, Serialize)] pub struct ObjectDisplay<'a> { pub id: ID, pub model: ID, pub name: Cow<'a, str>, } #[derive(Serialize, Debug, Clone)] pub struct ObjectCreateData<'a> { pub model_id: ID, pub schema: Schema<'a>, pub objects: Vec>, } #[get("/object/create/{model_id}")] pub(crate) async fn create_form( model_id: web::Path, store: crate::YopaStoreWrapper, session: Session, ) -> actix_web::Result { 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 { 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 = 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(); let mut models_to_fetch = vec![model_id]; models_to_fetch.extend(&related_ids); Ok(ObjectCreateData { model_id: model.id, schema: Schema { obj_models: rg.get_object_models_by_ids(models_to_fetch).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_types(related_ids) .map(|o| ObjectDisplay { id: o.id, model: o.model, name: rg.get_object_name(o), }) .collect(), }) } #[post("/object/create")] pub(crate) async fn create( form: web::Json, store: crate::YopaStoreWrapper, session: Session, ) -> actix_web::Result { warn!("{:?}", form); let mut wg = store.write().await; let form = form.into_inner(); let model_name = wg.get_model_name(form.model).to_owned().to_title_case(); match wg.insert_object(form) { Ok(id) => { wg.persist().err_to_500()?; let obj_name = wg.get_object_name_by_id(id); debug!("Object created, redirecting to root"); session.flash_success(format!("{} \"{}\" created.", model_name, obj_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>, } #[get("/objects")] pub(crate) async fn list( session: Session, store: crate::YopaStoreWrapper, ) -> actix_web::Result { list_inner(session, store).await } pub(crate) async fn list_inner( session: Session, store: crate::YopaStoreWrapper, ) -> actix_web::Result { let rg = store.read().await; let mut objects_by_model = rg.get_grouped_objects(); let models: Vec<_> = rg .get_object_models() .sorted_by_key(|m| &m.name) .map(|model| { let objects = objects_by_model.remove(&model.id).unwrap_or_default(); let mut objects = objects .into_iter() .map(|o| { ObjectDisplay { id: o.id, model: o.model, name: rg.get_object_name(o), // TODO optimize } }) .collect_vec(); objects.sort_by(|a, b| a.name.cmp(&b.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>, } #[derive(Debug, Serialize)] struct RelationInstanceView<'a> { related: ObjectDisplay<'a>, related_name: Cow<'a, str>, properties: Vec>, } #[get("/object/detail/{id}")] pub(crate) async fn detail( id: web::Path, store: crate::YopaStoreWrapper, session: Session, ) -> actix_web::Result { 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", &ObjectDisplay { id: object_id, model: object.model, name: rg.get_object_name(object), }, ); context.insert("model", model); context.insert("kind", &rg.get_model_name(object.model)); let 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 object_values_by_model = grouped_values .remove(&object_id) .unwrap_or_default() .group_by_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() .group_by_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 rel_values_by_model = grouped_values .remove(&rel.id) .unwrap_or_default() .group_by_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); let related_name = rg.get_object_name(related_obj); instances.push(RelationInstanceView { related: ObjectDisplay { id: related_obj.id, model: related_obj.model, name: rg.get_object_name(related_obj), }, related_name, properties: view_rel_properties, }) } instances.sort_by(|a, b| a.related_name.cmp(&b.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() .group_by_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 rel_values_by_model = grouped_values .remove(&rel.id) .unwrap_or_default() .group_by_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); let related_name = rg.get_object_name(related_obj); instances.push(RelationInstanceView { related: ObjectDisplay { id: related_obj.id, model: related_obj.model, name: rg.get_object_name(related_obj), }, related_name, properties: view_rel_properties, }) } // sort_by_key is not possible because rust is still stupid about lifetimes instances.sort_by(|a, b| a.related_name.cmp(&b.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: Cow<'a, str>, values: HashMap< String, /* ID but as string so serde will stop exploding */ Vec<&'a data::Value>, >, relations: HashMap>>, } #[derive(Serialize, Debug, Clone)] struct EnrichedRelation<'a> { id: ID, object: ID, model: ID, related: ID, values: HashMap>, } #[get("/object/update/{id}")] pub(crate) async fn update_form( id: web::Path, store: crate::YopaStoreWrapper, session: Session, ) -> actix_web::Result { 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", &ObjectDisplay { id: object.id, model: object.model, name: rg.get_object_name(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) .group_by_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 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(); let relation_ids = relations.iter().map(|r| r.id).collect_vec(); let mut relations_grouped_by_model = relations .iter() .group_by_model(); 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); 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![]; 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 let mut rel_values = relation_values_grouped_by_instance .remove(&rel.id) .unwrap_or_default() .group_by_model(); 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(), ); }); instances.push(EnrichedRelation { id: rel.id, object: rel.object, model: rel.model, related: rel.related, values: relation_values_map, }); } relation_map.insert(rel_model_id.to_string(), instances); } } let mut form = serde_json::to_value(create_data)?; let object = EnrichedObject { id: object.id, model: object.model, name: rg.get_object_name(object), values: value_map, relations: relation_map, }; form.dot_remove("model_id").unwrap(); form.dot_set("object", object).unwrap(); context.insert("form_data", &form); TERA.build_response("objects/object_update", &context) } #[post("/object/update/{id}")] pub(crate) async fn update( id: web::Path, form: web::Json, store: crate::YopaStoreWrapper, session: Session, ) -> actix_web::Result { let mut wg = store.write().await; let form = form.into_inner(); let object = wg .get_object(*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(); let id = object.id; match wg.update_object(form) { Ok(_id) => { wg.persist().err_to_500()?; debug!("Object created, redirecting to root"); session.flash_success(format!( "{} \"{}\" updated.", model_name, wg.get_object_name_by_id(id) )); Ok(HttpResponse::Ok().finish()) } Err(e) => { warn!("Error updating model: {}", e); Ok(HttpResponse::BadRequest().body(e.to_string())) } } } #[get("/object/delete/{id}")] pub(crate) async fn delete( id: web::Path, store: crate::YopaStoreWrapper, session: Session, ) -> actix_web::Result { let mut wg = store.write().await; let name = wg.get_object_name_by_id(*id).to_string(); match wg.delete_object(*id) { Ok(_obj) => { wg.persist().err_to_500()?; debug!("Object deleted, redirecting to root"); session.flash_success(format!("Object \"{}\" deleted.", name)); redirect("/") } Err(e) => { warn!("Error deleting object: {}", e); session.flash_error(e.to_string()); redirect("/") // back? } } }