recipe url import ld json
This commit is contained in:
parent
743d7bf608
commit
9e748552b2
@ -1,10 +1,177 @@
|
|||||||
<!DOCTYPE html>
|
{% extends "base.html" %}
|
||||||
<html lang="en">
|
{% load i18n %}
|
||||||
<head>
|
{% load static %}
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>$Title$</title>
|
{% block title %}{% trans 'URL Import' %}{% endblock %}
|
||||||
</head>
|
|
||||||
<body>
|
{% block extra_head %}
|
||||||
$END$
|
|
||||||
</body>
|
{% include 'include/vue_base.html' %}
|
||||||
</html>
|
|
||||||
|
|
||||||
|
<script src="https://unpkg.com/vue-multiselect@2.1.0"></script>
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/vue-multiselect@2.1.0/dist/vue-multiselect.min.css">
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div id="app">
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<input class="form-control" v-model="remote_url">
|
||||||
|
<div class="input-group-append">
|
||||||
|
<button @click="loadRecipe()" class="btn btn-outline-secondary" type="button"
|
||||||
|
id="button-addon2">Search
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
<template v-if="recipe_data !== undefined">
|
||||||
|
|
||||||
|
<form>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="id_name">{% trans 'Recipe Name' %}</label>
|
||||||
|
<input id="id_name" class="form-control" v-model="recipe_data.name">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col col-md-6">
|
||||||
|
<img v-bind:src="recipe_data.image" alt="{% trans 'Recipe Image' %}"
|
||||||
|
class="img-fluid img-responsive img-rounded">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="id_prep_time">{% trans 'Preparation time ca.' %}</label>
|
||||||
|
<input id="id_prep_time" class="form-control" v-model="recipe_data.prepTime">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="id_waiting_time">{% trans 'Waiting time ca.' %}</label>
|
||||||
|
<input id="id_waiting_time" class="form-control" v-model="recipe_data.cookTime">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<table class="table table-responsive-sm table-sm">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{% trans 'Amount' %}</th>
|
||||||
|
<th>{% trans 'Unit' %}</th>
|
||||||
|
<th>{% trans 'Ingredient' %}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
|
||||||
|
<tr v-for="i in recipe_data.recipeIngredient">
|
||||||
|
<td><input class="form-control" v-model="i.amount"></td>
|
||||||
|
<td><input class="form-control" v-model="i.unit"></td>
|
||||||
|
<td><input class="form-control" v-model="i.ingredient"></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="id_instructions">{% trans 'Instructions' %}</label>
|
||||||
|
<textarea id="id_instructions" class="form-control" v-model="recipe_data.recipeInstructions"
|
||||||
|
rows="8"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="id_keywords">{% trans 'Keywords' %}</label>
|
||||||
|
|
||||||
|
<multiselect
|
||||||
|
v-model="recipe_data.keywords"
|
||||||
|
:options="keywords"
|
||||||
|
:close-on-select="false"
|
||||||
|
:clear-on-select="true"
|
||||||
|
:hide-selected="true"
|
||||||
|
:preserve-search="true"
|
||||||
|
placeholder="Pick some"
|
||||||
|
label="text"
|
||||||
|
track-by="id"
|
||||||
|
id="id_keywords"
|
||||||
|
:multiple="true">
|
||||||
|
</multiselect>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
[[recipe_data]]
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
<br/>
|
||||||
|
<br/>
|
||||||
|
<br/>
|
||||||
|
<br/>
|
||||||
|
<br/>
|
||||||
|
<br/>
|
||||||
|
<br/>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
<script type="application/javascript">
|
||||||
|
let csrftoken = Cookies.get('csrftoken');
|
||||||
|
Vue.http.headers.common['X-CSRFToken'] = csrftoken;
|
||||||
|
|
||||||
|
Vue.component('vue-multiselect', window.VueMultiselect.default)
|
||||||
|
|
||||||
|
// micro data examples
|
||||||
|
// https://www.inspirationforall.de/pudding-selber-machen-vanillepudding-schokopudding-rezept/
|
||||||
|
// https://www.ichkoche.at/schokopudding-rezept-218012
|
||||||
|
// https://www.gutekueche.de/mamis-feiner-schokopudding-rezept-4274
|
||||||
|
// https://www.maizena.at/rezepte/schokopudding/13534
|
||||||
|
// https://kochkino.de/schokoladen-pudding/2159
|
||||||
|
// https://www.oetker.de/rezepte/r/schokopudding-mit-vanille-herzen
|
||||||
|
|
||||||
|
let app = new Vue({
|
||||||
|
components: {
|
||||||
|
Multiselect: window.VueMultiselect.default
|
||||||
|
},
|
||||||
|
delimiters: ['[[', ']]'],
|
||||||
|
el: '#app',
|
||||||
|
data: {
|
||||||
|
remote_url: 'https://www.chefkoch.de/rezepte/1716851280413039/Einfacher-Zwiebelkuchen.html',
|
||||||
|
keywords: [],
|
||||||
|
recipe_data: undefined,
|
||||||
|
},
|
||||||
|
mounted: function () {
|
||||||
|
this.loadRecipe();
|
||||||
|
this.getKeywords();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
loadRecipe: function () {
|
||||||
|
this.$http.get("{% url 'api_recipe_from_url' 12345 %}".replace(/12345/, this.remote_url)).then((response) => {
|
||||||
|
this.recipe_data = response.data;
|
||||||
|
|
||||||
|
}).catch((err) => {
|
||||||
|
console.log(err)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
getKeywords: function () {
|
||||||
|
this.$http.get("{% url 'dal_keyword' %}").then((response) => {
|
||||||
|
this.keywords = response.data.results;
|
||||||
|
}).catch((err) => {
|
||||||
|
console.log(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{% endblock %}
|
@ -53,12 +53,14 @@ urlpatterns = [
|
|||||||
path('data/batch/import', data.batch_import, name='data_batch_import'),
|
path('data/batch/import', data.batch_import, name='data_batch_import'),
|
||||||
path('data/sync/wait', data.sync_wait, name='data_sync_wait'),
|
path('data/sync/wait', data.sync_wait, name='data_sync_wait'),
|
||||||
path('data/statistics', data.statistics, name='data_stats'),
|
path('data/statistics', data.statistics, name='data_stats'),
|
||||||
|
path('data/import/url', data.import_url, name='data_import_url'),
|
||||||
|
|
||||||
path('api/get_external_file_link/<int:recipe_id>/', api.get_external_file_link, name='api_get_external_file_link'),
|
path('api/get_external_file_link/<int:recipe_id>/', api.get_external_file_link, name='api_get_external_file_link'),
|
||||||
path('api/get_recipe_file/<int:recipe_id>/', api.get_recipe_file, name='api_get_recipe_file'),
|
path('api/get_recipe_file/<int:recipe_id>/', api.get_recipe_file, name='api_get_recipe_file'),
|
||||||
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:html_week>/', api.get_plan_ical, name='api_get_plan_ical'),
|
path('api/plan-ical/<slug:html_week>/', api.get_plan_ical, name='api_get_plan_ical'),
|
||||||
|
path('api/recipe-from-url/<path:url>/', api.recipe_from_url, name='api_recipe_from_url'),
|
||||||
|
|
||||||
path('dal/keyword/', dal.KeywordAutocomplete.as_view(), name='dal_keyword'),
|
path('dal/keyword/', dal.KeywordAutocomplete.as_view(), name='dal_keyword'),
|
||||||
path('dal/ingredient/', dal.IngredientsAutocomplete.as_view(), name='dal_ingredient'),
|
path('dal/ingredient/', dal.IngredientsAutocomplete.as_view(), name='dal_ingredient'),
|
||||||
|
@ -2,12 +2,14 @@ import io
|
|||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
import requests
|
||||||
from annoying.decorators import ajax_request
|
from annoying.decorators import ajax_request
|
||||||
from annoying.functions import get_object_or_None
|
from annoying.functions import get_object_or_None
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.http import HttpResponse, FileResponse
|
from django.http import HttpResponse, FileResponse, JsonResponse
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from icalendar import Calendar, Event
|
from icalendar import Calendar, Event
|
||||||
@ -16,7 +18,7 @@ from rest_framework.exceptions import APIException
|
|||||||
from rest_framework.mixins import RetrieveModelMixin, UpdateModelMixin, ListModelMixin
|
from rest_framework.mixins import RetrieveModelMixin, UpdateModelMixin, ListModelMixin
|
||||||
|
|
||||||
from cookbook.helper.permission_helper import group_required, CustomIsOwner, CustomIsAdmin
|
from cookbook.helper.permission_helper import group_required, CustomIsOwner, CustomIsAdmin
|
||||||
from cookbook.models import Recipe, Sync, Storage, CookLog, MealPlan, MealType, ViewLog, UserPreference, RecipeBook
|
from cookbook.models import Recipe, Sync, Storage, CookLog, MealPlan, MealType, ViewLog, UserPreference, RecipeBook, Keyword
|
||||||
from cookbook.provider.dropbox import Dropbox
|
from cookbook.provider.dropbox import Dropbox
|
||||||
from cookbook.provider.nextcloud import Nextcloud
|
from cookbook.provider.nextcloud import Nextcloud
|
||||||
from cookbook.serializer import MealPlanSerializer, MealTypeSerializer, RecipeSerializer, ViewLogSerializer, UserNameSerializer, UserPreferenceSerializer, RecipeBookSerializer
|
from cookbook.serializer import MealPlanSerializer, MealTypeSerializer, RecipeSerializer, ViewLogSerializer, UserNameSerializer, UserPreferenceSerializer, RecipeBookSerializer
|
||||||
@ -242,3 +244,72 @@ def get_plan_ical(request, html_week):
|
|||||||
response["Content-Disposition"] = f'attachment; filename=meal_plan_{html_week}.ics'
|
response["Content-Disposition"] = f'attachment; filename=meal_plan_{html_week}.ics'
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@group_required('user')
|
||||||
|
def recipe_from_url(request, url):
|
||||||
|
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Safari/537.36'}
|
||||||
|
response = requests.get(url, headers=headers)
|
||||||
|
|
||||||
|
if response.status_code == 403:
|
||||||
|
return JsonResponse({'error': _('The requested page refused to provide any information (Status Code 403).')})
|
||||||
|
|
||||||
|
soup = BeautifulSoup(response.text, "html.parser")
|
||||||
|
for ld in soup.find_all('script', type='application/ld+json'):
|
||||||
|
ld_json = json.loads(ld.string)
|
||||||
|
|
||||||
|
# recipes type might be wrapped in @graph type
|
||||||
|
if '@graph' in ld_json:
|
||||||
|
for x in ld_json['@graph']:
|
||||||
|
if '@type' in x and x['@type'] == 'Recipe':
|
||||||
|
ld_json = x
|
||||||
|
|
||||||
|
if '@type' in ld_json and ld_json['@type'] == 'Recipe':
|
||||||
|
|
||||||
|
if 'recipeIngredient' in ld_json:
|
||||||
|
ingredients = []
|
||||||
|
|
||||||
|
for x in ld_json['recipeIngredient']:
|
||||||
|
ingredient_split = x.split()
|
||||||
|
if len(ingredient_split) > 2:
|
||||||
|
ingredients.append({'amount': ingredient_split[0], 'unit': ingredient_split[1], 'ingredient': " ".join(ingredient_split[2:])})
|
||||||
|
if len(ingredient_split) == 2:
|
||||||
|
ingredients.append({'amount': ingredient_split[0], 'unit': '', 'ingredient': " ".join(ingredient_split[1:])})
|
||||||
|
if len(ingredient_split) == 1:
|
||||||
|
ingredients.append({'amount': 0, 'unit': '', 'ingredient': " ".join(ingredient_split)})
|
||||||
|
|
||||||
|
ld_json['recipeIngredient'] = ingredients
|
||||||
|
|
||||||
|
if 'keywords' in ld_json:
|
||||||
|
keywords = []
|
||||||
|
if type(ld_json['keywords']) == str:
|
||||||
|
ld_json['keywords'] = ld_json['keywords'].split(',')
|
||||||
|
|
||||||
|
for kw in ld_json['keywords']:
|
||||||
|
if k := Keyword.objects.filter(name=kw).first():
|
||||||
|
keywords.append({'id': str(k.id), 'text': str(k).strip()})
|
||||||
|
else:
|
||||||
|
keywords.append({'id': "null", 'text': kw.strip()})
|
||||||
|
|
||||||
|
ld_json['keywords'] = keywords
|
||||||
|
|
||||||
|
if 'recipeInstructions' in ld_json:
|
||||||
|
instructions = ''
|
||||||
|
if type(ld_json['recipeInstructions']) == list:
|
||||||
|
for i in ld_json['recipeInstructions']:
|
||||||
|
if type(i) == str:
|
||||||
|
instructions += i
|
||||||
|
else:
|
||||||
|
instructions += i['text'] + '\n\n'
|
||||||
|
ld_json['recipeInstructions'] = instructions
|
||||||
|
|
||||||
|
if 'image' in ld_json:
|
||||||
|
if (type(ld_json['image'])) == list:
|
||||||
|
if type(ld_json['image'][0]) == str:
|
||||||
|
ld_json['image'] = ld_json['image'][0]
|
||||||
|
elif 'url' in ld_json['image'][0]:
|
||||||
|
ld_json['image'] = ld_json['image'][0]['url']
|
||||||
|
|
||||||
|
return JsonResponse(ld_json)
|
||||||
|
|
||||||
|
return JsonResponse({'error': _('The requested site does not provide any recognized data format to import the recipe from.')})
|
||||||
|
@ -88,6 +88,11 @@ def batch_edit(request):
|
|||||||
return render(request, 'batch/edit.html', {'form': form})
|
return render(request, 'batch/edit.html', {'form': form})
|
||||||
|
|
||||||
|
|
||||||
|
@group_required('user')
|
||||||
|
def import_url(request):
|
||||||
|
return render(request, 'url_import.html', {})
|
||||||
|
|
||||||
|
|
||||||
class Object(object):
|
class Object(object):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -22,4 +22,5 @@ webdavclient3==3.14.4
|
|||||||
whitenoise==5.1.0
|
whitenoise==5.1.0
|
||||||
icalendar==4.0.6
|
icalendar==4.0.6
|
||||||
pyyaml==5.3.1
|
pyyaml==5.3.1
|
||||||
uritemplate==3.0.1
|
uritemplate==3.0.1
|
||||||
|
beautifulsoup4==4.9.1
|
Loading…
Reference in New Issue
Block a user