From 349fec17ac8c6121f784ace130f45eb3e6d7ed3a Mon Sep 17 00:00:00 2001 From: Tim Holloway Date: Tue, 11 Jan 2022 19:29:03 -0500 Subject: [PATCH 1/6] Update revision --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index c8447cb..0aca58b 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ com.mousetech.gourmet gourmetj - 0.1.3-SNAPSHOT + 0.1.4-SNAPSHOT jar GourmetJ From 3fd7bdb842f9279ecf029e9941a4237012672431 Mon Sep 17 00:00:00 2001 From: Tim Holloway Date: Thu, 13 Jan 2022 18:46:28 -0500 Subject: [PATCH 2/6] Editable shopping categories --- recipes.sql | 121 ++++++++++ .../com/mousetech/gourmetj/AdminMainBean.java | 18 +- .../mousetech/gourmetj/RecipeDetailBean.java | 27 ++- .../mousetech/gourmetj/ShopIngredient.java | 161 +++++++++++++ .../mousetech/gourmetj/ShoppingListBean.java | 213 ++++++++++++++++++ .../com/mousetech/gourmetj/UserSession.java | 11 + .../persistence/dao/ShopcatRepository.java | 10 + .../resources/WEB-INF/layout/layout.xhtml | 11 +- .../META-INF/resources/css/style.css | 6 +- .../META-INF/resources/editShopcatList.xhtml | 68 ++++++ .../resources/META-INF/resources/main.xhtml | 3 + .../META-INF/resources/recipeDetails.xhtml | 14 +- .../META-INF/resources/shoppingList.xhtml | 198 ++++++++++++++++ .../gourmetj/ShoppingListBeanTest.java | 52 +++++ 14 files changed, 896 insertions(+), 17 deletions(-) create mode 100644 recipes.sql create mode 100644 src/main/java/com/mousetech/gourmetj/ShopIngredient.java create mode 100644 src/main/java/com/mousetech/gourmetj/ShoppingListBean.java create mode 100644 src/main/resources/META-INF/resources/editShopcatList.xhtml create mode 100644 src/main/resources/META-INF/resources/shoppingList.xhtml create mode 100644 src/test/java/com/mousetech/gourmetj/ShoppingListBeanTest.java 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()); + } + +} From 1290c660556d811a3db1d84cf7d095058c3589cf Mon Sep 17 00:00:00 2001 From: Tim Holloway Date: Thu, 13 Jan 2022 19:10:53 -0500 Subject: [PATCH 3/6] Moved ingkey/shopcat edit to a separate panel file. --- .../WEB-INF/layout/misctabs/ingshopkey.xhtml | 121 ++++++++++++++++++ .../META-INF/resources/shoppingList.xhtml | 119 +---------------- 2 files changed, 122 insertions(+), 118 deletions(-) create mode 100644 src/main/resources/META-INF/resources/WEB-INF/layout/misctabs/ingshopkey.xhtml diff --git a/src/main/resources/META-INF/resources/WEB-INF/layout/misctabs/ingshopkey.xhtml b/src/main/resources/META-INF/resources/WEB-INF/layout/misctabs/ingshopkey.xhtml new file mode 100644 index 0000000..bbf75cc --- /dev/null +++ b/src/main/resources/META-INF/resources/WEB-INF/layout/misctabs/ingshopkey.xhtml @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/META-INF/resources/shoppingList.xhtml b/src/main/resources/META-INF/resources/shoppingList.xhtml index a63fc96..7be6e37 100644 --- a/src/main/resources/META-INF/resources/shoppingList.xhtml +++ b/src/main/resources/META-INF/resources/shoppingList.xhtml @@ -74,125 +74,8 @@ icon="ui-icon-home" ajax="false" immediate="true" action="main.jsf" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file From 75a8487cfb2d6a403e1b0d7a1d4391e74dda88ea Mon Sep 17 00:00:00 2001 From: Tim Holloway Date: Fri, 14 Jan 2022 17:27:26 -0500 Subject: [PATCH 4/6] Tab for editing shopping categories completed. --- .../mousetech/gourmetj/ShoppingListBean.java | 134 ++++++++++-- .../persistence/dao/ShopcatRepository.java | 21 +- .../resources/WEB-INF/layout/layout.xhtml | 14 +- .../WEB-INF/layout/misctabs/ingshopkey.xhtml | 199 ++++++++---------- .../META-INF/resources/css/style.css | 6 + .../META-INF/resources/shoppingList.xhtml | 118 +++++++---- 6 files changed, 305 insertions(+), 187 deletions(-) diff --git a/src/main/java/com/mousetech/gourmetj/ShoppingListBean.java b/src/main/java/com/mousetech/gourmetj/ShoppingListBean.java index fc57512..0d73dc3 100644 --- a/src/main/java/com/mousetech/gourmetj/ShoppingListBean.java +++ b/src/main/java/com/mousetech/gourmetj/ShoppingListBean.java @@ -12,7 +12,6 @@ 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; @@ -151,9 +150,9 @@ public class ShoppingListBean implements Serializable { } } - private List shopcatList; + private List shopcatList; - public List getShopcatList() { + public List getShopcatList() { if (shopcatList == null) { shopcatList = loadShopcatList(); } @@ -163,9 +162,9 @@ public class ShoppingListBean implements Serializable { @Inject ShopcatRepository shopcatRepository; - private List loadShopcatList() { - return shopcatRepository - .findAllByOrderByShopcategoryAsc(); + private List loadShopcatList() { + return shopcatRepository.findDistinctCategoryNative(); +// .findAllByOrderByShopcategoryAsc(); } private Shopcat xeditShopcat = new Shopcat(); @@ -180,19 +179,19 @@ public class ShoppingListBean implements Serializable { } 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"); +// 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() { @@ -202,12 +201,103 @@ public class ShoppingListBean implements Serializable { } /** - * Updates all ingredient keys for shopcat name-change. - * Note that once done, this cannot be undone! + * 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.oldShopcategoryName, + xeditShopcat.getShopcategory()); this.shopcatList = null; } + + // === + private String selectedShopcat; + + /** + * @return the selectedShopcat + */ + public String getSelectedShopcat() { + return selectedShopcat; + } + + /** + * @param selectedShopcat the selectedShopcat to set + */ + public void setSelectedShopcat(String selectedShopcat) { + this.selectedShopcat = selectedShopcat; + this.ingkeyList = null; + } + + private List selectedIngkey; + + /** + * @return the selectedIngkey + */ + public List getSelectedIngkey() { + return selectedIngkey; + } + + /** + * @param selectedIngkey the selectedIngkey to set + */ + public void setSelectedIngkey(List selectedIngkey) { + this.selectedIngkey = selectedIngkey; + } + + private List ingkeyList; + + /** + * @return the ingkeyList + */ + public List getIngkeyList() { + if (ingkeyList == null) { + ingkeyList = loadIngkeyListFor(selectedShopcat); + } + return ingkeyList; + } + + private List loadIngkeyListFor( + String selectedShopcat2) { + List list = this.shopcatRepository + .findByIngkeySorted(selectedShopcat2); + return list; + } + + private String newShopcat; + + /** + * @return the newShopcat + */ + public String getNewShopcat() { + return newShopcat; + } + + /** + * @param newShopcat the newShopcat to set + */ + public void setNewShopcat(String newShopcat) { + this.newShopcat = newShopcat; + } + + public List suggestShopcat(String query) { + return this.shopcatList; + } + + public void doChangeShopcat() { + String oldCat = this.getSelectedShopcat(); + String newCat = this.getNewShopcat(); + if (oldCat.equals(newCat)) { + return; // effective NO-OP + } + newCat = newCat.trim(); + if ( StringUtils.isBlank(newCat)) { + this.shopcatRepository + .deleteShopcatFor(this.getSelectedIngkey()); + } else { + this.shopcatRepository + .updateShopcatFor(newCat, this.getSelectedIngkey()); + } + } + } 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 15c7886..576a784 100644 --- a/src/main/java/com/mousetech/gourmetj/persistence/dao/ShopcatRepository.java +++ b/src/main/java/com/mousetech/gourmetj/persistence/dao/ShopcatRepository.java @@ -12,8 +12,8 @@ import org.springframework.stereotype.Repository; import com.mousetech.gourmetj.persistence.model.Shopcat; /** - * JpaRepository for Shopping Categories, which relate ManyToOne to Ingredient. - * Service method is @see RecipeService + * JpaRepository for Shopping Categories, which relate ManyToOne + * to Ingredient. Service method is @see RecipeService * * @author timh * @since Dec 28, 2021 @@ -34,11 +34,24 @@ public interface ShopcatRepository @Transactional @Query(value = "UPDATE Shopcat set shopcategory = :newCatname WHERE shopcategory = :oldCatname") @Modifying - public int UpdateShopcats(String oldCatname, String newCatname); - + public int UpdateShopcats(String oldCatname, + String newCatname); + public Shopcat findShopcatByIngkey(String ingkey); public void deleteByIngkey(String key); public List findAllByOrderByShopcategoryAsc(); + + @Query(value = "SELECT s.ingkey FROM Shopcat s WHERE s.shopcategory = :shopcat ORDER BY s.ingkey") + public List findByIngkeySorted(String shopcat); + + @Transactional + @Query(value = "UPDATE Shopcat set shopcategory = :newCat WHERE ingkey IN :selectedIngkey") + @Modifying + public void updateShopcatFor(String newCat, List selectedIngkey); + + @Transactional + @Query(value = "DELETE Shopcat WHERE ingkey IN :selectedIngkey") + @Modifying public void deleteShopcatFor(List selectedIngkey); } 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 fc588df..800c7d5 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 @@ -25,11 +25,15 @@ - - (C) 2021 Tim Holloway, Licensed under the Apache License, Version 2.0. -

Based on Gourmet Recipe Manager by T. Hinkle

+ + + - - - - - - - - - - - - - - - + + + + - + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/META-INF/resources/css/style.css b/src/main/resources/META-INF/resources/css/style.css index 52fdb11..af4381a 100644 --- a/src/main/resources/META-INF/resources/css/style.css +++ b/src/main/resources/META-INF/resources/css/style.css @@ -21,3 +21,9 @@ textarea { font-family: 'latoregular', 'Trebuchet MS,Arial,Helvetica,sans-serif'; font-size: 1em } + +#footer { + position: absolute; + bottom: 90px; + width: 100%; +} \ No newline at end of file diff --git a/src/main/resources/META-INF/resources/shoppingList.xhtml b/src/main/resources/META-INF/resources/shoppingList.xhtml index 7be6e37..5cdc360 100644 --- a/src/main/resources/META-INF/resources/shoppingList.xhtml +++ b/src/main/resources/META-INF/resources/shoppingList.xhtml @@ -25,57 +25,87 @@ } - - - - - - - - - - - - - - - + + + + + + + + + - - - - - + + - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - + + + + + + + + - - \ No newline at end of file From 18410682de1b45737ff11cb477e77e74e0c4be6a Mon Sep 17 00:00:00 2001 From: Tim Holloway Date: Sat, 15 Jan 2022 18:06:45 -0500 Subject: [PATCH 5/6] Have downloadable shopping list --- .../mousetech/gourmetj/ShopIngredient.java | 14 +- .../mousetech/gourmetj/ShoppingListBean.java | 124 +++++++++++++++--- .../META-INF/resources/css/style.css | 1 - .../resources/META-INF/resources/main.xhtml | 11 +- .../META-INF/resources/shoppingList.xhtml | 46 +++++-- 5 files changed, 166 insertions(+), 30 deletions(-) diff --git a/src/main/java/com/mousetech/gourmetj/ShopIngredient.java b/src/main/java/com/mousetech/gourmetj/ShopIngredient.java index 1b71036..8d8321a 100644 --- a/src/main/java/com/mousetech/gourmetj/ShopIngredient.java +++ b/src/main/java/com/mousetech/gourmetj/ShopIngredient.java @@ -25,7 +25,7 @@ public class ShopIngredient implements Comparable { * @param ingkey * @param shopCat */ - public ShopIngredient( double amount, String unit, + public ShopIngredient( Double amount, String unit, String item, String ingkey, String shopCat) { this.amount = amount; @@ -68,14 +68,14 @@ public class ShopIngredient implements Comparable { /** * @return the amount */ - public double getAmount() { + public Double getAmount() { return amount; } /** * @param amount the amount to set */ - public void setAmount(double amount) { + public void setAmount(Double amount) { this.amount = amount; } @@ -83,8 +83,12 @@ public class ShopIngredient implements Comparable { * @return the displayAmount */ public String getDisplayAmount() { + Double amt = this.getAmount(); + if ( amt == null) { + return ""; + } return IngredientDigester.displayAmount( - IngredientAmountFormat.IA_TEXT, this.getAmount(), + IngredientAmountFormat.IA_TEXT, amt, null); } @@ -112,7 +116,7 @@ public class ShopIngredient implements Comparable { } private String ingkey; - private double amount; + private Double amount; private String displayAmount; private String unit; diff --git a/src/main/java/com/mousetech/gourmetj/ShoppingListBean.java b/src/main/java/com/mousetech/gourmetj/ShoppingListBean.java index 0d73dc3..6dbe31a 100644 --- a/src/main/java/com/mousetech/gourmetj/ShoppingListBean.java +++ b/src/main/java/com/mousetech/gourmetj/ShoppingListBean.java @@ -1,17 +1,22 @@ package com.mousetech.gourmetj; +import java.io.ByteArrayOutputStream; +import java.io.PrintWriter; import java.io.Serializable; import java.util.ArrayList; import java.util.Comparator; import java.util.List; +import java.util.stream.Collectors; 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.model.ByteArrayContent; +import org.primefaces.model.DefaultStreamedContent; +import org.primefaces.model.StreamedContent; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -19,11 +24,57 @@ 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; +import com.mousetech.gourmetj.utils.YamlShoppingList; @Named @ViewScoped public class ShoppingListBean implements Serializable { + public class RecipeReference { + + private int count; + private Recipe recipe; + + /** + * Constructor Constructor. + * + * @param r Recipe to reference (from Shopping List) + */ + public RecipeReference(Recipe r) { + count = 1; + recipe = r; + } + + /** + * @return the count + */ + public int getCount() { + return count; + } + + /** + * @param count the count to set + */ + public void setCount(int count) { + this.count = count; + } + + /** + * @return the recipe + */ + public Recipe getRecipe() { + return recipe; + } + + /** + * @param recipe the recipe to set + */ + public void setRecipe(Recipe recipe) { + this.recipe = recipe; + } + + } + /** * Serial version for session save/restore */ @@ -41,7 +92,7 @@ public class ShoppingListBean implements Serializable { private List siList; -// private List ingredientList; + private List recipeList; @PostConstruct public void init() { @@ -50,8 +101,19 @@ public class ShoppingListBean implements Serializable { buildMaps(); } - public List getRecipeList() { - return this.userSession.getShoppingList(); + public List getRecipeList() { + if (this.recipeList == null) { + this.recipeList = loadRecipeList(); + } + return this.recipeList; + } + + private List loadRecipeList() { + List list = + userSession.getShoppingList().stream() + .map(r -> new RecipeReference(r)) + .collect(Collectors.toList()); + return list; } public List getIngredientList() { @@ -59,7 +121,8 @@ public class ShoppingListBean implements Serializable { } private void buildMaps() { - for (Recipe r : this.getRecipeList()) { + this.siList = new ArrayList(30); + for (RecipeReference r : this.getRecipeList()) { buildMapsFor(r); } // Now consolidate amounts and sort by @@ -77,8 +140,13 @@ public class ShoppingListBean implements Serializable { * * @see #buildMaps() */ - private void buildMapsFor(Recipe r) { - for (Ingredient ing : r.getIngredientHash()) { + private void buildMapsFor(RecipeReference r) { + final int multiplier = r.getCount(); + if (multiplier == 0) { + return; + } + for (Ingredient ing : r.getRecipe() + .getIngredientHash()) { String ingkey = ing.getIngkey(); if (StringUtils.isBlank(ingkey)) { continue; @@ -86,10 +154,21 @@ public class ShoppingListBean implements Serializable { 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); + ShopIngredient sing; + try { + Double amt = ing.getAmount(); + if (multiplier > 1 && (amt != null)) { + amt *= multiplier; + } + sing = new ShopIngredient(amt, ing.getUnit(), + ing.getItem(), ing.getIngkey(), + shopCatName); + siList.add(sing); + } catch (Exception e) { + log.error("Unable to create ShopIngredient for " + + r.getRecipe() + " Ingredient " + ing); + e.printStackTrace(); + } } } @@ -150,6 +229,12 @@ public class ShoppingListBean implements Serializable { } } + public StreamedContent getDlIngredientList() { + return YamlShoppingList + .createDownload(getIngredientList()); + } + + // ============================================= private List shopcatList; public List getShopcatList() { @@ -211,6 +296,15 @@ public class ShoppingListBean implements Serializable { this.shopcatList = null; } + /** + * Primefaces AJAX listener for changes to amount values of + * recipes the recipe list. Forces re-computation of + * ingredient requirements. + */ + public void pfAmountChange() { + buildMaps(); + } + // === private String selectedShopcat; @@ -291,12 +385,12 @@ public class ShoppingListBean implements Serializable { return; // effective NO-OP } newCat = newCat.trim(); - if ( StringUtils.isBlank(newCat)) { + if (StringUtils.isBlank(newCat)) { this.shopcatRepository - .deleteShopcatFor(this.getSelectedIngkey()); + .deleteShopcatFor(this.getSelectedIngkey()); } else { - this.shopcatRepository - .updateShopcatFor(newCat, this.getSelectedIngkey()); + this.shopcatRepository.updateShopcatFor(newCat, + this.getSelectedIngkey()); } } diff --git a/src/main/resources/META-INF/resources/css/style.css b/src/main/resources/META-INF/resources/css/style.css index af4381a..fc7c387 100644 --- a/src/main/resources/META-INF/resources/css/style.css +++ b/src/main/resources/META-INF/resources/css/style.css @@ -23,7 +23,6 @@ textarea { } #footer { - position: absolute; bottom: 90px; width: 100%; } \ 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 8ce1080..3106eb5 100644 --- a/src/main/resources/META-INF/resources/main.xhtml +++ b/src/main/resources/META-INF/resources/main.xhtml @@ -33,7 +33,9 @@ - + + + diff --git a/src/main/resources/META-INF/resources/shoppingList.xhtml b/src/main/resources/META-INF/resources/shoppingList.xhtml index 5cdc360..81b4132 100644 --- a/src/main/resources/META-INF/resources/shoppingList.xhtml +++ b/src/main/resources/META-INF/resources/shoppingList.xhtml @@ -23,6 +23,15 @@ border-width: 0; border-style: none; } + +.noRecipe { + text-decoration: line-through; + color: gray; +} + +.plusRecipe { + +} + + + + + - + - + + + + + + @@ -64,11 +93,6 @@ /> - - - @@ -95,11 +119,17 @@ - + + + + + Date: Sat, 15 Jan 2022 18:07:01 -0500 Subject: [PATCH 6/6] Have downloadable shopping list --- .../gourmetj/utils/YamlShoppingList.java | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 src/main/java/com/mousetech/gourmetj/utils/YamlShoppingList.java diff --git a/src/main/java/com/mousetech/gourmetj/utils/YamlShoppingList.java b/src/main/java/com/mousetech/gourmetj/utils/YamlShoppingList.java new file mode 100644 index 0000000..61c23e9 --- /dev/null +++ b/src/main/java/com/mousetech/gourmetj/utils/YamlShoppingList.java @@ -0,0 +1,71 @@ +package com.mousetech.gourmetj.utils; + +import java.io.ByteArrayOutputStream; +import java.io.PrintWriter; +import java.util.List; + +import org.apache.commons.lang3.StringUtils; +import org.primefaces.model.ByteArrayContent; +import org.primefaces.model.StreamedContent; + +import com.mousetech.gourmetj.ShopIngredient; + +/** + * Construct a Primefaces file output content for an ingredient + * list in YAML format. + * + * @author timh + * @since Jan 15, 2022 + */ + +public class YamlShoppingList { + + public static StreamedContent createDownload( + List ingredientList) { + ByteArrayOutputStream ary = new ByteArrayOutputStream(); + PrintWriter wtr = new PrintWriter(ary); + wtr.println("---"); + formatContent(wtr, ingredientList); + wtr.close(); + byte[] bas = ary.toByteArray(); + + StreamedContent dlList = new ByteArrayContent(bas, + "text/text", "shopping_list.yml"); + return dlList; + } + + /** + * Output line items in the ingredient list with topics for + * each Shopping Category. + * + * @param wtr Output Writer + * @param ingredientList Ingredient list to output. + */ + private static void formatContent(PrintWriter wtr, + List ingredientList) { + String oldShopcat = null; + for (ShopIngredient ing : ingredientList) { + String newShopcat = ing.getShopCat(); + if (StringUtils.isBlank(newShopcat)) { + newShopcat = "Unassigned"; + } + if (!StringUtils.equals(newShopcat, oldShopcat)) { + wtr.println(newShopcat + ":"); + oldShopcat = newShopcat; + } + wtr.print(" - "); + String displa = ing.getDisplayAmount(); + wtr.print(displa); + if (!displa.isBlank()) { + wtr.print(' '); + } + String unit = ing.getUnit(); + if (StringUtils.isNotBlank(unit)) { + wtr.print(unit); + wtr.print(' '); + } + wtr.println(ing.getItem()); + } + } + +}