manually parse json

This commit is contained in:
smilerz 2021-03-15 15:56:44 -05:00
parent f8fedcac82
commit 44dee16e0a
6 changed files with 566 additions and 56 deletions

View File

@ -0,0 +1,123 @@
import json
from json.decoder import JSONDecodeError
from bs4 import BeautifulSoup
from bs4.element import Tag
# from cookbook.helper.ingredient_parser import parse as parse_ingredient
from cookbook.helper import recipe_url_import as helper
# %%
# %%
def get_from_raw(text):
def build_node(k, v):
if isinstance(v, dict):
node = {
'name': k,
'value': k,
'children': get_children_dict(v)
}
elif isinstance(v, list):
node = {
'name': k,
'value': k,
'children': get_children_list(v)
}
else:
node = {
'name': k + ": " + str(v),
'value': str(v)
}
return node
def get_children_dict(children):
kid_list = []
for k, v in children.items():
kid_list.append(build_node(k, v))
return kid_list
def get_children_list(children):
kid_list = []
for kid in children:
if type(kid) == list:
node = {
'name': "unknown list",
'value': "unknown list",
'children': get_children_list(kid)
}
kid_list.append(node)
elif type(kid) == dict:
for k, v in kid.items():
kid_list.append(build_node(k, v))
else:
kid_list.append({
'name': kid,
'value': kid
})
return kid_list
recipe_json = {
'name': '',
'description': '',
'image': '',
'keywords': [],
'recipeIngredient': [],
'recipeInstructions': '',
'servings': '',
'prepTime': '',
'cookTime': ''
}
recipe_tree = []
temp_tree = []
parse_list = []
try:
parse_list.append(json.loads(text))
except JSONDecodeError:
soup = BeautifulSoup(text, "html.parser")
for el in soup.find_all('script', type='application/ld+json'):
parse_list.append(el)
for el in soup.find_all(type='application/json'):
parse_list.append(el)
# first try finding ld+json as its most common
for el in parse_list:
if isinstance(el, Tag):
el = json.loads(el.string)
for k, v in el.items():
if isinstance(v, dict):
node = {
'name': k,
'value': k,
'children': get_children_dict(v)
}
elif isinstance(v, list):
node = {
'name': k,
'value': k,
'children': get_children_list(v)
}
else:
node = {
'name': k + ": " + str(v),
'value': str(v)
}
temp_tree.append(node)
if ('@type' in el and el['@type'] == 'Recipe'):
recipe_json = helper.find_recipe_json(el, None)
recipe_tree += [{'name': 'ld+json', 'children': temp_tree}]
else:
recipe_tree += [{'name': 'json', 'children': temp_tree}]
temp_tree = []
# overide keyword structure from dict to list
kws = []
for kw in recipe_json['keywords']:
kws.append(kw['text'])
recipe_json['keywords'] = kws
return recipe_json, recipe_tree

View File

@ -2,6 +2,8 @@ import json
import random import random
import re import re
from json import JSONDecodeError from json import JSONDecodeError
from isodate import parse_duration as iso_parse_duration
from isodate.isoerror import ISO8601Error
import microdata import microdata
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
@ -64,6 +66,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'] = ""
keywords = [] keywords = []
if 'keywords' in ld_json: if 'keywords' in ld_json:
@ -71,21 +75,39 @@ 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'])
try:
ld_json['keywords'] = parse_keywords(list(set(map(str.casefold, keywords)))) 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
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 ld_json['servings'] = 1
try: try:
if 'recipeYield' in ld_json: if 'recipeYield' in ld_json:
@ -117,6 +139,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:
@ -197,50 +225,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):
if type(cooktime) not in [int, float]:
try:
cooktime = float(re.search(r'\d+', cooktime).group())
except (ValueError, AttributeError):
try:
cooktime = round(iso_parse_duration(cooktime).seconds / 60)
except ISO8601Error:
try: try:
if (type(cooktime) == list and len(cooktime) > 0): if (type(cooktime) == list and len(cooktime) > 0):
cooktime = cooktime[0] cooktime = cooktime[0]
cooktime = round(parse_duration(cooktime).seconds / 60) cooktime = round(parse_duration(cooktime).seconds / 60)
except TypeError: except AttributeError:
cooktime = 0
if type(cooktime) != int or float:
cooktime = 0 cooktime = 0
return cooktime return cooktime
def parse_preptime(preptime): def parse_preptime(preptime):
if type(preptime) not in [int, float]:
try:
preptime = float(re.search(r'\d+', preptime).group())
except ValueError:
try:
preptime = round(iso_parse_duration(preptime).seconds / 60)
except ISO8601Error:
try: try:
if (type(preptime) == list and len(preptime) > 0): if (type(preptime) == list and len(preptime) > 0):
preptime = preptime[0] preptime = preptime[0]
preptime = round( preptime = round(parse_duration(preptime).seconds / 60)
parse_duration( except AttributeError:
preptime
).seconds / 60
)
except TypeError:
preptime = 0
if type(preptime) != int or float:
preptime = 0 preptime = 0
return preptime return preptime
@ -258,6 +295,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,22 +52,21 @@
<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> </div>
</div>
</template> </template>
</v-jstree> </v-jstree>
</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'];

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,8 +8,14 @@
{% 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>
.tree-anchor {
width:90%;
}
</style>
{% endblock %} {% endblock %}
@ -20,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">
@ -40,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>
@ -53,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>
@ -75,6 +100,169 @@
<i class="fas fa-spinner fa-spin fa-8x"></i> <i class="fas fa-spinner fa-spin fa-8x"></i>
</div> </div>
<!-- recipe preview on HTML Import -->
<div class="container-fluid" v-if="parsed" id="manage_tree">
<h2></h2>
<div class="row">
<div class="col" style="max-width:50%">
<div class="card card-border-primary" >
<div class="card-header">
<h3>{% trans 'Preview Recipe Data' %}</h3>
<div class='small text-muted'>{% trans 'Drag recipe attributes from the right into the appropriate box below.' %} </div>
</div>
<div class="card-body p-2">
<div class="card mb-2">
<div class="card-header" style="display:flex; justify-content:space-between;">
{% trans 'Name' %}
<i class="fas fa-eraser" style="cursor:pointer;" @click="deletePreview('name')" title="{% trans 'Clear Contents'%}"></i>
</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>
<div class="card mb-2">
<div class="card-header" style="display:flex; justify-content:space-between;">
{% trans 'Description' %}
<i class="fas fa-eraser" style="cursor:pointer;" @click="deletePreview('description')" title="{% trans 'Clear Contents'%}"></i>
</div>
<div class="card-body drop-zone" @drop="replacePreview('description', $event)" @dragover.prevent @dragenter.prevent>
<div class="card-text">[[recipe_json.description]]</div>
</div>
</div>
<div class="card mb-2">
<div class="card-header" style="display:flex; justify-content:space-between;">
{% trans 'Keywords' %}
<i class="fas fa-eraser" style="cursor:pointer;" @click="deletePreview('keywords')" title="{% trans 'Clear Contents'%}"></i>
</div>
<div class="card-body drop-zone" @drop="replacePreview('keywords', $event)" @dragover.prevent @dragenter.prevent>
<div v-for="kw in recipe_json.keywords">
<div class="card-text">[[kw]] </div>
</div>
</div>
</div>
<div class="card mb-2">
<div class="card-header" style="display:flex; justify-content:space-between;">
{% trans 'Image' %}
<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>
<div class = "row mb-2">
<div class="col">
<div class="card" >
<div class="card-header p-1" style="display:flex; justify-content:space-between;">
{% trans 'Servings' %}
<i class="fas fa-eraser" style="cursor:pointer;" @click="deletePreview('servings')" title="{% trans 'Clear Contents'%}"></i>
</div>
<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>
</div>
</div>
<div class="col">
<div class="card">
<div class="card-header p-1" style="display:flex; justify-content:space-between;">
{% trans 'Prep Time' %}
<i class="fas fa-eraser" style="cursor:pointer;" @click="deletePreview('prepTime')" title="{% trans 'Clear Contents'%}"></i>
</div>
<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>
</div>
</div>
<div class="col">
<div class="card">
<div class="card-header p-1" style="display:flex; justify-content:space-between;">
{% trans 'Cook Time' %}
<i class="fas fa-eraser" style="cursor:pointer;" @click="deletePreview('cookTime')" title="{% trans 'Clear Contents'%}"></i>
</div>
<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>
</div>
</div>
</div>
<div class="card mb-2">
<div class="card-header" style="display:flex; justify-content:space-between;">
{% trans 'Ingredients' %}
<i class="fas fa-eraser" style="cursor:pointer;" @click="deletePreview('ingredients')" title="{% trans 'Clear Contents'%}"></i>
</div>
<div class="card-body drop-zone" @drop="replacePreview('ingredients', $event)" @dragover.prevent @dragenter.prevent>
<div v-for="i in recipe_json.recipeIngredient">
<div class="card-text">[[i.amount]] [[i.unit.text]] [[i.ingredient.text]] [[i.note]]</div>
</div>
</div>
</div>
<div class="card mb-2">
<div class="card-header" style="display:flex; justify-content:space-between;">
{% trans 'Instructions' %}
<i class="fas fa-eraser" style="cursor:pointer;" @click="deletePreview('instructions')" title="{% trans 'Clear Contents'%}"></i>
</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>
</div>
</div>
<br/>
<button @click="loadRecipeHTML()" class="btn btn-primary shadow-none" type="button"
id="id_btn_json"><i class="fas fa-code"></i> {% trans 'Import' %}
</button>
</div>
<div class="col" style="max-width:50%">
<div class="card card-border-primary">
<div class="card-header">
<h3>{% trans 'Discovered Attributes' %}</h3>
<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.' %}
</div>
</div>
<div class="card-body">
<v-jstree :data="recipe_tree"
text-field-name="name"
collapse:true
draggable
@item-drag-start="itemDragStart"
@item-click="itemClick">
<template scope="_">
<div class="col" @click.ctrl="customItemClickWithCtrl">
<div class="row clearfix" style="width:100%" >
<div class="col-es" style="align-right">
<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>
</div>
<div class="col overflow-hidden">
<i :class="_.vm.themeIconClasses" role="presentation" v-if="!_.model.loading"></i>
{% verbatim %}
[[_.model.name]]
{% endverbatim %}
</div>
</div>
</div>
</template>
</v-jstree>
</div>
</div>
</div>
</div>
</div>
<!-- end of recipe preview on HTML Import -->
<template v-if="recipe_data !== undefined"> <template v-if="recipe_data !== undefined">
<form> <form>
@ -82,6 +270,10 @@
<label for="id_name">{% trans 'Recipe Name' %}</label> <label for="id_name">{% trans 'Recipe Name' %}</label>
<input id="id_name" class="form-control" v-model="recipe_data.name"> <input id="id_name" class="form-control" v-model="recipe_data.name">
</div> </div>
<div class="form-group">
<label for="id_description">{% trans 'Recipe Description' %}</label>
<textarea id="id_description" class="form-control" rows="3" v-model="recipe_data.description"></textarea>
</div>
<div class="form-group"> <div class="form-group">
<label for="id_description">{% trans 'Recipe Description' %}</label> <label for="id_description">{% trans 'Recipe Description' %}</label>
@ -326,9 +518,13 @@
recipe_data: undefined, recipe_data: undefined,
error: undefined, error: undefined,
loading: false, loading: false,
parsed: false,
all_keywords: false, all_keywords: false,
importing_recipe: false, importing_recipe: false,
json_data: '', recipe_json: undefined,
recipe_tree: undefined,
recipe_tree1: undefined,
html_data: undefined,
}, },
directives: { directives: {
tabindex: { tabindex: {
@ -410,6 +606,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')
@ -517,6 +765,93 @@
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')
}) })
}, },
deleteNode: function (node ,item, e) {
e.stopPropagation()
var index = node.parentItem.indexOf(item)
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) {
this.makeToast(gettext('Details'), node.model.value, 'info')
},
itemDragStart (node,item,e) {
if (node.model.children.length > 0) {
e.dataTransfer.setData('hasChildren', true)
}
e.dataTransfer.setData('value', node.model.value)
},
replacePreview: function(field, e) {
v = e.dataTransfer.getData('value')
if (e.dataTransfer.getData('hasChildren')) {
this.makeToast(gettext('Error'), gettext('Items with children cannot be dropped here!') , 'danger')
return
}
switch (field) {
case 'name':
this.recipe_json.name=v
break;
case 'description':
this.recipe_json.description=v
break;
case 'image':
this.recipe_json.image=v
break;
case 'keywords':
this.recipe_json.keywords.push(v)
break;
case 'servings':
this.recipe_json.servings=v
break;
case 'prepTime':
this.recipe_json.prepTime=v
break;
case 'cookTime':
this.recipe_json.cookTime=v
break;
case 'ingredients':
let new_ingredient={
unit: {id: Math.random() * 1000, text: ""},
amount: "",
ingredient: {id: Math.random() * 1000, text: v}
}
this.recipe_json.recipeIngredient=[new_ingredient]
break;
case 'instructions':
this.recipe_json.recipeInstructions=this.recipe_json.recipeInstructions.concat(v)
break;
}
}
} }
}); });
</script> </script>

View File

@ -29,7 +29,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,
@ -610,7 +610,16 @@ def recipe_from_url_old(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)
if len(recipe_tree) == 0 and len(recipe_json) == 0:
return JsonResponse(
{
'error': True,
'msg': _('The requested page refused to provide any information (Status Code 403).') # noqa: E501
},
status=400
)
else:
return JsonResponse({ return JsonResponse({
'recipe_tree': recipe_tree, 'recipe_tree': recipe_tree,
'recipe_json': recipe_json 'recipe_json': recipe_json