flash messages etc

This commit is contained in:
2021-02-07 17:52:10 +01:00
parent 8b87fd0079
commit 9706cf0a62
13 changed files with 470 additions and 124 deletions
+2
View File
@@ -15,11 +15,13 @@ serde_json = "1"
log = "0.4.14"
simple-logging = "2.0.2"
actix-web = "3"
actix-session = "0.4.0"
parking_lot = "0.11.1"
include_dir = "0.6.0"
tera = "1.6.1"
actix-web-static-files = "3.0"
once_cell = "1.5.2"
rand = "0.8.3"
tokio = { version="0.2.6", features=["full"] }
+15
View File
@@ -205,3 +205,18 @@ textarea:focus,
li {
padding-bottom: .5rem;
}
.toast {
border: 1px solid black;
border-radius: 5px;
padding: .5rem;
margin: .5rem 0;
}
.toast.error {
border-color: #dc143c;
}
.toast.success {
border-color: #32cd32;
}
+16 -3
View File
@@ -2,17 +2,30 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{% block title %}{% endblock title %} &bull; YOPA</title>
<title>{% block title -%}{%- endblock title %} &bull; YOPA</title>
<link rel="stylesheet" href="/static/style.css">
<link rel="stylesheet" href="/static/taggle.css">
<script src="/static/taggle.min.js"></script>
</head>
<body>
<nav class="top-nav">
{% block nav %}{% endblock %}
{%- block nav -%}{%- endblock -%}
</nav>
<div class="content">
{% block content %}{% endblock %}
{%- if flash_error -%}
<div class="toast error">
{{ flash_error }}
</div>
{%- endif -%}
{%- if flash_success -%}
<div class="toast success">
{{ flash_success }}
</div>
{%- endif -%}
{%- block content -%}{%- endblock -%}
</div>
</body>
</html>
@@ -0,0 +1,8 @@
{% macro describe_property(prop) %}
{{prop.name}}, {{prop.data_type}}
{%- if prop.default -%}
, default: "{{prop.default | print_typed_value}}"
{%- endif -%}
{%- if prop.optional %}, OPTIONAL{% endif %}
{%- if prop.multiple %}, MULTIPLE{% endif %}
{% endmacro input %}
+7 -13
View File
@@ -1,4 +1,5 @@
{% extends "_layout" %}
{% import "_macros" as macros %}
{% block title -%}
Index
@@ -26,12 +27,7 @@
<ul>
{% for prop in model.properties %}
<li>
{{prop.name}}, {{prop.data_type}}
{%- if prop.default -%}
, default: "{{prop.default | print_typed_value}}"
{%- endif -%}
{%- if prop.optional %}, OPTIONAL{% endif %}
{%- if prop.multiple %}, MULTIPLE{% endif %}
{{ macros::describe_property(prop=prop) }}
</li>
{% endfor %}
</ul>
@@ -44,19 +40,17 @@
<ul>
{% for rel in model.relations %}
<li>
<span title="{{rel.model.id}}">"{{rel.model.name}}", pointing to: <i>{{rel.related_name}}</i></span><br>
<span title="{{rel.model.id}}">"{{rel.model.name}}", pointing to: <i>{{rel.related_name}}</i></span>
{%- if rel.model.optional %}, OPTIONAL{% endif %}
{%- if rel.model.multiple %}, MULTIPLE{% endif %}
<br>
{% if rel.properties %}
Properties:
<ul>
{% for prop in rel.properties %}
<li title="{{prop.id}}">
{{prop.name}}, {{prop.data_type}}
{%- if prop.default -%}
, default: "{{prop.default | print_typed_value}}"
{%- endif -%}
{%- if prop.optional %}, OPTIONAL{% endif %}
{%- if prop.multiple %}, MULTIPLE{% endif %}
{{ macros::describe_property(prop=prop) }}
</li>
{% endfor %}
</ul>
@@ -13,14 +13,6 @@ Define object
<h1>Define new object model</h1>
<form action="/model/object/create" method="POST">
<label for="parent">Parent:</label>
<select name="parent" id="parent">
<option value="">No parent</option>
{%- for model in all_models %}
<option value="{{model.id}}">{{model.name}}</option>
{%- endfor %}
</select><br>
<label for="name">Name:</label>
<input type="text" id="name" name="name"><br>
+118 -85
View File
@@ -18,9 +18,13 @@ 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;
mod tera_ext;
mod routes;
mod session_ext;
// Embed static files
include!(concat!(env!("OUT_DIR"), "/static_files.rs"));
@@ -30,7 +34,7 @@ static TEMPLATES: include_dir::Dir = include_dir::include_dir!("./resources/temp
pub(crate) static TERA : Lazy<Tera> = Lazy::new(|| {
let mut tera = Tera::default();
tera.add_include_dir_templates(&TEMPLATES);
tera.add_include_dir_templates(&TEMPLATES).unwrap();
// Special filter for the TypedValue map
use serde_json::Value;
@@ -46,6 +50,16 @@ pub(crate) static TERA : Lazy<Tera> = Lazy::new(|| {
Err(tera::Error::msg("Expected nonenmpty object"))
});
// TODO need to inject HttpRequest::url_for() into tera context, but it then can't be accessed by the functions.
// tera.register_function("url_for", |args: HashMap<String, Value>| -> tera::Result<Value> {
// match args.get("name") {
// Some(Value::String(s)) => {
// let r =
// },
// _ => Err("Expected string argument".into()),
// }
// });
tera
});
@@ -58,98 +72,117 @@ async fn main() -> std::io::Result<()> {
// Ensure the lazy ref is initialized early (to catch template bugs ASAP)
let _ = TERA.deref();
let database : YopaStoreWrapper = {
let mut store = Storage::new();
let yopa_store: YopaStoreWrapper = init_yopa();
// Seed the store with some dummy data for view development
use yopa::model;
use yopa::DataType;
let mut session_key = [0u8; 32];
rand::thread_rng().fill(&mut session_key);
let id_recipe = store.define_object(model::ObjectModel {
id: Default::default(),
name: "Recipe".to_string(),
parent: None
}).unwrap();
let id_book = store.define_object(model::ObjectModel {
id: Default::default(),
name: "Book".to_string(),
parent: None
}).unwrap();
let id_ing = store.define_object(model::ObjectModel {
id: Default::default(),
name: "Ingredient".to_string(),
parent: None
}).unwrap();
store.define_property(model::PropertyModel {
id: Default::default(),
object: id_recipe,
name: "name".to_string(),
optional: false,
multiple: true,
data_type: DataType::String,
default: None
}).unwrap();
store.define_property(model::PropertyModel {
id: Default::default(),
object: id_book,
name: "title".to_string(),
optional: false,
multiple: false,
data_type: DataType::String,
default: None
}).unwrap();
store.define_property(model::PropertyModel {
id: Default::default(),
object: id_book,
name: "author".to_string(),
optional: true,
multiple: true,
data_type: DataType::String,
default: Some(TypedValue::String("Pepa Novák".into()))
}).unwrap();
let rel_book_id = store.define_relation(model::RelationModel {
id: Default::default(),
object: id_recipe,
name: "book reference".to_string(),
optional: true,
multiple: true,
related: id_book
}).unwrap();
store.define_property(model::PropertyModel {
id: Default::default(),
object: rel_book_id,
name: "page".to_string(),
optional: true,
multiple: false,
data_type: DataType::Integer,
default: None
}).unwrap();
store.define_relation(model::RelationModel {
id: Default::default(),
object: id_recipe,
name: "related recipe".to_string(),
optional: true,
multiple: true,
related: id_recipe
}).unwrap();
web::Data::new(tokio::sync::RwLock::new(store))
};
debug!("Session key: {:?}", session_key);
HttpServer::new(move || {
let static_files = actix_web_static_files::ResourceFiles::new("/static", included_static_files())
let static_files = StaticFiles::new("/static", included_static_files())
.do_not_resolve_defaults();
App::new()
.app_data(database.clone())
/* Middlewares */
.wrap(
CookieSession::signed(&session_key)
.secure(false)
)
/* Bind shared objects */
.app_data(yopa_store.clone())
/* Routes */
.service(routes::index)
.service(routes::object_model_create_form)
.service(routes::object_model_create)
.service(static_files)
.default_service(web::to(|| HttpResponse::NotFound().body("Not found")))
})
.bind("127.0.0.1:8080")?.run().await
.bind("127.0.0.1:8080")?
.run().await
}
fn init_yopa() -> YopaStoreWrapper {
let mut store = Storage::new();
// Seed the store with some dummy data for view development
use yopa::model;
use yopa::DataType;
let id_recipe = store.define_object(model::ObjectModel {
id: Default::default(),
name: "Recipe".to_string(),
}).unwrap();
let id_book = store.define_object(model::ObjectModel {
id: Default::default(),
name: "Book".to_string(),
}).unwrap();
let id_ing = store.define_object(model::ObjectModel {
id: Default::default(),
name: "Ingredient".to_string(),
}).unwrap();
store.define_property(model::PropertyModel {
id: Default::default(),
object: id_recipe,
name: "name".to_string(),
optional: false,
multiple: true,
data_type: DataType::String,
default: None
}).unwrap();
store.define_property(model::PropertyModel {
id: Default::default(),
object: id_book,
name: "title".to_string(),
optional: false,
multiple: false,
data_type: DataType::String,
default: None
}).unwrap();
store.define_property(model::PropertyModel {
id: Default::default(),
object: id_book,
name: "author".to_string(),
optional: true,
multiple: true,
data_type: DataType::String,
default: Some(TypedValue::String("Pepa Novák".into()))
}).unwrap();
let rel_book_id = store.define_relation(model::RelationModel {
id: Default::default(),
object: id_recipe,
name: "book reference".to_string(),
optional: true,
multiple: true,
related: id_book
}).unwrap();
store.define_property(model::PropertyModel {
id: Default::default(),
object: rel_book_id,
name: "page".to_string(),
optional: true,
multiple: false,
data_type: DataType::Integer,
default: None
}).unwrap();
store.define_relation(model::RelationModel {
id: Default::default(),
object: id_recipe,
name: "related recipe".to_string(),
optional: true,
multiple: true,
related: id_recipe
}).unwrap();
web::Data::new(tokio::sync::RwLock::new(store))
}
+54 -5
View File
@@ -1,9 +1,12 @@
use actix_web::{web, HttpRequest, Responder};
use actix_web::{web, HttpRequest, Responder, HttpResponse};
use crate::TERA;
use crate::tera_ext::TeraExt;
use yopa::Storage;
use serde::Serialize;
use yopa::model::{PropertyModel, RelationModel};
use yopa::{Storage, StorageError};
use serde::{Deserialize, Serialize};
use yopa::model::{PropertyModel, RelationModel, ObjectModel};
use std::ops::DerefMut;
use actix_session::Session;
use crate::session_ext::SessionExt;
#[derive(Serialize, Debug)]
struct ObjectModelDisplay<'a> {
@@ -21,7 +24,7 @@ struct RelationModelDisplay<'a> {
}
#[get("/")]
pub(crate) async fn index(req: HttpRequest, 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;
@@ -63,6 +66,52 @@ pub(crate) async fn index(req: HttpRequest, store : crate::YopaStoreWrapper) ->
let mut ctx = tera::Context::new();
ctx.insert("models", &models);
session.render_flash(&mut 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));
Ok(HttpResponse::SeeOther()
.header("location", "/")
.finish())
}
Err(e) => {
warn!("Error creating model: {}", e);
session.flash_error(e.to_string());
// Redirect back
Ok(HttpResponse::SeeOther()
.header("location", "/model/object/create")
.finish())
}
}
}
+39
View File
@@ -0,0 +1,39 @@
use serde::de::DeserializeOwned;
use actix_session::Session;
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 flash_error(&self, msg : impl AsRef<str>);
fn flash_success(&self, msg : impl AsRef<str>);
}
impl SessionExt for Session {
fn take<T: DeserializeOwned>(&self, key: &str) -> Result<Option<T>, actix_web::error::Error> {
let val = self.get(key);
self.remove(key); // Always remove, even if parsing failed
Ok(val?)
}
fn render_flash(&self, context : &mut tera::Context) {
if let Ok(Some(msg)) = self.take::<String>("flash_error") {
context.insert("flash_error", &msg);
}
if let Ok(Some(msg)) = self.take::<String>("flash_success") {
context.insert("flash_success", &msg);
}
}
fn flash_error(&self, msg : impl AsRef<str>) {
self.set("flash_error", msg.as_ref()).unwrap();
}
fn flash_success(&self, msg : impl AsRef<str>) {
self.set("flash_success", msg.as_ref()).unwrap();
}
}