Service for editing/creating recipe
authorGreg Burri <greg.burri@gmail.com>
Thu, 15 Dec 2022 00:13:57 +0000 (01:13 +0100)
committerGreg Burri <greg.burri@gmail.com>
Thu, 15 Dec 2022 00:13:57 +0000 (01:13 +0100)
Other stuff...

15 files changed:
.gitignore
backend/sql/data_test.sql
backend/sql/version_1.sql
backend/src/data/asynchronous.rs
backend/src/data/db.rs
backend/src/main.rs
backend/src/model.rs
backend/src/services.rs
backend/src/services/api.rs
backend/src/user.rs [deleted file]
backend/templates/base_with_header.html
backend/templates/edit_recipe.html
backend/templates/view_recipe.html
common/src/ron_api.rs
frontend/src/lib.rs

index 8d38471..deef046 100644 (file)
@@ -1,7 +1,6 @@
 target
 **/*.rs.bk
-backend/data/recipes.sqlite
-backend/data/recipes.sqlite-journal
+backend/data
 /deploy-to-pi.nu
 style.css.map
 backend/static/style.css
index 890baa5..72ccb3d 100644 (file)
@@ -2,7 +2,17 @@ INSERT INTO [User] ([id], [email], [name], [password], [creation_datetime], [val
 VALUES (
     1,
     'paul@atreides.com',
-    'paul',
+    'Paul',
+    '$argon2id$v=19$m=4096,t=3,p=1$1vtXcacYjUHZxMrN6b2Xng$wW8Z59MIoMcsIljnjHmxn3EBcc5ymEySZPUVXHlRxcY',
+    0,
+    NULL
+);
+
+INSERT INTO [User] ([id], [email], [name], [password], [creation_datetime], [validation_token])
+VALUES (
+    2,
+    'alia@atreides.com',
+    'Alia',
     '$argon2id$v=19$m=4096,t=3,p=1$1vtXcacYjUHZxMrN6b2Xng$wW8Z59MIoMcsIljnjHmxn3EBcc5ymEySZPUVXHlRxcY',
     0,
     NULL
@@ -16,3 +26,6 @@ VALUES (1, 'Gratin de thon aux olives');
 
 INSERT INTO [Recipe] ([user_id], [title])
 VALUES (1, 'Saumon en croute');
+
+INSERT INTO [Recipe] ([user_id], [title])
+VALUES (2, 'Ouiche lorraine');
index c3488db..b1f231f 100644 (file)
@@ -2,70 +2,74 @@
 CREATE TABLE [Version] (
     [id] INTEGER PRIMARY KEY,
     [version] INTEGER NOT NULL UNIQUE,
-    [datetime] DATETIME
-);
+    [datetime] TEXT
+) STRICT;
 
 CREATE TABLE [User] (
     [id] INTEGER PRIMARY KEY,
     [email] TEXT NOT NULL,
-    [name] TEXT,
+    [name] TEXT NOT NULL DEFAULT '',
     [default_servings] INTEGER DEFAULT 4,
 
     [password] TEXT NOT NULL, -- argon2(password_plain, salt).
 
-    [creation_datetime] DATETIME NOT NULL, -- Updated when the validation email is sent.
+    [creation_datetime] TEXT NOT NULL, -- Updated when the validation email is sent.
     [validation_token] TEXT, -- If not null then the user has not validated his account yet.
 
     [is_admin] INTEGER NOT NULL DEFAULT FALSE
-);
+) STRICT;
 
-CREATE UNIQUE INDEX [User_email_index] ON [User] ([email]);
+CREATE UNIQUE INDEX [User_email_index] ON [User]([email]);
 
 CREATE TABLE [UserLoginToken] (
     [id] INTEGER PRIMARY KEY,
     [user_id] INTEGER NOT NULL,
-    [last_login_datetime] DATETIME,
-    [token] TEXT NOT NULL, --  24 alphanumeric character token. Can be stored in a cookie to be able to authenticate without a password.
+    [last_login_datetime] TEXT,
+
+    -- 24 alphanumeric character token.
+    -- Can be stored in a cookie to be able to authenticate without a password.
+    [token] TEXT NOT NULL,
 
     [ip] TEXT, -- Can be ipv4 or ipv6
     [user_agent] TEXT,
 
     FOREIGN KEY([user_id]) REFERENCES [User]([id]) ON DELETE CASCADE
-);
+) STRICT;
 
-CREATE INDEX [UserLoginToken_token_index] ON [UserLoginToken] ([token]);
+CREATE INDEX [UserLoginToken_token_index] ON [UserLoginToken]([token]);
 
 CREATE TABLE [Recipe] (
     [id] INTEGER PRIMARY KEY,
     [user_id] INTEGER, -- Can be null if a user is deleted.
     [title] TEXT NOT NULL,
     [estimate_time] INTEGER,
-    [description] TEXT,
+    [description] TEXT NOT NULL DEFAULT '',
+    [difficulty] INTEGER NOT NULL DEFAULT 0,
     [servings] INTEGER DEFAULT 4,
     [is_published] INTEGER NOT NULL DEFAULT FALSE,
 
     FOREIGN KEY([user_id]) REFERENCES [User]([id]) ON DELETE SET NULL
-);
+) STRICT;
 
 CREATE TABLE [Image] (
     [Id] INTEGER PRIMARY KEY,
     [recipe_id] INTEGER NOT NULL,
-    [name] TEXT,
-    [description] TEXT,
+    [name] TEXT NOT NULL DEFAULT '',
+    [description] TEXT NOT NULL DEFAULT '',
     [image] BLOB,
 
     FOREIGN KEY([recipe_id]) REFERENCES [Recipe]([id]) ON DELETE CASCADE
-);
+) STRICT;
 
 CREATE TABLE [RecipeTag] (
     [id] INTEGER PRIMARY KEY,
 
     [recipe_id] INTEGER NOT NULL,
-    [tag_id] INTEGER NO NULL,
+    [tag_id] INTEGER NOT NULL,
 
     FOREIGN KEY([recipe_id]) REFERENCES [Recipe]([id]) ON DELETE CASCADE,
     FOREIGN KEY([tag_id]) REFERENCES [Tag]([id]) ON DELETE CASCADE
-);
+) STRICT;
 
 CREATE TABLE [Tag] (
     [id] INTEGER PRIMARY KEY,
@@ -73,56 +77,50 @@ CREATE TABLE [Tag] (
     [name] TEXT NOT NULL,
 
     FOREIGN KEY([recipe_tag_id]) REFERENCES [RecipeTag]([id]) ON DELETE SET NULL
-);
+) STRICT;
 
 CREATE UNIQUE INDEX [Tag_name_index] ON [Tag] ([name]);
 
-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,
+    [quantity_value] REAL,
+    [quantity_unit] TEXT NOT NULL DEFAULT '',
+    [input_group_id] INTEGER NOT NULL,
 
-    FOREIGN KEY([quantity_id]) REFERENCES Quantity([id]) ON DELETE CASCADE,
-    FOREIGN KEY([input_step_id]) REFERENCES Step([id]) ON DELETE CASCADE
-);
+    FOREIGN KEY([input_group_id]) REFERENCES [Group]([id]) ON DELETE CASCADE
+) STRICT;
 
 CREATE TABLE [Group] (
     [id] INTEGER PRIMARY KEY,
     [order] INTEGER NOT NULL DEFAULT 0,
     [recipe_id] INTEGER,
-    name TEXT,
+    [name] TEXT NOT NULL DEFAULT '',
 
     FOREIGN KEY([recipe_id]) REFERENCES [Recipe]([id]) ON DELETE CASCADE
-);
+) STRICT;
 
-CREATE INDEX [Group_order_index] ON [Group] ([order]);
+CREATE INDEX [Group_order_index] ON [Group]([order]);
 
 CREATE TABLE [Step] (
     [id] INTEGER PRIMARY KEY,
     [order] INTEGER NOT NULL DEFAULT 0,
-    [action] TEXT NOT NULL,
+    [action] TEXT NOT NULL DEFAULT '',
     [group_id] INTEGER NOT NULL,
 
     FOREIGN KEY(group_id) REFERENCES [Group](id) ON DELETE CASCADE
-);
+) STRICT;
 
-CREATE INDEX [Step_order_index] ON [Group] ([order]);
+CREATE INDEX [Step_order_index] ON [Group]([order]);
 
 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]) ON DELETE CASCADE,
-    FOREIGN KEY([output_step_id]) REFERENCES [Step]([id]) ON DELETE CASCADE,
-    FOREIGN KEY([input_step_id]) REFERENCES [Step]([id]) ON DELETE CASCADE
-);
+    [name] TEXT NOT NULL DEFAULT '',
+    [quantity_value] REAL,
+    [quantity_unit] TEXT NOT NULL DEFAULT '',
+    [output_group_id] INTEGER NOT NULL,
+    [input_group_id] INTEGER NOT NULL,
+
+    FOREIGN KEY([output_group_id]) REFERENCES [group]([id]) ON DELETE CASCADE,
+    FOREIGN KEY([input_group_id]) REFERENCES [group]([id]) ON DELETE CASCADE
+) STRICT;
index 7ef77b8..5d27388 100644 (file)
@@ -7,7 +7,6 @@ use chrono::{prelude::*, Duration};
 
 use super::db::*;
 use crate::model;
-use crate::user::User;
 
 #[derive(Debug)]
 pub enum DBAsyncError {
@@ -65,7 +64,7 @@ impl Connection {
         )
     }
 
-    pub async fn load_user_async(&self, user_id: i64) -> Result<User> {
+    pub async fn load_user_async(&self, user_id: i64) -> Result<model::User> {
         let self_copy = self.clone();
         combine_errors(
             web::block(move || self_copy.load_user(user_id).map_err(DBAsyncError::from)).await,
index 10b6f31..0358d20 100644 (file)
@@ -12,10 +12,11 @@ use r2d2_sqlite::SqliteConnectionManager;
 use rand::distributions::{Alphanumeric, DistString};
 use rusqlite::{named_params, params, OptionalExtension, Params};
 
-use crate::hash::{hash, verify_password};
-use crate::model;
-use crate::user::*;
-use crate::{consts, user};
+use crate::{
+    hash::{hash, verify_password},
+    model,
+    consts,
+};
 
 const CURRENT_DB_VERSION: u32 = 1;
 
@@ -221,11 +222,12 @@ impl Connection {
     pub fn get_recipe(&self, id: i64) -> Result<model::Recipe> {
         let con = self.get()?;
         con.query_row(
-            "SELECT [id], [title], [description] FROM [Recipe] WHERE [id] = ?1",
+            "SELECT [id], [user_id], [title], [description] FROM [Recipe] WHERE [id] = ?1",
             [id],
             |row| {
                 Ok(model::Recipe::new(
                     row.get("id")?,
+                    row.get("user_id")?,
                     row.get("title")?,
                     row.get("description")?,
                 ))
@@ -234,10 +236,10 @@ impl Connection {
         .map_err(DBError::from)
     }
 
-    pub fn get_user_login_info(&self, token: &str) -> Result<UserLoginInfo> {
+    pub fn get_user_login_info(&self, token: &str) -> Result<model::UserLoginInfo> {
         let con = self.get()?;
         con.query_row("SELECT [last_login_datetime], [ip], [user_agent] FROM [UserLoginToken] WHERE [token] = ?1", [token], |r| {
-            Ok(UserLoginInfo {
+            Ok(model::UserLoginInfo {
                 last_login_datetime: r.get("last_login_datetime")?,
                 ip: r.get("ip")?,
                 user_agent: r.get("user_agent")?,
@@ -245,13 +247,14 @@ impl Connection {
         }).map_err(DBError::from)
     }
 
-    pub fn load_user(&self, user_id: i64) -> Result<User> {
+    pub fn load_user(&self, user_id: i64) -> Result<model::User> {
         let con = self.get()?;
         con.query_row(
             "SELECT [email] FROM [User] WHERE [id] = ?1",
             [user_id],
             |r| {
-                Ok(User {
+                Ok(model::User {
+                    id: user_id,
                     email: r.get("email")?,
                 })
             },
@@ -290,13 +293,23 @@ impl Connection {
                 }
                 let token = generate_token();
                 let hashed_password = hash(password).map_err(|e| DBError::from_dyn_error(e))?;
-                tx.execute("UPDATE [User] SET [validation_token] = ?2, [creation_datetime] = ?3, [password] = ?4 WHERE [id] = ?1", params![id, token, datetime, hashed_password])?;
+                tx.execute(
+                    "UPDATE [User]
+                    SET [validation_token] = ?2, [creation_datetime] = ?3, [password] = ?4
+                    WHERE [id] = ?1",
+                    params![id, token, datetime, hashed_password],
+                )?;
                 token
             }
             None => {
                 let token = generate_token();
                 let hashed_password = hash(password).map_err(|e| DBError::from_dyn_error(e))?;
-                tx.execute("INSERT INTO [User] ([email], [validation_token], [creation_datetime], [password]) VALUES (?1, ?2, ?3, ?4)", params![email, token, datetime, hashed_password])?;
+                tx.execute(
+                    "INSERT INTO [User]
+                    ([email], [validation_token], [creation_datetime], [password])
+                    VALUES (?1, ?2, ?3, ?4)",
+                    params![email, token, datetime, hashed_password],
+                )?;
                 token
             }
         };
@@ -400,7 +413,12 @@ impl Connection {
             .optional()?
         {
             Some((login_id, user_id)) => {
-                tx.execute("UPDATE [UserLoginToken] SET [last_login_datetime] = ?2, [ip] = ?3, [user_agent] = ?4 WHERE [id] = ?1", params![login_id, Utc::now(), ip, user_agent])?;
+                tx.execute(
+                    "UPDATE [UserLoginToken]
+                    SET [last_login_datetime] = ?2, [ip] = ?3, [user_agent] = ?4
+                    WHERE [id] = ?1",
+                    params![login_id, Utc::now(), ip, user_agent],
+                )?;
                 tx.commit()?;
                 Ok(AuthenticationResult::Ok(user_id))
             }
@@ -435,21 +453,27 @@ impl Connection {
         let con = self.get()?;
 
         // Verify if an empty recipe already exists. Returns its id if one exists.
-        match con.query_row(
-            "SELECT [Recipe].[id] FROM [Recipe]
+        match con
+            .query_row(
+                "SELECT [Recipe].[id] FROM [Recipe]
             INNER JOIN [Image] ON [Image].[recipe_id] = [Recipe].[id]
             INNER JOIN [Group] ON [Group].[recipe_id] = [Recipe].[id]
-            WHERE [Recipe].[user_id] = ?1 AND [Recipe].[estimate_time] = NULL AND [Recipe].[description] = NULL",
-            [user_id],
-            |r| {
-                Ok(r.get::<&str, i64>("id")?)
-            }
-        ).optional()? {
+            WHERE [Recipe].[user_id] = ?1
+                AND [Recipe].[estimate_time] = NULL
+                AND [Recipe].[description] = NULL",
+                [user_id],
+                |r| Ok(r.get::<&str, i64>("id")?),
+            )
+            .optional()?
+        {
             Some(recipe_id) => Ok(recipe_id),
             None => {
-                con.execute("INSERT INTO [Recipe] ([user_id], [title]) VALUES (?1, '')", [user_id])?;
+                con.execute(
+                    "INSERT INTO [Recipe] ([user_id], [title]) VALUES (?1, '')",
+                    [user_id],
+                )?;
                 Ok(con.last_insert_rowid())
-            },
+            }
         }
     }
 
@@ -495,7 +519,12 @@ impl Connection {
         user_agent: &str,
     ) -> Result<String> {
         let token = generate_token();
-        tx.execute("INSERT INTO [UserLoginToken] ([user_id], [last_login_datetime], [token], [ip], [user_agent]) VALUES (?1, ?2, ?3, ?4, ?5)", params![user_id, Utc::now(), token, ip, user_agent])?;
+        tx.execute(
+            "INSERT INTO [UserLoginToken]
+            ([user_id], [last_login_datetime], [token], [ip], [user_agent])
+            VALUES (?1, ?2, ?3, ?4, ?5)",
+            params![user_id, Utc::now(), token, ip, user_agent],
+        )?;
         Ok(token)
     }
 }
@@ -542,7 +571,8 @@ mod tests {
     fn sign_up_to_an_already_existing_user() -> Result<()> {
         let connection = Connection::new_in_memory()?;
         connection.execute_sql("
-            INSERT INTO [User] ([id], [email], [name], [password], [creation_datetime], [validation_token])
+            INSERT INTO
+                [User] ([id], [email], [name], [password], [creation_datetime], [validation_token])
                 VALUES (
                     1,
                     'paul@atreides.com',
@@ -583,7 +613,8 @@ mod tests {
         let connection = Connection::new_in_memory()?;
         let token = generate_token();
         connection.execute_sql("
-            INSERT INTO [User] ([id], [email], [name], [password], [creation_datetime], [validation_token])
+            INSERT INTO
+                [User] ([id], [email], [name], [password], [creation_datetime], [validation_token])
                 VALUES (
                     1,
                     'paul@atreides.com',
@@ -794,7 +825,9 @@ mod tests {
         let connection = Connection::new_in_memory()?;
 
         connection.execute_sql(
-            "INSERT INTO [User] ([id], [email], [name], [password], [creation_datetime], [validation_token]) VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
+            "INSERT INTO [User]
+            ([id], [email], [name], [password], [creation_datetime], [validation_token])
+            VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
             params![
                 1,
                 "paul@atreides.com",
index 951fd78..74e0383 100644 (file)
@@ -14,7 +14,6 @@ mod email;
 mod hash;
 mod model;
 mod services;
-mod user;
 mod utils;
 
 #[actix_web::main]
index d0f95d2..0ed4825 100644 (file)
@@ -1,22 +1,41 @@
+use chrono::prelude::*;\r
+\r
+pub struct User {\r
+    pub id: i64,\r
+    pub email: String,\r
+}\r
+\r
+pub struct UserLoginInfo {\r
+    pub last_login_datetime: DateTime<Utc>,\r
+    pub ip: String,\r
+    pub user_agent: String,\r
+}\r
+\r
 pub struct Recipe {\r
     pub id: i64,\r
+    pub user_id: i64,\r
     pub title: String,\r
-    pub description: Option<String>,\r
+    pub description: String,\r
     pub estimate_time: Option<i32>, // [min].\r
-    pub difficulty: Option<Difficulty>,\r
+    pub difficulty: Difficulty,\r
 \r
     //ingredients: Vec<Ingredient>, // For four people.\r
     pub process: Vec<Group>,\r
 }\r
 \r
 impl Recipe {\r
-    pub fn new(id: i64, title: String, description: Option<String>) -> Recipe {\r
+    pub fn empty(id: i64, user_id: i64) -> Recipe {\r
+        Self::new(id, user_id, String::new(), String::new())\r
+    }\r
+\r
+    pub fn new(id: i64, user_id: i64, title: String, description: String) -> Recipe {\r
         Recipe {\r
             id,\r
+            user_id,\r
             title,\r
             description,\r
             estimate_time: None,\r
-            difficulty: None,\r
+            difficulty: Difficulty::Unknown,\r
             process: Vec::new(),\r
         }\r
     }\r
@@ -34,13 +53,13 @@ pub struct Quantity {
 \r
 pub struct Group {\r
     pub name: Option<String>,\r
+    pub input: Vec<StepInput>,\r
+    pub output: Vec<IntermediateSubstance>,\r
     pub steps: Vec<Step>,\r
 }\r
 \r
 pub struct Step {\r
     pub action: String,\r
-    pub input: Vec<StepInput>,\r
-    pub output: Vec<IntermediateSubstance>,\r
 }\r
 \r
 pub struct IntermediateSubstance {\r
index afa20a0..c283868 100644 (file)
@@ -11,13 +11,14 @@ use chrono::Duration;
 use log::{debug, error, info, log_enabled, Level};
 use serde::Deserialize;
 
-use crate::config::Config;
-use crate::consts;
-use crate::data::{asynchronous, db};
-use crate::email;
-use crate::model;
-use crate::user::User;
-use crate::utils;
+use crate::{
+    config::Config,
+    consts,
+    data::{asynchronous, db},
+    email,
+    model,
+    utils,
+};
 
 mod api;
 
@@ -45,7 +46,7 @@ fn get_ip_and_user_agent(req: &HttpRequest) -> (String, String) {
 async fn get_current_user(
     req: &HttpRequest,
     connection: web::Data<db::Connection>,
-) -> Option<User> {
+) -> Option<model::User> {
     let (client_ip, client_user_agent) = get_ip_and_user_agent(req);
 
     match req.cookie(consts::COOKIE_AUTH_TOKEN_NAME) {
@@ -149,7 +150,7 @@ impl actix_web::error::ResponseError for ServiceError {
 #[derive(Template)]
 #[template(path = "home.html")]
 struct HomeTemplate {
-    user: Option<User>,
+    user: Option<model::User>,
     recipes: Vec<(i64, String)>,
     current_recipe_id: Option<i64>,
 }
@@ -175,9 +176,10 @@ pub async fn home_page(
 #[derive(Template)]
 #[template(path = "view_recipe.html")]
 struct ViewRecipeTemplate {
-    user: Option<User>,
+    user: Option<model::User>,
     recipes: Vec<(i64, String)>,
     current_recipe_id: Option<i64>,
+
     current_recipe: model::Recipe,
 }
 
@@ -201,14 +203,15 @@ pub async fn view_recipe(
     .to_response())
 }
 
-///// EDIT RECIPE /////
+///// EDIT/NEW RECIPE /////
 
 #[derive(Template)]
 #[template(path = "edit_recipe.html")]
 struct EditRecipeTemplate {
-    user: Option<User>,
+    user: Option<model::User>,
     recipes: Vec<(i64, String)>,
     current_recipe_id: Option<i64>,
+
     current_recipe: model::Recipe,
 }
 
@@ -219,12 +222,28 @@ pub async fn edit_recipe(
     connection: web::Data<db::Connection>,
 ) -> Result<HttpResponse> {
     let (id,) = path.into_inner();
-    let user = get_current_user(&req, connection.clone()).await;
-    let recipes = connection.get_all_recipe_titles_async().await?;
+    let user = match get_current_user(&req, connection.clone()).await {
+        Some(u) => u,
+        None =>
+            return Ok(MessageTemplate {
+                user: None,
+                message: "Cannot edit a recipe without being logged in",
+            }.to_response())
+    };
+
     let recipe = connection.get_recipe_async(id).await?;
 
+    if recipe.user_id != user.id {
+        return Ok(MessageTemplate {
+            message: "Cannot edit a recipe you don't own",
+            user: Some(user)
+        }.to_response())
+    }
+
+    let recipes = connection.get_all_recipe_titles_async().await?;
+
     Ok(EditRecipeTemplate {
-        user,
+        user: Some(user),
         current_recipe_id: Some(recipe.id),
         recipes,
         current_recipe: recipe,
@@ -232,6 +251,34 @@ pub async fn edit_recipe(
     .to_response())
 }
 
+#[get("/recipe/new")]
+pub async fn new_recipe(
+    req: HttpRequest,
+    path: web::Path<(i64,)>,
+    connection: web::Data<db::Connection>,
+) -> Result<HttpResponse> {
+    let user = match get_current_user(&req, connection.clone()).await {
+        Some(u) => u,
+        None =>
+            return Ok(MessageTemplate {
+                message: "Cannot create a recipe without being logged in",
+                user: None
+            }.to_response())
+    };
+
+    let recipe_id = connection.create_recipe_async(user.id).await?;
+    let recipes = connection.get_all_recipe_titles_async().await?;
+    let user_id = user.id;
+
+    Ok(EditRecipeTemplate {
+        user: Some(user),
+        current_recipe_id: Some(recipe_id),
+        recipes,
+        current_recipe: model::Recipe::empty(recipe_id, user_id),
+    }
+    .to_response())
+}
+
 ///// MESSAGE /////
 
 #[derive(Template)]
@@ -243,7 +290,7 @@ struct MessageBaseTemplate<'a> {
 #[derive(Template)]
 #[template(path = "message.html")]
 struct MessageTemplate<'a> {
-    user: Option<User>,
+    user: Option<model::User>,
     message: &'a str,
 }
 
@@ -252,7 +299,7 @@ struct MessageTemplate<'a> {
 #[derive(Template)]
 #[template(path = "sign_up_form.html")]
 struct SignUpFormTemplate {
-    user: Option<User>,
+    user: Option<model::User>,
     email: String,
     message: String,
     message_email: String,
@@ -300,7 +347,7 @@ pub async fn sign_up_post(
     fn error_response(
         error: SignUpError,
         form: &web::Form<SignUpFormData>,
-        user: Option<User>,
+        user: Option<model::User>,
     ) -> Result<HttpResponse> {
         Ok(SignUpFormTemplate {
             user,
@@ -486,7 +533,7 @@ pub async fn sign_up_validation(
 #[derive(Template)]
 #[template(path = "sign_in_form.html")]
 struct SignInFormTemplate {
-    user: Option<User>,
+    user: Option<model::User>,
     email: String,
     message: String,
 }
@@ -524,7 +571,7 @@ pub async fn sign_in_post(
     fn error_response(
         error: SignInError,
         form: &web::Form<SignInFormData>,
-        user: Option<User>,
+        user: Option<model::User>,
     ) -> Result<HttpResponse> {
         Ok(SignInFormTemplate {
             user,
index 88dc048..c72d96d 100644 (file)
@@ -1,22 +1,12 @@
 use actix_web::{
-    cookie::Cookie,
-    get,
     http::{header, header::ContentType, StatusCode},
     post, put, web, HttpMessage, HttpRequest, HttpResponse, Responder,
 };
-use chrono::Duration;
-use futures::TryFutureExt;
 use log::{debug, error, info, log_enabled, Level};
 use ron::de::from_bytes;
-use serde::Deserialize;
 
 use super::Result;
-use crate::config::Config;
-use crate::consts;
 use crate::data::{asynchronous, db};
-use crate::model;
-use crate::user::User;
-use crate::utils;
 
 #[put("/ron-api/recipe/set-title")]
 pub async fn set_recipe_title(
@@ -43,3 +33,15 @@ pub async fn set_recipe_description(
         .await?;
     Ok(HttpResponse::Ok().finish())
 }
+
+// #[put("/ron-api/recipe/add-image)]
+// #[put("/ron-api/recipe/rm-photo")]
+// #[put("/ron-api/recipe/add-ingredient")]
+// #[put("/ron-api/recipe/rm-ingredient")]
+// #[put("/ron-api/recipe/set-ingredients-order")]
+// #[put("/ron-api/recipe/add-group")]
+// #[put("/ron-api/recipe/rm-group")]
+// #[put("/ron-api/recipe/set-groups-order")]
+// #[put("/ron-api/recipe/add-step")]
+// #[put("/ron-api/recipe/rm-step")]
+// #[put("/ron-api/recipe/set-steps-order")]
diff --git a/backend/src/user.rs b/backend/src/user.rs
deleted file mode 100644 (file)
index 753d714..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-use chrono::prelude::*;
-
-pub struct User {
-    pub email: String,
-}
-
-pub struct UserLoginInfo {
-    pub last_login_datetime: DateTime<Utc>,
-    pub ip: String,
-    pub user_agent: String,
-}
index e8ed659..9cff88c 100644 (file)
@@ -4,10 +4,9 @@
     <div class="header-container">
         <a class="title" href="/">~~ Recettes de cuisine ~~</a>
 
-        <span class="create-recipe">Create a new recipe</span>
-
         {% match user %}
         {% when Some with (user) %}
+            <a class="create-recipe" href="/recipe/new" >Create a new recipe</a>
             <span>{{ user.email }} / <a href="/signout" />Sign out</a></span>
         {% when None %}
             <span><a href="/signin" >Sign in</a> / <a href="/signup">Sign up</a></span>
index 81d9f2c..5569e6a 100644 (file)
@@ -2,17 +2,14 @@
 
 {% block content %}
 
-<h2 class="recipe-title" >{{ current_recipe.title }}</h2>
-
 <label for="title_field">Title</label>
-<input id="title_field" type="text" name="title" value="{{ current_recipe.title }}" autocapitalize="none" autocomplete="title" autofocus="autofocus" />
-
-{% match current_recipe.description %}
-    {% when Some with (description) %}
-        <div class="recipe-description" >
-        {{ description|markdown }}
-        </div>
-    {% when None %}
-{% endmatch %}
+<input
+    id="title_field"
+    type="text"
+    name="title"
+    value="{{ current_recipe.title }}"
+    autocapitalize="none"
+    autocomplete="title"
+    autofocus="autofocus" />
 
 {% endblock %}
\ No newline at end of file
index 1a5ba52..aba5c69 100644 (file)
@@ -4,12 +4,15 @@
 
 <h2 class="recipe-title" >{{ current_recipe.title }}</h2>
 
-{% match current_recipe.description %}
-    {% when Some with (description) %}
-        <div class="recipe-description" >
-        {{ description|markdown }}
-        </div>
-    {% when None %}
-{% endmatch %}
+
+{% if user.is_some() && current_recipe.user_id == user.as_ref().unwrap().id %}
+    <a class="edit-recipe" href="/recipe/edit/{{ current_recipe.id }}" >Edit</a>
+{% endif %}
+
+{% if !current_recipe.description.is_empty() %}
+    <div class="recipe-description" >
+    {{ current_recipe.description.clone()|markdown }}
+    </div>
+{% endif %}
 
 {% endblock %}
\ No newline at end of file
index 84b1619..566e430 100644 (file)
@@ -1,14 +1,94 @@
 use ron::de::from_reader;
-use serde::Deserialize;
+use serde::{Deserialize, Serialize};
 
-#[derive(Deserialize, Clone)]
+#[derive(Serialize, Deserialize, Clone)]
 pub struct SetRecipeTitle {
     pub recipe_id: i64,
     pub title: String,
 }
 
-#[derive(Deserialize, Clone)]
+#[derive(Serialize, Deserialize, Clone)]
 pub struct SetRecipeDescription {
     pub recipe_id: i64,
     pub description: String,
 }
+
+#[derive(Serialize, Deserialize, Clone)]
+pub struct AddRecipeImage {
+    pub recipe_id: i64,
+    pub image: Vec<u8>,
+}
+
+#[derive(Serialize, Deserialize, Clone)]
+pub struct AddRecipeImageReply {
+    pub image_id: i64,
+}
+
+#[derive(Serialize, Deserialize, Clone)]
+pub struct RemoveRecipeImage {
+    pub image_id: i64,
+}
+
+#[derive(Serialize, Deserialize, Clone)]
+pub struct AddRecipeIngredient {
+    pub group_id: i64,
+    pub name: String,
+    pub quantity_value: Option<f64>,
+    pub quantity_unit: String,
+}
+
+#[derive(Serialize, Deserialize, Clone)]
+pub struct AddRecipeIngredientReply {
+    pub ingredient_id: i64,
+}
+
+#[derive(Serialize, Deserialize, Clone)]
+pub struct RemoveRecipeIngredient {
+    pub group_id: i64,
+}
+
+#[derive(Serialize, Deserialize, Clone)]
+pub struct SetRecipeIngredientsOrder {
+    pub group_id: i64,
+    pub ingredient_ids: Vec<i64>,
+}
+
+#[derive(Serialize, Deserialize, Clone)]
+pub struct AddRecipeGroup {
+    pub recipe_id: i64,
+    pub name: String,
+    pub quantity_value: Option<f64>,
+    pub quantity_unit: String,
+}
+
+#[derive(Serialize, Deserialize, Clone)]
+pub struct AddRecipeGroupReply {
+    pub group_id: i64,
+}
+
+#[derive(Serialize, Deserialize, Clone)]
+pub struct RemoveRecipeGroupReply {
+    pub group_id: i64,
+}
+
+#[derive(Serialize, Deserialize, Clone)]
+pub struct SetRecipeGroupsOrder {
+    pub recipe_id: i64,
+    pub group_ids: Vec<i64>,
+}
+
+#[derive(Serialize, Deserialize, Clone)]
+pub struct AddRecipeStep {
+    pub group_id: i64,
+    pub name: String,
+}
+
+#[derive(Serialize, Deserialize, Clone)]
+pub struct AddRecipeStepReply {
+    pub step_id: i64,
+}
+
+#[derive(Serialize, Deserialize, Clone)]
+pub struct RemoveRecipeStep {
+    pub step_id: i64,
+}
index aea6567..65fbac1 100644 (file)
@@ -33,7 +33,7 @@ pub fn main() -> Result<(), JsValue> {
     let path: Vec<&str> = location.split('/').skip(1).collect();
 
     /*
-     * Todo:
+     * TODO:
      * [ok] get url (/recipe/edit/{id}) and extract the id
      * - Add a handle (event?) to the title field (when edited/changed?):
      *  - Call (as AJAR) /ron-api/set-title and set the body to a serialized RON of the type common::ron_api::SetTitle