diff --git a/recipes.sql b/recipes.sql new file mode 100644 index 0000000..437f207 --- /dev/null +++ b/recipes.sql @@ -0,0 +1,121 @@ +CREATE TABLE keylookup ( + id INTEGER NOT NULL, + word TEXT, + item TEXT, + ingkey TEXT, + count INTEGER, + PRIMARY KEY (id) +); +CREATE TABLE info ( + version_super INTEGER, + version_major INTEGER, + version_minor INTEGER, + last_access INTEGER, + rowid INTEGER NOT NULL, + PRIMARY KEY (rowid) +); +CREATE TABLE recipe ( + id INTEGER NOT NULL, + title TEXT, + instructions TEXT, + modifications TEXT, + cuisine TEXT, + rating INTEGER, + description TEXT, + source TEXT, + preptime INTEGER, + cooktime INTEGER, + servings FLOAT, + yields FLOAT, + yield_unit VARCHAR(32), + image BLOB, + thumb BLOB, + deleted BOOLEAN, + recipe_hash VARCHAR(32), + ingredient_hash VARCHAR(32), + link TEXT, + last_modified INTEGER, + PRIMARY KEY (id), + CHECK (deleted IN (0, 1)) +); +CREATE TABLE plugin_info ( + plugin TEXT, + id INTEGER NOT NULL, + version_super INTEGER, + version_major INTEGER, + version_minor INTEGER, + plugin_version VARCHAR(32), + PRIMARY KEY (id) +); +CREATE TABLE categories ( + id INTEGER NOT NULL, + recipe_id INTEGER, + category TEXT, + PRIMARY KEY (id), + FOREIGN KEY(recipe_id) REFERENCES recipe (id) +); +CREATE TABLE ingredients ( + id INTEGER NOT NULL, + recipe_id INTEGER, + refid INTEGER, + unit TEXT, + amount FLOAT, + rangeamount FLOAT, + item TEXT, + ingkey TEXT, + optional BOOLEAN, + shopoptional INTEGER, + inggroup TEXT, + position INTEGER, + deleted BOOLEAN, + PRIMARY KEY (id), + CHECK (deleted IN (0, 1)), + FOREIGN KEY(recipe_id) REFERENCES recipe (id), + FOREIGN KEY(refid) REFERENCES recipe (id), + CHECK (optional IN (0, 1)) +); +CREATE TABLE IF NOT EXISTS "pantry" ( +id INTEGER NOT NULL, +ingkey TEXT(32), +pantry BOOLEAN, +PRIMARY KEY (id), +CHECK (pantry IN (0, 1)) +); +CREATE TABLE unitdict ( +id INTEGER NOT NULL, +ukey VARCHAR(150), +value VARCHAR(150), +PRIMARY KEY (id) +); +CREATE TABLE crossunitdict ( +id INTEGER NOT NULL, +cukey VARCHAR(150), +value VARCHAR(150), +PRIMARY KEY (id) +); +CREATE TABLE density ( +id INTEGER NOT NULL, +dkey VARCHAR(150), +value VARCHAR(150), +PRIMARY KEY (id) +); +CREATE TABLE shopcatsorder ( +id INTEGER NOT NULL, +shopcategory TEXT(32), +position INTEGER, +PRIMARY KEY (id) +); +CREATE TABLE convtable ( +id INTEGER NOT NULL, +ckey VARCHAR(150), +value VARCHAR(150), +PRIMARY KEY (id) +); +CREATE TABLE IF NOT EXISTS "shopcats" ( + id INTEGER NOT NULL, + ingkey TEXT(32), + shopcategory TEXT, + position INTEGER, + PRIMARY KEY (id) +); +CREATE TABLE IF NOT EXISTS "recipe_ingredients" ("Recipe_id" integer not null, "ingredientHash_id" integer not null, unique ("ingredientHash_id")); diff --git a/src/main/java/com/mousetech/gourmetj/AdminMainBean.java b/src/main/java/com/mousetech/gourmetj/AdminMainBean.java index ec2fe5f..31a6311 100644 --- a/src/main/java/com/mousetech/gourmetj/AdminMainBean.java +++ b/src/main/java/com/mousetech/gourmetj/AdminMainBean.java @@ -101,17 +101,15 @@ public class AdminMainBean implements Serializable { public void resetSuggestions() { suggestionList = null; } - + public List searchSuggestionList(String query) { if (suggestionList == null) { switch (this.userSession.getSearchType()) { case rst_BY_CATEGORY: - suggestionList = - recipeService.findCategories(); + suggestionList = recipeService.findCategories(); break; case rst_BY_CUISINE: - suggestionList = - recipeService.findCuisines(); + suggestionList = recipeService.findCuisines(); break; default: suggestionList = new ArrayList(1); @@ -119,7 +117,7 @@ public class AdminMainBean implements Serializable { } return suggestionList; } - + /**/ transient DataModel searchResults; @@ -220,6 +218,14 @@ public class AdminMainBean implements Serializable { return "detailEdit?faces-redirect=true"; } + /** + * Navigate to "More features" page (shopping list and + * maint.) + */ + public String doMore() { + return "shoppingList.jsf"; + } + /** * Show selected recipe * diff --git a/src/main/java/com/mousetech/gourmetj/RecipeDetailBean.java b/src/main/java/com/mousetech/gourmetj/RecipeDetailBean.java index 9b6d326..5f95e1c 100644 --- a/src/main/java/com/mousetech/gourmetj/RecipeDetailBean.java +++ b/src/main/java/com/mousetech/gourmetj/RecipeDetailBean.java @@ -243,6 +243,9 @@ public class RecipeDetailBean implements Serializable { buildIngredientFacade(recipe.getIngredientHash())); stringifyCategories(recipe); + this.shop = this.getUserSession().getShoppingList() + .contains(recipe); + log.info("Set recipe: " + this.recipe); } @@ -262,8 +265,8 @@ public class RecipeDetailBean implements Serializable { } /** - * Categories are a Set attached to recipe. Build - * a displayable comma-separated list of them. + * Categories are a Set attached to recipe. Build a + * displayable comma-separated list of them. * * @param recipe Recipe to get categories from. * @@ -667,6 +670,26 @@ public class RecipeDetailBean implements Serializable { .getWrappedData(); } + private boolean shop = false; + + public boolean isShop() { + return shop; + } + + /** + * Add/remove recipe to shopping list (toggle) + */ + public void doShop() { + shop = !shop; + List shoppingList = + userSession.getShoppingList(); + if (shop) { + shoppingList.add(recipe); + } else { + shoppingList.remove(recipe); + } + } + /** * Save the recipe. * diff --git a/src/main/java/com/mousetech/gourmetj/ShopIngredient.java b/src/main/java/com/mousetech/gourmetj/ShopIngredient.java new file mode 100644 index 0000000..1b71036 --- /dev/null +++ b/src/main/java/com/mousetech/gourmetj/ShopIngredient.java @@ -0,0 +1,161 @@ +package com.mousetech.gourmetj; + +import com.mousetech.gourmetj.utils.IngredientDigester; +import com.mousetech.gourmetj.utils.IngredientDigester.IngredientAmountFormat; + +public class ShopIngredient implements Comparable { + /** + * Constructor. + * + * @param shopCat + * @param ingkey + */ + public ShopIngredient( String shopCat, String item, + String ingkey) { + this.shopCat = shopCat; + this.ingkey = ingkey; + } + + /** + * Constructor. + * + * @param amount + * @param unit + * @param item + * @param ingkey + * @param shopCat + */ + public ShopIngredient( double amount, String unit, + String item, String ingkey, + String shopCat) { + this.amount = amount; + this.unit = unit; + this.item = item; + this.ingkey = ingkey; + this.shopCat = shopCat; + } + + private String shopCat; + + /** + * @return the shopCat + */ + public String getShopCat() { + return shopCat; + } + + /** + * @param shopCat the shopCat to set + */ + public void setShopCat(String shopCat) { + this.shopCat = shopCat; + } + + /** + * @return the ingkey + */ + public String getIngkey() { + return ingkey; + } + + /** + * @param ingkey the ingkey to set + */ + public void setIngkey(String ingkey) { + this.ingkey = ingkey; + } + + /** + * @return the amount + */ + public double getAmount() { + return amount; + } + + /** + * @param amount the amount to set + */ + public void setAmount(double amount) { + this.amount = amount; + } + + /** + * @return the displayAmount + */ + public String getDisplayAmount() { + return IngredientDigester.displayAmount( + IngredientAmountFormat.IA_TEXT, this.getAmount(), + null); + } + + /** + * @return the unit + */ + public String getUnit() { + return unit; + } + + /** + * @param unit the unit to set + */ + public void setUnit(String unit) { + this.unit = unit; + } + + private String item; + + /** + * @return the item + */ + public String getItem() { + return item; + } + + private String ingkey; + private double amount; + private String displayAmount; + private String unit; + + @Override + public int compareTo(Object o) { + if ((o == null) || !(o instanceof ShopIngredient)) { + throw new RuntimeException( + "Invalid shipIngredient comparison"); + } + ShopIngredient o1 = (ShopIngredient) o; + int i = relate(this.getItem(), o1.getItem()); + if (i != 0) { + return i; + } + i = relate(this.getShopCat(), o1.getShopCat()); + if (i != 0) { + return i; + } + // TODO: normalize case, singular/plural/abbreviations + i = relate(this.getUnit(), o1.getUnit()); + if (i != 0) { + return i; + } + return i; // ZERO + } + + private int relate(String item2, String item3) { + if ((item2 == null) && (item3 == null)) { + return 0; + } + if (item2 == null) { + return -1; + } + if (item3 == null) { + return 1; + } + return item2.compareTo(item3); + } + + @Override + public String toString() { + return this.getAmount() + " " + this.getUnit() + " " + + this.getItem() + " " + this.getIngkey() + " " + + this.getShopCat(); + } +} diff --git a/src/main/java/com/mousetech/gourmetj/ShoppingListBean.java b/src/main/java/com/mousetech/gourmetj/ShoppingListBean.java new file mode 100644 index 0000000..fc57512 --- /dev/null +++ b/src/main/java/com/mousetech/gourmetj/ShoppingListBean.java @@ -0,0 +1,213 @@ +package com.mousetech.gourmetj; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +import javax.annotation.PostConstruct; +import javax.faces.event.AjaxBehaviorEvent; +import javax.faces.view.ViewScoped; +import javax.inject.Inject; +import javax.inject.Named; + +import org.apache.commons.lang3.StringUtils; +import org.primefaces.event.ReorderEvent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.mousetech.gourmetj.persistence.dao.ShopcatRepository; +import com.mousetech.gourmetj.persistence.model.Ingredient; +import com.mousetech.gourmetj.persistence.model.Recipe; +import com.mousetech.gourmetj.persistence.model.Shopcat; + +@Named +@ViewScoped +public class ShoppingListBean implements Serializable { + + /** + * Serial version for session save/restore + */ + private static final long serialVersionUID = + 7449440266704831598L; + + /* Logger */ + + @SuppressWarnings("unused") + private static final Logger log = + LoggerFactory.getLogger(ShoppingListBean.class); + + @Inject + private UserSession userSession; + + private List siList; + +// private List ingredientList; + + @PostConstruct + public void init() { + // Load up details on recipes + this.siList = new ArrayList(30); + buildMaps(); + } + + public List getRecipeList() { + return this.userSession.getShoppingList(); + } + + public List getIngredientList() { + return this.siList; + } + + private void buildMaps() { + for (Recipe r : this.getRecipeList()) { + buildMapsFor(r); + } + // Now consolidate amounts and sort by + // shopcat/item/ingkey + optimizeIngredients(this.siList); + + this.siList.sort(new ShopclassComparator()); + } + + /** + * Run the ingredient list for the selected recipe and add + * them to siList + * + * @param r + * + * @see #buildMaps() + */ + private void buildMapsFor(Recipe r) { + for (Ingredient ing : r.getIngredientHash()) { + String ingkey = ing.getIngkey(); + if (StringUtils.isBlank(ingkey)) { + continue; + } + String shopCatName = ing.getShopCat() != null + ? ing.getShopCat().getShopcategory() + : null; + ShopIngredient sing = new ShopIngredient( + ing.getAmount(), ing.getUnit(), + ing.getItem(), ing.getIngkey(), shopCatName); + siList.add(sing); + } + } + + /** + * Sort ShopIngredient list, then optimize it by + * consolidating amounts where possible. + * + * @param victim List to optimize + * + * #TestedBy @see ShoppingListBeanTest + */ + static void optimizeIngredients( + List victim) { + victim.sort(null); + for (int i = 0; i < (victim.size() - 1); i++) { + ShopIngredient si = victim.get(i); + ShopIngredient si2 = victim.get(i + 1); + if (si.compareTo(si2) == 0) { + si.setAmount(si.getAmount() + si2.getAmount()); + victim.remove(si2); // reduces size() + } + } + } + + class ShopclassComparator + implements Comparator { + + @Override + public int compare(ShopIngredient ing1, + ShopIngredient ing2) { + int i = 0; + i = relate(ing1.getShopCat(), ing2.getShopCat()); + if (i != 0) { + return i; + } + i = relate(ing1.getItem(), ing2.getItem()); + if (i != 0) { + return i; + } + i = relate(ing1.getIngkey(), ing2.getIngkey()); + if (i != 0) { + return i; + } + return 0; + } + + private int relate(String item2, String item3) { + if ((item2 == null) && (item3 == null)) { + return 0; + } + if (item2 == null) { + return -1; + } + if (item3 == null) { + return 1; + } + return item2.compareTo(item3); + } + } + + private List shopcatList; + + public List getShopcatList() { + if (shopcatList == null) { + shopcatList = loadShopcatList(); + } + return shopcatList; + } + + @Inject + ShopcatRepository shopcatRepository; + + private List loadShopcatList() { + return shopcatRepository + .findAllByOrderByShopcategoryAsc(); + } + + private Shopcat xeditShopcat = new Shopcat(); + + private String oldShopcategoryName; + + /** + * @return the editShopcat + */ + public Shopcat getEditShopcat() { + return xeditShopcat; + } + + public void doEditShopcat(int scId) { + xeditShopcat = null; + final List scl = getShopcatList(); + for (Shopcat sc : scl) { + if (sc.getId() == scId) { + xeditShopcat = sc; + this.oldShopcategoryName = sc.getShopcategory(); + if (sc.getPosition() == null) { + sc.setPosition(0); + } + return; + } + } + log.error("SHOPCAT " + scId + " NOT FOUND"); + } + + public void ajaxOnClickShopcatIngkey() { + // Saves 1 shopcat/ingkey + this.shopcatRepository.save(xeditShopcat); + this.shopcatList = null; + } + + /** + * Updates all ingredient keys for shopcat name-change. + * Note that once done, this cannot be undone! + */ + public void ajaxOnClickShopcat() { + this.shopcatRepository.UpdateShopcats( + this.oldShopcategoryName, xeditShopcat.getShopcategory()); + this.shopcatList = null; + } +} diff --git a/src/main/java/com/mousetech/gourmetj/UserSession.java b/src/main/java/com/mousetech/gourmetj/UserSession.java index ec8acb4..5d8e0c6 100644 --- a/src/main/java/com/mousetech/gourmetj/UserSession.java +++ b/src/main/java/com/mousetech/gourmetj/UserSession.java @@ -182,6 +182,13 @@ public class UserSession implements Serializable { // Session timeout, ms (25 minutes) long sessionTimeoutInterval = 25 * 60_000L; + /** + * When you click the "shop" button on a recipe, it + * gets added to this list. Note that it's the fully-expanded + * recipe! + */ + private List shoppingList = new ArrayList(); + /** * @return the sessionTimeoutInterval */ @@ -199,4 +206,8 @@ public class UserSession implements Serializable { log.warn("Session Idle listener logout"); return "/main.jsf"; } + + public List getShoppingList() { + return this.shoppingList ; + } } diff --git a/src/main/java/com/mousetech/gourmetj/persistence/dao/ShopcatRepository.java b/src/main/java/com/mousetech/gourmetj/persistence/dao/ShopcatRepository.java index 164fa39..15c7886 100644 --- a/src/main/java/com/mousetech/gourmetj/persistence/dao/ShopcatRepository.java +++ b/src/main/java/com/mousetech/gourmetj/persistence/dao/ShopcatRepository.java @@ -2,7 +2,10 @@ package com.mousetech.gourmetj.persistence.dao; import java.util.List; +import javax.transaction.Transactional; + import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; @@ -28,7 +31,14 @@ public interface ShopcatRepository @Query(value = SQL_FIND_CATEGORIES, nativeQuery = true) public List findDistinctCategoryNative(); + @Transactional + @Query(value = "UPDATE Shopcat set shopcategory = :newCatname WHERE shopcategory = :oldCatname") + @Modifying + public int UpdateShopcats(String oldCatname, String newCatname); + public Shopcat findShopcatByIngkey(String ingkey); public void deleteByIngkey(String key); + + public List findAllByOrderByShopcategoryAsc(); } diff --git a/src/main/resources/META-INF/resources/WEB-INF/layout/layout.xhtml b/src/main/resources/META-INF/resources/WEB-INF/layout/layout.xhtml index 5f66996..fc588df 100644 --- a/src/main/resources/META-INF/resources/WEB-INF/layout/layout.xhtml +++ b/src/main/resources/META-INF/resources/WEB-INF/layout/layout.xhtml @@ -21,6 +21,7 @@

Gourmet Recipe Manager (web version)

+ @@ -29,10 +30,10 @@ href="http://www.apache.org/licenses/LICENSE-2.0" >Apache License, Version 2.0.

Based on Gourmet Recipe Manager by T. Hinkle

- + @@ -60,7 +61,7 @@ severity="alert" widgetVar="opError" > diff --git a/src/main/resources/META-INF/resources/css/style.css b/src/main/resources/META-INF/resources/css/style.css index 2c80602..52fdb11 100644 --- a/src/main/resources/META-INF/resources/css/style.css +++ b/src/main/resources/META-INF/resources/css/style.css @@ -13,7 +13,11 @@ font-weight: bold; } +.greenButton { + background-color: green !important; +} + textarea { font-family: 'latoregular', 'Trebuchet MS,Arial,Helvetica,sans-serif'; font-size: 1em -} \ No newline at end of file +} diff --git a/src/main/resources/META-INF/resources/editShopcatList.xhtml b/src/main/resources/META-INF/resources/editShopcatList.xhtml new file mode 100644 index 0000000..1d42743 --- /dev/null +++ b/src/main/resources/META-INF/resources/editShopcatList.xhtml @@ -0,0 +1,68 @@ + + + + Shopping Category + + + + + + + + + + + + + +
Ingredient key: + #{editShopcatBean.ingkey}
+ + + + + + + + + + + + +
+
+
+
+ \ No newline at end of file diff --git a/src/main/resources/META-INF/resources/main.xhtml b/src/main/resources/META-INF/resources/main.xhtml index e3263c1..8ce1080 100644 --- a/src/main/resources/META-INF/resources/main.xhtml +++ b/src/main/resources/META-INF/resources/main.xhtml @@ -43,6 +43,9 @@ + diff --git a/src/main/resources/META-INF/resources/recipeDetails.xhtml b/src/main/resources/META-INF/resources/recipeDetails.xhtml index d3a8077..aaa6a54 100644 --- a/src/main/resources/META-INF/resources/recipeDetails.xhtml +++ b/src/main/resources/META-INF/resources/recipeDetails.xhtml @@ -57,7 +57,16 @@ styleClass="ui-button-print" immediate="true" /> - + + + + Gourmet Recipe Manager - Shopping + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/java/com/mousetech/gourmetj/ShoppingListBeanTest.java b/src/test/java/com/mousetech/gourmetj/ShoppingListBeanTest.java new file mode 100644 index 0000000..a5eb3a7 --- /dev/null +++ b/src/test/java/com/mousetech/gourmetj/ShoppingListBeanTest.java @@ -0,0 +1,52 @@ +/** + * Copyright (C) 2022, Tim Holloway + * + * Date written: Jan 12, 2022 + * Author: Tim Holloway + */ +package com.mousetech.gourmetj; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import com.mousetech.gourmetj.ShopIngredient; +import com.mousetech.gourmetj.persistence.model.Shopcat; + +/** + * @author timh + * @since Jan 12, 2022 + */ +class ShoppingListBeanTest { + + static List testList; + + /** + * @throws java.lang.Exception + */ + @BeforeAll + static void setUpBeforeClass() throws Exception { + testList = new ArrayList(); + testList.add(new ShopIngredient(2.0d, "cup", + "sugar", "sugar", "baking")); + testList.add(new ShopIngredient(1.5, "tsp", + "salt", "salt", "condiments")); + testList.add(new ShopIngredient(0.5, "tsp", + "pepper", "pepper", "condiments")); + testList.add(new ShopIngredient(2.0d, "cup", + "milk", "milk", "dairy")); + testList.add(new ShopIngredient(0.75d, "cup", + "sugar", "sugar", "baking")); + } + + @Test + void test() { + ShoppingListBean.optimizeIngredients(testList); + assertEquals(4, testList.size()); + } + +}