Browse Source

add error checking and ability to edit and save recipe

Daniel Sheffield 2 years ago
parent
commit
bd27b9bb3b
2 changed files with 88 additions and 11 deletions
  1. 83 6
      app/activities/RecipeEditor.py
  2. 5 5
      recipe.py

+ 83 - 6
app/activities/RecipeEditor.py

@@ -4,8 +4,9 @@
 # All rights reserved
 # All rights reserved
 #
 #
 # THIS SOFTWARE IS PROVIDED AS IS WITHOUT WARRANTY
 # THIS SOFTWARE IS PROVIDED AS IS WITHOUT WARRANTY
-import itertools
-from itertools import chain
+from xml.etree.ElementTree import fromstring, ParseError
+from markdown import markdown
+from itertools import chain, product
 from decimal import Decimal, InvalidOperation
 from decimal import Decimal, InvalidOperation
 from typing import List, Tuple, Union, Iterable, Callable
 from typing import List, Tuple, Union, Iterable, Callable
 from urwid import (
 from urwid import (
@@ -35,6 +36,43 @@ from ..widgets import (
 from ..db_utils import QueryManager
 from ..db_utils import QueryManager
 from . import ActivityManager, show_or_exit
 from . import ActivityManager, show_or_exit
 from .Rating import Rating
 from .Rating import Rating
+import yaml
+def change_style(style, representer):
+    def new_representer(dumper, data):
+        scalar = representer(dumper, data)
+        scalar.style = style
+        return scalar
+    return new_representer
+
+import yaml
+from yaml.representer import SafeRepresenter
+class folded_str(str): pass
+
+class literal_str(str): pass
+
+# represent_str does handle some corner cases, so use that
+# instead of calling represent_scalar directly
+represent_folded_str = change_style('>', SafeRepresenter.represent_str)
+represent_literal_str = change_style('|', SafeRepresenter.represent_str)
+yaml.add_representer(folded_str, represent_folded_str)
+yaml.add_representer(literal_str, represent_literal_str)
+def depth_first_elements(tree):
+    for e in tree:
+        for y in depth_first_elements(e):
+            yield y
+    yield tree
+
+def get_products_from_xhtml(md: str):
+    try:
+        xhtml = fromstring(
+f"""<root>
+{md}
+</root>
+""")
+    except ParseError:
+        return
+    for e in filter(lambda x: x.tag == 'strong', depth_first_elements(xhtml)):
+        yield e.text
 
 
 def to_numbered_field(x):
 def to_numbered_field(x):
     if len(x[0].split('#', 1)) > 1:
     if len(x[0].split('#', 1)) > 1:
@@ -90,6 +128,8 @@ class RecipeEditor(FocusWidget):
             self.advance_focus(reverse=True)
             self.advance_focus(reverse=True)
         elif key == 'ctrl delete':
         elif key == 'ctrl delete':
             self.clear()
             self.clear()
+        elif key == 'ctrl w':
+            self.save()
         else:
         else:
             return super().keypress(size, key)
             return super().keypress(size, key)
 
 
@@ -124,7 +164,7 @@ class RecipeEditor(FocusWidget):
             ['product', 'quantity', 'unit'],
             ['product', 'quantity', 'unit'],
             map(extract_values, unzip(self.ingredients)),
             map(extract_values, unzip(self.ingredients)),
         )
         )
-        ret = dict(itertools.chain(
+        ret = dict(chain(
             *[ map(to_named_value(n), enumerate(l)) for n,l in zipped ],
             *[ map(to_named_value(n), enumerate(l)) for n,l in zipped ],
             [ ('organic', self.organic.state) ]
             [ ('organic', self.organic.state) ]
         ))
         ))
@@ -204,6 +244,23 @@ class RecipeEditor(FocusWidget):
                 ))))
                 ))))
             ))
             ))
 
 
+    def save(self):
+        yml = dict()
+        yml['ingredients'] = list(map(lambda x: ' '.join(x), filter(
+            lambda x: None not in map(lambda x: x or None, x), [
+            (
+                x[0].get_edit_text(),
+                x[1].get_text()[0],
+                x[2].get_edit_text(),
+            ) for x in self.ingredients
+        ])))
+        yml['feeds'] = int(self.feeds.get_text()[0].rsplit(':', 1)[1].strip())
+        yml['instructions'] = literal_str('\n'.join(map(
+			lambda x: x.strip(),
+			self.instructions.get_text()[0].splitlines()
+		)).strip())
+        with open(f'{self.fname}-modified.yaml', 'w') as f:
+            yaml.dump(yml, f)
 
 
 
 
     def update(self):
     def update(self):
@@ -244,13 +301,31 @@ class RecipeEditor(FocusWidget):
             price[1] += _avg*quantity
             price[1] += _avg*quantity
             price[2] += _max*quantity
             price[2] += _max*quantity
         self.price.set_text(f'Cost: {not_found}{price[1]:.2f}')
         self.price.set_text(f'Cost: {not_found}{price[1]:.2f}')
+        notice = ''
+        ingredients = list(filter(lambda x: x, map(lambda x: x[0].get_edit_text(), self.ingredients)))
+        parsed_products = list(get_products_from_xhtml(markdown(self.instructions.get_edit_text())))
+        if not parsed_products:
+            self.notice.set_text('Failed to parse recipe instructions')
+            return self
+        products = set(parsed_products)
+        for product in products - set(ingredients):
+            notice += f"Product '{product}' not found in list of ingredients\n";
+        for ingredient in set(ingredients) - products:
+            notice += f"Ingredient '{ingredient}' is not used\n";
+        if len(set(ingredients)) != len(ingredients):
+            notice += f"Some ingredients listed more than onece\n"
+
+        self.notice.set_text(notice or 'None')
+
         return self
         return self
 
 
     def __init__(self,
     def __init__(self,
         activity_manager: ActivityManager,
         activity_manager: ActivityManager,
         query_manager: QueryManager,
         query_manager: QueryManager,
-        recipe,
+        fname: str,
+        recipe: dict,
     ):
     ):
+        self.fname = fname
         self.components = dict()
         self.components = dict()
         self.buttons = {
         self.buttons = {
           'clear': Button(('streak', 'Clear')),
           'clear': Button(('streak', 'Clear')),
@@ -267,14 +342,16 @@ class RecipeEditor(FocusWidget):
 
 
         self.organic = NoTabCheckBox(('bg', "Organic"), state='mixed')
         self.organic = NoTabCheckBox(('bg', "Organic"), state='mixed')
         self.instructions = Edit('', edit_text=recipe['instructions'] or u'', multiline=True, allow_tab=True)
         self.instructions = Edit('', edit_text=recipe['instructions'] or u'', multiline=True, allow_tab=True)
-        self.feeds = Text(f"Feeds: {recipe['feeds'] or ''}")
+        self.feeds = Text(f"Serves: {recipe['feeds'] or ''}")
         self.price = Text(f"Cost: 0")
         self.price = Text(f"Cost: 0")
+        self.notice = Text(f"")
 
 
         bottom_pane = [
         bottom_pane = [
             self.organic,
             self.organic,
             LineBox(self.instructions, title=f'Instructions'),
             LineBox(self.instructions, title=f'Instructions'),
             self.feeds,
             self.feeds,
             self.price,
             self.price,
+            LineBox(self.notice, title='Errors'),
         ]
         ]
         self.activity_manager = activity_manager
         self.activity_manager = activity_manager
         self.query_manager = query_manager
         self.query_manager = query_manager
@@ -320,7 +397,7 @@ class RecipeEditor(FocusWidget):
             ) for idx, ingredient in enumerate(self.ingredients)
             ) for idx, ingredient in enumerate(self.ingredients)
         ]
         ]
         gutter = [
         gutter = [
-            *[ Divider() for _ in itertools.product(
+            *[ Divider() for _ in product(
                 range(3), self.ingredients[:-1]
                 range(3), self.ingredients[:-1]
             )],
             )],
             Divider(),
             Divider(),

+ 5 - 5
recipe.py

@@ -11,6 +11,8 @@ from app.activities import ActivityManager, show_or_exit
 from app.activities.RecipeEditor import RecipeEditor
 from app.activities.RecipeEditor import RecipeEditor
 from app.db_utils import QueryManager, display_mapper
 from app.db_utils import QueryManager, display_mapper
 from app.palette import high_contrast
 from app.palette import high_contrast
+from app import parse_recipe
+import sys
 
 
 try:
 try:
     from db_credentials import HOST, PASSWORD
     from db_credentials import HOST, PASSWORD
@@ -53,8 +55,6 @@ with open('units.sql') as f:
             continue
             continue
         if quote:
         if quote:
             continue
             continue
-        #print(f'exec {to_exec}')
-        #input()
         cur.execute(to_exec)
         cur.execute(to_exec)
         try:
         try:
             print(cur.fetchall())
             print(cur.fetchall())
@@ -65,14 +65,14 @@ with open('units.sql') as f:
 activity_manager = ActivityManager()
 activity_manager = ActivityManager()
 query_manager = QueryManager(cur, display_mapper)
 query_manager = QueryManager(cur, display_mapper)
 
 
-from app import parse_recipe
-with open('recipe.yaml') as f:
+fname = sys.argv[1]
+with open(fname) as f:
     recipe = parse_recipe.parse_recipe(f, query_manager)
     recipe = parse_recipe.parse_recipe(f, query_manager)
 
 
 activity_manager.create(RecipeEditor,
 activity_manager.create(RecipeEditor,
     'recipe_editor',
     'recipe_editor',
     activity_manager, query_manager,
     activity_manager, query_manager,
-    recipe,
+    fname.rsplit('.', 1)[0], recipe,
 )
 )
 
 
 app = Recipe(activity_manager)
 app = Recipe(activity_manager)