Other stuff...
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
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
INSERT INTO [Recipe] ([user_id], [title])
VALUES (1, 'Saumon en croute');
+
+INSERT INTO [Recipe] ([user_id], [title])
+VALUES (2, 'Ouiche lorraine');
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,
[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;
use super::db::*;
use crate::model;
-use crate::user::User;
#[derive(Debug)]
pub enum DBAsyncError {
)
}
- 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,
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;
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")?,
))
.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")?,
}).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")?,
})
},
}
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
}
};
.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))
}
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())
- },
+ }
}
}
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)
}
}
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',
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',
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",
mod hash;
mod model;
mod services;
-mod user;
mod utils;
#[actix_web::main]
+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
\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
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;
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) {
#[derive(Template)]
#[template(path = "home.html")]
struct HomeTemplate {
- user: Option<User>,
+ user: Option<model::User>,
recipes: Vec<(i64, String)>,
current_recipe_id: Option<i64>,
}
#[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,
}
.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,
}
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,
.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)]
#[derive(Template)]
#[template(path = "message.html")]
struct MessageTemplate<'a> {
- user: Option<User>,
+ user: Option<model::User>,
message: &'a str,
}
#[derive(Template)]
#[template(path = "sign_up_form.html")]
struct SignUpFormTemplate {
- user: Option<User>,
+ user: Option<model::User>,
email: String,
message: String,
message_email: String,
fn error_response(
error: SignUpError,
form: &web::Form<SignUpFormData>,
- user: Option<User>,
+ user: Option<model::User>,
) -> Result<HttpResponse> {
Ok(SignUpFormTemplate {
user,
#[derive(Template)]
#[template(path = "sign_in_form.html")]
struct SignInFormTemplate {
- user: Option<User>,
+ user: Option<model::User>,
email: String,
message: String,
}
fn error_response(
error: SignInError,
form: &web::Form<SignInFormData>,
- user: Option<User>,
+ user: Option<model::User>,
) -> Result<HttpResponse> {
Ok(SignInFormTemplate {
user,
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(
.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")]
+++ /dev/null
-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,
-}
<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>
{% 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
<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
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,
+}
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