From 4948195495909efb5091989036e23f7be05d70fb Mon Sep 17 00:00:00 2001 From: finga Date: Tue, 12 Jan 2021 17:55:25 +0100 Subject: [PATCH] Add creation of posts with logging --- .gitignore | 1 + rust_web.org | 342 +++++++++++++----- simple_text_board/.env | 1 + simple_text_board/Cargo.lock | 4 + simple_text_board/Cargo.toml | 5 +- simple_text_board/Rocket.toml | 3 + simple_text_board/diesel.toml | 5 + .../2021-01-12-011936_create_posts/down.sql | 1 + .../2021-01-12-011936_create_posts/up.sql | 10 + simple_text_board/src/main.rs | 66 +++- simple_text_board/src/models.rs | 20 + simple_text_board/src/schema.rs | 11 + simple_text_board/templates/create.html.tera | 7 +- 13 files changed, 379 insertions(+), 97 deletions(-) create mode 100644 simple_text_board/.env create mode 100644 simple_text_board/diesel.toml create mode 100644 simple_text_board/migrations/2021-01-12-011936_create_posts/down.sql create mode 100644 simple_text_board/migrations/2021-01-12-011936_create_posts/up.sql create mode 100644 simple_text_board/src/models.rs create mode 100644 simple_text_board/src/schema.rs diff --git a/.gitignore b/.gitignore index 9d051aa..ddd14cd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /rust_web.pdf /rust_web.tex /simple_text_board/target +/simple_text_board/simple_text_board.sqlite diff --git a/rust_web.org b/rust_web.org index 974dd15..d7755be 100644 --- a/rust_web.org +++ b/rust_web.org @@ -260,112 +260,280 @@ *** Steps to reproduce :noexport: **** Create new crate -#+BEGIN_SRC sh -cargo init simple_text_board -cd simple_text_board -rustup override set nightly -cargo r -#+END_SRC + #+BEGIN_SRC sh + cargo init simple_text_board + cd simple_text_board + rustup override set nightly + cargo r + #+END_SRC **** Setup Rocket - Add Rocket dependency to ~Cargo.toml~: -#+BEGIN_SRC toml -[dependencies] -rocket = "0.4" -#+END_SRC +***** Add Rocket to ~Cargo.toml~ + #+BEGIN_SRC toml + [dependencies] + rocket = "0.4" + #+END_SRC -**** Modify ~src/main.rs~ for Rockets "~Hello, world!~" program -#+BEGIN_SRC rust -#![feature(proc_macro_hygiene, decl_macro)] +***** Add ~Rocket.toml~ + #+BEGIN_SRC toml + [development] + address = "0.0.0.0" + port = 8000 + workers = 2 + keep_alive = 5 + log = "normal" + #+END_SRC -#[macro_use] extern crate rocket; +***** Modify ~src/main.rs~ for Rockets "~Hello, world!~" program + #+BEGIN_SRC rust + #![feature(proc_macro_hygiene, decl_macro)] -#[get("/")] -fn index() -> &'static str { - "Hello, world!" -} + #[macro_use] extern crate rocket; -fn main() { - rocket::ignite().mount("/", routes![index]).launch(); -} -#+END_SRC + #[get("/")] + fn index() -> &'static str { + "Hello, world!" + } -**** Add ~Rocket.toml~ -#+BEGIN_SRC toml -[development] -address = "0.0.0.0" -port = 8000 -workers = 2 -keep_alive = 5 -log = "normal" -#+END_SRC + fn main() { + rocket::ignite().mount("/", routes![index]).launch(); + } + #+END_SRC -**** Create the base template at ~templates/base.html.tera~ -#+BEGIN_SRC html - - - - - - simple text board - - - - {% block content %} {% endblock %} - - -#+END_SRC +**** Create first templates +***** Create the base template at ~templates/base.html.tera~ + #+BEGIN_SRC html + + + + + + simple text board + + + + {% block content %} {% endblock %} + + + #+END_SRC -**** Create the create template at ~templates/create.html.tera~ -#+BEGIN_SRC html -{% extends "base" %} +***** Create the create template at ~templates/create.html.tera~ + #+BEGIN_SRC html + {% extends "base" %} -{% block content %} -

simple text board

-

create a post

-{% if flash %} -

{{ flash }}

-{% endif %} -
- - + {% block content %} +

simple text board

+

create a post

+ + + - - + + - - + + - - + + - -
-{% endblock content %} -#+END_SRC + + {% if flash %}

{{ flash }}

{% endif %} + + {% endblock content %} + #+END_SRC -**** Create the ~create_form~ function -#+BEGIN_SRC rust -use std::collections::HashMap; +***** Create the ~create_form~ function and add it to routing + #+BEGIN_SRC rust + #![feature(proc_macro_hygiene, decl_macro)] -use rocket::{get, request::FlashMessage, routes}; -use rocket_contrib::templates::Template; + use std::collections::HashMap; -#[get("/create")] -fn create_form(flash: Option) -> Template { - let mut context = HashMap::new(); + use rocket::{get, request::FlashMessage, routes}; + use rocket_contrib::templates::Template; - if let Some(ref msg) = flash { - context.insert("flash", msg.msg()); - } + #[get("/create")] + fn create_form(flash: Option) -> Template { + let mut context = HashMap::new(); - Template::render("create", &context) -} -#+END_SRC + if let Some(ref msg) = flash { + context.insert("flash", msg.msg()); + } + + Template::render("create", &context) + } + + ... + + fn main() { + rocket::ignite() + .mount("/", routes![index, create_form]) + .attach(Template::fairing()) + .launch(); + } + #+END_SRC + +**** Setup Database and posts +***** Create the ~.env~ file + #+BEGIN_SRC sh + DATABASE_URL=simple_text_board.sqlite + #+END_SRC + +***** Add diesel to the dependencies and to ~rocket_contrib~ in ~Cargo.toml~ + #+BEGIN_SRC toml + ... + chrono = "0.4" + diesel = { version = "1.4", features = ["sqlite", "chrono"] } + log = "0.4" + ... + [dependencies.rocket_contrib] + ... + features = ["diesel_sqlite_pool", "tera_templates"] + #+END_SRC + +***** Configure database for Rocket in ~Rocket.toml~ + #+BEGIN_SRC toml + [global.databases] + sqlite = { url = "simple_text_board.sqlite" } + #+END_SRC + +***** Create the first table schema + #+BEGIN_SRC sh + diesel setup + diesel migration generate create_posts + #+END_SRC + +***** How to create the table + #+BEGIN_SRC sql + CREATE TABLE posts ( + id INTEGER NOT NULL PRIMARY KEY, + parent INTEGER, + timestamp DATETIME NOT NULL, + author VARCHAR NOT NULL, + email VARCHAR NOT NULL, + title VARCHAR NOT NULL, + content VARCHAR NOT NULL, + FOREIGN KEY (parent) REFERENCES posts(id) + ); + #+END_SRC + +***** How to destroy it + #+BEGIN_SRC sql + DROP TABLE posts; + #+END_SRC + +***** Applay and test rollback + #+BEGIN_SRC sh + diesel migration run + diesel migration redo + #+END_SRC + +***** Create data model in ~src/models.rs~ + #+BEGIN_SRC rust + use crate::schema::posts::{self, dsl::posts as table_posts}; + use chrono::NaiveDateTime; + use diesel::{QueryResult, RunQueryDsl}; + + #[derive(Insertable)] + #[table_name = "posts"] + pub struct NewPost<'a> { + pub parent: Option<&'a i32>, + pub timestamp: NaiveDateTime, + pub author: &'a str, + pub email: &'a str, + pub title: &'a str, + pub content: &'a str, + } + + impl NewPost<'_> { + pub fn insert(&self, conn: &diesel::SqliteConnection) -> QueryResult { + diesel::insert_into(table_posts).values(self).execute(conn) + } + } + #+END_SRC + +***** Add create post functionality to ~src/main.rs~ + #+BEGIN_SRC rust + #![feature(proc_macro_hygiene, decl_macro)] + + #[macro_use] + extern crate diesel; + + use std::{collections::HashMap, net::SocketAddr}; + + use chrono::Local; + use diesel::SqliteConnection; + use log::{error, info}; + use rocket::{ + get, post, + request::{FlashMessage, Form}, + response::{Flash, Redirect}, + routes, uri, FromForm, + }; + use rocket_contrib::{database, templates::Template}; + + mod models; + mod schema; + + use models::NewPost; + + #[database("sqlite")] + struct DbCon(SqliteConnection); + + #[derive(FromForm)] + pub struct PostForm { + author: String, + email: String, + title: String, + content: String, + } + + ... + + #[post("/create", data = "")] + fn create_post( + conn: DbCon, + remote_address: SocketAddr, + post: Form, + ) -> Result> { + let post = NewPost { + parent: None, + timestamp: Local::now().naive_local(), + author: &post.author.trim(), + email: &post.email.trim(), + title: &post.title.trim(), + content: &post.content.trim(), + }; + + match post.insert(&conn) { + Ok(_) => { + info!("New post from {}", remote_address); + Ok(Redirect::to(uri!(index))) + } + Err(e) => { + error!("Could not create post from {}: {:?}", remote_address, e); + Err(Flash::error( + Redirect::to(uri!(create_form)), + "Could not create post.".to_string(), + )) + } + } + } + + ... + + fn main() { + rocket::ignite() + .mount("/", routes![index, create_form, create_post]) + .attach(DbCon::fairing()) + .attach(Template::fairing()) + .launch(); + } + #+END_SRC * Conclusion diff --git a/simple_text_board/.env b/simple_text_board/.env new file mode 100644 index 0000000..9e95dbe --- /dev/null +++ b/simple_text_board/.env @@ -0,0 +1 @@ +DATABASE_URL=simple_text_board.sqlite diff --git a/simple_text_board/Cargo.lock b/simple_text_board/Cargo.lock index 583813d..57dfce3 100644 --- a/simple_text_board/Cargo.lock +++ b/simple_text_board/Cargo.lock @@ -270,6 +270,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e2de9deab977a153492a1468d1b1c0662c1cf39e5ea87d0c060ecd59ef18d8c" dependencies = [ "byteorder", + "chrono", "diesel_derives", "libsqlite3-sys", "r2d2", @@ -1163,6 +1164,9 @@ dependencies = [ name = "simple_text_board" version = "0.1.0" dependencies = [ + "chrono", + "diesel", + "log 0.4.13", "rocket", "rocket_contrib", ] diff --git a/simple_text_board/Cargo.toml b/simple_text_board/Cargo.toml index 67e1079..462cfb5 100644 --- a/simple_text_board/Cargo.toml +++ b/simple_text_board/Cargo.toml @@ -6,8 +6,11 @@ edition = "2018" [dependencies] rocket = "0.4" +chrono = "0.4" +diesel = { version = "1.4", features = ["sqlite", "chrono"] } +log = "0.4" [dependencies.rocket_contrib] version = "0.4" default-features = false -features = ["tera_templates"] +features = ["diesel_sqlite_pool", "tera_templates"] diff --git a/simple_text_board/Rocket.toml b/simple_text_board/Rocket.toml index ae113ff..b6a1ef5 100644 --- a/simple_text_board/Rocket.toml +++ b/simple_text_board/Rocket.toml @@ -1,3 +1,6 @@ +[global.databases] +sqlite = { url = "simple_text_board.sqlite" } + [development] address = "0.0.0.0" port = 8000 diff --git a/simple_text_board/diesel.toml b/simple_text_board/diesel.toml new file mode 100644 index 0000000..92267c8 --- /dev/null +++ b/simple_text_board/diesel.toml @@ -0,0 +1,5 @@ +# For documentation on how to configure this file, +# see diesel.rs/guides/configuring-diesel-cli + +[print_schema] +file = "src/schema.rs" diff --git a/simple_text_board/migrations/2021-01-12-011936_create_posts/down.sql b/simple_text_board/migrations/2021-01-12-011936_create_posts/down.sql new file mode 100644 index 0000000..1651d89 --- /dev/null +++ b/simple_text_board/migrations/2021-01-12-011936_create_posts/down.sql @@ -0,0 +1 @@ +DROP TABLE posts; diff --git a/simple_text_board/migrations/2021-01-12-011936_create_posts/up.sql b/simple_text_board/migrations/2021-01-12-011936_create_posts/up.sql new file mode 100644 index 0000000..c419851 --- /dev/null +++ b/simple_text_board/migrations/2021-01-12-011936_create_posts/up.sql @@ -0,0 +1,10 @@ +CREATE TABLE posts ( + id INTEGER NOT NULL PRIMARY KEY, + parent INTEGER, + timestamp DATETIME NOT NULL, + author VARCHAR NOT NULL, + email VARCHAR NOT NULL, + title VARCHAR NOT NULL, + content VARCHAR NOT NULL, + FOREIGN KEY (parent) REFERENCES posts(id) +); diff --git a/simple_text_board/src/main.rs b/simple_text_board/src/main.rs index 4ae496a..75a4094 100644 --- a/simple_text_board/src/main.rs +++ b/simple_text_board/src/main.rs @@ -1,9 +1,36 @@ #![feature(proc_macro_hygiene, decl_macro)] -use std::collections::HashMap; +#[macro_use] +extern crate diesel; -use rocket::{get, request::FlashMessage, routes}; -use rocket_contrib::templates::Template; +use std::{collections::HashMap, net::SocketAddr}; + +use chrono::Local; +use diesel::SqliteConnection; +use log::{error, info}; +use rocket::{ + get, post, + request::{FlashMessage, Form}, + response::{Flash, Redirect}, + routes, uri, FromForm, +}; +use rocket_contrib::{database, templates::Template}; + +mod models; +mod schema; + +use models::NewPost; + +#[database("sqlite")] +struct DbCon(SqliteConnection); + +#[derive(FromForm)] +struct PostForm { + author: String, + email: String, + title: String, + content: String, +} #[get("/create")] fn create_form(flash: Option) -> Template { @@ -16,6 +43,36 @@ fn create_form(flash: Option) -> Template { Template::render("create", &context) } +#[post("/create", data = "")] +fn create_post( + conn: DbCon, + remote_address: SocketAddr, + post: Form, +) -> Result> { + let post = NewPost { + parent: None, + timestamp: Local::now().naive_local(), + author: &post.author.trim(), + email: &post.email.trim(), + title: &post.title.trim(), + content: &post.content.trim(), + }; + + match post.insert(&conn) { + Ok(_) => { + info!("New post from {}", remote_address); + Ok(Redirect::to(uri!(index))) + } + Err(e) => { + error!("Could not create post from {}: {:?}", remote_address, e); + Err(Flash::error( + Redirect::to(uri!(create_form)), + "Could not create post.".to_string(), + )) + } + } +} + #[get("/")] fn index() -> &'static str { "Hello, world!" @@ -23,7 +80,8 @@ fn index() -> &'static str { fn main() { rocket::ignite() - .mount("/", routes![index, create_form]) + .mount("/", routes![index, create_form, create_post]) + .attach(DbCon::fairing()) .attach(Template::fairing()) .launch(); } diff --git a/simple_text_board/src/models.rs b/simple_text_board/src/models.rs new file mode 100644 index 0000000..96edd43 --- /dev/null +++ b/simple_text_board/src/models.rs @@ -0,0 +1,20 @@ +use crate::schema::posts::{self, dsl::posts as table_posts}; +use chrono::NaiveDateTime; +use diesel::{QueryResult, RunQueryDsl}; + +#[derive(Insertable)] +#[table_name = "posts"] +pub struct NewPost<'a> { + pub parent: Option<&'a i32>, + pub timestamp: NaiveDateTime, + pub author: &'a str, + pub email: &'a str, + pub title: &'a str, + pub content: &'a str, +} + +impl NewPost<'_> { + pub fn insert(&self, conn: &diesel::SqliteConnection) -> QueryResult { + diesel::insert_into(table_posts).values(self).execute(conn) + } +} diff --git a/simple_text_board/src/schema.rs b/simple_text_board/src/schema.rs new file mode 100644 index 0000000..34adcb3 --- /dev/null +++ b/simple_text_board/src/schema.rs @@ -0,0 +1,11 @@ +table! { + posts (id) { + id -> Integer, + parent -> Nullable, + timestamp -> Timestamp, + author -> Text, + email -> Text, + title -> Text, + content -> Text, + } +} diff --git a/simple_text_board/templates/create.html.tera b/simple_text_board/templates/create.html.tera index 16048bf..cc5897d 100644 --- a/simple_text_board/templates/create.html.tera +++ b/simple_text_board/templates/create.html.tera @@ -3,9 +3,6 @@ {% block content %}

simple text board

create a post

-{% if flash %} -

{{ flash }}

-{% endif %}
@@ -17,9 +14,9 @@ - + + {% if flash %}

{{ flash }}

{% endif %}
{% endblock content %}