Initial model + some various changes

This commit is contained in:
Greg Burri 2022-11-18 00:24:29 +01:00
parent a080d19cb9
commit 108476e355
17 changed files with 1070 additions and 1286 deletions

1
.gitignore vendored
View file

@ -6,3 +6,4 @@ backend/data/recipes.sqlite-journal
style.css.map style.css.map
backend/static/style.css backend/static/style.css
backend/file.db backend/file.db
*.sqlite

2014
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -6,6 +6,8 @@ members = [
] ]
[profile.release] [profile.release]
strip = true
codegen-units = 1 codegen-units = 1
lto = true lto = true
# opt-level = 'z' # Optimize for size. # Tell `rustc` to optimize for small code size.
# opt-level = "s"

View file

@ -1,9 +1,30 @@
== Autoreload # Use cases
https://actix.rs/docs/autoreload/ ## Create a recipe
(A group is automatically created)
## Create a groupe
== Documentation ## Move a group
## Delete a group
## Create a step
## Move a step
## Delete a step
# Technical
## Useful URLs
* Rust patterns : https://github.com/rust-unofficial/patterns/tree/master/patterns * Rust patterns : https://github.com/rust-unofficial/patterns/tree/master/patterns
* Rusqlite (SQLite) : https://docs.rs/rusqlite/0.20.0/rusqlite/ * Rusqlite (SQLite) : https://docs.rs/rusqlite/0.20.0/rusqlite/
## How to develop
Autoreload: https://actix.rs/docs/autoreload/

View file

@ -1,5 +1,6 @@
* (WIP) Enable Logging to file. * (WIP) Enable Logging to file.
* Define the recipes model (UML). * Describe the use cases.
* Define the recipes model (SYSML).
* Implement the model as relational with SQLite. * Implement the model as relational with SQLite.
* Create and update functions. * Create and update functions.
* Define the UI (mockups). * Define the UI (mockups).

View file

@ -2,29 +2,26 @@
name = "recipes" name = "recipes"
version = "1.0.0" version = "1.0.0"
authors = ["Grégory Burri <greg.burri@gmail.com>"] authors = ["Grégory Burri <greg.burri@gmail.com>"]
edition = "2018" edition = "2021"
[dependencies] [dependencies]
actix-web = "2" actix-web = "4"
actix-rt = "1" actix-files = "0.6"
actix-files = "0.2" serde = { version = "1.0", features = [ "derive" ] }
serde = { version = "1.0", features = ["derive"] }
listenfd = "0.3" # To watch file modifications and automatically launch a build process (only used in dev/debug).
ron = "0.6" # Rust object notation, to load configuration files.
itertools = "0.9"
r2d2_sqlite = "0.16" # Connection pool with rusqlite (SQLite access). ron = "0.8" # Rust object notation, to load configuration files.
itertools = "0.10"
env_logger = "0.9"
r2d2_sqlite = "0.21" # Connection pool with rusqlite (SQLite access).
r2d2 = "0.8" r2d2 = "0.8"
futures = "0.3" # Needed by askam with the feature 'with-actix-web'. futures = "0.3" # Needed by askam with the feature 'with-actix-web'.
common = { path = "../common" } common = { path = "../common" }
[dependencies.rusqlite] askama = { version = "0.11", features = ["with-actix-web", "mime", "mime_guess"] }
version = "0.23" askama_actix = "0.13"
features = ["bundled"]
# Template system. rusqlite = { version = "0.28", features = ["bundled"] }
[dependencies.askama]
version = "0.9"
features = ["with-actix-web"]

View file

@ -5,21 +5,37 @@ What is build here:
- Compile the SASS file to CSS file. - Compile the SASS file to CSS file.
*/ */
use std::process::Command; use std::{ env, process::{ Command, Output }, path::Path };
fn exists_in_path<P>(filename: P) -> bool
where P: AsRef<Path> {
for path in env::split_paths(&env::var_os("PATH").unwrap()) {
if path.join(&filename).is_file() { return true; }
}
false
}
fn main() { fn main() {
println!("cargo:rerun-if-changed=style.scss"); println!("cargo:rerun-if-changed=style.scss");
let output = fn run_sass(command: &mut Command) -> Output {
Command::new("sass") command
.arg("./style.scss") .arg("style.scss")
.arg("./static/style.css") .arg("static/style.css")
.output() .output()
.expect("Unable to compile SASS file, install SASS, see https://sass-lang.com/"); .expect("Unable to compile SASS file, install SASS, see https://sass-lang.com/")
}
let output =
if exists_in_path("sass.bat") {
run_sass(Command::new("cmd").args(&["/C", "sass.bat"]))
} else {
run_sass(&mut Command::new("sass"))
};
if !output.status.success() { if !output.status.success() {
//panic!("Unable to compile SASS file, install SASS, see https://sass-lang.com/") // SASS will put the error in the file.
let error = std::fs::read_to_string("./static/style.css").expect("unable to read style.css"); let error = std::fs::read_to_string("./static/style.css").expect("unable to read style.css");
panic!(error); panic!("{}", error);
} }
} }

View file

@ -1,2 +1,2 @@
# To launch RUP and watching source. See https://actix.rs/docs/autoreload/. # To launch RUP and watching source. See https://actix.rs/docs/autoreload/.
systemfd --no-pid -s http::8082 -- cargo watch -x run cargo watch -x run

64
backend/sql/version_1.sql Normal file
View file

@ -0,0 +1,64 @@
-- Version 1 is the initial structure.
CREATE TABLE Version (
id INTEGER PRIMARY KEY,
version INTEGER NOT NULL UNIQUE,
datetime DATETIME
);
CREATE TABLE User (
id INTEGER PRIMARY KEY,
email TEXT NOT NULL,
password TEXT NOT NULL, -- Hashed and salted.
name TEXT NOT NULL
);
CREATE TABLE Recipe (
id INTEGER PRIMARY KEY,
user_id INTEGER NOT NULL,
title TEXT NOT NULL,
estimate_time INTEGER,
description DATETIME,
FOREIGN KEY(user_id) REFERENCES User(id)
);
CREATE TABLE Quantity (
id INTEGER PRIMARY KEY,
value REAL,
unit TEXT
);
CREATE TABLE Ingredient (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
quantity_id INTEGER,
input_step_id INTEGER NOT NULL,
FOREIGN KEY(quantity_id) REFERENCES Quantity(id),
FOREIGN KEY(input_step_id) REFERENCES Step(id)
);
CREATE TABLE [Group] (
id INTEGER PRIMARY KEY,
name TEXT
);
CREATE TABLE Step (
id INTEGER PRIMARY KEY,
action TEXT NOT NULL,
group_id INTEGER NOT NULL,
FOREIGN KEY(group_id) REFERENCES [Group](id)
);
CREATE TABLE IntermediateSubstance (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
quantity_id INTEGER,
output_step_id INTEGER NOT NULL,
input_step_id INTEGER NOT NULL,
FOREIGN KEY(quantity_id) REFERENCES Quantity(id),
FOREIGN KEY(output_step_id) REFERENCES Step(id),
FOREIGN KEY(input_step_id) REFERENCES Step(id)
);

View file

@ -1,3 +1,4 @@
pub static FILE_CONF: &str = "conf.ron"; pub const FILE_CONF: &str = "conf.ron";
pub static DB_DIRECTORY: &str = "data"; pub const DB_DIRECTORY: &str = "data";
pub static DB_FILENAME: &str = "recipes.sqlite"; pub const DB_FILENAME: &str = "data/recipes.sqlite";
pub const SQL_FILENAME: &str = "sql/version_{VERSION}.sql";

View file

@ -1,27 +1,24 @@
use std::path::Path; use crate::consts::SQL_FILENAME;
use std::fs;
//use rusqlite::types::ToSql;
//use rusqlite::{Connection, Result, NO_PARAMS};
//extern crate r2d2;
//extern crate r2d2_sqlite;
//extern crate rusqlite;
use r2d2_sqlite::SqliteConnectionManager;
use r2d2::Pool;
#[derive(Debug)]
pub enum DbError {
SqliteError(rusqlite::Error),
R2d2Error(r2d2::Error),
UnsupportedVersion(i32),
}
use super::consts; use super::consts;
use std::{fs::{self, File}, path::Path, io::Read};
//use rusqlite::types::ToSql;
//use rusqlite::{Connection, Result, NO_PARAMS};
use r2d2::Pool;
use r2d2_sqlite::SqliteConnectionManager;
const CURRENT_DB_VERSION: u32 = 1; const CURRENT_DB_VERSION: u32 = 1;
#[derive(Debug)]
pub enum DBError {
SqliteError(rusqlite::Error),
R2d2Error(r2d2::Error),
UnsupportedVersion(u32),
Other(String),
}
pub struct Connection { pub struct Connection {
//con: rusqlite::Connection //con: rusqlite::Connection
pool: Pool<SqliteConnectionManager> pool: Pool<SqliteConnectionManager>
@ -32,20 +29,20 @@ pub struct Recipe {
pub id: i32, pub id: i32,
} }
impl std::convert::From<rusqlite::Error> for DbError { impl std::convert::From<rusqlite::Error> for DBError {
fn from(error: rusqlite::Error) -> Self { fn from(error: rusqlite::Error) -> Self {
DbError::SqliteError(error) DBError::SqliteError(error)
} }
} }
impl std::convert::From<r2d2::Error> for DbError { impl std::convert::From<r2d2::Error> for DBError {
fn from(error: r2d2::Error) -> Self { fn from(error: r2d2::Error) -> Self {
DbError::R2d2Error(error) DBError::R2d2Error(error)
} }
} }
impl Connection { impl Connection {
pub fn new() -> Result<Connection, DbError> { pub fn new() -> Result<Connection, DBError> {
let data_dir = Path::new(consts::DB_DIRECTORY); let data_dir = Path::new(consts::DB_DIRECTORY);
@ -53,7 +50,7 @@ impl Connection {
fs::DirBuilder::new().create(data_dir).unwrap(); fs::DirBuilder::new().create(data_dir).unwrap();
} }
let manager = SqliteConnectionManager::file("file.db"); let manager = SqliteConnectionManager::file(consts::DB_FILENAME);
let pool = r2d2::Pool::new(manager).unwrap(); let pool = r2d2::Pool::new(manager).unwrap();
let connection = Connection { pool }; let connection = Connection { pool };
@ -65,7 +62,7 @@ impl Connection {
* Called after the connection has been established for creating or updating the database. * Called after the connection has been established for creating or updating the database.
* The 'Version' table tracks the current state of the database. * The 'Version' table tracks the current state of the database.
*/ */
fn create_or_update(self: &Self) -> Result<(), DbError> { fn create_or_update(self: &Self) -> Result<(), DBError> {
// let connection = Connection::new(); // let connection = Connection::new();
// let mut stmt = connection.sqlite_con.prepare("SELECT * FROM versions ORDER BY date").unwrap(); // let mut stmt = connection.sqlite_con.prepare("SELECT * FROM versions ORDER BY date").unwrap();
// let mut stmt = connection.sqlite_con.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='versions'").unwrap(); // let mut stmt = connection.sqlite_con.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='versions'").unwrap();
@ -74,13 +71,14 @@ impl Connection {
let mut con = self.pool.get()?; let mut con = self.pool.get()?;
let tx = con.transaction()?; let tx = con.transaction()?;
// Version 0 corresponds to an empty database.
let mut version = { let mut version = {
match tx.query_row( match tx.query_row(
"SELECT [name] FROM [sqlite_master] WHERE [type] = 'table' AND [name] = 'Version'", "SELECT [name] FROM [sqlite_master] WHERE [type] = 'table' AND [name] = 'Version'",
rusqlite::NO_PARAMS, [],
|row| row.get::<usize, String>(0) |row| row.get::<usize, String>(0)
) { ) {
Ok(_) => tx.query_row("SELECT [version] FROM [Version]", rusqlite::NO_PARAMS, |row| row.get(0)).unwrap_or_default(), Ok(_) => tx.query_row("SELECT [version] FROM [Version]", [], |row| row.get(0)).unwrap_or_default(),
Err(_) => 0 Err(_) => 0
} }
}; };
@ -94,45 +92,33 @@ impl Connection {
Ok(()) Ok(())
} }
fn update_to_next_version(version: i32, tx: &rusqlite::Transaction) -> Result<bool, DbError> { fn update_to_next_version(current_version: u32, tx: &rusqlite::Transaction) -> Result<bool, DBError> {
match version { let next_version = current_version + 1;
0 => {
println!("Update to version 1...");
// Initial structure. if next_version <= CURRENT_DB_VERSION {
tx.execute_batch( println!("Update to version {}...", next_version);
" }
CREATE TABLE [Version] (
[id] INTEGER PRIMARY KEY,
[version] INTEGER NOT NULL UNIQUE,
[datetime] INTEGER DATETIME
);
CREATE TABLE [Recipe] ( fn ok(updated: bool) -> Result<bool, DBError> {
[id] INTEGER PRIMARY KEY, if updated {
[title] INTEGER NOT NULL, println!("Version updated");
[description] INTEGER DATETIME }
); Ok(updated)
" }
)?;
/* match next_version {
tx.execute( 1 => {
" tx.execute_batch(&load_sql_file(next_version)?)?;
INSERT INTO Version
",
rusqlite::NO_PARAMS
);*/
Ok(true) ok(true)
} }
// Current version. // Version 1 doesn't exist yet.
1 => 2 =>
Ok(false), ok(false),
v => v =>
Err(DbError::UnsupportedVersion(v)), Err(DBError::UnsupportedVersion(v)),
} }
} }
@ -140,3 +126,11 @@ impl Connection {
} }
} }
fn load_sql_file(version: u32) -> Result<String, DBError> {
let sql_file = SQL_FILENAME.replace("{VERSION}", &version.to_string());
let mut file = File::open(&sql_file).map_err(|err| DBError::Other(format!("Cannot open SQL file ({}): {}", &sql_file, err.to_string())))?;
let mut sql = String::new();
file.read_to_string(&mut sql).map_err(|err| DBError::Other(format!("Cannot read SQL file ({}) : {}", &sql_file, err.to_string())))?;
Ok(sql)
}

View file

@ -4,14 +4,14 @@ use std::{fs::File, env::args};
use actix_files as fs; use actix_files as fs;
use actix_web::{get, web, Responder, middleware, App, HttpServer, HttpResponse, HttpRequest, web::Query}; use actix_web::{get, web, Responder, middleware, App, HttpServer, HttpResponse, HttpRequest, web::Query};
use askama::Template; use askama_actix::Template;
use listenfd::ListenFd;
use ron::de::from_reader; use ron::de::from_reader;
use serde::Deserialize; use serde::Deserialize;
use itertools::Itertools; use itertools::Itertools;
mod consts; mod consts;
mod model;
mod db; mod db;
#[derive(Template)] #[derive(Template)]
@ -53,10 +53,13 @@ fn get_exe_name() -> String {
first_arg[first_arg.rfind(sep).unwrap()+1..].to_string() first_arg[first_arg.rfind(sep).unwrap()+1..].to_string()
} }
#[actix_rt::main] #[actix_web::main]
async fn main() -> std::io::Result<()> { async fn main() -> std::io::Result<()> {
if process_args() { return Ok(()) } if process_args() { return Ok(()) }
std::env::set_var("RUST_LOG", "actix_web=debug");
env_logger::init();
println!("Starting Recipes as web server..."); println!("Starting Recipes as web server...");
let config: Config = { let config: Config = {
@ -73,11 +76,11 @@ async fn main() -> std::io::Result<()> {
std::env::set_var("RUST_LOG", "actix_web=info"); std::env::set_var("RUST_LOG", "actix_web=info");
let mut listenfd = ListenFd::from_env();
let mut server = let mut server =
HttpServer::new( HttpServer::new(
|| { || {
App::new() App::new()
.wrap(middleware::Logger::default())
.wrap(middleware::Compress::default()) .wrap(middleware::Compress::default())
.service(home_page) .service(home_page)
.service(view_page) .service(view_page)
@ -85,12 +88,7 @@ async fn main() -> std::io::Result<()> {
} }
); );
server = server = server.bind(&format!("0.0.0.0:{}", config.port)).unwrap();
if let Some(l) = listenfd.take_tcp_listener(0).unwrap() {
server.listen(l).unwrap()
} else {
server.bind(&format!("0.0.0.0:{}", config.port)).unwrap()
};
server.run().await server.run().await
} }

View file

@ -1,10 +1,14 @@
struct Recipe { struct Recipe {
ingredients: Vec<Ingredient>, title: String,
process: Vec<Group>, estimate_time: Option<i32>, // [min].
difficulty: Option<Difficulty>,
//ingredients: Vec<Ingredient>, // For four people.
process: Vec<Group>,
} }
struct Ingredient { struct Ingredient {
quantity: Quantity, quantity: Option<Quantity>,
name: String, name: String,
} }
@ -12,15 +16,16 @@ struct Quantity {
value: f32, value: f32,
unit: String, unit: String,
} }
struct Group { struct Group {
name: String, name: Option<String>,
steps: Vec<Step>, steps: Vec<Step>,
} }
struct Step { struct Step {
action: String, action: String,
input: Vec<StepInput>, input: Vec<StepInput>,
output: Vec<IntermediateSubstance>, output: Vec<IntermediateSubstance>,
} }
struct IntermediateSubstance { struct IntermediateSubstance {
@ -30,5 +35,12 @@ struct IntermediateSubstance {
enum StepInput { enum StepInput {
Ingredient(Ingredient), Ingredient(Ingredient),
IntermediateSubstance, IntermediateSubstance(IntermediateSubstance),
}
enum Difficulty {
Unknown,
Easy,
Medium,
Hard,
} }

View file

@ -8,10 +8,10 @@
</head> </head>
<body> <body>
<div class="header-container"><h1><a href="/">RECIPES</a></h1></div> <div class="header-container"><h1><a href="/">~ RECIPES ~</a></h1></div>
<div class="main-container"> <div class="main-container">
{% block main_container %}{% endblock %} {% block main_container %}{% endblock %}
</div> </div>
<div class="footer-container">gburri - 2020</div> <div class="footer-container">gburri - 2022</div>
</body> </body>
</html> </html>

View file

@ -1,5 +1,7 @@
{% extends "base_with_list.html" %} {% extends "base_with_list.html" %}
{% block content %} {% block content %}
HOME
*** HOME - PUT SOMETHING HERE ***
{% endblock %} {% endblock %}

View file

@ -2,7 +2,7 @@
name = "common" name = "common"
version = "0.1.0" version = "0.1.0"
authors = ["Grégory Burri <greg.burri@gmail.com>"] authors = ["Grégory Burri <greg.burri@gmail.com>"]
edition = "2018" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

View file

@ -38,8 +38,9 @@ Copy-SSH -source "./target/arm-unknown-linux-gnueabihf/release/recipes" -destina
Invoke-SSH "rm -rf recipes/static" Invoke-SSH "rm -rf recipes/static"
Copy-SSH -source "./backend/static/" -destination "~/recipes/" Copy-SSH -source "./backend/static/" -destination "~/recipes/"
Copy-SSH -source "./backend/sql/" -destination "~/recipes/"
Invoke-SSH "chmod u+x recipes/recipes" Invoke-SSH "chmod u+x recipes/recipes"
Invoke-SSH "strip recipes/recipes"
Invoke-SSH "sudo systemctl start recipes" Invoke-SSH "sudo systemctl start recipes"
Write-Output "Deployment finished" Write-Output "Deployment finished"