Works on ingredients

This commit is contained in:
2019-02-03 15:17:43 +01:00
parent 28afe8ece0
commit d3259c82b3
10 changed files with 151 additions and 61 deletions

Binary file not shown.

View File

@@ -2,7 +2,7 @@
CREATE TABLE recipes ( CREATE TABLE recipes (
id INTEGER PRIMARY KEY NOT NULL, id INTEGER PRIMARY KEY NOT NULL,
title VARCHAR NOT NULL, title VARCHAR NOT NULL,
category INTEGER NOT NULL, category SMALLINT NOT NULL,
ingredients TEXT NOT NULL, ingredients TEXT NOT NULL,
preparation TEXT NOT NULL preparation TEXT NOT NULL
) )

View File

@@ -3,7 +3,22 @@ extern crate diesel;
use self::cookbook::*; use self::cookbook::*;
use self::models::*; use self::models::*;
use self::diesel::prelude::*;
use diesel::sqlite::SqliteConnection;
use cookbook::models::fields::IngredientList;
struct IngredientListManager<'a>(&'a SqliteConnection, IngredientList);
impl<'a> IngredientListManager<'a> {
fn display_lines(&self) -> String {
let mut repr = String::new();
for id in self.1.iter() {
let ingdt = ingredients::get(self.0, id).expect("Database integrity error");
repr.push_str(&format!("{}\n", ingdt.alias));
}
repr
}
}
fn main() { fn main() {
@@ -13,6 +28,6 @@ fn main() {
for rec in result { for rec in result {
println!("*************\n{}\n({:?})", rec.title, rec.category); println!("*************\n{}\n({:?})", rec.title, rec.category);
println!("-------------\n"); println!("-------------\n");
println!("{}", rec.ingredients); println!("{}", rec.ingredients.as_string());
} }
} }

View File

@@ -1,16 +1,16 @@
extern crate cookbook; extern crate cookbook;
extern crate diesel; extern crate diesel;
use std::io::{Read, stdin}; use std::io::{stdin};
use diesel::SqliteConnection; use diesel::SqliteConnection;
use self::cookbook::*; use self::cookbook::*;
use self::models::{NewRecipe, fields::RecipeCategory}; use self::models::{NewRecipe, fields::RecipeCategory, fields::IngredientList};
struct CreateRecipe<'a> { struct CreateRecipe<'a> {
connection: &'a SqliteConnection, connection: &'a SqliteConnection,
title: &'a str, title: &'a str,
category: Option<RecipeCategory>, category: Option<RecipeCategory>,
ingredients: String, ingredients: IngredientList,
} }
impl<'a> CreateRecipe<'a> { impl<'a> CreateRecipe<'a> {
@@ -19,7 +19,7 @@ impl<'a> CreateRecipe<'a> {
connection: conn, connection: conn,
title: "New recipe", title: "New recipe",
category: None, category: None,
ingredients: String::new(), ingredients: IngredientList::new(),
} }
} }
@@ -35,23 +35,23 @@ impl<'a> CreateRecipe<'a> {
use crate::ingredients::*; use crate::ingredients::*;
// Check it exists or create // Check it exists or create
if let Some(_ingdt) = find(self.connection, &name) { match get_id_or_create(self.connection, &name) {
println!("="); Ok(id) => {
} else { println!("Got id {}", id);
create(self.connection, &name); self.ingredients.push(id);
println!("+{}", &name); },
Err(_) => println!("Error adding ingredient")
} }
self.ingredients.push_str(&name);
} }
/// Builds a NewRecipe instance from current data and insert it. /// Builds a NewRecipe instance from current data and insert it.
fn insert(self) { fn insert(self) {
let new_recipe = NewRecipe::new( let new_recipe = NewRecipe {
self.title, title: self.title,
self.category.unwrap_or(RecipeCategory::Breakfast), category: self.category.unwrap_or(RecipeCategory::Breakfast),
&self.ingredients, ingredients: self.ingredients,
""); preparation: ""
};
match new_recipe.insert(self.connection) { match new_recipe.insert(self.connection) {
Ok(new) => println!("Added {}", new.title), Ok(new) => println!("Added {}", new.title),
Err(e) => println!("Error: {}", e), Err(e) => println!("Error: {}", e),

View File

@@ -40,34 +40,52 @@ pub mod recipes {
} }
} }
pub mod ingredients { pub mod ingredients {
use crate::models::{Ingredient, NewIngredient}; use crate::models::{Ingredient, NewIngredient, fields::IngredientId};
use super::{SqliteConnection, schema}; use super::{SqliteConnection, schema};
use super::diesel::prelude::*; use super::diesel::prelude::*;
pub fn find(conn: &SqliteConnection, name: &str) -> Option<Ingredient> { /// Returns the id of the ingredient identifiable by `name: &str`
use self::schema::ingredients::dsl::*; /// If the ingredient does not yet exists, it is inserted in database.
pub fn get_id_or_create(conn: &SqliteConnection, name: &str) -> Result<i32,String> {
let results = ingredients.filter(alias.like(name)) if let Some(ingdt) = find(conn, name) {
.limit(1) return Ok(ingdt.id);
.load::<Ingredient>(conn)
.expect("Error finding ingredient");
if !results.is_empty() {
Some(results.into_iter().nth(0).unwrap())
} else { } else {
None return create(conn, name);
} }
} }
pub fn create(conn: &SqliteConnection, name: &str) -> usize { pub fn get(conn: &SqliteConnection, ingdt_id: &IngredientId) -> Result<Ingredient,String> {
use self::schema::ingredients; use self::schema::ingredients::dsl::*;
diesel::insert_into(ingredients::table) ingredients.filter(id.eq(ingdt_id))
.first::<Ingredient>(conn)
.map_err(|e| format!("Could not retrieve : {}", e))
}
fn find(conn: &SqliteConnection, name: &str) -> Option<Ingredient> {
use self::schema::ingredients::dsl::*;
match ingredients.filter(alias.like(name))
.first(conn)
{
Ok(ingdt) => Some(ingdt),
Err(_) => None,
}
}
fn create(conn: &SqliteConnection, name: &str) -> Result<i32,String> {
use self::schema::ingredients::dsl::*;
let _ = diesel::insert_into(ingredients)
.values(&NewIngredient { alias: name }) .values(&NewIngredient { alias: name })
.execute(conn) .execute(conn)
.expect("Error inserting ingredient") .map_err(|e| format!("Error inserting ingredient ! {:?}", e))?;
let inserted = ingredients
.order(id.desc())
.first::<Ingredient>(conn)
.map_err(|e| format!("No ingredient at all ! {:?}", e))?;
Ok(inserted.id)
} }
} }

View File

@@ -57,8 +57,7 @@ pub mod fields {
} }
impl<DB: Backend> FromSql<SmallInt, DB> for RecipeCategory impl<DB: Backend> FromSql<SmallInt, DB> for RecipeCategory
where where i16: FromSql<SmallInt, DB>
i16: FromSql<SmallInt, DB>
{ {
fn from_sql(bytes: Option<&DB::RawValue>) -> deserialize::Result<Self> { fn from_sql(bytes: Option<&DB::RawValue>) -> deserialize::Result<Self> {
let v = i16::from_sql(bytes)?; let v = i16::from_sql(bytes)?;
@@ -68,13 +67,70 @@ pub mod fields {
} }
impl<DB: Backend> ToSql<SmallInt, DB> for RecipeCategory impl<DB: Backend> ToSql<SmallInt, DB> for RecipeCategory
where where i16: ToSql<SmallInt, DB>
i16: ToSql<SmallInt, DB>{ {
fn to_sql<W: Write>(&self, out: &mut Output<W, DB>) -> serialize::Result { fn to_sql<W: Write>(&self, out: &mut Output<W, DB>) -> serialize::Result {
i16::to_sql(&(*self as i16), out) i16::to_sql(&(*self as i16), out)
} }
} }
pub type IngredientId = i32;
#[derive(Debug, Clone, FromSqlRow, AsExpression)]
#[sql_type = "Text"]
pub struct IngredientList(Vec<IngredientId>);
/// Just a basic method for now
impl IngredientList {
pub fn new() -> Self {
IngredientList(Vec::new())
}
pub fn as_string(&self) -> String {
self.0.iter()
.map(|i| format!("{}", i))
.collect::<Vec<String>>()
.join(" ")
}
}
impl std::ops::Deref for IngredientList {
type Target = Vec<IngredientId>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl std::ops::DerefMut for IngredientList {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl<DB: Backend> FromSql<Text, DB> for IngredientList
where String: FromSql<Text, DB>
{
fn from_sql(bytes: Option<&DB::RawValue>) -> deserialize::Result<Self> {
let data = String::from_sql(bytes)?;
Ok(
IngredientList(
data.split(" ")
.map(|i| i.parse::<IngredientId>().unwrap())
.collect()
))
}
}
impl<DB: Backend> ToSql<Text, DB> for IngredientList
where String: ToSql<Text, DB>
{
fn to_sql<W: Write>(&self, out: &mut Output<W, DB>) -> serialize::Result {
String::to_sql(&self.as_string(), out)
}
}
} }
@@ -84,7 +140,7 @@ pub struct Recipe {
pub id: i32, pub id: i32,
pub title: String, pub title: String,
pub category: fields::RecipeCategory, pub category: fields::RecipeCategory,
pub ingredients: String, pub ingredients: fields::IngredientList,
pub preparation: String, pub preparation: String,
} }
@@ -93,20 +149,11 @@ pub struct Recipe {
pub struct NewRecipe<'a> { pub struct NewRecipe<'a> {
pub title: &'a str, pub title: &'a str,
pub category: fields::RecipeCategory, pub category: fields::RecipeCategory,
pub ingredients: &'a str, pub ingredients: fields::IngredientList,
pub preparation: &'a str, pub preparation: &'a str,
} }
impl<'a> NewRecipe<'a> { impl<'a> NewRecipe<'a> {
pub fn new(title: &'a str, category: fields::RecipeCategory, ingredients: &'a str, preparation: &'a str) -> Self {
NewRecipe{
title,
category,
ingredients,
preparation,
}
}
pub fn insert(self, conn: &SqliteConnection) -> Result<Self, String> { pub fn insert(self, conn: &SqliteConnection) -> Result<Self, String> {
diesel::insert_into(recipes::table) diesel::insert_into(recipes::table)
.values(&self) .values(&self)

View File

@@ -14,6 +14,7 @@ use self::planner::solver::{Variables, Domain, Problem};
type Day = String; type Day = String;
const DAYS: &[&str] = &["Lundi", "Mardi", "Mercredi"]; const DAYS: &[&str] = &["Lundi", "Mardi", "Mercredi"];
#[allow(dead_code)]
enum Meals { enum Meals {
Breakfast(Day), Breakfast(Day),
Lunch(Day), Lunch(Day),
@@ -41,8 +42,9 @@ fn generate_variables<V>(domain: &Domain<V>) -> Vec<(String, &Domain<V>, Option<
} }
fn ingredients_contains<'a>(assign: &Variables<'a,Recipe>) -> bool { fn ingredients_contains<'a>(assign: &Variables<'a,Recipe>) -> bool {
assign.get("Lundi_Lunch").unwrap().unwrap().ingredients.contains("Patates") let id = 0;
&& !assign.get("Mardi_Lunch").unwrap().unwrap().ingredients.contains("Patates") assign.get("Lundi_Lunch").unwrap().unwrap().ingredients.contains(&id)
&& !assign.get("Mardi_Lunch").unwrap().unwrap().ingredients.contains(&id)
} }

View File

@@ -13,6 +13,7 @@ enum Assignment<'a, V> {
Clear(String) Clear(String)
} }
type Domains<'a, V> = HashMap<String, &'a Domain<V>>; type Domains<'a, V> = HashMap<String, &'a Domain<V>>;
/// The domain of values that can be assigned to variables /// The domain of values that can be assigned to variables
#[derive(Clone)] #[derive(Clone)]
@@ -71,10 +72,11 @@ impl<'a,V> Problem<'a, V> {
/// Returns all possible Updates for next assignements, prepended with /// Returns all possible Updates for next assignements, prepended with
/// a Clear to ensure the variable is unset before when leaving the branch. /// a Clear to ensure the variable is unset before when leaving the branch.
fn _assign_next(&self) -> Option<Vec<Assignment<'a,V>>> { fn _push_updates(&self) -> Option<Vec<Assignment<'a,V>>> {
// TODO: should be able to inject a choosing strategy // TODO: should be able to inject a choosing strategy
if let Some((key,_)) = self.variables.iter().find(|(_, val)| val.is_none()) { if let Some((key,_)) = self.variables.iter().find(|(_, val)| val.is_none()) {
let domain = self.domains.get(key).expect("No domain for variable !"); let domain = self.domains.get(key).expect("No domain for variable !");
// Push a clear assignment first, just before going up the stack.
let mut updates = vec![Assignment::Clear(key.clone())]; let mut updates = vec![Assignment::Clear(key.clone())];
if domain.values.is_empty() { panic!("No value in domain !"); } if domain.values.is_empty() { panic!("No value in domain !"); }
@@ -96,13 +98,13 @@ impl<'a,V> Problem<'a, V> {
return true; return true;
} }
/// Visit all possible solutions, using a stack. /// Visit all possible solutions, using a stack (DFS).
pub fn solve_all(&mut self) -> Vec<Variables<'a,V>> pub fn solve_all(&mut self) -> Vec<Variables<'a,V>>
where V: Clone + fmt::Debug where V: Clone + fmt::Debug
{ {
let mut solutions: Vec<Variables<V>> = vec![]; let mut solutions: Vec<Variables<V>> = vec![];
let mut stack: Vec<Assignment<'a, V>> = vec![]; let mut stack: Vec<Assignment<'a, V>> = vec![];
stack.append(&mut self._assign_next().unwrap()); stack.append(&mut self._push_updates().unwrap());
loop { loop {
let node = stack.pop(); let node = stack.pop();
if node.is_none() { break; }; if node.is_none() { break; };
@@ -111,7 +113,7 @@ impl<'a,V> Problem<'a, V> {
// Assign the variable and open new branches, if any. // Assign the variable and open new branches, if any.
*self.variables.get_mut(&key).unwrap() = Some(val); *self.variables.get_mut(&key).unwrap() = Some(val);
// TODO: handle case of empty domain.values // TODO: handle case of empty domain.values
if let Some(mut nodes) = self._assign_next() { if let Some(mut nodes) = self._push_updates() {
stack.append(&mut nodes); stack.append(&mut nodes);
} else { } else {
// Assignements are completed // Assignements are completed

View File

@@ -20,7 +20,7 @@
<h4 class="title">{{ items[active_view].title }}</h4> <h4 class="title">{{ items[active_view].title }}</h4>
<h6 class="subtitle">{{ categories[items[active_view].category].name }}</h6> <h6 class="subtitle">{{ categories[items[active_view].category].name }}</h6>
<p><strong>{{ items[active_view].ingredients }}</strong></p> <p><strong>{{ items[active_view].ingredients }}</strong></p>
<button @click="deleteRecipe(active_view + 1)" class="button is-danger is-pulled-right">DELETE !</button> <button @click="deleteRecipe(items[active_view].id)" class="button is-danger is-pulled-right">DELETE !</button>
</div> </div>
</section> </section>
<!-- Category List View --> <!-- Category List View -->
@@ -75,9 +75,15 @@
deleteRecipe: function(id) { deleteRecipe: function(id) {
fetch("/api/delete/" + id) fetch("/api/delete/" + id)
.then((res) => res.json()) .then((res) => res.json())
.then((data) => console.log("Deleted :" + data)) .then((data) => {
if (data === true) {
this.items.splice(this.active_view, 1);
this.closeActiveView();
console.log("Deleted :" + data);
} else {
console.log("Error deleting");
}})
.catch((err) => console.error(err)); .catch((err) => console.error(err));
this.closeActiveView();
}, },
fetchRecipesList: function() { fetchRecipesList: function() {
fetch("/api/list") fetch("/api/list")

View File

@@ -40,7 +40,7 @@ mod api {
id: rec.id, id: rec.id,
title: rec.title, title: rec.title,
category: rec.category as i16, category: rec.category as i16,
ingredients: rec.ingredients, ingredients: rec.ingredients.as_string(),
preparation: rec.preparation, preparation: rec.preparation,
} }
} }