New layout & new seed

This commit is contained in:
Adrian Woźniak 2024-10-31 16:00:26 +01:00
parent b4e307496e
commit fead4e970a
34 changed files with 3558 additions and 179 deletions

47
Cargo.lock generated
View File

@ -87,6 +87,7 @@ dependencies = [
"bytestring",
"derive_more 0.99.18",
"encoding_rs",
"flate2",
"futures-core",
"h2",
"http",
@ -104,6 +105,7 @@ dependencies = [
"tokio",
"tokio-util",
"tracing",
"zstd",
]
[[package]]
@ -1039,7 +1041,7 @@ version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "482aa5695bca086022be453c700a40c02893f1ba7098a2c88351de55341ae894"
dependencies = [
"entities",
"entities 1.0.1",
"memchr",
"once_cell",
"regex",
@ -1130,6 +1132,7 @@ dependencies = [
"askama_actix",
"chrono",
"derive_more 1.0.0",
"entities 0.1.0",
"humantime",
"humantime-serde",
"migration",
@ -1519,6 +1522,14 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "entities"
version = "0.1.0"
dependencies = [
"sea-orm",
"serde",
]
[[package]]
name = "entities"
version = "1.0.1"
@ -1591,6 +1602,16 @@ dependencies = [
"pin-project-lite",
]
[[package]]
name = "fake"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38bdb1a5a77008bd1208d3fccd90611d7bee8f6d3296175f69fb056747a220fb"
dependencies = [
"deunicode",
"rand",
]
[[package]]
name = "fastdivide"
version = "0.4.1"
@ -1624,6 +1645,16 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "flate2"
version = "1.0.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0"
dependencies = [
"crc32fast",
"miniz_oxide",
]
[[package]]
name = "flume"
version = "0.11.1"
@ -4026,6 +4057,20 @@ dependencies = [
"libc",
]
[[package]]
name = "seed"
version = "0.1.0"
dependencies = [
"actix-web",
"entities 0.1.0",
"fake",
"migration",
"rand",
"sea-orm",
"serde",
"tokio",
]
[[package]]
name = "semver"
version = "1.0.23"

View File

@ -1,36 +1,2 @@
[package]
name = "cooked"
version = "0.1.0"
edition = "2021"
[dependencies]
actix = "0.13.5"
actix-files = { version = "0.6.6", features = ["experimental-io-uring"] }
actix-web = { version = "4.9.0", features = ["compress-brotli", "cookies", "experimental-io-uring", "macros", "rustls", "secure-cookies", "unicode"], default-features = false }
askama = { version = "0.12.1", features = ["with-actix-web", "serde_json", "mime_guess", "markdown", "comrak", "mime"] }
askama_actix = "0.14.0"
redis = { version = "0.27.5", features = ["tokio", "json", "uuid", "tokio-comp", "connection-manager"] }
sea-orm = { version = "1.1.0", default-features = false, features = ["chrono", "macros", "runtime-actix-rustls", "serde_json", "sqlx-postgres", "with-uuid"] }
tracing = "0.1.40"
tracing-subscriber = "0.3.18"
serde = "1.0.210"
serde_json = "1.0.132"
uuid = { version = "1.11.0", features = ["serde", "v4", "v8"] }
migration = { path = "./migration" }
rswind = "0.0.1-alpha.1"
rswind_cli = "0.0.1-alpha.1"
derive_more = { version = "1.0.0", features = ["deref"] }
chrono = "0.4.38"
humantime = "2.1.0"
humantime-serde = "1.1.1"
actix-session = { version = "0.10.1", features = ["redis-session-rustls"] }
actix-identity = "0.8.0"
tantivy = "0.22.0"
tempfile = "3.13.0"
pulldown-cmark = "0.12.2"
thiserror = "1.0.65"
[build-dependencies]
[dev-dependencies]
tracing-test = "0.2.5"
[workspace]
members = ["cooked", "seed", "entities"]

37
cooked/Cargo.toml Normal file
View File

@ -0,0 +1,37 @@
[package]
name = "cooked"
version = "0.1.0"
edition = "2021"
[dependencies]
actix = "0.13.5"
actix-files = { version = "0.6.6", features = ["experimental-io-uring"] }
actix-web = { version = "4.9.0", features = ["compress-brotli", "cookies", "experimental-io-uring", "macros", "rustls", "secure-cookies", "unicode"], default-features = false }
askama = { version = "0.12.1", features = ["with-actix-web", "serde_json", "mime_guess", "markdown", "comrak", "mime"] }
askama_actix = "0.14.0"
redis = { version = "0.27.5", features = ["tokio", "json", "uuid", "tokio-comp", "connection-manager"] }
sea-orm = { version = "1.1.0", default-features = false, features = ["chrono", "macros", "runtime-actix-rustls", "serde_json", "sqlx-postgres", "with-uuid"] }
tracing = "0.1.40"
tracing-subscriber = "0.3.18"
serde = "1.0.210"
serde_json = "1.0.132"
uuid = { version = "1.11.0", features = ["serde", "v4", "v8"] }
migration = { path = "../migration" }
entities = { path = "../entities" }
rswind = "0.0.1-alpha.1"
rswind_cli = "0.0.1-alpha.1"
derive_more = { version = "1.0.0", features = ["deref"] }
chrono = "0.4.38"
humantime = "2.1.0"
humantime-serde = "1.1.1"
actix-session = { version = "0.10.1", features = ["redis-session-rustls"] }
actix-identity = "0.8.0"
tantivy = "0.22.0"
tempfile = "3.13.0"
pulldown-cmark = "0.12.2"
thiserror = "1.0.65"
[build-dependencies]
[dev-dependencies]
tracing-test = "0.2.5"

View File

@ -11,7 +11,7 @@ use std::str::FromStr;
use types::Admins;
pub mod actors;
pub mod entities;
pub use entities;
pub mod filters;
pub mod routes;
pub mod types;

View File

@ -1,6 +1,6 @@
use crate::actors::search::{Find, Search};
use crate::types::*;
use crate::{entities, filters};
use crate::{entities, entities::prelude::*, filters};
use actix_identity::Identity;
use actix_web::http::header::{CONTENT_LENGTH, CONTENT_TYPE};
use actix_web::web::{Data, Form, Path};
@ -8,8 +8,9 @@ use actix_web::HttpMessage;
use actix_web::{get, post, HttpRequest, HttpResponse, Responder};
use askama::Template;
use askama_actix::TemplateToResponse;
use sea_orm::{prelude::*, QuerySelect};
use sea_orm::{prelude::*, DatabaseTransaction, QuerySelect, TransactionTrait};
use serde::Deserialize;
use std::collections::{BTreeMap, BTreeSet};
#[derive(Debug, Template, derive_more::Deref)]
#[template(path = "recipe_card.jinja")]
@ -122,13 +123,6 @@ async fn sign_in(
}
}
#[derive(Debug, serde::Deserialize)]
struct CreateRecipe {
// #[serde(default)]
// #[serde(with = "humantime_serde")]
// time: Option<chrono::Duration>,
}
#[derive(Debug, serde::Deserialize)]
struct SignIn {
email: String,
@ -229,9 +223,9 @@ async fn index_html(
#[template(path = "recipies/show.jinja", ext = "html")]
struct RecipeDetailTemplate {
recipe: entities::recipies::Model,
tags: Vec<entities::recipe_tags::Model>,
tags: Vec<entities::tags::Model>,
steps: Vec<entities::recipe_steps::Model>,
ingeredients: Vec<entities::recipe_ingeredients::Model>,
ingredients: Vec<Ingredient>,
session: Option<User>,
page: Page,
}
@ -253,7 +247,8 @@ async fn show(
.append_header(("location", "/"))
.finish();
};
let tags = entities::prelude::RecipeTags::find()
let tags = entities::prelude::Tags::find()
.left_join(entities::prelude::RecipeTags)
.filter(entities::recipe_tags::Column::RecipeId.eq(id))
.all(db)
.await
@ -263,18 +258,43 @@ async fn show(
.all(db)
.await
.unwrap_or_default();
let ingeredients = entities::prelude::RecipeIngeredients::find()
.filter(entities::recipe_ingeredients::Column::RecipeId.eq(id))
let recipe_ingredients = entities::prelude::RecipeIngredients::find()
.filter(entities::recipe_ingredients::Column::RecipeId.eq(recipe.id))
.all(db)
.await
.unwrap_or_default();
let ingredients = entities::prelude::Ingredients::find()
.filter(
entities::ingredients::Column::Id.is_in(
recipe_ingredients
.iter()
.map(|i| i.ingredient_id)
.collect::<Vec<_>>(),
),
)
.all(db)
.await
.unwrap_or_default()
.into_iter()
.map(|ingredient| (ingredient.id, ingredient.name))
.collect::<BTreeMap<_, _>>();
let ingredients = recipe_ingredients
.into_iter()
.filter_map(|ri| {
Some(Ingredient {
name: ingredients.get(&ri.ingredient_id)?.to_owned(),
qty: ri.qty,
unit: ri.unit,
})
})
.collect::<Vec<_>>();
HttpResponse::Ok().body(
RecipeDetailTemplate {
steps,
tags,
recipe,
ingeredients,
ingredients,
session: admin.and_then(|s| s.id().ok()),
page: Page::Recipe,
}
@ -305,22 +325,31 @@ async fn create_recipe(
_admin: Identity,
form: Form<RecipeForm>,
db: Data<DatabaseConnection>,
) -> HttpResponse {
) -> Result<HttpResponse, Error> {
let form = form.into_inner();
let mut failure = form.clone();
match save_recipe(form, db.into_inner()).await {
let mut t = db
.begin()
.await
.inspect_err(|e| tracing::error!("Create recipe transaction: {e}"))
.map_err(|_| Error::DatabaseError)?;
match save_recipe(form, &mut t).await {
Err(e) => {
failure.error = Some(e.to_string());
failure.to_response()
let _ = t.rollback().await;
Ok(failure.to_response())
}
Ok(_) => {
let _ = t.commit().await;
Ok(HttpResponse::TemporaryRedirect()
.append_header(("location", "/"))
.finish())
}
Ok(_) => HttpResponse::TemporaryRedirect()
.append_header(("location", "/"))
.finish(),
}
}
async fn save_recipe(form: RecipeForm, db: DatabaseConnection) -> Result<(), Error> {
async fn save_recipe(form: RecipeForm, t: &mut DatabaseTransaction) -> Result<(), Error> {
use crate::entities::recipies::ActiveModel as RAM;
use sea_orm::ActiveValue::*;
@ -328,17 +357,107 @@ async fn save_recipe(form: RecipeForm, db: DatabaseConnection) -> Result<(), Err
let parser = pulldown_cmark::Parser::new(&form.summary);
pulldown_cmark::html::push_html(&mut summary, parser);
let model = RAM {
title: Set(form.title),
image_url: Set(form.image_url),
author: Set(None),
time: Set(None),
summary: Set(Some(summary)),
..Default::default()
let time = match form.time {
Some(s) => Some(
humantime::parse_duration(&s)
.inspect_err(|e| tracing::warn!("Invalid duration format: {e}"))
.map_err(|_| Error::InvalidTime)?
.as_secs() as i32,
),
None => None,
};
let steps = crate::utils::parse_steps(&form.steps, 0)?;
let ingeredients = crate::utils::parse_steps(&form.ingeredients, 0)?;
let recipe_id = Recipies::insert(RAM {
title: Set(form.title),
image_url: Set(form.image_url),
author: Set(form.author),
time: Set(time),
summary: Set(Some(summary)),
..Default::default()
})
.exec(&mut *t)
.await
.inspect_err(|e| tracing::error!("Failed to save Recipe: {e}"))
.map_err(|_| Error::SaveRecipe)?
.last_insert_id;
let steps = crate::utils::parse_steps(&form.steps)?
.into_iter()
.map(|(body, hint)| entities::recipe_steps::ActiveModel {
body: Set(body),
hint: Set(hint),
recipe_id: Set(recipe_id),
..Default::default()
})
.collect::<Vec<_>>();
let _steps = RecipeSteps::insert_many(steps)
.exec(&mut *t)
.await
.inspect_err(|e| tracing::error!("Save steps: {e}"))
.map_err(|_| Error::SaveRecipeStep)?;
let ingredients = crate::utils::parse_ingenedients(&form.ingeredients)?;
let known = Ingredients::find()
.filter(
entities::ingredients::Column::Name.is_in(
ingredients
.iter()
.map(|(name, ..)| name.to_string())
.collect::<Vec<_>>(),
),
)
.all(&mut *t)
.await
.inspect_err(|e| tracing::warn!("Failed to find ingredients: {e}"))
.map_err(|_| Error::SaveRecipeIngeredient)?;
let missing = {
let known = known
.iter()
.map(|model| &model.name)
.collect::<BTreeSet<_>>();
let missing = ingredients
.iter()
.filter(|(name, ..)| !known.contains(name))
.map(|(name, ..)| entities::ingredients::ActiveModel {
name: Set(name.to_owned()),
..Default::default()
})
.collect::<Vec<_>>();
let mut v = Vec::with_capacity(missing.len());
for missing in missing {
v.push(
Ingredients::insert(missing)
.exec_with_returning(&mut *t)
.await
.inspect_err(|e| tracing::error!("Failed to create ingredients: {e}"))
.map_err(|_| Error::DatabaseError)?,
)
}
v
};
let map = known
.into_iter()
.chain(missing.into_iter())
.map(|row| (row.name, row.id))
.collect::<BTreeMap<_, _>>();
RecipeIngredients::insert_many(
ingredients
.into_iter()
.filter_map(|(name, unit, qty)| {
Some(entities::recipe_ingredients::ActiveModel {
ingredient_id: Set(*map.get(&name)?),
qty: Set(qty),
unit: Set(unit.unwrap_or_default()),
recipe_id: Set(recipe_id),
..Default::default()
})
})
.collect::<Vec<_>>(),
)
.exec(&mut *t)
.await
.inspect_err(|e| tracing::error!("Save ingeredients: {e}"))
.map_err(|_| Error::SaveRecipeIngeredient)?;
Ok(())
}

87
cooked/src/types.rs Normal file
View File

@ -0,0 +1,87 @@
use std::str::FromStr;
use actix_web::http::StatusCode;
use serde::Deserialize;
pub type User = String;
#[derive(Debug)]
pub struct Ingredient {
pub qty: i32,
pub name: String,
pub unit: String,
}
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Invalid step list")]
InvalidStepList,
#[error("Invalid ingeredients list")]
InvalidIngeredientList,
#[error("Time has invalid format")]
InvalidTime,
#[error("Internal server error")]
SaveRecipe,
#[error("Internal server error")]
SaveRecipeStep,
#[error("Internal server error")]
SaveRecipeIngeredient,
#[error("Internal server error")]
SaveRecipeTag,
#[error("Internal server error")]
DatabaseError,
}
impl actix_web::error::ResponseError for Error {
fn status_code(&self) -> StatusCode {
match self {
Self::InvalidStepList => StatusCode::BAD_REQUEST,
Self::InvalidIngeredientList => StatusCode::BAD_REQUEST,
Self::InvalidTime => StatusCode::BAD_REQUEST,
Self::SaveRecipe => StatusCode::INTERNAL_SERVER_ERROR,
Self::SaveRecipeStep => StatusCode::INTERNAL_SERVER_ERROR,
Self::SaveRecipeIngeredient => StatusCode::INTERNAL_SERVER_ERROR,
Self::SaveRecipeTag => StatusCode::INTERNAL_SERVER_ERROR,
Self::DatabaseError => StatusCode::INTERNAL_SERVER_ERROR,
}
}
}
#[derive(Debug, PartialEq, Clone, Copy, Deserialize)]
pub enum Page {
Index,
Recipe,
Search,
SignIn,
}
#[derive(Debug, Deserialize)]
pub struct Admin {
pub email: String,
pub pass: String,
}
impl FromStr for Admin {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut it = s.split(':');
Ok(Self {
email: it.next().expect("Admin login is required").into(),
pass: it.next().expect("Admin password is required").into(),
})
}
}
#[derive(Debug, derive_more::Deref)]
pub struct Admins(Vec<Admin>);
impl FromStr for Admins {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self(
s.trim().split(',').filter_map(|s| s.parse().ok()).collect(),
))
}
}

View File

@ -1,11 +1,7 @@
use crate::entities::recipe_ingeredients::ActiveModel as RIAM;
use crate::entities::recipe_steps::ActiveModel as RSAM;
use crate::types::Error;
use crate::types::Error::*;
use sea_orm::prelude::*;
use sea_orm::ActiveValue::*;
pub fn parse_steps(s: &str, recipe_id: i32) -> Result<Vec<RSAM>, Error> {
pub fn parse_steps(s: &str) -> Result<Vec<(String, Option<String>)>, Error> {
s.lines()
.filter(|s| !s.trim().is_empty())
.try_fold(Vec::new(), |mut v, line| {
@ -13,15 +9,11 @@ pub fn parse_steps(s: &str, recipe_id: i32) -> Result<Vec<RSAM>, Error> {
match line.chars().next() {
Some('*') => {
v.push(RSAM {
body: Set(line.replacen("*", "", 1).trim().to_string()),
recipe_id: Set(recipe_id),
..Default::default()
});
v.push((line.replacen("*", "", 1).trim().to_string(), None));
}
Some('>') => {
v.last_mut().ok_or(InvalidStepList)?.hint =
Set(Some(line.replacen(">", "", 1).trim().to_string()));
v.last_mut().ok_or(InvalidStepList)?.1 =
Some(line.replacen(">", "", 1).trim().to_string());
}
_ => return Err(InvalidStepList),
};
@ -30,7 +22,7 @@ pub fn parse_steps(s: &str, recipe_id: i32) -> Result<Vec<RSAM>, Error> {
})
}
pub fn parse_ingenedients(s: &str, recipe_id: i32) -> Result<Vec<RIAM>, Error> {
pub fn parse_ingenedients(s: &str) -> Result<Vec<(String, Option<String>, i32)>, Error> {
s.lines()
.map(|s| s.trim())
.filter(|s| !s.is_empty())
@ -47,12 +39,7 @@ pub fn parse_ingenedients(s: &str, recipe_id: i32) -> Result<Vec<RIAM>, Error> {
.remove(0)
.parse()
.map_err(|_| InvalidIngeredientList)?;
Ok(RIAM {
recipe_id: Set(recipe_id),
name: Set(pieces.join(" ")),
qty: Set(qty),
..Default::default()
})
Ok((pieces.join(" "), None, qty))
}
_ => {
let qty = pieces
@ -60,15 +47,13 @@ pub fn parse_ingenedients(s: &str, recipe_id: i32) -> Result<Vec<RIAM>, Error> {
.parse()
.map_err(|_| InvalidIngeredientList)?;
let unit = pieces.remove(0);
Ok(RIAM {
recipe_id: Set(recipe_id),
name: Set(pieces.join(" ")),
unit: Set(unit.to_string()),
qty: Set(qty),
..Default::default()
})
Ok((pieces.join(" "), Some(unit.to_string()), qty))
}
}
})
.try_collect()
}
pub fn parse_tags(s: &str) -> Result<Vec<String>, Error> {
s.lines().map(|line| Ok(line.to_string())).try_collect()
}

8
entities/Cargo.toml Normal file
View File

@ -0,0 +1,8 @@
[package]
name = "entities"
version = "0.1.0"
edition = "2021"
[dependencies]
sea-orm = { version = "1.1.0", default-features = false, features = ["chrono", "macros", "runtime-actix-rustls", "serde_json", "sqlx-postgres", "with-uuid"] }
serde = "1.0.210"

View File

@ -0,0 +1,26 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.0
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
#[sea_orm(table_name = "ingredients")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub name: String,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(has_many = "super::recipe_ingredients::Entity")]
RecipeIngredients,
}
impl Related<super::recipe_ingredients::Entity> for Entity {
fn to() -> RelationDef {
Relation::RecipeIngredients.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -2,7 +2,9 @@
pub mod prelude;
pub mod recipe_ingeredients;
pub mod ingredients;
pub mod recipe_ingredients;
pub mod recipe_steps;
pub mod recipe_tags;
pub mod recipies;
pub mod tags;

View File

@ -1,6 +1,8 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.0
pub use super::recipe_ingeredients::Entity as RecipeIngeredients;
pub use super::ingredients::Entity as Ingredients;
pub use super::recipe_ingredients::Entity as RecipeIngredients;
pub use super::recipe_steps::Entity as RecipeSteps;
pub use super::recipe_tags::Entity as RecipeTags;
pub use super::recipies::Entity as Recipies;
pub use super::tags::Entity as Tags;

View File

@ -4,11 +4,11 @@ use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
#[sea_orm(table_name = "recipe_ingeredients")]
#[sea_orm(table_name = "recipe_ingredients")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub name: String,
pub ingredient_id: i32,
pub qty: i32,
pub unit: String,
pub recipe_id: i32,
@ -16,6 +16,14 @@ pub struct Model {
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::ingredients::Entity",
from = "Column::IngredientId",
to = "super::ingredients::Column::Id",
on_update = "NoAction",
on_delete = "NoAction"
)]
Ingredients,
#[sea_orm(
belongs_to = "super::recipies::Entity",
from = "Column::RecipeId",
@ -26,6 +34,12 @@ pub enum Relation {
Recipies,
}
impl Related<super::ingredients::Entity> for Entity {
fn to() -> RelationDef {
Relation::Ingredients.def()
}
}
impl Related<super::recipies::Entity> for Entity {
fn to() -> RelationDef {
Relation::Recipies.def()

View File

@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize};
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub name: String,
pub tag_id: i32,
pub recipe_id: i32,
}
@ -22,6 +22,14 @@ pub enum Relation {
on_delete = "NoAction"
)]
Recipies,
#[sea_orm(
belongs_to = "super::tags::Entity",
from = "Column::TagId",
to = "super::tags::Column::Id",
on_update = "NoAction",
on_delete = "NoAction"
)]
Tags,
}
impl Related<super::recipies::Entity> for Entity {
@ -30,4 +38,10 @@ impl Related<super::recipies::Entity> for Entity {
}
}
impl Related<super::tags::Entity> for Entity {
fn to() -> RelationDef {
Relation::Tags.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -17,17 +17,17 @@ pub struct Model {
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(has_many = "super::recipe_ingeredients::Entity")]
RecipeIngeredients,
#[sea_orm(has_many = "super::recipe_ingredients::Entity")]
RecipeIngredients,
#[sea_orm(has_many = "super::recipe_steps::Entity")]
RecipeSteps,
#[sea_orm(has_many = "super::recipe_tags::Entity")]
RecipeTags,
}
impl Related<super::recipe_ingeredients::Entity> for Entity {
impl Related<super::recipe_ingredients::Entity> for Entity {
fn to() -> RelationDef {
Relation::RecipeIngeredients.def()
Relation::RecipeIngredients.def()
}
}

26
entities/src/tags.rs Normal file
View File

@ -0,0 +1,26 @@
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.0
use sea_orm::entity::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
#[sea_orm(table_name = "tags")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub name: String,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(has_many = "super::recipe_tags::Entity")]
RecipeTags,
}
impl Related<super::recipe_tags::Entity> for Entity {
fn to() -> RelationDef {
Relation::RecipeTags.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -6,6 +6,40 @@ pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(Tag::Tags)
.if_not_exists()
.col(
ColumnDef::new(Tag::Id)
.integer()
.not_null()
.unique_key()
.auto_increment()
.primary_key(),
)
.col(ColumnDef::new(Tag::Name).string().not_null())
.to_owned(),
)
.await?;
manager
.create_table(
Table::create()
.table(Ingredient::Ingredients)
.if_not_exists()
.col(
ColumnDef::new(Ingredient::Id)
.integer()
.not_null()
.unique_key()
.auto_increment()
.primary_key(),
)
.col(ColumnDef::new(Ingredient::Name).string().not_null())
.to_owned(),
)
.await?;
manager
.create_table(
Table::create()
@ -56,7 +90,7 @@ impl MigrationTrait for Migration {
manager
.create_table(
Table::create()
.table(RecipeIngredient::RecipeIngeredients)
.table(RecipeIngredient::RecipeIngredients)
.if_not_exists()
.col(
ColumnDef::new(RecipeIngredient::Id)
@ -66,7 +100,11 @@ impl MigrationTrait for Migration {
.auto_increment()
.primary_key(),
)
.col(ColumnDef::new(RecipeIngredient::Name).string().not_null())
.col(
ColumnDef::new(RecipeIngredient::IngredientId)
.integer()
.not_null(),
)
.col(ColumnDef::new(RecipeIngredient::Qty).integer().not_null())
.col(ColumnDef::new(RecipeIngredient::Unit).string().not_null())
.col(
@ -78,9 +116,16 @@ impl MigrationTrait for Migration {
&mut ForeignKeyCreateStatement::new()
.to_tbl(Recipe::Recipies)
.to_col(Recipe::Id)
.from_tbl(RecipeIngredient::RecipeIngeredients)
.from_tbl(RecipeIngredient::RecipeIngredients)
.from_col(RecipeIngredient::RecipeId),
)
.foreign_key(
&mut ForeignKeyCreateStatement::new()
.to_tbl(Ingredient::Ingredients)
.to_col(Recipe::Id)
.from_tbl(RecipeIngredient::RecipeIngredients)
.from_col(RecipeIngredient::IngredientId),
)
.to_owned(),
)
.await?;
@ -97,7 +142,7 @@ impl MigrationTrait for Migration {
.auto_increment()
.primary_key(),
)
.col(ColumnDef::new(RecipeTag::Name).string().not_null())
.col(ColumnDef::new(RecipeTag::TagId).integer().not_null())
.col(ColumnDef::new(RecipeTag::RecipeId).integer().not_null())
.foreign_key(
&mut ForeignKeyCreateStatement::new()
@ -106,6 +151,13 @@ impl MigrationTrait for Migration {
.from_tbl(RecipeTag::RecipeTags)
.from_col(RecipeTag::RecipeId),
)
.foreign_key(
&mut ForeignKeyCreateStatement::new()
.to_tbl(Tag::Tags)
.to_col(Tag::Id)
.from_tbl(RecipeTag::RecipeTags)
.from_col(RecipeTag::TagId),
)
.to_owned(),
)
.await?;
@ -116,7 +168,7 @@ impl MigrationTrait for Migration {
manager
.drop_table(
Table::drop()
.table(RecipeIngredient::RecipeIngeredients)
.table(RecipeIngredient::RecipeIngredients)
.to_owned(),
)
.await?;
@ -129,6 +181,12 @@ impl MigrationTrait for Migration {
manager
.drop_table(Table::drop().table(Recipe::Recipies).to_owned())
.await?;
manager
.drop_table(Table::drop().table(Tag::Tags).to_owned())
.await?;
manager
.drop_table(Table::drop().table(Ingredient::Ingredients).to_owned())
.await?;
Ok(())
}
}
@ -154,19 +212,33 @@ enum RecipeStep {
}
#[derive(DeriveIden)]
enum RecipeIngredient {
RecipeIngeredients,
enum Ingredient {
Ingredients,
Id,
Name,
}
#[derive(DeriveIden)]
enum RecipeIngredient {
RecipeIngredients,
Id,
IngredientId,
Qty,
Unit,
RecipeId,
}
#[derive(DeriveIden)]
enum Tag {
Tags,
Id,
Name,
}
#[derive(DeriveIden)]
enum RecipeTag {
RecipeTags,
Id,
Name,
TagId,
RecipeId,
}

View File

@ -1,2 +1,2 @@
mkdir -p src/entities
sea-orm-cli generate -v entity --database-url postgres://postgres@localhost/cooked --output-dir ./src/entities --with-serde=both
sea-orm-cli generate -v entity --lib --database-url postgres://postgres@localhost/cooked --output-dir ./entities/src --with-serde=both

7
scripts/migrate Executable file
View File

@ -0,0 +1,7 @@
export DATABASE_URL=postgres://postgres@localhost/cooked
cargo build -p migration
./target/debug/migration
./scripts/seed.sh

View File

@ -1,4 +1,4 @@
psql cooked postgres -h localhost < ./seed/recipies.sql
psql cooked postgres -h localhost < ./seed/recipe_tags.sql
psql cooked postgres -h localhost < ./seed/recipe_ingeredients.sql
psql cooked postgres -h localhost < ./seed/recipe_steps.sql
psql -b cooked postgres -h localhost < ./seed/recipies.sql &&
psql -b cooked postgres -h localhost < ./seed/recipe_tags.sql &&
psql -b cooked postgres -h localhost < ./seed/recipe_ingeredients.sql &&
psql -b cooked postgres -h localhost < ./seed/recipe_steps.sql

2733
seed/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

15
seed/Cargo.toml Normal file
View File

@ -0,0 +1,15 @@
[package]
name = "seed"
version = "0.1.0"
edition = "2021"
[dependencies]
tokio = "*"
sea-orm = { version = "1.1.0", default-features = false, features = ["chrono", "macros", "runtime-actix-rustls", "serde_json", "sqlx-postgres", "with-uuid"] }
serde = "1.0.210"
actix-web = "*"
entities = { path = "../entities" }
migration = { path = "../migration" }
fake = "3.0.0"
rand = "0.8.5"

265
seed/src/main.rs Normal file
View File

@ -0,0 +1,265 @@
use entities::prelude::*;
use fake::Fake;
use migration::*;
use rand::Rng;
use sea_orm::Set;
use sea_orm::*;
#[actix_web::main]
async fn main() {
let psql =
std::env::var("PSQL").expect("PSQL is required. Please provide postgresql connection url");
let db = Database::connect(&psql)
.await
.expect("Failed to connect to postgresql");
{
use migration::*;
Migrator::up(&db, None).await.unwrap();
}
let mut tags = Vec::with_capacity(30);
for _ in 0..30 {
use entities::tags::ActiveModel;
tags.push(
Tags::insert(ActiveModel {
id: NotSet,
name: Set(fake::faker::name::en::FirstName().fake()),
})
.exec_with_returning(&db)
.await
.unwrap(),
);
}
let mut ingredients = Vec::with_capacity(INGREDIENTS.len());
for name in INGREDIENTS {
use entities::tags::ActiveModel;
tags.push(
Tags::insert(ActiveModel {
id: NotSet,
name: Set(name.to_string()),
})
.exec_with_returning(&db)
.await
.unwrap(),
);
}
let mut recipies = Vec::with_capacity(1_000);
for name in DISHES {
use entities::recipies::ActiveModel;
recipies.push(
Recipies::insert(ActiveModel {
id: NotSet,
title: Set(name.to_string()),
image_url: Set(format!(
"https://picsum.photos/{w}/{h}",
w = fake::rand::random::<i32>() + 200,
h = fake::rand::random::<i32>() + 200
)),
author: Set(Some(fake::faker::name::en::FirstName().fake())),
time: Set(Some(fake::rand::random::<i32>() * 15)),
summary: Set(fake::faker::lorem::en::Paragraph(2..4).fake()),
})
.exec_with_returning(&db)
.await
.unwrap(),
);
}
use rand::seq::SliceRandom;
{
use entities::recipe_ingredients::ActiveModel;
RecipeIngredients::insert_many(
recipies
.iter()
.flat_map(|recipe| {
(0..rand::thread_rng().gen_range(3..6))
.map(|_| ingredients.choose(&mut rand::thread_rng()))
.map(|igredient| ActiveModel {
id: NotSet,
ingredient_id: Set(ingredient.id),
qty: Set(rand::thread_rng().get_range(4..8)),
unit: Set(Some(UNIT.choose(&mut rand::thread_rng()))),
recipe_id: Set(recipe.id),
})
})
.collect::<Vec<_>>(),
)
.exec(&db)
.await
.unwrap();
}
}
const INGREDIENTS: [&'static str; 69] = [
"Almond Meal",
"Almonds",
"Amaranth",
"Apples",
"Apricots",
"Avocados",
"Bananas",
"Barley",
"Beef",
"Beef Chuck",
"Beef Ribs",
"Beef Tenderloin",
"Brisket",
"Brown Rice",
"Buckwheat",
"Bulgur",
"Cheese",
"Cherries",
"Chia Seeds",
"Chicken",
"Chicken Breasts",
"Chicken Legs",
"Chicken Thighs",
"Chicken Wings",
"Chocolate",
"Coconut",
"Corn Flour",
"Cornish Hens",
"Cornmeal",
"Duck",
"Fish",
"Flax Seeds",
"Goat",
"Ground Beef",
"Ground Chicken",
"Ground Pork",
"Ground Turkey",
"Lamb",
"Mangos",
"Millet",
"Mushroom",
"Nectarines",
"Oat Flour",
"Oats",
"Peaches",
"Peanuts",
"Pears",
"Pineapples",
"Plums",
"Pomegranates",
"Pork",
"Pork Ribs",
"Pork Shoulder",
"Pork Tenderloin",
"Prime Rib",
"Quinoa",
"Sausage",
"Seafood",
"Shellfish",
"Sirloin",
"Spelt",
"Steak",
"Tapioca Flour",
"Turkey",
"Veal",
"Venison",
"White Rice Flour",
"Wild Game",
"Wild Rice",
];
const DISHES: [&'static str; 96] = [
"Achari baingan",
"Aloo gobi",
"Aloo tikki",
"Aloo tuk",
"Aloo matar",
"Aloo kulcha",
"Aloo methi",
"Aloo shimla mirch",
"Amriti with rabdi",
"Talit Macchi
(Indian fish fry)",
"Baati",
"Bhatura",
"Bhindi masala",
"Biryani",
"Butter chicken",
"Chaat",
"Chana masala",
"Chapati",
"Chicken razala",
"Chicken Tikka",
"Chicken Tikka masala",
"Chole bhature",
"Daal baati churma",
"Daal puri",
"Dal makhani (kali dal)",
"Dal fara",
"Dal",
"Dal fry with tadka",
"Dum aloo",
"Poha",
"Fara",
"phirni",
"Aloo Phalliyaan",
"Gajar Pak[2]",
"Gatte ki Sabzi",
"Gajar matar aloo",
"Gobhi matar",
"Imarti",
"Hari mutter ka nimona (green peas daal)",
"Jalebi",
"Jaleba",
"Kachori",
"Kadai paneer",
"Kadhi pakoda",
"Karela bharta",
"Katha meetha petha / kaddu halwa",
"Kheer",
"Khichdi",
"Kadhi and Khichdi",
"Kofta",
"Kulfi falooda",
"Laapsi",
"Lauki ke kofte",
"Lauki ki bhaaji",
"Litti chokha",
"Makhaan ka kheer",
"Makki ki roti, sarson ka saag",
"Mathura ke pede",
"Methi saag, chaulai saag",
"Millet Lapsi",
"Mirchi Bada",
"Missi roti",
"Mixed vegetable",
"Moong dal ki Lapsi",
"Murgh musallam",
"Mushroom do pyaza (Kanda Khumb)",
"Mushroom matar (Matar Khumb)",
"Naan",
"Navrattan korma",
"Pakhala",
"Palak paneer",
"Paneer butter masala",
"Paneer tikka masala",
"Pani puri",
"Panjeeri",
"Papad",
"Paratha",
"Pattor",
"Phirni",
"Pinni",
"Rajma chaval",
"Rajma",
"Ramatori bhaaji",
"Lobiya",
"Samosa",
"Samose",
"Sattu ki roti",
"Rajwadi Chhena/Paneer[4]",
"Shahi tukra",
"Singhada Lapsi",
"Sooji halwa (Suji Lapsi)",
"Sweet pethas / kesar petha / pista petha",
"Vegetable jalfrezi",
"Tandoori Chicken",
"Tamatar Chaat",
"Tandoori Fish Tikka",
];

View File

@ -0,0 +1 @@
{"rustc_fingerprint":5966236372496131995,"outputs":{"4614504638168534921":{"success":true,"status":"","code":0,"stdout":"rustc 1.84.0-nightly (3ed6e3cc6 2024-10-17)\nbinary: rustc\ncommit-hash: 3ed6e3cc69857129c1d314daec00119ff47986ed\ncommit-date: 2024-10-17\nhost: x86_64-unknown-linux-gnu\nrelease: 1.84.0-nightly\nLLVM version: 19.1.1\n","stderr":""},"14371922958718593042":{"success":true,"status":"","code":0,"stdout":"___\nlib___.rlib\nlib___.so\nlib___.so\nlib___.a\nlib___.so\n/home/eraden/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu\noff\npacked\nunpacked\n___\ndebug_assertions\nfmt_debug=\"full\"\noverflow_checks\npanic=\"unwind\"\nproc_macro\nrelocation_model=\"pic\"\ntarget_abi=\"\"\ntarget_arch=\"x86_64\"\ntarget_endian=\"little\"\ntarget_env=\"gnu\"\ntarget_family=\"unix\"\ntarget_feature=\"fxsr\"\ntarget_feature=\"sse\"\ntarget_feature=\"sse2\"\ntarget_has_atomic\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_has_atomic_equal_alignment=\"16\"\ntarget_has_atomic_equal_alignment=\"32\"\ntarget_has_atomic_equal_alignment=\"64\"\ntarget_has_atomic_equal_alignment=\"8\"\ntarget_has_atomic_equal_alignment=\"ptr\"\ntarget_has_atomic_load_store\ntarget_has_atomic_load_store=\"16\"\ntarget_has_atomic_load_store=\"32\"\ntarget_has_atomic_load_store=\"64\"\ntarget_has_atomic_load_store=\"8\"\ntarget_has_atomic_load_store=\"ptr\"\ntarget_os=\"linux\"\ntarget_pointer_width=\"64\"\ntarget_thread_local\ntarget_vendor=\"unknown\"\nub_checks\nunix\n","stderr":""},"15729799797837862367":{"success":true,"status":"","code":0,"stdout":"___\nlib___.rlib\nlib___.so\nlib___.so\nlib___.a\nlib___.so\n/home/eraden/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu\noff\npacked\nunpacked\n___\ndebug_assertions\nfmt_debug=\"full\"\noverflow_checks\npanic=\"unwind\"\nproc_macro\nrelocation_model=\"pic\"\ntarget_abi=\"\"\ntarget_arch=\"x86_64\"\ntarget_endian=\"little\"\ntarget_env=\"gnu\"\ntarget_family=\"unix\"\ntarget_feature=\"fxsr\"\ntarget_feature=\"sse\"\ntarget_feature=\"sse2\"\ntarget_has_atomic\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_has_atomic_equal_alignment=\"16\"\ntarget_has_atomic_equal_alignment=\"32\"\ntarget_has_atomic_equal_alignment=\"64\"\ntarget_has_atomic_equal_alignment=\"8\"\ntarget_has_atomic_equal_alignment=\"ptr\"\ntarget_has_atomic_load_store\ntarget_has_atomic_load_store=\"16\"\ntarget_has_atomic_load_store=\"32\"\ntarget_has_atomic_load_store=\"64\"\ntarget_has_atomic_load_store=\"8\"\ntarget_has_atomic_load_store=\"ptr\"\ntarget_os=\"linux\"\ntarget_pointer_width=\"64\"\ntarget_thread_local\ntarget_vendor=\"unknown\"\nub_checks\nunix\n","stderr":""}},"successes":{}}

3
seed/target/CACHEDIR.TAG Normal file
View File

@ -0,0 +1,3 @@
Signature: 8a477f597d28d172789f06886806bc55
# This file is a cache directory tag created by cargo.
# For information about cache directory tags see https://bford.info/cachedir/

View File

@ -1,52 +0,0 @@
use std::str::FromStr;
use serde::Deserialize;
pub type User = String;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Invalid step list")]
InvalidStepList,
#[error("Invalid ingeredients list")]
InvalidIngeredientList,
}
#[derive(Debug, PartialEq, Clone, Copy, Deserialize)]
pub enum Page {
Index,
Recipe,
Search,
SignIn,
}
#[derive(Debug, Deserialize)]
pub struct Admin {
pub email: String,
pub pass: String,
}
impl FromStr for Admin {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut it = s.split(':');
Ok(Self {
email: it.next().expect("Admin login is required").into(),
pass: it.next().expect("Admin password is required").into(),
})
}
}
#[derive(Debug, derive_more::Deref)]
pub struct Admins(Vec<Admin>);
impl FromStr for Admins {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self(
s.trim().split(',').filter_map(|s| s.parse().ok()).collect(),
))
}
}

View File

@ -12,9 +12,13 @@
<h1 class="font-young-serif text-desktop-heading-l text-stone-700 sm:text-stone-800 text-center md:text-left">
{{ recipe.title }}
</h1>
{% match recipe.summary %}
{% when Some with (summary) %}
<p class="font-outfit-regular text-stone-500 sm:text-base">
{{ recipe.summary.clone().unwrap_or_default() }}
{{ summary }}
</p>
{% when None %}
{% endmatch %}
</div>
<div class="divider"></div>
<div class="flex flex-col gap-7">
@ -22,16 +26,16 @@
Ingredients
</p>
<ul class="list-disc marker:text-rose-900 list-inside flex flex-col gap-3">
{% for ingeredient in ingeredients %}
{% for ingredient in ingredients %}
<li class="paragraph">
<span class="mr-1">
{{ ingeredient.qty }}
{{ ingredient.qty }}
</span>
<span class="mr-4">
{{ ingeredient.unit }}
{{ ingredient.unit }}
</span>
<span>
{{ ingeredient.name }}
{{ ingredient.name }}
</span>
</li>
{% endfor %}
@ -44,7 +48,7 @@
</p>
<ol class="list-decimal list-inside flex flex-col gap-3">
{% for step in steps %}
<li class="font-outfit-regular text-stone-500 text-base text-base">
<li class="font-outfit-regular text-stone-500 text-base">
{{ step.body }}
<div>{{ step.hint.clone().unwrap_or_default() }}</div>
</li>
@ -56,7 +60,7 @@
<p class="text-desktop-heading-m">
Tags
</p>
<div class="flex gap-4 gap-7">
<div class="flex gap-7">
{% for tag in tags %}
<a class="bg-gray-300 dark:bg-gray-700 text-base text-gray-700 dark:text-white py-2 px-4 rounded-full font-bold hover:bg-gray-400 dark:hover:bg-gray-600 flex justify-center items-center">
{{ tag.name }}
@ -69,10 +73,10 @@
<p class="text-desktop-heading-m">
Ingredients
</p>
<div class="flex gap-4 gap-7">
{% for ingeredient in ingeredients %}
<div class="flex gap-7">
{% for ingredient in ingredients %}
<a class="bg-gray-300 dark:bg-gray-700 text-base text-gray-700 dark:text-white py-2 px-4 rounded-full font-bold hover:bg-gray-400 dark:hover:bg-gray-600 flex justify-center items-center">
{{ ingeredient.name }}
{{ ingredient.name }}
</a>
{% endfor %}
</div>