Add creation of posts with logging

This commit is contained in:
finga 2021-01-12 17:55:25 +01:00
parent 56544cd0d4
commit 4948195495
13 changed files with 379 additions and 97 deletions

1
.gitignore vendored
View file

@ -1,3 +1,4 @@
/rust_web.pdf /rust_web.pdf
/rust_web.tex /rust_web.tex
/simple_text_board/target /simple_text_board/target
/simple_text_board/simple_text_board.sqlite

View file

@ -260,112 +260,280 @@
*** Steps to reproduce :noexport: *** Steps to reproduce :noexport:
**** Create new crate **** Create new crate
#+BEGIN_SRC sh #+BEGIN_SRC sh
cargo init simple_text_board cargo init simple_text_board
cd simple_text_board cd simple_text_board
rustup override set nightly rustup override set nightly
cargo r cargo r
#+END_SRC #+END_SRC
**** Setup Rocket **** Setup Rocket
Add Rocket dependency to ~Cargo.toml~: ***** Add Rocket to ~Cargo.toml~
#+BEGIN_SRC toml #+BEGIN_SRC toml
[dependencies] [dependencies]
rocket = "0.4" rocket = "0.4"
#+END_SRC #+END_SRC
**** Modify ~src/main.rs~ for Rockets "~Hello, world!~" program ***** Add ~Rocket.toml~
#+BEGIN_SRC rust #+BEGIN_SRC toml
#![feature(proc_macro_hygiene, decl_macro)] [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("/")] #[macro_use] extern crate rocket;
fn index() -> &'static str {
"Hello, world!"
}
fn main() { #[get("/")]
rocket::ignite().mount("/", routes![index]).launch(); fn index() -> &'static str {
} "Hello, world!"
#+END_SRC }
**** Add ~Rocket.toml~ fn main() {
#+BEGIN_SRC toml rocket::ignite().mount("/", routes![index]).launch();
[development] }
address = "0.0.0.0" #+END_SRC
port = 8000
workers = 2
keep_alive = 5
log = "normal"
#+END_SRC
**** Create the base template at ~templates/base.html.tera~ **** Create first templates
#+BEGIN_SRC html ***** Create the base template at ~templates/base.html.tera~
<!DOCTYPE html> #+BEGIN_SRC html
<html> <!DOCTYPE html>
<head> <html>
<meta charset="utf-8" /> <head>
<meta name="viewport" content="width=device-width" /> <meta charset="utf-8" />
<title>simple text board</title> <meta name="viewport" content="width=device-width" />
</head> <title>simple text board</title>
<body> </head>
<ul> <body>
<li><a href="/">home</a></li> <ul>
<li><a href="/create">create post</a></li> <li><a href="/">home</a></li>
</ul> <li><a href="/create">create post</a></li>
{% block content %} {% endblock %} </ul>
</body> {% block content %} {% endblock %}
</html> </body>
#+END_SRC </html>
#+END_SRC
**** Create the create template at ~templates/create.html.tera~ ***** Create the create template at ~templates/create.html.tera~
#+BEGIN_SRC html #+BEGIN_SRC html
{% extends "base" %} {% extends "base" %}
{% block content %} {% block content %}
<h1>simple text board</h1> <h1>simple text board</h1>
<h2>create a post</h2> <h2>create a post</h2>
{% if flash %} <form action="/create" method="post" accept-charset="utf-8">
<p>{{ flash }}</p> <label for="author">author name</label>
{% endif %} <input type="text" placeholder="author" name="author" required>
<form action="/create" method="post" accept-charset="utf-8">
<label for="author">author name</label>
<input type="text" placeholder="author" name="author" required>
<label for="email">email</label> <label for="email">email</label>
<input type="text" placeholder="email" name="email"> <input type="text" placeholder="email" name="email">
<label for="title">title</label> <label for="title">title</label>
<input type="text" placeholder="title" name="title" required> <input type="text" placeholder="title" name="title" required>
<label for="content">content</label> <label for="content">content</label>
<textarea type="text" placeholder="content" name="content" rows="8" cols="50" required> <textarea type="text" placeholder="content" name="content" rows="8" cols="50" required>
</textarea> </textarea>
<button type="submit">create</button> <button type="submit">create</button>
</form> {% if flash %}<p>{{ flash }}</p>{% endif %}
{% endblock content %} </form>
#+END_SRC {% endblock content %}
#+END_SRC
**** Create the ~create_form~ function ***** Create the ~create_form~ function and add it to routing
#+BEGIN_SRC rust #+BEGIN_SRC rust
use std::collections::HashMap; #![feature(proc_macro_hygiene, decl_macro)]
use rocket::{get, request::FlashMessage, routes}; use std::collections::HashMap;
use rocket_contrib::templates::Template;
#[get("/create")] use rocket::{get, request::FlashMessage, routes};
fn create_form(flash: Option<FlashMessage>) -> Template { use rocket_contrib::templates::Template;
let mut context = HashMap::new();
if let Some(ref msg) = flash { #[get("/create")]
context.insert("flash", msg.msg()); fn create_form(flash: Option<FlashMessage>) -> Template {
} let mut context = HashMap::new();
Template::render("create", &context) if let Some(ref msg) = flash {
} context.insert("flash", msg.msg());
#+END_SRC }
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<usize> {
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 = "<post>")]
fn create_post(
conn: DbCon,
remote_address: SocketAddr,
post: Form<PostForm>,
) -> Result<Redirect, Flash<Redirect>> {
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 * Conclusion

1
simple_text_board/.env Normal file
View file

@ -0,0 +1 @@
DATABASE_URL=simple_text_board.sqlite

View file

@ -270,6 +270,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e2de9deab977a153492a1468d1b1c0662c1cf39e5ea87d0c060ecd59ef18d8c" checksum = "3e2de9deab977a153492a1468d1b1c0662c1cf39e5ea87d0c060ecd59ef18d8c"
dependencies = [ dependencies = [
"byteorder", "byteorder",
"chrono",
"diesel_derives", "diesel_derives",
"libsqlite3-sys", "libsqlite3-sys",
"r2d2", "r2d2",
@ -1163,6 +1164,9 @@ dependencies = [
name = "simple_text_board" name = "simple_text_board"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"chrono",
"diesel",
"log 0.4.13",
"rocket", "rocket",
"rocket_contrib", "rocket_contrib",
] ]

View file

@ -6,8 +6,11 @@ edition = "2018"
[dependencies] [dependencies]
rocket = "0.4" rocket = "0.4"
chrono = "0.4"
diesel = { version = "1.4", features = ["sqlite", "chrono"] }
log = "0.4"
[dependencies.rocket_contrib] [dependencies.rocket_contrib]
version = "0.4" version = "0.4"
default-features = false default-features = false
features = ["tera_templates"] features = ["diesel_sqlite_pool", "tera_templates"]

View file

@ -1,3 +1,6 @@
[global.databases]
sqlite = { url = "simple_text_board.sqlite" }
[development] [development]
address = "0.0.0.0" address = "0.0.0.0"
port = 8000 port = 8000

View file

@ -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"

View file

@ -0,0 +1 @@
DROP TABLE posts;

View file

@ -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)
);

View file

@ -1,9 +1,36 @@
#![feature(proc_macro_hygiene, decl_macro)] #![feature(proc_macro_hygiene, decl_macro)]
use std::collections::HashMap; #[macro_use]
extern crate diesel;
use rocket::{get, request::FlashMessage, routes}; use std::{collections::HashMap, net::SocketAddr};
use rocket_contrib::templates::Template;
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")] #[get("/create")]
fn create_form(flash: Option<FlashMessage>) -> Template { fn create_form(flash: Option<FlashMessage>) -> Template {
@ -16,6 +43,36 @@ fn create_form(flash: Option<FlashMessage>) -> Template {
Template::render("create", &context) Template::render("create", &context)
} }
#[post("/create", data = "<post>")]
fn create_post(
conn: DbCon,
remote_address: SocketAddr,
post: Form<PostForm>,
) -> Result<Redirect, Flash<Redirect>> {
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("/")] #[get("/")]
fn index() -> &'static str { fn index() -> &'static str {
"Hello, world!" "Hello, world!"
@ -23,7 +80,8 @@ fn index() -> &'static str {
fn main() { fn main() {
rocket::ignite() rocket::ignite()
.mount("/", routes![index, create_form]) .mount("/", routes![index, create_form, create_post])
.attach(DbCon::fairing())
.attach(Template::fairing()) .attach(Template::fairing())
.launch(); .launch();
} }

View file

@ -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<usize> {
diesel::insert_into(table_posts).values(self).execute(conn)
}
}

View file

@ -0,0 +1,11 @@
table! {
posts (id) {
id -> Integer,
parent -> Nullable<Integer>,
timestamp -> Timestamp,
author -> Text,
email -> Text,
title -> Text,
content -> Text,
}
}

View file

@ -3,9 +3,6 @@
{% block content %} {% block content %}
<h1>simple text board</h1> <h1>simple text board</h1>
<h2>create a post</h2> <h2>create a post</h2>
{% if flash %}
<p>{{ flash }}</p>
{% endif %}
<form action="/create" method="post" accept-charset="utf-8"> <form action="/create" method="post" accept-charset="utf-8">
<label for="author">author name</label> <label for="author">author name</label>
<input type="text" placeholder="author" name="author" required> <input type="text" placeholder="author" name="author" required>
@ -17,9 +14,9 @@
<input type="text" placeholder="title" name="title" required> <input type="text" placeholder="title" name="title" required>
<label for="content">content</label> <label for="content">content</label>
<textarea type="text" placeholder="content" name="content" rows="8" cols="50" required> <textarea type="text" placeholder="content" name="content" rows="8" cols="50" required></textarea>
</textarea>
<button type="submit">create</button> <button type="submit">create</button>
{% if flash %}<p>{{ flash }}</p>{% endif %}
</form> </form>
{% endblock content %} {% endblock content %}