combined json import and source import

This commit is contained in:
smilerz 2021-03-21 13:13:56 -05:00
parent 4015517c0a
commit 40a2f7ff90
5 changed files with 98 additions and 137 deletions

View File

@ -7,10 +7,7 @@ from bs4.element import Tag
from cookbook.helper import recipe_url_import as helper from cookbook.helper import recipe_url_import as helper
# %% def get_recipe_from_source(text, space):
# %%
def get_from_raw(text, space):
def build_node(k, v): def build_node(k, v):
if isinstance(v, dict): if isinstance(v, dict):
node = { node = {
@ -113,17 +110,20 @@ def get_from_raw(text, space):
if '@graph' in el: if '@graph' in el:
for x in el['@graph']: for x in el['@graph']:
if '@type' in x and x['@type'] == 'Recipe': if '@type' in x and x['@type'] == 'Recipe':
recipe_json = helper.find_recipe_json(x, None, space) el = x
recipe_tree += [{'name': 'ld+json', 'children': temp_tree}]
if '@type' in el and el['@type'] == 'Recipe':
recipe_json = helper.find_recipe_json(el, None, space)
recipe_tree += [{'name': 'ld+json', 'children': temp_tree}]
else: else:
recipe_tree += [{'name': 'json', 'children': temp_tree}] recipe_tree += [{'name': 'json', 'children': temp_tree}]
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 return recipe_json, recipe_tree
def get_from_html(text, space):
for s in soup.strings:
if ((s.parent.name not in INVISIBLE_ELEMS) and (len(s.strip()) > 0)):
print(s.parent.name, s, len(s))

View File

@ -15,7 +15,7 @@ from django.utils.translation import gettext as _
from recipe_scrapers import _utils from recipe_scrapers import _utils
def get_from_html(html_text, url, space): def get_from_html_old(html_text, url, space):
soup = BeautifulSoup(html_text, "html.parser") soup = BeautifulSoup(html_text, "html.parser")
# first try finding ld+json as its most common # first try finding ld+json as its most common

View File

@ -25,72 +25,54 @@
<h2> {% trans 'Import' %} </h2> <h2> {% trans 'Import' %} </h2>
<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-source" data-toggle="tab" role="tab" aria-controls="nav-source">Source</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-text" data-toggle="tab" role="tab" aria-controls="nav-text">Text</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-file" data-toggle="tab" role="tab" aria-controls="nav-file">File</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>
</nav> </nav>
<div class="tab-content" id="nav-tabContent"> <div class="tab-content" id="nav-tabContent">
<!-- Import URL --> <!-- Import URL -->
<div class="row tab-pane fade show active" id="nav-url" role="tabpanel"> <div class="tab-pane fade show active" id="nav-url" role="tabpanel">
<div class="col-md-12"> <div class="btn-group btn-group-toggle" data-toggle="buttons">
<div class="input-group mb-3"> <label class="btn btn-outline-info btn-sm active" @click="automatic=true">
<input class="form-control" v-model="remote_url" placeholder="{% trans 'Enter website URL' %}"> <input type="radio" name="auto" id="auto" autocomplete="off" checked> Automatic
<div class="input-group-append"> </label>
<button @click="loadRecipe()" class="btn btn-primary shadow-none" type="button"
id="id_btn_search"><i class="fas fa-search"></i>
</button>
</div>
</div>
</div>
</div>
<!-- Automatically import LD+JSON --> <label class="btn btn-outline-info btn-sm disabled" @click="automatic=false">
<div class="row tab-pane fade show" id="nav-ldjson" role="tabpanel"> <input type="radio" name="auto" id="manual" autocomplete="off"> Manual
<div class="col-md-12"> </label>
<div class="input-group input-group-lg"> </div>
<textarea class="form-control input-group-append" v-model="raw_data" rows=10 placeholder="{% trans 'Paste ld+json here to parse recipe automatically.' %}" style="font-size: 12px"> <div class="input-group mb-3">
</textarea> <input class="form-control" v-model="remote_url" placeholder="{% trans 'Enter website URL' %}">
</div> <div class="input-group-append">
<br> <button @click="loadRecipe()" class="btn btn-primary shadow-none" type="button"
<button @click="loadRecipeJson()" class="btn btn-primary shadow-none" type="button" id="id_btn_search"><i class="fas fa-search"></i>
id="id_btn_json"><i class="fas fa-code"></i> {% trans 'Import' %}
</button> </button>
</div> </div>
</div>
<!-- Manually import from JSON -->
<div class="row tab-pane fade show" id="nav-json" role="tabpanel">
<div class="col-md-12">
<div class="input-group input-group-lg">
<textarea class="form-control input-group-append" v-model="raw_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>
</div>
<br>
<button @click="loadPreviewRaw()" class="btn btn-primary shadow-none" type="button"
id="id_btn_raw"><i class="fas fa-code"></i> {% trans 'Preview Import' %}
</button>
</div> </div>
</div> </div>
<!-- Manually import from HTML --> <!-- Import JSON or HTML -->
<div class="row tab-pane fade show" id="nav-html" role="tabpanel"> <div class=" tab-pane fade show" id="nav-source" role="tabpanel">
<div class="col-md-12"> <div class="btn-group btn-group-toggle" data-toggle="buttons">
<div class="input-group input-group-lg"> <label class="btn btn-outline-info btn-sm active" @click="automatic=true">
<textarea class="form-control input-group-append" v-model="raw_data" rows=10 placeholder="{% trans 'Paste html source here to parse recipe manually.' %}" style="font-size: 12px"> <input type="radio" name="auto" id="auto" autocomplete="off" checked> Automatic
</textarea> </label>
</div>
<br>
<button @click="loadPreviewRaw()" class="btn btn-primary shadow-none" type="button"
id="id_btn_HTML"><i class="fas fa-code"></i> {% trans 'Preview Import' %}
</button>
</div>
</div>
<label class="btn btn-outline-info btn-sm" @click="automatic=false">
<input type="radio" name="auto" id="manual" autocomplete="off"> Manual
</label>
</div>
<div class="input-group input-group-lg">
<textarea class="form-control input-group-append" v-model="source_data" rows=10 placeholder="{% trans 'Paste json or html source here to load recipe.' %}" style="font-size: 12px">
</textarea>
</div>
<br>
<button @click="loadSource()" class="btn btn-primary shadow-none" type="button"
id="id_btn_json"><i class="fas fa-code"></i> {% trans 'Import' %}
</button>
</div>
</div> </div>
<br/> <br/>
@ -101,9 +83,7 @@
</div> </div>
<!-- recipe preview before Import --> <!-- recipe preview before Import -->
<div class="container-fluid" v-if="parsed" id="manage_tree"> <div class="container-fluid" v-if="preview" 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%">
<div class="card card-border-primary" > <div class="card card-border-primary" >
@ -116,6 +96,7 @@
<div class="card mb-2"> <div class="card mb-2">
<div class="card-header" style="display:flex; justify-content:space-between;"> <div class="card-header" style="display:flex; justify-content:space-between;">
{% trans 'Name' %} {% trans 'Name' %}
<!-- this and subsequent delete commands should be fixed to identify which model attribute the element is referring to -->
<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="deletePreview('name')" title="{% trans 'Clear Contents'%}"></i>
</div> </div>
@ -232,11 +213,13 @@
</div> </div>
</div> </div>
<br/> <br/>
<button @click="loadRecipeRaw()" class="btn btn-primary shadow-none" type="button" <!-- end of preview card -->
<button @click="loadRecipeManual()" 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 json tree -->
<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">
@ -274,9 +257,9 @@
</div> </div>
</div> </div>
</div> </div>
<!-- end of json tree -->
</div> </div>
</div> </div>
<!-- end of recipe preview before Import --> <!-- end of recipe preview before Import -->
<template v-if="recipe_data !== undefined"> <template v-if="recipe_data !== undefined">
@ -518,7 +501,7 @@
el: '#app', el: '#app',
data: { data: {
remote_url: '', remote_url: '',
raw_data: undefined, source_data: undefined,
keywords: [], keywords: [],
keywords_loading: false, keywords_loading: false,
units: [], units: [],
@ -528,11 +511,12 @@
recipe_data: undefined, recipe_data: undefined,
error: undefined, error: undefined,
loading: false, loading: false,
parsed: false, preview: false,
all_keywords: false, all_keywords: false,
importing_recipe: false, importing_recipe: false,
recipe_json: undefined, recipe_json: undefined,
recipe_tree: undefined, recipe_tree: undefined,
automatic: true
}, },
directives: { directives: {
tabindex: { tabindex: {
@ -561,6 +545,9 @@
this.recipe_data = undefined this.recipe_data = undefined
this.error = undefined this.error = undefined
this.loading = true this.loading = true
if (this.automatic) {
console.log('true')
}
this.$http.post("{% url 'api_recipe_from_url' %}", {'url': this.remote_url}, {emulateJSON: true}).then((response) => { this.$http.post("{% url 'api_recipe_from_url' %}", {'url': this.remote_url}, {emulateJSON: true}).then((response) => {
console.log(response.data) console.log(response.data)
this.recipe_data = response.data; this.recipe_data = response.data;
@ -578,13 +565,22 @@
this.makeToast(gettext('Error'), msg, 'danger') this.makeToast(gettext('Error'), msg, 'danger')
}) })
}, },
loadRecipeJson: function () { loadSource: function() {
this.recipe_data = undefined this.recipe_data = undefined
this.recipe_json = undefined
this.recipe_tree = undefined
this.error = undefined this.error = undefined
this.loading = true this.loading = true
this.$http.post("{% url 'api_recipe_from_json' %}", {'json': this.raw_data}, {emulateJSON: true}).then((response) => { this.$http.post("{% url 'api_recipe_from_source' %}", {'data': this.source_data, 'auto':this.automatic}, {emulateJSON: true}).then((response) => {
console.log(response.data) console.log(response.data)
this.recipe_data = response.data; if (this.automatic) {
this.recipe_data = response.data['recipe_json'];
this.preview = false
} else {
this.recipe_json = response.data['recipe_json'];
this.recipe_tree = response.data['recipe_tree'];
this.preview = true
}
this.loading = false this.loading = false
}).catch((err) => { }).catch((err) => {
this.error = err.data this.error = err.data
@ -599,15 +595,16 @@
this.makeToast(gettext('Error'), msg, 'danger') this.makeToast(gettext('Error'), msg, 'danger')
}) })
}, },
loadRecipeRaw: function () { loadRecipeManual: function () {
this.error = undefined this.error = undefined
this.preview = false
this.loading = true this.loading = true
this.parsed = false
this.recipe_json['@type'] = "Recipe" this.recipe_json['@type'] = "Recipe"
this.$http.post("{% url 'api_recipe_from_json' %}", {'json': JSON.stringify(this.recipe_json)}, {emulateJSON: true}).then((response) => { this.$http.post("{% url 'api_recipe_from_source' %}", {'data': JSON.stringify(this.recipe_json), 'auto':'true'}, {emulateJSON: true}).then((response) => {
console.log(response.data) console.log(response.data)
this.recipe_data = response.data; this.recipe_data = response.data['recipe_json'];
this.loading = false this.loading = false
this.preview = false
}).catch((err) => { }).catch((err) => {
this.error = err.data this.error = err.data
this.loading = false this.loading = false
@ -615,41 +612,6 @@
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_manual_recipe_from_json' %}", {'data': this.raw_data}, {emulateJSON: true}).then((response) => {
console.log(response.data)
this.recipe_json = response.data['recipe_json'];
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')
})
},
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')
@ -852,16 +814,6 @@
break; break;
} }
}, },
parseIngredient: function(txt) {
this.$http.post('{% url 'api_ingredient_from_string' %}', {text: txt}, {emulateJSON: true}).then((response) => {
console.log(response)
let ing = [response.body.amount, response.body.unit, response.body.food, response.body.note]
return ing
}).catch((err) => {
console.log(err)
this.makeToast(gettext('Error'), gettext('Something went wrong.'), 'danger')
})
}
} }
}); });
</script> </script>

View File

@ -92,9 +92,8 @@ urlpatterns = [
path('api/sync_all/', api.sync_all, name='api_sync'), path('api/sync_all/', api.sync_all, name='api_sync'),
path('api/log_cooking/<int:recipe_id>/', api.log_cooking, name='api_log_cooking'), path('api/log_cooking/<int:recipe_id>/', api.log_cooking, name='api_log_cooking'),
path('api/plan-ical/<slug:from_date>/<slug:to_date>/', api.get_plan_ical, name='api_get_plan_ical'), path('api/plan-ical/<slug:from_date>/<slug:to_date>/', api.get_plan_ical, name='api_get_plan_ical'),
path('api/recipe-from-url/', api.recipe_from_url, name='api_recipe_from_url'), path('api/recipe-from-url/', api.recipe_from_url, name='api_recipe_from_url'),
path('api/recipe-from-html/', api.manual_recipe_from_json, name='api_manual_recipe_from_json'), path('api/recipe-from-source/', api.recipe_from_source, name='api_recipe_from_source'),
path('api/recipe-from-json/', api.recipe_from_json, name='api_recipe_from_json'),
path('api/backup/', api.get_backup, name='api_backup'), path('api/backup/', api.get_backup, name='api_backup'),
path('api/ingredient-from-string/', api.ingredient_from_string, name='api_ingredient_from_string'), path('api/ingredient-from-string/', api.ingredient_from_string, name='api_ingredient_from_string'),

View File

@ -28,8 +28,8 @@ from cookbook.helper.permission_helper import (CustomIsAdmin, CustomIsGuest,
CustomIsOwner, CustomIsShare, CustomIsOwner, CustomIsShare,
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_html_import import get_recipe_from_source
from cookbook.helper.recipe_html_import import get_from_raw from cookbook.helper.recipe_url_import import get_from_scraper, find_recipe_json
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,
@ -616,9 +616,11 @@ def recipe_from_url_old(request):
@group_required('user') @group_required('user')
def manual_recipe_from_json(request): def recipe_from_source(request):
json_data = request.POST['data'] json_data = request.POST['data']
recipe_json, recipe_tree = get_from_raw(json_data, request.space) auto = request.POST['auto']
recipe_json, recipe_tree = get_recipe_from_source(json_data, request.space)
if len(recipe_tree) == 0 and len(recipe_json) == 0: if len(recipe_tree) == 0 and len(recipe_json) == 0:
return JsonResponse( return JsonResponse(
{ {
@ -628,10 +630,18 @@ def manual_recipe_from_json(request):
status=400 status=400
) )
else: else:
return JsonResponse({ if auto == "true":
'recipe_tree': recipe_tree, return JsonResponse({'recipe_json': recipe_json})
'recipe_json': recipe_json else:
}) # overide keyword structure from dict to list
kws = []
for kw in recipe_json['keywords']:
kws.append(kw['text'])
recipe_json['keywords'] = kws
return JsonResponse({
'recipe_tree': recipe_tree,
'recipe_json': recipe_json
})
@group_required('admin') @group_required('admin')