Procházet zdrojové kódy

add error checking and ability to edit and save recipe

Daniel Sheffield před 2 roky
rodič
revize
bd27b9bb3b
2 změnil soubory, kde provedl 88 přidání a 11 odebrání
  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
 #
 # 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 typing import List, Tuple, Union, Iterable, Callable
 from urwid import (
@@ -35,6 +36,43 @@ from ..widgets import (
 from ..db_utils import QueryManager
 from . import ActivityManager, show_or_exit
 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):
     if len(x[0].split('#', 1)) > 1:
@@ -90,6 +128,8 @@ class RecipeEditor(FocusWidget):
             self.advance_focus(reverse=True)
         elif key == 'ctrl delete':
             self.clear()
+        elif key == 'ctrl w':
+            self.save()
         else:
             return super().keypress(size, key)
 
@@ -124,7 +164,7 @@ class RecipeEditor(FocusWidget):
             ['product', 'quantity', 'unit'],
             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 ],
             [ ('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):
@@ -244,13 +301,31 @@ class RecipeEditor(FocusWidget):
             price[1] += _avg*quantity
             price[2] += _max*quantity
         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
 
     def __init__(self,
         activity_manager: ActivityManager,
         query_manager: QueryManager,
-        recipe,
+        fname: str,
+        recipe: dict,
     ):
+        self.fname = fname
         self.components = dict()
         self.buttons = {
           'clear': Button(('streak', 'Clear')),
@@ -267,14 +342,16 @@ class RecipeEditor(FocusWidget):
 
         self.organic = NoTabCheckBox(('bg', "Organic"), state='mixed')
         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.notice = Text(f"")
 
         bottom_pane = [
             self.organic,
             LineBox(self.instructions, title=f'Instructions'),
             self.feeds,
             self.price,
+            LineBox(self.notice, title='Errors'),
         ]
         self.activity_manager = activity_manager
         self.query_manager = query_manager
@@ -320,7 +397,7 @@ class RecipeEditor(FocusWidget):
             ) for idx, ingredient in enumerate(self.ingredients)
         ]
         gutter = [
-            *[ Divider() for _ in itertools.product(
+            *[ Divider() for _ in product(
                 range(3), self.ingredients[:-1]
             )],
             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.db_utils import QueryManager, display_mapper
 from app.palette import high_contrast
+from app import parse_recipe
+import sys
 
 try:
     from db_credentials import HOST, PASSWORD
@@ -53,8 +55,6 @@ with open('units.sql') as f:
             continue
         if quote:
             continue
-        #print(f'exec {to_exec}')
-        #input()
         cur.execute(to_exec)
         try:
             print(cur.fetchall())
@@ -65,14 +65,14 @@ with open('units.sql') as f:
 activity_manager = ActivityManager()
 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)
 
 activity_manager.create(RecipeEditor,
     'recipe_editor',
     activity_manager, query_manager,
-    recipe,
+    fname.rsplit('.', 1)[0], recipe,
 )
 
 app = Recipe(activity_manager)