manually parse json

This commit is contained in:
smilerz 2021-03-15 15:56:44 -05:00
parent 25fb41baed
commit 71e02c0916
6 changed files with 306 additions and 339 deletions

View File

@ -1,16 +1,16 @@
import json import json
import re from json.decoder import JSONDecodeError
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from bs4.element import Tag from bs4.element import Tag
# from cookbook.helper.ingredient_parser import parse as parse_ingredient
from cookbook.helper import recipe_url_import as helper from cookbook.helper import recipe_url_import as helper
from cookbook.helper.scrapers.scrapers import text_scraper
from json import JSONDecodeError
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_from_raw(text):
def build_node(k, v): def build_node(k, v):
if isinstance(v, dict): if isinstance(v, dict):
node = { node = {
@ -26,8 +26,8 @@ def get_recipe_from_source(text, url, space):
} }
else: else:
node = { node = {
'name': k + ": " + normalize_string(str(v)), 'name': k + ": " + str(v),
'value': normalize_string(str(v)) 'value': str(v)
} }
return node return node
@ -52,14 +52,13 @@ def get_recipe_from_source(text, url, space):
kid_list.append(build_node(k, v)) kid_list.append(build_node(k, v))
else: else:
kid_list.append({ kid_list.append({
'name': normalize_string(str(kid)), 'name': kid,
'value': normalize_string(str(kid)) 'value': kid
}) })
return kid_list return kid_list
recipe_json = { recipe_json = {
'name': '', 'name': '',
'url': '',
'description': '', 'description': '',
'image': '', 'image': '',
'keywords': [], 'keywords': [],
@ -68,51 +67,26 @@ def get_recipe_from_source(text, url, space):
'servings': '', 'servings': '',
'prepTime': '', 'prepTime': '',
'cookTime': '' 'cookTime': ''
} }
recipe_tree = [] recipe_tree = []
temp_tree = []
parse_list = [] parse_list = []
html_data = []
images = []
text = unquote(text)
try: try:
parse_list.append(remove_graph(json.loads(text))) parse_list.append(json.loads(text))
if not url and 'url' in parse_list[0]:
url = parse_list[0]['url']
scrape = text_scraper("<script type='application/ld+json'>" + text + "</script>", url=url)
except JSONDecodeError: except JSONDecodeError:
soup = BeautifulSoup(text, "html.parser") soup = BeautifulSoup(text, "html.parser")
html_data = get_from_html(soup)
images += get_images_from_source(soup, url)
for el in soup.find_all('script', type='application/ld+json'): for el in soup.find_all('script', type='application/ld+json'):
el = remove_graph(el) parse_list.append(el)
if not url and 'url' in el:
url = el['url']
if type(el) == list:
for le in el:
parse_list.append(le)
elif type(el) == dict:
parse_list.append(el)
for el in soup.find_all(type='application/json'): for el in soup.find_all(type='application/json'):
el = remove_graph(el) parse_list.append(el)
if type(el) == list:
for le in el:
parse_list.append(le)
elif type(el) == dict:
parse_list.append(el)
scrape = text_scraper(text, url=url)
recipe_json = helper.get_from_scraper(scrape, space)
# first try finding ld+json as its most common
for el in parse_list: for el in parse_list:
temp_tree = []
if isinstance(el, Tag):
try:
el = json.loads(el.string)
except TypeError:
continue
if isinstance(el, Tag):
el = json.loads(el.string)
for k, v in el.items(): for k, v in el.items():
if isinstance(v, dict): if isinstance(v, dict):
node = { node = {
@ -128,66 +102,22 @@ def get_recipe_from_source(text, url, space):
} }
else: else:
node = { node = {
'name': k + ": " + normalize_string(str(v)), 'name': k + ": " + str(v),
'value': normalize_string(str(v)) 'value': str(v)
} }
temp_tree.append(node) temp_tree.append(node)
if ('@type' in el and el['@type'] == 'Recipe'):
if '@type' in el and el['@type'] == 'Recipe': recipe_json = helper.find_recipe_json(el, None)
recipe_tree += [{'name': 'ld+json', 'children': temp_tree}] recipe_tree += [{'name': 'ld+json', 'children': temp_tree}]
else: else:
recipe_tree += [{'name': 'json', 'children': temp_tree}] recipe_tree += [{'name': 'json', 'children': temp_tree}]
return recipe_json, recipe_tree, html_data, images temp_tree = []
# overide keyword structure from dict to list
kws = []
for kw in recipe_json['keywords']:
kws.append(kw['text'])
recipe_json['keywords'] = kws
def get_from_html(soup): return recipe_json, recipe_tree
INVISIBLE_ELEMS = ('style', 'script', 'head', 'title')
html = []
for s in soup.strings:
if ((s.parent.name not in INVISIBLE_ELEMS) and (len(s.strip()) > 0)):
html.append(s)
return html
def get_images_from_source(soup, url):
sources = ['src', 'srcset', 'data-src']
images = []
img_tags = soup.find_all('img')
if url:
site = get_host_name(url)
prot = url.split(':')[0]
urls = []
for img in img_tags:
for src in sources:
try:
urls.append(img[src])
except KeyError:
pass
for u in urls:
u = u.split('?')[0]
filename = re.search(r'/([\w_-]+[.](jpg|jpeg|gif|png))$', u)
if filename:
if (('http' not in u) and (url)):
# sometimes an image source can be relative
# if it is provide the base url
u = '{}://{}{}'.format(prot, site, u)
if 'http' in u:
images.append(u)
return images
def remove_graph(el):
# recipes type might be wrapped in @graph type
if isinstance(el, Tag):
try:
el = json.loads(el.string)
if '@graph' in el:
for x in el['@graph']:
if '@type' in x and x['@type'] == 'Recipe':
el = x
except TypeError:
pass
return el

View File

@ -1,5 +1,6 @@
import random import random
import re import re
from json import JSONDecodeError
from isodate import parse_duration as iso_parse_duration from isodate import parse_duration as iso_parse_duration
from isodate.isoerror import ISO8601Error from isodate.isoerror import ISO8601Error
@ -63,6 +64,8 @@ def find_recipe_json(ld_json, url):
if 'recipeIngredient' in ld_json: if 'recipeIngredient' in ld_json:
ld_json['recipeIngredient'] = parse_ingredients(ld_json['recipeIngredient']) ld_json['recipeIngredient'] = parse_ingredients(ld_json['recipeIngredient'])
else:
ld_json['recipeIngredient'] = ""
try: try:
servings = scrape.yields() servings = scrape.yields()
@ -90,22 +93,40 @@ def find_recipe_json(ld_json, url):
if 'recipeCategory' in ld_json: if 'recipeCategory' in ld_json:
keywords += listify_keywords(ld_json['recipeCategory']) keywords += listify_keywords(ld_json['recipeCategory'])
if 'recipeCuisine' in ld_json: if 'recipeCuisine' in ld_json:
keywords += listify_keywords(ld_json['keywords']) keywords += listify_keywords(ld_json['recipeCuisine'])
ld_json['keywords'] = parse_keywords(list(set(map(str.casefold, keywords)))) try:
ld_json['keywords'] = parse_keywords(list(set(map(str.casefold, keywords))))
except TypeError:
pass
if 'recipeInstructions' in ld_json: if 'recipeInstructions' in ld_json:
ld_json['recipeInstructions'] = parse_instructions(ld_json['recipeInstructions']) ld_json['recipeInstructions'] = parse_instructions(ld_json['recipeInstructions'])
else:
ld_json['recipeInstructions'] = ""
if 'image' in ld_json: if 'image' in ld_json:
ld_json['image'] = parse_image(ld_json['image']) ld_json['image'] = parse_image(ld_json['image'])
else:
ld_json['image'] = ""
if 'description' not in ld_json:
ld_json['description'] = ""
if 'cookTime' in ld_json: if 'cookTime' in ld_json:
ld_json['cookTime'] = parse_cooktime(ld_json['cookTime']) ld_json['cookTime'] = parse_cooktime(ld_json['cookTime'])
else:
ld_json['cookTime'] = 0
if 'prepTime' in ld_json: if 'prepTime' in ld_json:
ld_json['prepTime'] = parse_cooktime(ld_json['prepTime']) ld_json['prepTime'] = parse_cooktime(ld_json['prepTime'])
else:
ld_json['prepTime'] = 0
ld_json['servings'] = 1 if 'servings' in ld_json:
if type(ld_json['servings']) == str:
ld_json['servings'] = int(re.search(r'\d+', ld_json['servings']).group())
else:
ld_json['servings'] = 1
try: try:
if 'recipeYield' in ld_json: if 'recipeYield' in ld_json:
if type(ld_json['recipeYield']) == str: if type(ld_json['recipeYield']) == str:
@ -118,7 +139,7 @@ def find_recipe_json(ld_json, url):
for key in list(ld_json): for key in list(ld_json):
if key not in [ if key not in [
'prepTime', 'cookTime', 'image', 'recipeInstructions', 'prepTime', 'cookTime', 'image', 'recipeInstructions',
'keywords', 'name', 'recipeIngredient', 'servings' 'keywords', 'name', 'recipeIngredient', 'servings', 'description'
]: ]:
ld_json.pop(key, None) ld_json.pop(key, None)
@ -136,6 +157,12 @@ def parse_name(name):
def parse_ingredients(ingredients): def parse_ingredients(ingredients):
# some pages have comma separated ingredients in a single array entry # some pages have comma separated ingredients in a single array entry
try:
if type(ingredients[0]) == dict:
return ingredients
except (KeyError, IndexError):
pass
if (len(ingredients) == 1 and type(ingredients) == list): if (len(ingredients) == 1 and type(ingredients) == list):
ingredients = ingredients[0].split(',') ingredients = ingredients[0].split(',')
elif type(ingredients) == str: elif type(ingredients) == str:
@ -216,50 +243,59 @@ def parse_instructions(instructions):
instructions = re.sub(r'\n\s*\n', '\n\n', instructions) instructions = re.sub(r'\n\s*\n', '\n\n', instructions)
instructions = re.sub(' +', ' ', instructions) instructions = re.sub(' +', ' ', instructions)
instructions = instructions.replace('<p>', '') instructions = re.sub('</p>', '\n', instructions)
instructions = instructions.replace('</p>', '') instructions = re.sub('<[^<]+?>', '', instructions)
return instruction_text return instructions
def parse_image(image): def parse_image(image):
# check if list of images is returned, take first if so # check if list of images is returned, take first if so
if (type(image)) == list: if type(image) == list:
if type(image[0]) == str: for pic in image:
image = image[0] if (type(pic) == str) and (pic[:4] == 'http'):
elif 'url' in image[0]: image = pic
image = image[0]['url'] elif 'url' in pic:
image = pic['url']
# ignore relative image paths # ignore relative image paths
if 'http' not in image: if image[:4] != 'http':
image = '' image = ''
return image return image
def parse_cooktime(cooktime): def parse_cooktime(cooktime):
try: if type(cooktime) not in [int, float]:
if (type(cooktime) == list and len(cooktime) > 0): try:
cooktime = cooktime[0] cooktime = float(re.search(r'\d+', cooktime).group())
cooktime = round(parse_duration(cooktime).seconds / 60) except (ValueError, AttributeError):
except TypeError: try:
cooktime = 0 cooktime = round(iso_parse_duration(cooktime).seconds / 60)
if type(cooktime) != int or float: except ISO8601Error:
cooktime = 0 try:
if (type(cooktime) == list and len(cooktime) > 0):
cooktime = cooktime[0]
cooktime = round(parse_duration(cooktime).seconds / 60)
except AttributeError:
cooktime = 0
return cooktime return cooktime
def parse_preptime(preptime): def parse_preptime(preptime):
try: if type(preptime) not in [int, float]:
if (type(preptime) == list and len(preptime) > 0): try:
preptime = preptime[0] preptime = float(re.search(r'\d+', preptime).group())
preptime = round( except ValueError:
parse_duration( try:
preptime preptime = round(iso_parse_duration(preptime).seconds / 60)
).seconds / 60 except ISO8601Error:
) try:
except TypeError: if (type(preptime) == list and len(preptime) > 0):
preptime = 0 preptime = preptime[0]
if type(preptime) != int or float: preptime = round(parse_duration(preptime).seconds / 60)
preptime = 0 except AttributeError:
preptime = 0
return preptime return preptime
@ -277,6 +313,11 @@ def parse_keywords(keyword_json):
def listify_keywords(keyword_list): def listify_keywords(keyword_list):
# keywords as string # keywords as string
try:
if type(keyword_list[0]) == dict:
return keyword_list
except KeyError:
pass
if type(keyword_list) == str: if type(keyword_list) == str:
keyword_list = keyword_list.split(',') keyword_list = keyword_list.split(',')

View File

@ -1,3 +1,4 @@
<!--I PROBLABLY DON'T NEED THIS??-->
{% extends "base.html" %} {% extends "base.html" %}
{% load crispy_forms_filters %} {% load crispy_forms_filters %}
{% load i18n %} {% load i18n %}
@ -24,7 +25,7 @@
<div v-if="!parsed"> <div v-if="!parsed">
<h2>{% trans 'Import From Source' %}</h2> <h2>{% trans 'Import From Source' %}</h2>
<div class="input-group input-group-lg"> <div class="input-group input-group-lg">
<textarea class="form-control" v-model="raw_recipe" rows="12" style="font-size: 12px"></textarea> <textarea class="form-control" v-model="html_recipe" rows="12" style="font-size: 12px"></textarea>
</div> </div>
<small class="text-muted">Simply paste a web page source or JSON document into this textarea and click import.</small> <small class="text-muted">Simply paste a web page source or JSON document into this textarea and click import.</small>
<br> <br>
@ -51,18 +52,17 @@
<template scope="_"> <template scope="_">
<div class="container-fluid" > <div class="container-fluid" >
<div class="col" @click.ctrl="customItemClickWithCtrl"> <div class="col-md-12" >
<div class="row clearfix" style="width:50%" > <div class="row clearfix" style="width:95%" >
<div class="col"> <div class="col">
<i :class="_.vm.themeIconClasses" role="presentation" v-if="!_.model.loading"></i> <i :class="_.vm.themeIconClasses" role="presentation" v-if="!_.model.loading"></i>
{% verbatim %} {% verbatim %}
[[_.model.name]] [[_.model.name]]
{% endverbatim %} {% endverbatim %}
</div> </div>
<div class="col-es" style="align-right"> <div class="col-es-auto" style="align-right">
<button style="border: 0px; background-color: transparent; cursor: pointer;" <button style="border: 0px; background-color: transparent; cursor: pointer;"
@click="deleteNode(_.vm, _.model, $event)"><i class="fas fa-minus-square" style="color:red"></i></button> @click="deleteNode(_.vm, _.model, $event)"><i class="fas fa-minus-square" style="color:red"></i></button>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -93,7 +93,7 @@
delimiters: ['[[', ']]'], delimiters: ['[[', ']]'],
el: '#app', el: '#app',
data: { data: {
raw_recipe: '', html_recipe: '',
keywords: [], keywords: [],
keywords_loading: false, keywords_loading: false,
units: [], units: [],
@ -137,7 +137,7 @@
this.error = undefined this.error = undefined
this.parsed = true this.parsed = true
this.loading = true this.loading = true
this.$http.post("{% url 'api_recipe_from_raw' %}", {'raw_text': this.raw_recipe}, {emulateJSON: true}).then((response) => { this.$http.post("{% url 'api_recipe_from_html' %}", {'html_text': this.html_recipe}, {emulateJSON: true}).then((response) => {
console.log(response.data) console.log(response.data)
this.recipe_data = response.data['recipe_data']; this.recipe_data = response.data['recipe_data'];
this.recipe_tree = response.data['recipe_tree']; this.recipe_tree = response.data['recipe_tree'];
@ -278,7 +278,7 @@
e.stopPropagation() e.stopPropagation()
var index = node.parentItem.indexOf(item) var index = node.parentItem.indexOf(item)
node.parentItem.splice(index, 1) node.parentItem.splice(index, 1)
}, },
} }
}); });
</script> </script>

View File

@ -1,3 +1,4 @@
<!--I PROBLABLY DON'T NEED THIS??-->
{% extends "base.html" %} {% extends "base.html" %}
{% load crispy_forms_filters %} {% load crispy_forms_filters %}
{% load i18n %} {% load i18n %}
@ -24,7 +25,7 @@
<div v-if="!parsed"> <div v-if="!parsed">
<h2>{% trans 'Import From Source' %}</h2> <h2>{% trans 'Import From Source' %}</h2>
<div class="input-group input-group-lg"> <div class="input-group input-group-lg">
<textarea class="form-control" v-model="raw_recipe" rows="12" style="font-size: 12px"></textarea> <textarea class="form-control" v-model="html_recipe" rows="12" style="font-size: 12px"></textarea>
</div> </div>
<small class="text-muted">Simply paste a web page source or JSON document into this textarea and click import.</small> <small class="text-muted">Simply paste a web page source or JSON document into this textarea and click import.</small>
<br> <br>
@ -94,7 +95,7 @@
delimiters: ['[[', ']]'], delimiters: ['[[', ']]'],
el: '#app', el: '#app',
data: { data: {
raw_recipe: '', html_recipe: '',
keywords: [], keywords: [],
keywords_loading: false, keywords_loading: false,
units: [], units: [],
@ -151,7 +152,7 @@
this.error = undefined this.error = undefined
this.parsed = true this.parsed = true
this.loading = true this.loading = true
this.$http.post("{% url 'api_recipe_from_raw' %}", {'raw_text': this.raw_recipe}, {emulateJSON: true}).then((response) => { this.$http.post("{% url 'api_recipe_from_html' %}", {'html_text': this.html_recipe}, {emulateJSON: true}).then((response) => {
console.log(response.data) console.log(response.data)
this.recipe_data = response.data['recipe_data']; this.recipe_data = response.data['recipe_data'];
this.recipe_tree = response.data['recipe_tree']; this.recipe_tree = response.data['recipe_tree'];

View File

@ -8,6 +8,7 @@
{% include 'include/vue_base.html' %} {% include 'include/vue_base.html' %}
<script src="{% static 'js/jquery-3.5.1.min.js' %}"></script> <script src="{% static 'js/jquery-3.5.1.min.js' %}"></script>
<script src="{% static 'js/vue-jstree.js' %}"></script>
<script src="{% static 'js/vue-multiselect.min.js' %}"></script> <script src="{% static 'js/vue-multiselect.min.js' %}"></script>
<link rel="stylesheet" href="{% static 'css/vue-multiselect.min.css' %}"> <link rel="stylesheet" href="{% static 'css/vue-multiselect.min.css' %}">
<style> <style>
@ -25,13 +26,15 @@
<nav class="nav nav-pills flex-sm-row" style="margin-bottom:10px"> <nav class="nav nav-pills flex-sm-row" style="margin-bottom:10px">
<a class="nav-link active" href="#nav-url" data-toggle="tab" role="tab" aria-controls="nav-url" aria-selected="true">URL</a> <a class="nav-link active" href="#nav-url" data-toggle="tab" role="tab" aria-controls="nav-url" aria-selected="true">URL</a>
<a class="nav-link" href="#nav-ldjson" data-toggle="tab" role="tab" aria-controls="nav-ldjson">ld+json</a> <a class="nav-link" href="#nav-ldjson" data-toggle="tab" role="tab" aria-controls="nav-ldjson">ld+json</a>
<a class="nav-link disabled" href="#nav-json" data-toggle="tab" role="tab" aria-controls="nav-json">json</a> <a class="nav-link" href="#nav-json" data-toggle="tab" role="tab" aria-controls="nav-json">json</a>
<a class="nav-link disabled" href="#nav-html" data-toggle="tab" role="tab" aria-controls="nav-html">HTML</a> <a class="nav-link disabled" href="#nav-html" data-toggle="tab" role="tab" aria-controls="nav-html">HTML</a>
<a class="nav-link disabled" href="#nav-text" data-toggle="tab" role="tab" aria-controls="nav-text">text</a> <a class="nav-link disabled" href="#nav-text" data-toggle="tab" role="tab" aria-controls="nav-text">text</a>
<a class="nav-link disabled" href="#nav-pdf" data-toggle="tab" role="tab" aria-controls="nav-pdf">PDF</a> <a class="nav-link disabled" href="#nav-pdf" data-toggle="tab" role="tab" aria-controls="nav-pdf">PDF</a>
</nav> </nav>
<div class="tab-content" id="nav-tabContent"> <div class="tab-content" id="nav-tabContent">
<!-- Import URL -->
<div class="row tab-pane fade show active" id="nav-url" role="tabpanel"> <div class="row tab-pane fade show active" id="nav-url" role="tabpanel">
<div class="col-md-12"> <div class="col-md-12">
<div class="input-group mb-3"> <div class="input-group mb-3">
@ -45,10 +48,11 @@
</div> </div>
</div> </div>
<!-- Automatically import LD+JSON -->
<div class="row tab-pane fade show" id="nav-ldjson" role="tabpanel"> <div class="row tab-pane fade show" id="nav-ldjson" role="tabpanel">
<div class="col-md-12"> <div class="col-md-12">
<div class="input-group input-group-lg"> <div class="input-group input-group-lg">
<textarea class="form-control input-group-append" v-model="json_data" rows=10; placeholder="{% trans 'Paste ld+json here' %}"> <textarea class="form-control input-group-append" v-model="json_data" rows=10 placeholder="{% trans 'Paste ld+json here to parse recipe automatically.' %}" style="font-size: 12px">
</textarea> </textarea>
</div> </div>
<br> <br>
@ -58,15 +62,31 @@
</div> </div>
</div> </div>
<div class="row tab-pane fade show" id="nav-html" role="tabpanel"> <!-- Manually import from JSON -->
<div class="row tab-pane fade show" id="nav-json" role="tabpanel">
<div class="col-md-12"> <div class="col-md-12">
<div class="input-group input-group-lg"> <div class="input-group input-group-lg">
<textarea class="form-control input-group-append" v-model="html_data" rows=10; placeholder="{% trans 'Paste html source here' %}"> <textarea class="form-control input-group-append" v-model="html_data" rows=10
placeholder="{% trans 'To parse recipe manually: Paste JSON document here or a web page source that contains one or more JSON elements here.' %}" style="font-size: 12px">
</textarea> </textarea>
</div> </div>
<br> <br>
<button @click="loadRecipeJson()" class="btn btn-primary shadow-none" type="button" <button @click="loadPreviewRaw()" class="btn btn-primary shadow-none" type="button"
id="id_btn_html"><i class="fas fa-code"></i> {% trans 'Import' %} id="id_btn_raw"><i class="fas fa-code"></i> {% trans 'Preview Import' %}
</button>
</div>
</div>
<!-- Manually import from HTML -->
<div class="row tab-pane fade show" id="nav-html" role="tabpanel">
<div class="col-md-12">
<div class="input-group input-group-lg">
<textarea class="form-control input-group-append" v-model="html_data" rows=10 placeholder="{% trans 'Paste html source here to parse recipe manually.' %}" style="font-size: 12px">
</textarea>
</div>
<br>
<button @click="loadPreviewHTML()" class="btn btn-primary shadow-none" type="button"
id="id_btn_HTML"><i class="fas fa-code"></i> {% trans 'Preview Import' %}
</button> </button>
</div> </div>
</div> </div>
@ -80,11 +100,12 @@
<i class="fas fa-spinner fa-spin fa-8x"></i> <i class="fas fa-spinner fa-spin fa-8x"></i>
</div> </div>
<!-- recipe preview before Import --> <!-- recipe preview on HTML Import -->
<div class="container-fluid" v-if="preview" id="manage_tree"> <div class="container-fluid" v-if="parsed" id="manage_tree">
<h2></h2>
<div class="row"> <div class="row">
<div class="col" style="max-width:50%"> <div class="col" style="max-width:50%">
<!-- start of preview card -->
<div class="card card-border-primary" > <div class="card card-border-primary" >
<div class="card-header"> <div class="card-header">
<h3>{% trans 'Preview Recipe Data' %}</h3> <h3>{% trans 'Preview Recipe Data' %}</h3>
@ -93,70 +114,54 @@
<div class="card-body p-2"> <div class="card-body p-2">
<div class="card mb-2"> <div class="card mb-2">
<div class="card-header" v-b-toggle.collapse-name> <div class="card-header" style="display:flex; justify-content:space-between;">
<div class="row px-3" style="justify-content:space-between;"> {% trans 'Name' %}
{% trans 'Name' %} <i class="fas fa-eraser" style="cursor:pointer;" @click="deletePreview('name')" title="{% trans 'Clear Contents'%}"></i>
<i class="fas fa-eraser" style="cursor:pointer;" @click="recipe_json.name=''" title="{% trans 'Clear Contents'%}"></i> </div>
</div>
<div class="small text-muted">{% trans 'Text dragged here will be appended to the name.'%}</div> <div class="card-body drop-zone" @drop="replacePreview('name', $event)" @dragover.prevent @dragenter.prevent>
<div class="card-text">[[recipe_json.name]]</div>
</div> </div>
<b-collapse id="collapse-name" visible class="mt-2">
<div class="card-body drop-zone" @drop="replacePreview('name', $event)" @dragover.prevent @dragenter.prevent>
<div class="card-text">[[recipe_json.name]]</div>
</div>
</b-collapse>
</div> </div>
<div class="card mb-2"> <div class="card mb-2">
<div class="card-header" v-b-toggle.collapse-description> <div class="card-header" style="display:flex; justify-content:space-between;">
<div class="row px-3" style="justify-content:space-between;"> {% trans 'Description' %}
{% trans 'Description' %} <i class="fas fa-eraser" style="cursor:pointer;" @click="deletePreview('description')" title="{% trans 'Clear Contents'%}"></i>
<i class="fas fa-eraser" style="cursor:pointer;" @click="recipe_json.description=''" title="{% trans 'Clear Contents'%}"></i> </div>
</div> <div class="card-body drop-zone" @drop="replacePreview('description', $event)" @dragover.prevent @dragenter.prevent>
<div class="small text-muted">{% trans 'Text dragged here will be appended to the description.'%}</div> <div class="card-text">[[recipe_json.description]]</div>
</div> </div>
<b-collapse id="collapse-description" visible class="mt-2">
<div class="card-body drop-zone" @drop="replacePreview('description', $event)" @dragover.prevent @dragenter.prevent>
<div class="card-text">[[recipe_json.description]]</div>
</div>
</b-collapse>
</div> </div>
<div class="card mb-2"> <div class="card mb-2">
<div class="card-header" v-b-toggle.collapse-kw> <div class="card-header" style="display:flex; justify-content:space-between;">
<div class="row px-3" style="justify-content:space-between;"> {% trans 'Keywords' %}
{% trans 'Keywords' %} <i class="fas fa-eraser" style="cursor:pointer;" @click="deletePreview('keywords')" title="{% trans 'Clear Contents'%}"></i>
<i class="fas fa-eraser" style="cursor:pointer;" @click="recipe_json.keywords=[]" title="{% trans 'Clear Contents'%}"></i>
</div>
<div class="small text-muted">{% trans 'Keywords dragged here will be appended to current list'%}</div>
</div> </div>
<b-collapse id="collapse-kw" visible class="mt-2"> <div class="card-body drop-zone" @drop="replacePreview('keywords', $event)" @dragover.prevent @dragenter.prevent>
<div class="card-body drop-zone" @drop="replacePreview('keywords', $event)" @dragover.prevent @dragenter.prevent> <div v-for="kw in recipe_json.keywords">
<div v-for="kw in recipe_json.keywords"> <div class="card-text">[[kw]] </div>
<div class="card-text">[[kw.text]] </div>
</div>
</div> </div>
</b-collapse> </div>
</div> </div>
<div class="card mb-2"> <div class="card mb-2">
<div class="card-header" v-b-toggle.collapse-image style="display:flex; justify-content:space-between;"> <div class="card-header" style="display:flex; justify-content:space-between;">
{% trans 'Image' %} {% trans 'Image' %}
<i class="fas fa-eraser" style="cursor:pointer;" @click="recipe_json.image=''" title="{% trans 'Clear Contents'%}"></i> <i class="fas fa-eraser" style="cursor:pointer;" @click="deletePreview('image')" title="{% trans 'Clear Contents'%}"></i>
</div>
<div class="card-body m-0 p-0 drop-zone" @drop="replacePreview('image', $event)" @dragover.prevent @dragenter.prevent>
<img class="card-img" v-bind:src="[[recipe_json.image]]" alt="Recipe Image">
</div> </div>
<b-collapse id="collapse-image" visible class="mt-2">
<div class="card-body m-0 p-0 drop-zone" @drop="replacePreview('image', $event)" @dragover.prevent @dragenter.prevent>
<img class="card-img" v-bind:src="[[recipe_json.image]]" alt="Recipe Image">
</div>
</b-collapse>
</div> </div>
<div class = "row mb-2"> <div class = "row mb-2">
<div class="col"> <div class="col">
<div class="card"> <div class="card" >
<div class="card-header p-1" style="display:flex; justify-content:space-between;"> <div class="card-header p-1" style="display:flex; justify-content:space-between;">
{% trans 'Servings' %} {% trans 'Servings' %}
<i class="fas fa-eraser" style="cursor:pointer;" @click="recipe_json.servings=''" title="{% trans 'Clear Contents'%}"></i> <i class="fas fa-eraser" style="cursor:pointer;" @click="deletePreview('servings')" title="{% trans 'Clear Contents'%}"></i>
</div> </div>
<div class="card-body p-2 drop-zone" @drop="replacePreview('servings', $event)" @dragover.prevent @dragenter.prevent> <div class="card-body p-2 drop-zone" @drop="replacePreview('servings', $event)" @dragover.prevent @dragenter.prevent>
<div class="card-text">[[recipe_json.servings]]</div> <div class="card-text">[[recipe_json.servings]]</div>
@ -167,7 +172,7 @@
<div class="card"> <div class="card">
<div class="card-header p-1" style="display:flex; justify-content:space-between;"> <div class="card-header p-1" style="display:flex; justify-content:space-between;">
{% trans 'Prep Time' %} {% trans 'Prep Time' %}
<i class="fas fa-eraser" style="cursor:pointer;" @click="recipe_json.prepTime=''" title="{% trans 'Clear Contents'%}"></i> <i class="fas fa-eraser" style="cursor:pointer;" @click="deletePreview('prepTime')" title="{% trans 'Clear Contents'%}"></i>
</div> </div>
<div class="card-body p-2 drop-zone" @drop="replacePreview('prepTime', $event)" @dragover.prevent @dragenter.prevent> <div class="card-body p-2 drop-zone" @drop="replacePreview('prepTime', $event)" @dragover.prevent @dragenter.prevent>
<div class="card-text">[[recipe_json.prepTime]]</div> <div class="card-text">[[recipe_json.prepTime]]</div>
@ -178,7 +183,7 @@
<div class="card"> <div class="card">
<div class="card-header p-1" style="display:flex; justify-content:space-between;"> <div class="card-header p-1" style="display:flex; justify-content:space-between;">
{% trans 'Cook Time' %} {% trans 'Cook Time' %}
<i class="fas fa-eraser" style="cursor:pointer;" @click="recipe_json.cookTime=''" title="{% trans 'Clear Contents'%}"></i> <i class="fas fa-eraser" style="cursor:pointer;" @click="deletePreview('cookTime')" title="{% trans 'Clear Contents'%}"></i>
</div> </div>
<div class="card-body p-2 drop-zone" @drop="replacePreview('cookTime', $event)" @dragover.prevent @dragenter.prevent> <div class="card-body p-2 drop-zone" @drop="replacePreview('cookTime', $event)" @dragover.prevent @dragenter.prevent>
<div class="card-text">[[recipe_json.cookTime]]</div> <div class="card-text">[[recipe_json.cookTime]]</div>
@ -188,96 +193,45 @@
</div> </div>
<div class="card mb-2"> <div class="card mb-2">
<div class="card-header" v-b-toggle.collapse-ing> <div class="card-header" style="display:flex; justify-content:space-between;">
<div class="row px-3" style="display:flex; justify-content:space-between;"> {% trans 'Ingredients' %}
{% trans 'Ingredients' %} <i class="fas fa-eraser" style="cursor:pointer;" @click="deletePreview('ingredients')" title="{% trans 'Clear Contents'%}"></i>
<i class="fas fa-eraser" style="cursor:pointer;" @click="recipe_json.recipeIngredient=[]" title="{% trans 'Clear Contents'%}"></i>
</div>
<div class="small text-muted">{% trans 'Ingredients dragged here will be appended to current list.'%}</div>
</div> </div>
<b-collapse id="collapse-ing" visible class="mt-2"> <div class="card-body drop-zone" @drop="replacePreview('ingredients', $event)" @dragover.prevent @dragenter.prevent>
<div class="card-body drop-zone" @drop="replacePreview('ingredients', $event)" @dragover.prevent @dragenter.prevent> <div v-for="i in recipe_json.recipeIngredient">
<ul class="list-group list-group"> <div class="card-text">[[i.amount]] [[i.unit.text]] [[i.ingredient.text]] [[i.note]]</div>
<div v-for="i in recipe_json.recipeIngredient">
<li class="row border-light" >
<div class="col-sm-1 border">[[i.amount]]</div>
<div class="col-sm border">[[i.unit.text]]</div>
<div class="col-sm border">[[i.ingredient.text]]</div>
<div class="col-sm border">[[i.note]]</div>
</li>
</div>
</ul>
</div> </div>
</b-collapse> </div>
</div> </div>
<div class="card mb-2"> <div class="card mb-2">
<div class="card-header" v-b-toggle.collapse-instructions> <div class="card-header" style="display:flex; justify-content:space-between;">
<div class="row px-3" style="justify-content:space-between;">
{% trans 'Instructions' %} {% trans 'Instructions' %}
<i class="fas fa-eraser" style="cursor:pointer;" @click="recipe_json.recipeInstructions=''" title="{% trans 'Clear Contents'%}"></i> <i class="fas fa-eraser" style="cursor:pointer;" @click="deletePreview('instructions')" title="{% trans 'Clear Contents'%}"></i>
</div> </div>
<div class="small text-muted">{% trans 'Recipe instructions dragged here will be appended to current instructions.'%}</div> <div class="card-body drop-zone" @drop="replacePreview('instructions', $event)" @dragover.prevent @dragenter.prevent>
<div class="card-text">[[recipe_json.recipeInstructions]]</div>
</div> </div>
<b-collapse id="collapse-instructions" visible class="mt-2">
<div class="card-body drop-zone" @drop="replacePreview('instructions', $event)" @dragover.prevent @dragenter.prevent>
<div class="card-text">[[recipe_json.recipeInstructions]]</div>
</div>
</b-collapse>
</div> </div>
</div> </div>
</div> </div>
<br/> <br/>
<!-- end of preview card --> <button @click="loadRecipeHTML()" class="btn btn-primary shadow-none" type="button"
<button @click="showRecipe()" class="btn btn-primary shadow-none" type="button"
id="id_btn_json"><i class="fas fa-code"></i> {% trans 'Import' %} id="id_btn_json"><i class="fas fa-code"></i> {% trans 'Import' %}
</button> </button>
</div> </div>
<!-- start of source data -->
<div class="col" style="max-width:50%"> <div class="col" style="max-width:50%">
<div class="card card-border-primary"> <div class="card card-border-primary">
<div class="card-header"> <div class="card-header">
<h3>{% trans 'Discovered Attributes' %}</h3> <h3>{% trans 'Discovered Attributes' %}</h3>
<div class='small text-muted'> <div class='small text-muted'>
{% trans 'Drag recipe attributes from below into the appropriate box on the left. Click any node to display its full properties.' %} {% trans 'Drag recipe attributes from below into the appropriate box on the left. Click any node to display its full properties.' %}
</div> </div>
</div> </div>
<div class="btn-group btn-group-toggle" data-toggle="buttons">
<label class="btn btn-outline-info btn-sm active" @click="preview_type='json'"> <div class="card-body">
<input type="radio" autocomplete="off" checked> json <v-jstree :data="recipe_tree"
</label>
<label class="btn btn-outline-info btn-sm" @click="preview_type='html'">
<input type="radio" autocomplete="off"> html
</label>
<label class="btn btn-outline-info btn-sm" @click="preview_type='image'">
<input type="radio" autocomplete="off"> images
</label>
</div>
<i :class="[show_blank ? 'fa-chevron-up' : 'fa-chevron-down', 'fas']"
style="cursor:pointer;"
@click="show_blank=!show_blank"
title="{% trans 'Show Blank Field' %}"></i>
<div class="card-body p-1">
<div class="card card-border-primary" v-if="show_blank">
<div class="card-header">
<div class="row px-3" style="justify-content:space-between;">
{% trans 'Blank Field' %}
<i class="fas fa-eraser justify-content-end" style="cursor:pointer;" @click="blank_field=''" title="{% trans 'Clear Contents'%}"></i>
</div>
<div class="small text-muted">{% trans 'Items dragged to Blank Field will be appended.'%}</div>
</div>
<div class="card-body drop-zone"
@drop="replacePreview('blank', $event)"
@dragover.prevent
@dragenter.prevent
draggable
@dragstart="htmlDragStart($event)">
[[blank_field]]
</div>
</div>
<!-- start of json data -->
<v-jstree v-if="preview_type=='json'" :data="recipe_tree"
text-field-name="name" text-field-name="name"
collapse:true collapse:true
draggable draggable
@ -301,37 +255,13 @@
</template> </template>
</v-jstree> </v-jstree>
<!-- start of html data -->
<div v-if="preview_type=='html'">
<ul class="list-group list-group-flush" v-for="(txt, key) in recipe_html">
<div class="list-group-item bg-light m-0 small"
draggable
@dragstart="htmlDragStart($event)"
style="display:flex; justify-content:space-between;">
[[txt]]
<i class="fas fa-minus-square" style="cursor:pointer; color:red" @click="$delete(recipe_html, key)" title="{% trans 'Delete Text'%}"></i>
</div>
</ul>
</div>
<!-- start of images -->
<div v-if="preview_type=='image'">
<ul class="list-group list-group-flush" v-for="(img, key) in images">
<div class="list-group-item bg-light m-0 small"
draggable
@dragstart="imageDragStart($event)"
style="display:flex; justify-content:space-between;">
<img class="card-img" v-bind:src=[[img]] alt="Image">
<i class="fas fa-minus-square" style="cursor:pointer; color:red" @click="$delete(images, key)" title="{% trans 'Delete image'%}"></i>
</div>
</ul>
</div>
</div> </div>
</div> </div>
</div> </div>
<!-- end of json tree -->
</div> </div>
</div> </div>
<!-- end of recipe preview before Import -->
<!-- end of recipe preview on HTML Import -->
<template v-if="recipe_data !== undefined"> <template v-if="recipe_data !== undefined">
@ -582,20 +512,13 @@
recipe_data: undefined, recipe_data: undefined,
error: undefined, error: undefined,
loading: false, loading: false,
preview: false, parsed: false,
preview_type: 'json',
all_keywords: false, all_keywords: false,
importing_recipe: false, importing_recipe: false,
recipe_json: undefined, recipe_json: undefined,
recipe_tree: undefined, recipe_tree: undefined,
recipe_html: undefined, recipe_tree1: undefined,
automatic: true, html_data: undefined,
show_blank: false,
blank_field: '',
recipe_app: 'DEFAULT',
recipe_files: [],
images: [],
mode: 'url'
}, },
directives: { directives: {
tabindex: { tabindex: {
@ -673,6 +596,58 @@
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger') this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
}) })
}, },
loadRecipeJson: function () {
this.recipe_data = undefined
this.error = undefined
this.loading = true
this.$http.post("{% url 'api_recipe_from_json' %}", {'json': this.json_data}, {emulateJSON: true}).then((response) => {
console.log(response.data)
this.recipe_data = response.data;
this.loading = false
}).catch((err) => {
this.error = err.data
this.loading = false
console.log(err)
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
})
},
loadPreviewRaw: function () {
this.recipe_json = undefined
this.recipe_tree = undefined
this.error = undefined
this.loading = true
this.$http.post("{% url 'api_recipe_from_html' %}", {'html_data': this.html_data}, {emulateJSON: true}).then((response) => {
console.log(response.data)
this.recipe_json = response.data['recipe_json'];
this.recipe_tree1 = JSON.stringify(response.data['recipe_tree'], null, 2);
this.recipe_tree = response.data['recipe_tree'];
this.loading = false
this.parsed = true
}).catch((err) => {
this.error = err.data
this.loading = false
this.parsed = false
console.log(err)
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
})
},
loadRecipeHTML: function () {
this.error = undefined
this.loading = true
this.parsed = false
this.recipe_json['@type'] = "Recipe"
this.$http.post("{% url 'api_recipe_from_json' %}", {'json': JSON.stringify(this.recipe_json)}, {emulateJSON: true}).then((response) => {
console.log(response.data)
this.recipe_data = response.data;
this.loading = false
}).catch((err) => {
this.error = err.data
this.loading = false
console.log(err)
this.makeToast(gettext('Error'), gettext('There was an error loading a resource!') + err.bodyText, 'danger')
})
},
importRecipe: function () { importRecipe: function () {
if (this.recipe_data.name.length > 128) { if (this.recipe_data.name.length > 128) {
this.makeToast(gettext('Error'), gettext('Recipe name is longer than 128 characters'), 'danger') this.makeToast(gettext('Error'), gettext('Recipe name is longer than 128 characters'), 'danger')
@ -806,6 +781,37 @@
var index = node.parentItem.indexOf(item) var index = node.parentItem.indexOf(item)
node.parentItem.splice(index, 1) node.parentItem.splice(index, 1)
}, },
deletePreview: function(field) {
switch (field) {
case 'name':
this.recipe_json.name=""
break;
case 'description':
this.recipe_json.description=""
break;
case 'image':
this.recipe_json.image=""
break;
case 'keywords':
this.recipe_json.keywords=[]
break;
case 'servings':
this.recipe_json.servings=""
break;
case 'prepTime':
this.recipe_json.prepTime=""
break;
case 'cookTime':
this.recipe_json.cookTime=""
break;
case 'ingredients':
this.recipe_json.recipeIngredient=[]
break;
case 'instructions':
this.recipe_json.recipeInstructions=""
break;
}
},
itemClick: function (node, item, e) { itemClick: function (node, item, e) {
this.makeToast(gettext('Details'), node.model.value, 'info') this.makeToast(gettext('Details'), node.model.value, 'info')
}, },
@ -816,14 +822,6 @@
e.dataTransfer.setData('value', node.model.value) e.dataTransfer.setData('value', node.model.value)
}, },
htmlDragStart: function (e) {
console.log(e.target.innerText)
e.dataTransfer.setData('value', e.target.innerText)
},
imageDragStart: function (e) {
console.log(e.target.src)
e.dataTransfer.setData('value', e.target.src)
},
replacePreview: function(field, e) { replacePreview: function(field, e) {
v = e.dataTransfer.getData('value') v = e.dataTransfer.getData('value')
if (e.dataTransfer.getData('hasChildren')) { if (e.dataTransfer.getData('hasChildren')) {
@ -832,20 +830,19 @@
} }
switch (field) { switch (field) {
case 'name': case 'name':
this.recipe_json.name = [this.recipe_json.name, v].filter(Boolean).join(" "); this.recipe_json.name=v
break; break;
case 'description': case 'description':
this.recipe_json.description = [this.recipe_json.description, v].filter(Boolean).join(" "); this.recipe_json.description=v
break; break;
case 'image': case 'image':
this.recipe_json.image=v this.recipe_json.image=v
break; break;
case 'keywords': case 'keywords':
let new_keyword = {'text': v, 'id': null} this.recipe_json.keywords.push(v)
this.recipe_json.keywords.push(new_keyword)
break; break;
case 'servings': case 'servings':
this.recipe_json.servings=parseInt(v.match(/\b\d+\b/)[0]) this.recipe_json.servings=v
break; break;
case 'prepTime': case 'prepTime':
this.recipe_json.prepTime=v this.recipe_json.prepTime=v
@ -854,29 +851,18 @@
this.recipe_json.cookTime=v this.recipe_json.cookTime=v
break; break;
case 'ingredients': case 'ingredients':
this.$http.post('{% url 'api_ingredient_from_string' %}', {text: v}, {emulateJSON: true}).then((response) => { let new_ingredient={
console.log(response) unit: {id: Math.random() * 1000, text: ""},
let new_ingredient={ amount: "",
unit: {id: Math.random() * 1000, text: response.body.unit}, ingredient: {id: Math.random() * 1000, text: v}
amount: String(response.body.amount),
ingredient: {id: Math.random() * 1000, text: response.body.food},
note: response.body.note,
original: v
} }
this.recipe_json.recipeIngredient.push(new_ingredient) this.recipe_json.recipeIngredient=[new_ingredient]
}).catch((err) => {
console.log(err)
this.makeToast(gettext('Error'), gettext('Something went wrong.'), 'danger')
})
break; break;
case 'instructions': case 'instructions':
this.recipe_json.recipeInstructions = [this.recipe_json.recipeInstructions, v].filter(Boolean).join("\n\n"); this.recipe_json.recipeInstructions=this.recipe_json.recipeInstructions.concat(v)
break;
case 'blank':
this.blank_field = [this.blank_field, v].filter(Boolean).join(" ");
break; break;
} }
}, }
} }
}); });
</script> </script>

View File

@ -36,7 +36,7 @@ from cookbook.helper.permission_helper import (CustomIsAdmin, CustomIsGuest,
CustomIsShared, CustomIsUser, CustomIsShared, CustomIsUser,
group_required) group_required)
from cookbook.helper.recipe_url_import import get_from_html, find_recipe_json from cookbook.helper.recipe_url_import import get_from_html, find_recipe_json
from cookbook.helper.recipe_html_import import get_from_html from cookbook.helper.recipe_html_import import get_from_raw
from cookbook.models import (CookLog, Food, Ingredient, Keyword, MealPlan, from cookbook.models import (CookLog, Food, Ingredient, Keyword, MealPlan,
MealType, Recipe, RecipeBook, ShoppingList, MealType, Recipe, RecipeBook, ShoppingList,
ShoppingListEntry, ShoppingListRecipe, Step, ShoppingListEntry, ShoppingListRecipe, Step,
@ -700,11 +700,20 @@ def recipe_from_url(request):
@group_required('user') @group_required('user')
def recipe_from_html(request): def recipe_from_html(request):
html_data = request.POST['html_data'] html_data = request.POST['html_data']
recipe_json, recipe_tree = get_from_html(html_data) recipe_json, recipe_tree = get_from_raw(html_data)
return JsonResponse({ if len(recipe_tree) == 0 and len(recipe_json) == 0:
'recipe_tree': recipe_tree, return JsonResponse(
'recipe_json': recipe_json {
}) 'error': True,
'msg': _('The requested page refused to provide any information (Status Code 403).') # noqa: E501
},
status=400
)
else:
return JsonResponse({
'recipe_tree': recipe_tree,
'recipe_json': recipe_json
})
@group_required('admin') @group_required('admin')