converted ingredient parser to class and added automation beta hint

This commit is contained in:
vabene1111 2021-09-16 17:52:11 +02:00
parent ecd300d2db
commit a7dc23194e
28 changed files with 505 additions and 337 deletions

View File

@ -5,190 +5,212 @@ import unicodedata
from cookbook.models import Unit, Food
def parse_fraction(x):
if len(x) == 1 and 'fraction' in unicodedata.decomposition(x):
frac_split = unicodedata.decomposition(x[-1:]).split()
return (float((frac_split[1]).replace('003', ''))
/ float((frac_split[3]).replace('003', '')))
else:
frac_split = x.split('/')
if not len(frac_split) == 2:
raise ValueError
try:
return int(frac_split[0]) / int(frac_split[1])
except ZeroDivisionError:
raise ValueError
class IngredientParser:
request = None
ignore_rules = False
food_aliases = []
unit_aliases = []
def __init__(self, request, cache_mode, ignore_automations=False):
"""
Initialize ingredient parser
:param request: request context (to control caching, rule ownership, etc.)
:param cache_mode: defines if all rules should be loaded on initialization (good when parser is used many times) or if they should be retrieved every time (good when parser is not used many times in a row)
:param ignore_automations: ignore automation rules, allows to use ingredient parser without database access/request (request can be None)
"""
self.request = request
self.ignore_rules = ignore_automations
if cache_mode:
self.food_aliases = []
self.unit_aliases = []
def parse_amount(x):
amount = 0
unit = ''
note = ''
did_check_frac = False
end = 0
while (end < len(x) and (x[end] in string.digits
or (
(x[end] == '.' or x[end] == ',' or x[end] == '/')
and end + 1 < len(x)
and x[end + 1] in string.digits
))):
end += 1
if end > 0:
if "/" in x[:end]:
amount = parse_fraction(x[:end])
else:
amount = float(x[:end].replace(',', '.'))
else:
amount = parse_fraction(x[0])
end += 1
did_check_frac = True
if end < len(x):
if did_check_frac:
unit = x[end:]
def parse_fraction(self, x):
if len(x) == 1 and 'fraction' in unicodedata.decomposition(x):
frac_split = unicodedata.decomposition(x[-1:]).split()
return (float((frac_split[1]).replace('003', ''))
/ float((frac_split[3]).replace('003', '')))
else:
frac_split = x.split('/')
if not len(frac_split) == 2:
raise ValueError
try:
amount += parse_fraction(x[end])
unit = x[end + 1:]
except ValueError:
unit = x[end:]
return int(frac_split[0]) / int(frac_split[1])
except ZeroDivisionError:
raise ValueError
if unit.startswith('(') or unit.startswith('-'): # i dont know any unit that starts with ( or - so its likely an alternative like 1L (500ml) Water or 2-3
def parse_amount(self, x):
amount = 0
unit = ''
note = x
return amount, unit, note
note = ''
def parse_ingredient_with_comma(tokens):
ingredient = ''
note = ''
start = 0
# search for first occurrence of an argument ending in a comma
while start < len(tokens) and not tokens[start].endswith(','):
start += 1
if start == len(tokens):
# no token ending in a comma found -> use everything as ingredient
ingredient = ' '.join(tokens)
else:
ingredient = ' '.join(tokens[:start + 1])[:-1]
note = ' '.join(tokens[start + 1:])
return ingredient, note
def parse_ingredient(tokens):
ingredient = ''
note = ''
if tokens[-1].endswith(')'):
# Check if the matching opening bracket is in the same token
if (not tokens[-1].startswith('(')) and ('(' in tokens[-1]):
return parse_ingredient_with_comma(tokens)
# last argument ends with closing bracket -> look for opening bracket
start = len(tokens) - 1
while not tokens[start].startswith('(') and not start == 0:
start -= 1
if start == 0:
# the whole list is wrapped in brackets -> assume it is an error (e.g. assumed first argument was the unit) # noqa: E501
raise ValueError
elif start < 0:
# no opening bracket anywhere -> just ignore the last bracket
ingredient, note = parse_ingredient_with_comma(tokens)
else:
# opening bracket found -> split in ingredient and note, remove brackets from note # noqa: E501
note = ' '.join(tokens[start:])[1:-1]
ingredient = ' '.join(tokens[:start])
else:
ingredient, note = parse_ingredient_with_comma(tokens)
return ingredient, note
def parse(x):
# initialize default values
amount = 0
unit = ''
ingredient = ''
note = ''
unit_note = ''
# if the string contains parenthesis early on remove it and place it at the end
# because its likely some kind of note
if re.match('(.){1,6}\s\((.[^\(\)])+\)\s', x):
match = re.search('\((.[^\(])+\)', x)
x = x[:match.start()] + x[match.end():] + ' ' + x[match.start():match.end()]
tokens = x.split()
if len(tokens) == 1:
# there only is one argument, that must be the ingredient
ingredient = tokens[0]
else:
try:
# try to parse first argument as amount
amount, unit, unit_note = parse_amount(tokens[0])
# only try to parse second argument as amount if there are at least
# three arguments if it already has a unit there can't be
# a fraction for the amount
if len(tokens) > 2:
try:
if not unit == '':
# a unit is already found, no need to try the second argument for a fraction
# probably not the best method to do it, but I didn't want to make an if check and paste the exact same thing in the else as already is in the except # noqa: E501
raise ValueError
# try to parse second argument as amount and add that, in case of '2 1/2' or '2 ½'
amount += parse_fraction(tokens[1])
# assume that units can't end with a comma
if len(tokens) > 3 and not tokens[2].endswith(','):
# try to use third argument as unit and everything else as ingredient, use everything as ingredient if it fails # noqa: E501
try:
ingredient, note = parse_ingredient(tokens[3:])
unit = tokens[2]
except ValueError:
ingredient, note = parse_ingredient(tokens[2:])
else:
ingredient, note = parse_ingredient(tokens[2:])
except ValueError:
# assume that units can't end with a comma
if not tokens[1].endswith(','):
# try to use second argument as unit and everything else as ingredient, use everything as ingredient if it fails # noqa: E501
try:
ingredient, note = parse_ingredient(tokens[2:])
if unit == '':
unit = tokens[1]
else:
note = tokens[1]
except ValueError:
ingredient, note = parse_ingredient(tokens[1:])
else:
ingredient, note = parse_ingredient(tokens[1:])
did_check_frac = False
end = 0
while (end < len(x) and (x[end] in string.digits
or (
(x[end] == '.' or x[end] == ',' or x[end] == '/')
and end + 1 < len(x)
and x[end + 1] in string.digits
))):
end += 1
if end > 0:
if "/" in x[:end]:
amount = self.parse_fraction(x[:end])
else:
# only two arguments, first one is the amount
# which means this is the ingredient
ingredient = tokens[1]
except ValueError:
amount = float(x[:end].replace(',', '.'))
else:
amount = self.parse_fraction(x[0])
end += 1
did_check_frac = True
if end < len(x):
if did_check_frac:
unit = x[end:]
else:
try:
amount += self.parse_fraction(x[end])
unit = x[end + 1:]
except ValueError:
unit = x[end:]
if unit.startswith('(') or unit.startswith('-'): # i dont know any unit that starts with ( or - so its likely an alternative like 1L (500ml) Water or 2-3
unit = ''
note = x
return amount, unit, note
def parse_ingredient_with_comma(self, tokens):
ingredient = ''
note = ''
start = 0
# search for first occurrence of an argument ending in a comma
while start < len(tokens) and not tokens[start].endswith(','):
start += 1
if start == len(tokens):
# no token ending in a comma found -> use everything as ingredient
ingredient = ' '.join(tokens)
else:
ingredient = ' '.join(tokens[:start + 1])[:-1]
note = ' '.join(tokens[start + 1:])
return ingredient, note
def parse_ingredient(self, tokens):
ingredient = ''
note = ''
if tokens[-1].endswith(')'):
# Check if the matching opening bracket is in the same token
if (not tokens[-1].startswith('(')) and ('(' in tokens[-1]):
return self.parse_ingredient_with_comma(tokens)
# last argument ends with closing bracket -> look for opening bracket
start = len(tokens) - 1
while not tokens[start].startswith('(') and not start == 0:
start -= 1
if start == 0:
# the whole list is wrapped in brackets -> assume it is an error (e.g. assumed first argument was the unit) # noqa: E501
raise ValueError
elif start < 0:
# no opening bracket anywhere -> just ignore the last bracket
ingredient, note = self.parse_ingredient_with_comma(tokens)
else:
# opening bracket found -> split in ingredient and note, remove brackets from note # noqa: E501
note = ' '.join(tokens[start:])[1:-1]
ingredient = ' '.join(tokens[:start])
else:
ingredient, note = self.parse_ingredient_with_comma(tokens)
return ingredient, note
def parse(self, x):
# initialize default values
amount = 0
unit = ''
ingredient = ''
note = ''
unit_note = ''
# if the string contains parenthesis early on remove it and place it at the end
# because its likely some kind of note
if re.match('(.){1,6}\s\((.[^\(\)])+\)\s', x):
match = re.search('\((.[^\(])+\)', x)
x = x[:match.start()] + x[match.end():] + ' ' + x[match.start():match.end()]
tokens = x.split()
if len(tokens) == 1:
# there only is one argument, that must be the ingredient
ingredient = tokens[0]
else:
try:
# can't parse first argument as amount
# -> no unit -> parse everything as ingredient
ingredient, note = parse_ingredient(tokens)
# try to parse first argument as amount
amount, unit, unit_note = self.parse_amount(tokens[0])
# only try to parse second argument as amount if there are at least
# three arguments if it already has a unit there can't be
# a fraction for the amount
if len(tokens) > 2:
try:
if not unit == '':
# a unit is already found, no need to try the second argument for a fraction
# probably not the best method to do it, but I didn't want to make an if check and paste the exact same thing in the else as already is in the except # noqa: E501
raise ValueError
# try to parse second argument as amount and add that, in case of '2 1/2' or '2 ½'
amount += self.parse_fraction(tokens[1])
# assume that units can't end with a comma
if len(tokens) > 3 and not tokens[2].endswith(','):
# try to use third argument as unit and everything else as ingredient, use everything as ingredient if it fails # noqa: E501
try:
ingredient, note = self.parse_ingredient(tokens[3:])
unit = tokens[2]
except ValueError:
ingredient, note = self.parse_ingredient(tokens[2:])
else:
ingredient, note = self.parse_ingredient(tokens[2:])
except ValueError:
# assume that units can't end with a comma
if not tokens[1].endswith(','):
# try to use second argument as unit and everything else as ingredient, use everything as ingredient if it fails # noqa: E501
try:
ingredient, note = self.parse_ingredient(tokens[2:])
if unit == '':
unit = tokens[1]
else:
note = tokens[1]
except ValueError:
ingredient, note = self.parse_ingredient(tokens[1:])
else:
ingredient, note = self.parse_ingredient(tokens[1:])
else:
# only two arguments, first one is the amount
# which means this is the ingredient
ingredient = tokens[1]
except ValueError:
ingredient = ' '.join(tokens[1:])
try:
# can't parse first argument as amount
# -> no unit -> parse everything as ingredient
ingredient, note = self.parse_ingredient(tokens)
except ValueError:
ingredient = ' '.join(tokens[1:])
if unit_note not in note:
note += ' ' + unit_note
return amount, unit.strip(), ingredient.strip(), note.strip()
if unit_note not in note:
note += ' ' + unit_note
return amount, unit.strip(), ingredient.strip(), note.strip()
# small utility functions to prevent emtpy unit/food creation
def get_unit(unit, space):
if not unit:
def get_unit(self, unit):
"""
Get or create a unit for given space respecting possible automations
:param unit: string unit
:return: None if unit passed is invalid, Unit object otherwise
"""
if not unit:
return None
if len(unit) > 0:
u, created = Unit.objects.get_or_create(name=unit, space=self.request.space)
return u
return None
if len(unit) > 0:
u, created = Unit.objects.get_or_create(name=unit, space=space)
return u
return None
def get_food(food, space):
if not food:
def get_food(self, food):
"""
Get or create a food for given space respecting possible automations
:param food: string food
:return: None if food passed is invalid, Food object otherwise
"""
if not food:
return None
if len(food) > 0:
f, created = Food.objects.get_or_create(name=food, space=self.request.space)
return f
return None
if len(food) > 0:
f, created = Food.objects.get_or_create(name=food, space=space)
return f
return None

View File

@ -10,7 +10,7 @@ from recipe_scrapers._utils import get_host_name, normalize_string
from urllib.parse import unquote
def get_recipe_from_source(text, url, space):
def get_recipe_from_source(text, url, request):
def build_node(k, v):
if isinstance(v, dict):
node = {
@ -103,7 +103,7 @@ def get_recipe_from_source(text, url, space):
parse_list.append(el)
scrape = text_scraper(text, url=url)
recipe_json = helper.get_from_scraper(scrape, space)
recipe_json = helper.get_from_scraper(scrape, request)
for el in parse_list:
temp_tree = []

View File

@ -3,14 +3,14 @@ import re
from isodate import parse_duration as iso_parse_duration
from isodate.isoerror import ISO8601Error
from cookbook.helper.ingredient_parser import parse as parse_single_ingredient
from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.models import Keyword
from django.utils.dateparse import parse_duration
from html import unescape
from recipe_scrapers._utils import get_minutes
def get_from_scraper(scrape, space):
def get_from_scraper(scrape, request):
# converting the scrape_me object to the existing json format based on ld+json
recipe_json = {}
try:
@ -91,15 +91,16 @@ def get_from_scraper(scrape, space):
except Exception:
pass
try:
recipe_json['keywords'] = parse_keywords(list(set(map(str.casefold, keywords))), space)
recipe_json['keywords'] = parse_keywords(list(set(map(str.casefold, keywords))), request.space)
except AttributeError:
recipe_json['keywords'] = keywords
ingredient_parser = IngredientParser(request, True)
try:
ingredients = []
for x in scrape.ingredients():
try:
amount, unit, ingredient, note = parse_single_ingredient(x)
amount, unit, ingredient, note = ingredient_parser.parse(x)
ingredients.append(
{
'amount': amount,

View File

@ -1,6 +1,6 @@
import re
from cookbook.helper.ingredient_parser import parse, get_food, get_unit
from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Ingredient
@ -42,11 +42,12 @@ class ChefTap(Integration):
step.instruction += '\n' + source_url
step.save()
ingredient_parser = IngredientParser(self.request, True)
for ingredient in ingredients:
if len(ingredient.strip()) > 0:
amount, unit, ingredient, note = parse(ingredient)
f = get_food(ingredient, self.request.space)
u = get_unit(unit, self.request.space)
amount, unit, ingredient, note = ingredient_parser.parse(ingredient)
f = ingredient_parser.get_food(ingredient)
u = ingredient_parser.get_unit(unit)
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note, space=self.request.space,
))

View File

@ -3,7 +3,7 @@ from io import BytesIO
from zipfile import ZipFile
from cookbook.helper.image_processing import get_filetype
from cookbook.helper.ingredient_parser import parse, get_food, get_unit
from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Ingredient, Keyword
@ -58,10 +58,11 @@ class Chowdown(Integration):
instruction='\n'.join(directions) + '\n\n' + '\n'.join(descriptions), space=self.request.space,
)
ingredient_parser = IngredientParser(self.request, True)
for ingredient in ingredients:
amount, unit, ingredient, note = parse(ingredient)
f = get_food(ingredient, self.request.space)
u = get_unit(unit, self.request.space)
amount, unit, ingredient, note = ingredient_parser.parse(ingredient)
f = ingredient_parser.get_food(ingredient)
u = ingredient_parser.get_unit(unit)
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note, space=self.request.space,
))

View File

@ -6,7 +6,7 @@ from io import BytesIO
import yaml
from cookbook.helper.ingredient_parser import parse, get_food, get_unit
from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Ingredient, Keyword
from gettext import gettext as _
@ -53,11 +53,12 @@ class CookBookApp(Integration):
step.save()
recipe.steps.add(step)
ingredient_parser = IngredientParser(self.request, True)
for ingredient in recipe_yml['ingredients'].split('\n'):
if ingredient.strip() != '':
amount, unit, ingredient, note = parse(ingredient)
f = get_food(ingredient, self.request.space)
u = get_unit(unit, self.request.space)
amount, unit, ingredient, note = ingredient_parser.parse(ingredient)
f = ingredient_parser.get_food(ingredient)
u = ingredient_parser.get_unit(unit)
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note, space=self.request.space,
))

View File

@ -2,7 +2,7 @@ import base64
import json
from io import BytesIO
from cookbook.helper.ingredient_parser import parse, get_food, get_unit
from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Ingredient
@ -34,11 +34,12 @@ class Domestica(Integration):
if file['source'] != '':
step.instruction += '\n' + file['source']
ingredient_parser = IngredientParser(self.request, True)
for ingredient in file['ingredients'].split('\n'):
if len(ingredient.strip()) > 0:
amount, unit, ingredient, note = parse(ingredient)
f = get_food(ingredient, self.request.space)
u = get_unit(unit, self.request.space)
amount, unit, ingredient, note = ingredient_parser.parse(ingredient)
f = ingredient_parser.get_food(ingredient)
u = ingredient_parser.get_unit(unit)
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note, space=self.request.space,
))

View File

@ -4,7 +4,7 @@ from io import BytesIO
from zipfile import ZipFile
from cookbook.helper.image_processing import get_filetype
from cookbook.helper.ingredient_parser import parse, get_food, get_unit
from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Ingredient
@ -37,17 +37,18 @@ class Mealie(Integration):
if len(recipe_json['description'].strip()) > 500:
step.instruction = recipe_json['description'].strip() + '\n\n' + step.instruction
ingredient_parser = IngredientParser(self.request, True)
for ingredient in recipe_json['recipe_ingredient']:
try:
if ingredient['food']:
f = get_food(ingredient['food'], self.request.space)
u = get_unit(ingredient['unit'], self.request.space)
f = ingredient_parser.get_food(ingredient['food'])
u = ingredient_parser.get_unit(ingredient['unit'])
amount = ingredient['quantity']
note = ingredient['note']
else:
amount, unit, ingredient, note = parse(ingredient['note'])
f = get_food(ingredient, self.request.space)
u = get_unit(unit, self.request.space)
amount, unit, ingredient, note = ingredient_parser.parse(ingredient['note'])
f = ingredient_parser.get_food(ingredient)
u = ingredient_parser.get_unit(unit)
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note, space=self.request.space,
))

View File

@ -1,6 +1,6 @@
import re
from cookbook.helper.ingredient_parser import parse, get_food, get_unit
from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Ingredient, Keyword
@ -42,11 +42,12 @@ class MealMaster(Integration):
instruction='\n'.join(directions) + '\n\n', space=self.request.space,
)
ingredient_parser = IngredientParser(self.request, True)
for ingredient in ingredients:
if len(ingredient.strip()) > 0:
amount, unit, ingredient, note = parse(ingredient)
f = get_food(ingredient, self.request.space)
u = get_unit(unit, self.request.space)
amount, unit, ingredient, note = ingredient_parser.parse(ingredient)
f = ingredient_parser.get_food(ingredient)
u = ingredient_parser.get_unit(unit)
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note, space=self.request.space,
))

View File

@ -4,7 +4,7 @@ from io import BytesIO
from zipfile import ZipFile
from cookbook.helper.image_processing import get_filetype
from cookbook.helper.ingredient_parser import parse, get_food, get_unit
from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Ingredient
@ -39,6 +39,7 @@ class NextcloudCookbook(Integration):
ingredients_added = True
ingredient_parser = IngredientParser(self.request, True)
for ingredient in recipe_json['recipeIngredient']:
amount, unit, ingredient, note = parse(ingredient)
f = get_food(ingredient, self.request.space)

View File

@ -1,6 +1,6 @@
import json
from cookbook.helper.ingredient_parser import get_food, get_unit
from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Ingredient
@ -23,9 +23,10 @@ class OpenEats(Integration):
step = Step.objects.create(instruction=instructions, space=self.request.space,)
ingredient_parser = IngredientParser(self.request, True)
for ingredient in file['ingredients']:
f = get_food(ingredient['food'], self.request.space)
u = get_unit(ingredient['unit'], self.request.space)
f = ingredient_parser.get_food(ingredient['food'])
u = ingredient_parser.get_unit(ingredient['unit'])
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=ingredient['amount'], space=self.request.space,
))

View File

@ -4,7 +4,7 @@ import json
import re
from io import BytesIO
from cookbook.helper.ingredient_parser import parse, get_food, get_unit
from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Ingredient, Keyword
from gettext import gettext as _
@ -66,12 +66,13 @@ class Paprika(Integration):
keyword, created = Keyword.objects.get_or_create(name=c.strip(), space=self.request.space)
recipe.keywords.add(keyword)
ingredient_parser = IngredientParser(self.request, True)
try:
for ingredient in recipe_json['ingredients'].split('\n'):
if len(ingredient.strip()) > 0:
amount, unit, ingredient, note = parse(ingredient)
f = get_food(ingredient, self.request.space)
u = get_unit(unit, self.request.space)
amount, unit, ingredient, note = ingredient_parser.parse(ingredient)
f = ingredient_parser.get_food(ingredient)
u = ingredient_parser.get_unit(unit)
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note, space=self.request.space,
))

View File

@ -1,4 +1,4 @@
from cookbook.helper.ingredient_parser import parse, get_food, get_unit
from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Ingredient
@ -38,11 +38,12 @@ class Pepperplate(Integration):
instruction='\n'.join(directions) + '\n\n', space=self.request.space,
)
ingredient_parser = IngredientParser(self.request, True)
for ingredient in ingredients:
if len(ingredient.strip()) > 0:
amount, unit, ingredient, note = parse(ingredient)
f = get_food(ingredient, self.request.space)
u = get_unit(unit, self.request.space)
amount, unit, ingredient, note = ingredient_parser.parse(ingredient)
f = ingredient_parser.get_food(ingredient)
u = ingredient_parser.get_unit(unit)
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note, space=self.request.space,
))

View File

@ -2,7 +2,7 @@ from io import BytesIO
import requests
from cookbook.helper.ingredient_parser import parse, get_food, get_unit
from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Ingredient, Keyword
@ -53,11 +53,12 @@ class Plantoeat(Integration):
keyword, created = Keyword.objects.get_or_create(name=k.strip(), space=self.request.space)
recipe.keywords.add(keyword)
ingredient_parser = IngredientParser(self.request, True)
for ingredient in ingredients:
if len(ingredient.strip()) > 0:
amount, unit, ingredient, note = parse(ingredient)
f = get_food(ingredient, self.request.space)
u = get_unit(unit, self.request.space)
amount, unit, ingredient, note = ingredient_parser.parse(ingredient)
f = ingredient_parser.get_food(ingredient)
u = ingredient_parser.get_unit(unit)
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note, space=self.request.space,
))

View File

@ -6,7 +6,7 @@ from zipfile import ZipFile
import imghdr
from cookbook.helper.image_processing import get_filetype
from cookbook.helper.ingredient_parser import parse, get_food, get_unit
from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Ingredient, Keyword
@ -55,11 +55,12 @@ class RecetteTek(Integration):
try:
# Process the ingredients. Assumes 1 ingredient per line.
ingredient_parser = IngredientParser(self.request, True)
for ingredient in file['ingredients'].split('\n'):
if len(ingredient.strip()) > 0:
amount, unit, ingredient, note = parse(ingredient)
f = get_food(ingredient, self.request.space)
u = get_unit(unit, self.request.space)
amount, unit, ingredient, note = ingredient_parser.parse(ingredient)
f = ingredient_parser.get_food(ingredient)
u = ingredient_parser.get_unit(unit)
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note, space=self.request.space,
))

View File

@ -3,7 +3,7 @@ from bs4 import BeautifulSoup
from io import BytesIO
from zipfile import ZipFile
from cookbook.helper.ingredient_parser import parse, get_food, get_unit
from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.helper.recipe_url_import import parse_servings, iso_duration_to_minutes
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Ingredient, Keyword
@ -41,12 +41,13 @@ class RecipeKeeper(Integration):
step = Step.objects.create(instruction='', space=self.request.space,)
ingredient_parser = IngredientParser(self.request, True)
for ingredient in file.find("div", {"itemprop": "recipeIngredients"}).findChildren("p"):
if ingredient.text == "":
continue
amount, unit, ingredient, note = parse(ingredient.text.strip())
f = get_food(ingredient, self.request.space)
u = get_unit(unit, self.request.space)
amount, unit, ingredient, note = ingredient_parser.parse(ingredient.text.strip())
f = ingredient_parser.get_food(ingredient)
u = ingredient_parser.get_unit(unit)
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note, space=self.request.space,
))

View File

@ -3,7 +3,7 @@ from io import BytesIO
import requests
from cookbook.helper.ingredient_parser import parse, get_food, get_unit
from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Ingredient
@ -31,6 +31,7 @@ class RecipeSage(Integration):
except Exception as e:
print('failed to parse yield or time ', str(e))
ingredient_parser = IngredientParser(self.request,True)
ingredients_added = False
for s in file['recipeInstructions']:
step = Step.objects.create(
@ -40,9 +41,9 @@ class RecipeSage(Integration):
ingredients_added = True
for ingredient in file['recipeIngredient']:
amount, unit, ingredient, note = parse(ingredient)
f = get_food(ingredient, self.request.space)
u = get_unit(unit, self.request.space)
amount, unit, ingredient, note = ingredient_parser.parse(ingredient)
f = ingredient_parser.get_food(ingredient)
u = ingredient_parser.get_unit(unit)
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note, space=self.request.space,
))

View File

@ -1,4 +1,4 @@
from cookbook.helper.ingredient_parser import parse, get_food, get_unit
from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Ingredient, Keyword
@ -41,11 +41,12 @@ class RezKonv(Integration):
instruction='\n'.join(directions) + '\n\n', space=self.request.space,
)
ingredient_parser = IngredientParser(self.request, True)
for ingredient in ingredients:
if len(ingredient.strip()) > 0:
amount, unit, ingredient, note = parse(ingredient)
f = get_food(ingredient, self.request.space)
u = get_unit(unit, self.request.space)
amount, unit, ingredient, note = ingredient_parser.parse(ingredient)
f = ingredient_parser.get_food(ingredient)
u = ingredient_parser.get_unit(unit)
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note, space=self.request.space,
))

View File

@ -1,6 +1,6 @@
from django.utils.translation import gettext as _
from cookbook.helper.ingredient_parser import parse, get_food, get_unit
from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Ingredient
@ -43,12 +43,13 @@ class Safron(Integration):
recipe = Recipe.objects.create(name=title, description=description, created_by=self.request.user, internal=True, space=self.request.space, )
step = Step.objects.create(instruction='\n'.join(directions), space=self.request.space,)
step = Step.objects.create(instruction='\n'.join(directions), space=self.request.space, )
ingredient_parser = IngredientParser(self.request, True)
for ingredient in ingredients:
amount, unit, ingredient, note = parse(ingredient)
f = get_food(ingredient, self.request.space)
u = get_unit(unit, self.request.space)
amount, unit, ingredient, note = ingredient_parser.parse(ingredient)
f = ingredient_parser.get_food(ingredient)
u = ingredient_parser.get_unit(unit)
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note, space=self.request.space,
))

View File

@ -1,4 +1,4 @@
from cookbook.helper.ingredient_parser import parse
from cookbook.helper.ingredient_parser import IngredientParser
def test_ingredient_parser():
@ -58,14 +58,16 @@ def test_ingredient_parser():
"2L Wasser": (2, "L", "Wasser", ""),
"1 (16 ounce) package dry lentils, rinsed": (1, "package", "dry lentils, rinsed", "16 ounce"),
"2-3 c Water": (2, "c", "Water", "2-3"),
"Pane (raffermo o secco) 80 g": (0, "", "Pane 80 g", "raffermo o secco"), #TODO this is actually not a good result but currently expected
"Pane (raffermo o secco) 80 g": (0, "", "Pane 80 g", "raffermo o secco"), # TODO this is actually not a good result but currently expected
}
# for German you could say that if an ingredient does not have
# an amount # and it starts with a lowercase letter, then that
# is a unit ("etwas", "evtl.") does not apply to English tho
ingredient_parser = IngredientParser(None, False, ignore_automations=True)
count = 0
for key, val in expectations.items():
count += 1
parsed = parse(key)
parsed = ingredient_parser.parse(key)
assert val == parsed

View File

@ -31,7 +31,7 @@ from rest_framework.viewsets import ViewSetMixin
from treebeard.exceptions import PathOverflow, InvalidMoveToDescendant, InvalidPosition
from cookbook.helper.image_processing import handle_image
from cookbook.helper.ingredient_parser import parse
from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.helper.permission_helper import (CustomIsAdmin, CustomIsGuest,
CustomIsOwner, CustomIsShare,
CustomIsShared, CustomIsUser,
@ -853,11 +853,11 @@ def recipe_from_source(request):
},
status=400)
else:
return JsonResponse({"recipe_json": get_from_scraper(scrape, request.space)})
return JsonResponse({"recipe_json": get_from_scraper(scrape, request)})
elif (mode == 'source') or (mode == 'url' and auto == 'false'):
if not data or data == 'undefined':
data = requests.get(url, headers=HEADERS).content
recipe_json, recipe_tree, recipe_html, images = get_recipe_from_source(data, url, request.space)
recipe_json, recipe_tree, recipe_html, images = get_recipe_from_source(data, url, request)
if len(recipe_tree) == 0 and len(recipe_json) == 0:
return JsonResponse(
{
@ -893,7 +893,9 @@ def get_backup(request):
@group_required('user')
def ingredient_from_string(request):
text = request.POST['text']
amount, unit, food, note = parse(text)
ingredient_parser = IngredientParser(request, False)
amount, unit, food, note = ingredient_parser.parse(text)
return JsonResponse(
{

View File

@ -6,7 +6,7 @@ from django.http import JsonResponse
from django.shortcuts import get_object_or_404
from django.views.decorators.csrf import csrf_exempt
from cookbook.helper.ingredient_parser import parse, get_unit, get_food
from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.helper.permission_helper import group_required
from cookbook.models import TelegramBot, ShoppingList, ShoppingListEntry
@ -49,9 +49,12 @@ def hook(request, token):
if not sl:
sl = ShoppingList.objects.create(created_by=tb.created_by, space=tb.space)
amount, unit, ingredient, note = parse(data['message']['text'])
f = get_food(ingredient, tb.space)
u = get_unit(unit, tb.space)
request.space = tb.space # TODO this is likely a bad idea. Verify and test
request.user = tb.created_by
ingredient_parser = IngredientParser(request, False)
amount, unit, ingredient, note = ingredient_parser.parse(data['message']['text'])
f = ingredient_parser.get_food(ingredient)
u = ingredient_parser.get_unit(unit)
sl.entries.add(
ShoppingListEntry.objects.create(
food=f, unit=u, amount=amount

View File

@ -26,10 +26,9 @@ from cookbook.forms import (CommentForm, Recipe, User,
UserCreateForm, UserNameForm, UserPreference,
UserPreferenceForm, SpaceJoinForm, SpaceCreateForm,
SearchPreferenceForm)
from cookbook.helper.ingredient_parser import parse
from cookbook.helper.permission_helper import group_required, share_link_valid, has_group_permission
from cookbook.models import (Comment, CookLog, InviteLink, MealPlan,
RecipeBook, RecipeBookEntry, ViewLog, ShoppingList, Space, Keyword, RecipeImport, Unit,
ViewLog, ShoppingList, Space, Keyword, RecipeImport, Unit,
Food, UserFile, ShareLink)
from cookbook.tables import (CookLogTable, RecipeTable, RecipeTableSmall,
ViewLogTable, InviteLinkTable)

View File

@ -8,11 +8,22 @@
:show="show_modal"
@finish-action="finishAction"/>
<div class="row">
<div class="col-md-2 d-none d-md-block">
</div>
<div class="col-xl-8 col-12">
<div class="container-fluid d-flex flex-column flex-grow-1">
<div class="row" v-if="this_model === Models.AUTOMATION">
<div class="col-md-12">
<b-alert show variant="warning">
<b-badge>BETA</b-badge>
{{ $t('warning_feature_beta') }}
</b-alert>
</div>
</div>
<div class="row">
<div class="col-md-6" style="margin-top: 1vh">
<h3>
@ -38,10 +49,10 @@
<!-- model isn't paginated and loads in one API call -->
<div v-if="!paginated">
<generic-horizontal-card v-for="i in items_left" v-bind:key="i.id"
:item=i
:model="this_model"
@item-action="startAction($event, 'left')"
@finish-action="finishAction"/>
:item=i
:model="this_model"
@item-action="startAction($event, 'left')"
@finish-action="finishAction"/>
</div>
<!-- model is paginated and needs managed -->
<generic-infinite-cards v-if="paginated"
@ -343,7 +354,7 @@ export default {
this.makeToast(this.$t('Error'), err.bodyText, 'danger')
})
if (automate){
if (automate) {
let apiClient = new ApiApiFactory()
let automation = {
@ -352,13 +363,13 @@ export default {
param_2: target.id
}
if (this.this_model === this.Models.FOOD){
if (this.this_model === this.Models.FOOD) {
automation.type = 'FOOD_ALIAS'
}
if (this.this_model === this.Models.UNIT){
if (this.this_model === this.Models.UNIT) {
automation.type = 'UNIT_ALIAS'
}
if (this.this_model === this.Models.KEYWORD){
if (this.this_model === this.Models.KEYWORD) {
automation.type = 'KEYWORD_ALIAS'
}

View File

@ -21,7 +21,7 @@
</b-dropdown-item>
<b-dropdown-item v-if="show_merge" v-on:click="$emit('item-action', 'merge-automate')">
<i class="fas fa-robot fa-fw"></i> {{$t('Merge')}} & {{$t('Automate')}}
<i class="fas fa-robot fa-fw"></i> {{$t('Merge')}} & {{$t('Automate')}} <b-badge v-b-tooltip.hover :title="$t('warning_feature_beta')">BETA</b-badge>
</b-dropdown-item>
</b-dropdown>

View File

@ -86,7 +86,7 @@
<i class="fas fa-compress-arrows-alt fa-fw"></i> <b>{{$t('Merge')}}</b>: <span v-html="$t('merge_confirmation', {'source': source.name,'target':item.name})"></span>
</b-list-group-item>
<b-list-group-item v-if="useMerge" action v-on:click="$emit('item-action',{'action': 'merge-automate', 'target': item, 'source': source}); closeMenu()">
<i class="fas fa-robot fa-fw"></i> <b>{{$t('Merge')}} & {{$t('Automate')}}</b>: <span v-html="$t('merge_confirmation', {'source': source.name,'target':item.name})"></span> {{$t('create_rule')}}
<i class="fas fa-robot fa-fw"></i> <b>{{$t('Merge')}} & {{$t('Automate')}}</b>: <span v-html="$t('merge_confirmation', {'source': source.name,'target':item.name})"></span> {{$t('create_rule')}} <b-badge v-b-tooltip.hover :title="$t('warning_feature_beta')" >BETA</b-badge>
</b-list-group-item>
<b-list-group-item action v-on:click="closeMenu()">
<i class="fas fa-times fa-fw"></i> <b>{{$t('Cancel')}}</b>

View File

@ -1,4 +1,5 @@
{
"warning_feature_beta": "This feature is currently in a BETA (testing) state. Please expect bugs and possibly breaking changes in the future (possibly loosing feature related data) when using this feature.",
"err_fetching_resource": "There was an error fetching a resource!",
"err_creating_resource": "There was an error creating a resource!",
"err_updating_resource": "There was an error updating a resource!",

View File

@ -1,183 +1,294 @@
{
"status": "done",
"assets": {
"../../templates/sw.js": {
"name": "../../templates/sw.js",
"path": "..\\..\\templates\\sw.js"
},
"css/chunk-vendors.css": {
"name": "css/chunk-vendors.css",
"path": "css\\chunk-vendors.css"
},
"js/chunk-vendors.js": {
"name": "js/chunk-vendors.js",
"path": "js\\chunk-vendors.js"
},
"css/cookbook_view.css": {
"name": "css/cookbook_view.css",
"path": "css\\cookbook_view.css"
"path": "js\\chunk-vendors.js",
"publicPath": "http://localhost:8080/js/chunk-vendors.js"
},
"js/cookbook_view.js": {
"name": "js/cookbook_view.js",
"path": "js\\cookbook_view.js"
},
"css/edit_internal_recipe.css": {
"name": "css/edit_internal_recipe.css",
"path": "css\\edit_internal_recipe.css"
"path": "js\\cookbook_view.js",
"publicPath": "http://localhost:8080/js/cookbook_view.js"
},
"js/edit_internal_recipe.js": {
"name": "js/edit_internal_recipe.js",
"path": "js\\edit_internal_recipe.js"
"path": "js\\edit_internal_recipe.js",
"publicPath": "http://localhost:8080/js/edit_internal_recipe.js"
},
"js/import_response_view.js": {
"name": "js/import_response_view.js",
"path": "js\\import_response_view.js"
},
"css/meal_plan_view.css": {
"name": "css/meal_plan_view.css",
"path": "css\\meal_plan_view.css"
"path": "js\\import_response_view.js",
"publicPath": "http://localhost:8080/js/import_response_view.js"
},
"js/meal_plan_view.js": {
"name": "js/meal_plan_view.js",
"path": "js\\meal_plan_view.js"
},
"css/model_list_view.css": {
"name": "css/model_list_view.css",
"path": "css\\model_list_view.css"
"path": "js\\meal_plan_view.js",
"publicPath": "http://localhost:8080/js/meal_plan_view.js"
},
"js/model_list_view.js": {
"name": "js/model_list_view.js",
"path": "js\\model_list_view.js"
"path": "js\\model_list_view.js",
"publicPath": "http://localhost:8080/js/model_list_view.js"
},
"js/offline_view.js": {
"name": "js/offline_view.js",
"path": "js\\offline_view.js"
},
"css/recipe_search_view.css": {
"name": "css/recipe_search_view.css",
"path": "css\\recipe_search_view.css"
"path": "js\\offline_view.js",
"publicPath": "http://localhost:8080/js/offline_view.js"
},
"js/recipe_search_view.js": {
"name": "js/recipe_search_view.js",
"path": "js\\recipe_search_view.js"
},
"css/recipe_view.css": {
"name": "css/recipe_view.css",
"path": "css\\recipe_view.css"
"path": "js\\recipe_search_view.js",
"publicPath": "http://localhost:8080/js/recipe_search_view.js"
},
"js/recipe_view.js": {
"name": "js/recipe_view.js",
"path": "js\\recipe_view.js"
"path": "js\\recipe_view.js",
"publicPath": "http://localhost:8080/js/recipe_view.js"
},
"js/supermarket_view.js": {
"name": "js/supermarket_view.js",
"path": "js\\supermarket_view.js"
"path": "js\\supermarket_view.js",
"publicPath": "http://localhost:8080/js/supermarket_view.js"
},
"js/user_file_view.js": {
"name": "js/user_file_view.js",
"path": "js\\user_file_view.js"
"path": "js\\user_file_view.js",
"publicPath": "http://localhost:8080/js/user_file_view.js"
},
"recipe_search_view.html": {
"name": "recipe_search_view.html",
"path": "recipe_search_view.html"
"path": "recipe_search_view.html",
"publicPath": "http://localhost:8080/recipe_search_view.html"
},
"recipe_view.html": {
"name": "recipe_view.html",
"path": "recipe_view.html"
"path": "recipe_view.html",
"publicPath": "http://localhost:8080/recipe_view.html"
},
"offline_view.html": {
"name": "offline_view.html",
"path": "offline_view.html"
"path": "offline_view.html",
"publicPath": "http://localhost:8080/offline_view.html"
},
"import_response_view.html": {
"name": "import_response_view.html",
"path": "import_response_view.html"
"path": "import_response_view.html",
"publicPath": "http://localhost:8080/import_response_view.html"
},
"supermarket_view.html": {
"name": "supermarket_view.html",
"path": "supermarket_view.html"
"path": "supermarket_view.html",
"publicPath": "http://localhost:8080/supermarket_view.html"
},
"user_file_view.html": {
"name": "user_file_view.html",
"path": "user_file_view.html"
"path": "user_file_view.html",
"publicPath": "http://localhost:8080/user_file_view.html"
},
"model_list_view.html": {
"name": "model_list_view.html",
"path": "model_list_view.html"
"path": "model_list_view.html",
"publicPath": "http://localhost:8080/model_list_view.html"
},
"edit_internal_recipe.html": {
"name": "edit_internal_recipe.html",
"path": "edit_internal_recipe.html"
"path": "edit_internal_recipe.html",
"publicPath": "http://localhost:8080/edit_internal_recipe.html"
},
"cookbook_view.html": {
"name": "cookbook_view.html",
"path": "cookbook_view.html"
"path": "cookbook_view.html",
"publicPath": "http://localhost:8080/cookbook_view.html"
},
"meal_plan_view.html": {
"name": "meal_plan_view.html",
"path": "meal_plan_view.html"
"path": "meal_plan_view.html",
"publicPath": "http://localhost:8080/meal_plan_view.html"
},
"manifest.json": {
"name": "manifest.json",
"path": "manifest.json"
"path": "manifest.json",
"publicPath": "http://localhost:8080/manifest.json"
},
"model_list_view.7cf8cfb04383ac21dae2.hot-update.js": {
"name": "model_list_view.7cf8cfb04383ac21dae2.hot-update.js",
"path": "model_list_view.7cf8cfb04383ac21dae2.hot-update.js",
"publicPath": "http://localhost:8080/model_list_view.7cf8cfb04383ac21dae2.hot-update.js"
},
"7cf8cfb04383ac21dae2.hot-update.json": {
"name": "7cf8cfb04383ac21dae2.hot-update.json",
"path": "7cf8cfb04383ac21dae2.hot-update.json",
"publicPath": "http://localhost:8080/7cf8cfb04383ac21dae2.hot-update.json"
},
"model_list_view.65f25fe27091809f7fcf.hot-update.js": {
"name": "model_list_view.65f25fe27091809f7fcf.hot-update.js",
"path": "model_list_view.65f25fe27091809f7fcf.hot-update.js",
"publicPath": "http://localhost:8080/model_list_view.65f25fe27091809f7fcf.hot-update.js"
},
"65f25fe27091809f7fcf.hot-update.json": {
"name": "65f25fe27091809f7fcf.hot-update.json",
"path": "65f25fe27091809f7fcf.hot-update.json",
"publicPath": "http://localhost:8080/65f25fe27091809f7fcf.hot-update.json"
},
"model_list_view.032182ad6990812f8035.hot-update.js": {
"name": "model_list_view.032182ad6990812f8035.hot-update.js",
"path": "model_list_view.032182ad6990812f8035.hot-update.js",
"publicPath": "http://localhost:8080/model_list_view.032182ad6990812f8035.hot-update.js"
},
"032182ad6990812f8035.hot-update.json": {
"name": "032182ad6990812f8035.hot-update.json",
"path": "032182ad6990812f8035.hot-update.json",
"publicPath": "http://localhost:8080/032182ad6990812f8035.hot-update.json"
},
"model_list_view.cbedf3e2489989eec8dd.hot-update.js": {
"name": "model_list_view.cbedf3e2489989eec8dd.hot-update.js",
"path": "model_list_view.cbedf3e2489989eec8dd.hot-update.js",
"publicPath": "http://localhost:8080/model_list_view.cbedf3e2489989eec8dd.hot-update.js"
},
"cbedf3e2489989eec8dd.hot-update.json": {
"name": "cbedf3e2489989eec8dd.hot-update.json",
"path": "cbedf3e2489989eec8dd.hot-update.json",
"publicPath": "http://localhost:8080/cbedf3e2489989eec8dd.hot-update.json"
},
"model_list_view.87a9208c092e31885f51.hot-update.js": {
"name": "model_list_view.87a9208c092e31885f51.hot-update.js",
"path": "model_list_view.87a9208c092e31885f51.hot-update.js",
"publicPath": "http://localhost:8080/model_list_view.87a9208c092e31885f51.hot-update.js"
},
"87a9208c092e31885f51.hot-update.json": {
"name": "87a9208c092e31885f51.hot-update.json",
"path": "87a9208c092e31885f51.hot-update.json",
"publicPath": "http://localhost:8080/87a9208c092e31885f51.hot-update.json"
},
"cookbook_view.307bb5ac629ff8f5dba5.hot-update.js": {
"name": "cookbook_view.307bb5ac629ff8f5dba5.hot-update.js",
"path": "cookbook_view.307bb5ac629ff8f5dba5.hot-update.js",
"publicPath": "http://localhost:8080/cookbook_view.307bb5ac629ff8f5dba5.hot-update.js"
},
"edit_internal_recipe.307bb5ac629ff8f5dba5.hot-update.js": {
"name": "edit_internal_recipe.307bb5ac629ff8f5dba5.hot-update.js",
"path": "edit_internal_recipe.307bb5ac629ff8f5dba5.hot-update.js",
"publicPath": "http://localhost:8080/edit_internal_recipe.307bb5ac629ff8f5dba5.hot-update.js"
},
"import_response_view.307bb5ac629ff8f5dba5.hot-update.js": {
"name": "import_response_view.307bb5ac629ff8f5dba5.hot-update.js",
"path": "import_response_view.307bb5ac629ff8f5dba5.hot-update.js",
"publicPath": "http://localhost:8080/import_response_view.307bb5ac629ff8f5dba5.hot-update.js"
},
"meal_plan_view.307bb5ac629ff8f5dba5.hot-update.js": {
"name": "meal_plan_view.307bb5ac629ff8f5dba5.hot-update.js",
"path": "meal_plan_view.307bb5ac629ff8f5dba5.hot-update.js",
"publicPath": "http://localhost:8080/meal_plan_view.307bb5ac629ff8f5dba5.hot-update.js"
},
"model_list_view.307bb5ac629ff8f5dba5.hot-update.js": {
"name": "model_list_view.307bb5ac629ff8f5dba5.hot-update.js",
"path": "model_list_view.307bb5ac629ff8f5dba5.hot-update.js",
"publicPath": "http://localhost:8080/model_list_view.307bb5ac629ff8f5dba5.hot-update.js"
},
"offline_view.307bb5ac629ff8f5dba5.hot-update.js": {
"name": "offline_view.307bb5ac629ff8f5dba5.hot-update.js",
"path": "offline_view.307bb5ac629ff8f5dba5.hot-update.js",
"publicPath": "http://localhost:8080/offline_view.307bb5ac629ff8f5dba5.hot-update.js"
},
"recipe_search_view.307bb5ac629ff8f5dba5.hot-update.js": {
"name": "recipe_search_view.307bb5ac629ff8f5dba5.hot-update.js",
"path": "recipe_search_view.307bb5ac629ff8f5dba5.hot-update.js",
"publicPath": "http://localhost:8080/recipe_search_view.307bb5ac629ff8f5dba5.hot-update.js"
},
"recipe_view.307bb5ac629ff8f5dba5.hot-update.js": {
"name": "recipe_view.307bb5ac629ff8f5dba5.hot-update.js",
"path": "recipe_view.307bb5ac629ff8f5dba5.hot-update.js",
"publicPath": "http://localhost:8080/recipe_view.307bb5ac629ff8f5dba5.hot-update.js"
},
"supermarket_view.307bb5ac629ff8f5dba5.hot-update.js": {
"name": "supermarket_view.307bb5ac629ff8f5dba5.hot-update.js",
"path": "supermarket_view.307bb5ac629ff8f5dba5.hot-update.js",
"publicPath": "http://localhost:8080/supermarket_view.307bb5ac629ff8f5dba5.hot-update.js"
},
"user_file_view.307bb5ac629ff8f5dba5.hot-update.js": {
"name": "user_file_view.307bb5ac629ff8f5dba5.hot-update.js",
"path": "user_file_view.307bb5ac629ff8f5dba5.hot-update.js",
"publicPath": "http://localhost:8080/user_file_view.307bb5ac629ff8f5dba5.hot-update.js"
},
"307bb5ac629ff8f5dba5.hot-update.json": {
"name": "307bb5ac629ff8f5dba5.hot-update.json",
"path": "307bb5ac629ff8f5dba5.hot-update.json",
"publicPath": "http://localhost:8080/307bb5ac629ff8f5dba5.hot-update.json"
},
"model_list_view.e2073a90ad8ff66f374c.hot-update.js": {
"name": "model_list_view.e2073a90ad8ff66f374c.hot-update.js",
"path": "model_list_view.e2073a90ad8ff66f374c.hot-update.js",
"publicPath": "http://localhost:8080/model_list_view.e2073a90ad8ff66f374c.hot-update.js"
},
"e2073a90ad8ff66f374c.hot-update.json": {
"name": "e2073a90ad8ff66f374c.hot-update.json",
"path": "e2073a90ad8ff66f374c.hot-update.json",
"publicPath": "http://localhost:8080/e2073a90ad8ff66f374c.hot-update.json"
},
"model_list_view.1b14899ea7152cc46f4e.hot-update.js": {
"name": "model_list_view.1b14899ea7152cc46f4e.hot-update.js",
"path": "model_list_view.1b14899ea7152cc46f4e.hot-update.js",
"publicPath": "http://localhost:8080/model_list_view.1b14899ea7152cc46f4e.hot-update.js"
},
"1b14899ea7152cc46f4e.hot-update.json": {
"name": "1b14899ea7152cc46f4e.hot-update.json",
"path": "1b14899ea7152cc46f4e.hot-update.json",
"publicPath": "http://localhost:8080/1b14899ea7152cc46f4e.hot-update.json"
},
"model_list_view.7a2ce4182c97623ab7e8.hot-update.js": {
"name": "model_list_view.7a2ce4182c97623ab7e8.hot-update.js",
"path": "model_list_view.7a2ce4182c97623ab7e8.hot-update.js",
"publicPath": "http://localhost:8080/model_list_view.7a2ce4182c97623ab7e8.hot-update.js"
},
"7a2ce4182c97623ab7e8.hot-update.json": {
"name": "7a2ce4182c97623ab7e8.hot-update.json",
"path": "7a2ce4182c97623ab7e8.hot-update.json",
"publicPath": "http://localhost:8080/7a2ce4182c97623ab7e8.hot-update.json"
}
},
"chunks": {
"recipe_search_view": [
"css/chunk-vendors.css",
"js/chunk-vendors.js",
"css/recipe_search_view.css",
"js/recipe_search_view.js"
],
"recipe_view": [
"css/chunk-vendors.css",
"js/chunk-vendors.js",
"css/recipe_view.css",
"js/recipe_view.js"
],
"offline_view": [
"css/chunk-vendors.css",
"js/chunk-vendors.js",
"js/offline_view.js"
],
"import_response_view": [
"css/chunk-vendors.css",
"js/chunk-vendors.js",
"js/import_response_view.js"
],
"supermarket_view": [
"css/chunk-vendors.css",
"js/chunk-vendors.js",
"js/supermarket_view.js"
],
"user_file_view": [
"css/chunk-vendors.css",
"js/chunk-vendors.js",
"js/user_file_view.js"
],
"model_list_view": [
"css/chunk-vendors.css",
"js/chunk-vendors.js",
"css/model_list_view.css",
"js/model_list_view.js"
"js/model_list_view.js",
"model_list_view.7a2ce4182c97623ab7e8.hot-update.js"
],
"edit_internal_recipe": [
"css/chunk-vendors.css",
"js/chunk-vendors.js",
"css/edit_internal_recipe.css",
"js/edit_internal_recipe.js"
],
"cookbook_view": [
"css/chunk-vendors.css",
"js/chunk-vendors.js",
"css/cookbook_view.css",
"js/cookbook_view.js"
],
"meal_plan_view": [
"css/chunk-vendors.css",
"js/chunk-vendors.js",
"css/meal_plan_view.css",
"js/meal_plan_view.js"
]
}
},
"publicPath": "http://localhost:8080/"
}