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.tex
/simple_text_board/target
/simple_text_board/simple_text_board.sqlite

View file

@ -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
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>simple text board</title>
</head>
<body>
<ul>
<li><a href="/">home</a></li>
<li><a href="/create">create post</a></li>
</ul>
{% block content %} {% endblock %}
</body>
</html>
#+END_SRC
**** Create first templates
***** Create the base template at ~templates/base.html.tera~
#+BEGIN_SRC html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>simple text board</title>
</head>
<body>
<ul>
<li><a href="/">home</a></li>
<li><a href="/create">create post</a></li>
</ul>
{% block content %} {% endblock %}
</body>
</html>
#+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 %}
<h1>simple text board</h1>
<h2>create a post</h2>
{% if flash %}
<p>{{ flash }}</p>
{% endif %}
<form action="/create" method="post" accept-charset="utf-8">
<label for="author">author name</label>
<input type="text" placeholder="author" name="author" required>
{% block content %}
<h1>simple text board</h1>
<h2>create a post</h2>
<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>
<input type="text" placeholder="email" name="email">
<label for="email">email</label>
<input type="text" placeholder="email" name="email">
<label for="title">title</label>
<input type="text" placeholder="title" name="title" required>
<label for="title">title</label>
<input type="text" placeholder="title" name="title" required>
<label for="content">content</label>
<textarea type="text" placeholder="content" name="content" rows="8" cols="50" required>
</textarea>
<label for="content">content</label>
<textarea type="text" placeholder="content" name="content" rows="8" cols="50" required>
</textarea>
<button type="submit">create</button>
</form>
{% endblock content %}
#+END_SRC
<button type="submit">create</button>
{% if flash %}<p>{{ flash }}</p>{% endif %}
</form>
{% 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<FlashMessage>) -> 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<FlashMessage>) -> 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<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

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"
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",
]

View file

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

View file

@ -1,3 +1,6 @@
[global.databases]
sqlite = { url = "simple_text_board.sqlite" }
[development]
address = "0.0.0.0"
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)]
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<FlashMessage>) -> Template {
@ -16,6 +43,36 @@ fn create_form(flash: Option<FlashMessage>) -> Template {
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("/")]
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();
}

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