basics of new import page
This commit is contained in:
@ -591,6 +591,8 @@ class Ingredient(ExportModelOperationsMixin('ingredient'), models.Model, Permiss
|
|||||||
no_amount = models.BooleanField(default=False)
|
no_amount = models.BooleanField(default=False)
|
||||||
order = models.IntegerField(default=0)
|
order = models.IntegerField(default=0)
|
||||||
|
|
||||||
|
original_text = models.CharField(max_length=512, null=True, blank=True, default=None)
|
||||||
|
|
||||||
space = models.ForeignKey(Space, on_delete=models.CASCADE)
|
space = models.ForeignKey(Space, on_delete=models.CASCADE)
|
||||||
objects = ScopedManager(space='space')
|
objects = ScopedManager(space='space')
|
||||||
|
|
||||||
@ -673,9 +675,9 @@ class Recipe(ExportModelOperationsMixin('recipe'), models.Model, PermissionModel
|
|||||||
working_time = models.IntegerField(default=0)
|
working_time = models.IntegerField(default=0)
|
||||||
waiting_time = models.IntegerField(default=0)
|
waiting_time = models.IntegerField(default=0)
|
||||||
internal = models.BooleanField(default=False)
|
internal = models.BooleanField(default=False)
|
||||||
nutrition = models.ForeignKey(
|
nutrition = models.ForeignKey( NutritionInformation, blank=True, null=True, on_delete=models.CASCADE )
|
||||||
NutritionInformation, blank=True, null=True, on_delete=models.CASCADE
|
|
||||||
)
|
source_url = models.CharField(max_length=1024, default=None, blank=True, null=True)
|
||||||
created_by = models.ForeignKey(User, on_delete=models.PROTECT)
|
created_by = models.ForeignKey(User, on_delete=models.PROTECT)
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
@ -926,6 +926,7 @@ class CustomFilterViewSet(viewsets.ModelViewSet, StandardFilterMixin):
|
|||||||
space=self.request.space).distinct()
|
space=self.request.space).distinct()
|
||||||
return super().get_queryset()
|
return super().get_queryset()
|
||||||
|
|
||||||
|
|
||||||
# -------------- non django rest api views --------------------
|
# -------------- non django rest api views --------------------
|
||||||
|
|
||||||
|
|
||||||
@ -1063,45 +1064,45 @@ def get_plan_ical(request, from_date, to_date):
|
|||||||
|
|
||||||
@group_required('user')
|
@group_required('user')
|
||||||
def recipe_from_source(request):
|
def recipe_from_source(request):
|
||||||
url = request.POST.get('url', None)
|
"""
|
||||||
data = request.POST.get('data', None)
|
function to retrieve a recipe from a given url or source string
|
||||||
mode = request.POST.get('mode', None)
|
:param request: standard request with additional post parameters
|
||||||
auto = request.POST.get('auto', 'true')
|
- url: url to use for importing recipe
|
||||||
|
- data: if no url is given recipe is imported from provided source data
|
||||||
|
- auto: true to return just the recipe as json, false to return source json, html and images as well
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
request_payload = json.loads(request.body.decode('utf-8'))
|
||||||
|
url = request_payload.get('url', None)
|
||||||
|
data = request_payload.get('data', None)
|
||||||
|
auto = True if request_payload.get('auto', 'true') == 'true' else False
|
||||||
|
|
||||||
HEADERS = {
|
# headers to use for request to external sites
|
||||||
"User-Agent": "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.0.7) Gecko/2009021910 Firefox/3.0.7"
|
external_request_headers = {"User-Agent": "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.0.7) Gecko/2009021910 Firefox/3.0.7"}
|
||||||
}
|
|
||||||
|
|
||||||
if (not url and not data) or (mode == 'url' and not url) or (mode == 'source' and not data):
|
if not url and not data:
|
||||||
return JsonResponse(
|
return JsonResponse({
|
||||||
{
|
|
||||||
'error': True,
|
'error': True,
|
||||||
'msg': _('Nothing to do.')
|
'msg': _('Nothing to do.')
|
||||||
},
|
}, status=400)
|
||||||
status=400
|
|
||||||
)
|
|
||||||
|
|
||||||
if mode == 'url' and auto == 'true':
|
# in auto mode scrape url directly with recipe scrapers library
|
||||||
|
if url and auto:
|
||||||
try:
|
try:
|
||||||
scrape = scrape_me(url)
|
scrape = scrape_me(url)
|
||||||
except (WebsiteNotImplementedError, AttributeError):
|
except (WebsiteNotImplementedError, AttributeError):
|
||||||
try:
|
try:
|
||||||
scrape = scrape_me(url, wild_mode=True)
|
scrape = scrape_me(url, wild_mode=True)
|
||||||
except NoSchemaFoundInWildMode:
|
except NoSchemaFoundInWildMode:
|
||||||
return JsonResponse(
|
return JsonResponse({
|
||||||
{
|
|
||||||
'error': True,
|
'error': True,
|
||||||
'msg': _('The requested site provided malformed data and cannot be read.') # noqa: E501
|
'msg': _('The requested site provided malformed data and cannot be read.')
|
||||||
},
|
}, status=400)
|
||||||
status=400)
|
|
||||||
except ConnectionError:
|
except ConnectionError:
|
||||||
return JsonResponse(
|
return JsonResponse({
|
||||||
{
|
|
||||||
'error': True,
|
'error': True,
|
||||||
'msg': _('The requested page could not be found.')
|
'msg': _('The requested page could not be found.')
|
||||||
},
|
}, status=400)
|
||||||
status=400
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
instructions = scrape.instructions()
|
instructions = scrape.instructions()
|
||||||
@ -1111,38 +1112,30 @@ def recipe_from_source(request):
|
|||||||
ingredients = scrape.ingredients()
|
ingredients = scrape.ingredients()
|
||||||
except Exception:
|
except Exception:
|
||||||
ingredients = []
|
ingredients = []
|
||||||
|
|
||||||
if len(ingredients) + len(instructions) == 0:
|
if len(ingredients) + len(instructions) == 0:
|
||||||
return JsonResponse(
|
return JsonResponse({
|
||||||
{
|
|
||||||
'error': True,
|
'error': True,
|
||||||
'msg': _(
|
'msg': _('The requested site does not provide any recognized data format to import the recipe from.')
|
||||||
'The requested site does not provide any recognized data format to import the recipe from.')
|
}, status=400)
|
||||||
# noqa: E501
|
|
||||||
},
|
|
||||||
status=400)
|
|
||||||
else:
|
else:
|
||||||
return JsonResponse({"recipe_json": get_from_scraper(scrape, request)})
|
return JsonResponse({"recipe_json": get_from_scraper(scrape, request)})
|
||||||
elif (mode == 'source') or (mode == 'url' and auto == 'false'):
|
elif data or (url and not auto):
|
||||||
|
# in manual mode request complete page to return it later
|
||||||
if not data or data == 'undefined':
|
if not data or data == 'undefined':
|
||||||
try:
|
try:
|
||||||
data = requests.get(url, headers=HEADERS).content
|
data = requests.get(url, headers=external_request_headers).content
|
||||||
except requests.exceptions.ConnectionError:
|
except requests.exceptions.ConnectionError:
|
||||||
return JsonResponse(
|
return JsonResponse({
|
||||||
{
|
|
||||||
'error': True,
|
'error': True,
|
||||||
'msg': _('Connection Refused.')
|
'msg': _('Connection Refused.')
|
||||||
},
|
}, status=400)
|
||||||
status=400
|
|
||||||
)
|
|
||||||
recipe_json, recipe_tree, recipe_html, images = get_recipe_from_source(data, url, request)
|
recipe_json, recipe_tree, recipe_html, images = get_recipe_from_source(data, url, request)
|
||||||
if len(recipe_tree) == 0 and len(recipe_json) == 0:
|
if len(recipe_tree) == 0 and len(recipe_json) == 0:
|
||||||
return JsonResponse(
|
return JsonResponse({
|
||||||
{
|
|
||||||
'error': True,
|
'error': True,
|
||||||
'msg': _('No usable data could be found.')
|
'msg': _('No usable data could be found.')
|
||||||
},
|
}, status=400)
|
||||||
status=400
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
return JsonResponse({
|
return JsonResponse({
|
||||||
'recipe_tree': recipe_tree,
|
'recipe_tree': recipe_tree,
|
||||||
@ -1152,13 +1145,10 @@ def recipe_from_source(request):
|
|||||||
})
|
})
|
||||||
|
|
||||||
else:
|
else:
|
||||||
return JsonResponse(
|
return JsonResponse({
|
||||||
{
|
|
||||||
'error': True,
|
'error': True,
|
||||||
'msg': _('I couldn\'t find anything to do.')
|
'msg': _('I couldn\'t find anything to do.')
|
||||||
},
|
}, status=400)
|
||||||
status=400
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@group_required('admin')
|
@group_required('admin')
|
||||||
|
@ -1,6 +1,269 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
Test
|
<div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col col-md-12">
|
||||||
|
<h2>{{ $t('Import') }}</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col col-md-12">
|
||||||
|
<b-tabs content-class="mt-3">
|
||||||
|
<b-tab v-bind:title="$t('Website')" active>
|
||||||
|
<h6>Website</h6>
|
||||||
|
|
||||||
|
<b-input-group>
|
||||||
|
<b-input v-model="website_url" placeholder="Website URL"
|
||||||
|
@paste="loadRecipe()"></b-input>
|
||||||
|
<b-input-group-append>
|
||||||
|
<b-button variant="primary" @click="loadRecipe()"><i class="fas fa-search"></i>
|
||||||
|
</b-button>
|
||||||
|
</b-input-group-append>
|
||||||
|
</b-input-group>
|
||||||
|
|
||||||
|
<a href="#" @click="clearRecentImports()">Clear recent imports</a>
|
||||||
|
<ul>
|
||||||
|
<li v-for="x in recent_urls" v-bind:key="x">
|
||||||
|
<a href="#" @click="website_url=x; loadRecipe()">{{ x }}</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h6>Options</h6>
|
||||||
|
<!-- preview column -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col col-md-6" v-if="recipe_json !== undefined">
|
||||||
|
<div >
|
||||||
|
<!-- start of preview card -->
|
||||||
|
<div class="card card-border-primary">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3>Recipe Preview</h3> <!-- TODO localize -->
|
||||||
|
<div class='small text-muted'>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" v-b-toggle.collapse-name>
|
||||||
|
<div class="row px-3" style="justify-content:space-between;">
|
||||||
|
Name
|
||||||
|
<i class="fas fa-eraser" style="cursor:pointer;"
|
||||||
|
@click="recipe_json.name=''"
|
||||||
|
title="{% trans 'Clear Contents' %}"></i>
|
||||||
|
</div>
|
||||||
|
<div class="small text-muted">Text dragged here will
|
||||||
|
be appended to the name.
|
||||||
|
</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 class="card mb-2">
|
||||||
|
<div class="card-header" v-b-toggle.collapse-description>
|
||||||
|
<div class="row px-3" style="justify-content:space-between;">
|
||||||
|
{% trans 'Description' %}
|
||||||
|
<i class="fas fa-eraser" style="cursor:pointer;"
|
||||||
|
@click="recipe_json.description=''"
|
||||||
|
title="{% trans 'Clear Contents' %}"></i>
|
||||||
|
</div>
|
||||||
|
<div class="small text-muted">{% trans 'Text dragged here will
|
||||||
|
be appended to the description.' %}
|
||||||
|
</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 class="card mb-2">
|
||||||
|
<div class="card-header" v-b-toggle.collapse-kw>
|
||||||
|
<div class="row px-3" style="justify-content:space-between;">
|
||||||
|
{% trans 'Keywords' %}
|
||||||
|
<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>
|
||||||
|
<b-collapse id="collapse-kw" visible class="mt-2">
|
||||||
|
<div class="card-body drop-zone"
|
||||||
|
@drop="replacePreview('keywords', $event)"
|
||||||
|
@dragover.prevent @dragenter.prevent>
|
||||||
|
<div v-for="kw in recipe_json.keywords" v-bind:key="kw.id">
|
||||||
|
<div class="card-text">{{ kw.text }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</b-collapse>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card mb-2">
|
||||||
|
<div class="card-header" v-b-toggle.collapse-image
|
||||||
|
style="display:flex; justify-content:space-between;">
|
||||||
|
{% trans 'Image' %}
|
||||||
|
<i class="fas fa-eraser" style="cursor:pointer;"
|
||||||
|
@click="recipe_json.image=''"
|
||||||
|
title="{% trans 'Clear Contents' %}"></i>
|
||||||
|
</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 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="recipe_json.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="recipe_json.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="recipe_json.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" v-b-toggle.collapse-ing>
|
||||||
|
<div class="row px-3"
|
||||||
|
style="display:flex; justify-content:space-between;">
|
||||||
|
{% trans 'Ingredients' %}
|
||||||
|
<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>
|
||||||
|
<b-collapse id="collapse-ing" visible class="mt-2">
|
||||||
|
<div class="card-body drop-zone"
|
||||||
|
@drop="replacePreview('ingredients', $event)"
|
||||||
|
@dragover.prevent @dragenter.prevent>
|
||||||
|
<ul class="list-group list-group">
|
||||||
|
<div v-for="i in recipe_json.recipeIngredient" v-bind:key="i.id">
|
||||||
|
<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>
|
||||||
|
</b-collapse>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card mb-2">
|
||||||
|
<div class="card-header" v-b-toggle.collapse-instructions>
|
||||||
|
<div class="row px-3" style="justify-content:space-between;">
|
||||||
|
{% trans 'Instructions' %}
|
||||||
|
<i class="fas fa-eraser" style="cursor:pointer;"
|
||||||
|
@click="recipe_json.recipeInstructions=''"
|
||||||
|
title="{% trans 'Clear Contents' %}"></i>
|
||||||
|
</div>
|
||||||
|
<div class="small text-muted">{% trans 'Recipe instructions
|
||||||
|
dragged here will be appended to current instructions.' %}
|
||||||
|
</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>
|
||||||
|
<br/>
|
||||||
|
<!-- end of preview card -->
|
||||||
|
<button @click="showRecipe()" class="btn btn-primary shadow-none" type="button"
|
||||||
|
style="margin-bottom: 2vh"
|
||||||
|
id="id_btn_json"><i class="fas fa-code"></i> {% trans 'Import' %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</b-tab>
|
||||||
|
<b-tab v-bind:title="$t('App')">
|
||||||
|
<!-- TODO implement app import -->
|
||||||
|
</b-tab>
|
||||||
|
<b-tab v-bind:title="$t('Source')">
|
||||||
|
<!-- TODO implement source import -->
|
||||||
|
</b-tab>
|
||||||
|
<b-tab v-bind:title="$t('Bookmarklet')">
|
||||||
|
<!-- TODO get code for bookmarklet here and provide some instructions -->
|
||||||
|
<a class="btn btn-outline-info btn-sm" href="#">
|
||||||
|
Bookmark Text </a>
|
||||||
|
</b-tab>
|
||||||
|
|
||||||
|
</b-tabs>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
@ -12,7 +275,8 @@ import {BootstrapVue} from 'bootstrap-vue'
|
|||||||
|
|
||||||
import 'bootstrap-vue/dist/bootstrap-vue.css'
|
import 'bootstrap-vue/dist/bootstrap-vue.css'
|
||||||
|
|
||||||
import {ResolveUrlMixin, ToastMixin} from "@/utils/utils";
|
import {resolveDjangoUrl, ResolveUrlMixin, StandardToasts, ToastMixin} from "@/utils/utils";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
Vue.use(BootstrapVue)
|
Vue.use(BootstrapVue)
|
||||||
|
|
||||||
@ -22,20 +286,82 @@ export default {
|
|||||||
ResolveUrlMixin,
|
ResolveUrlMixin,
|
||||||
ToastMixin,
|
ToastMixin,
|
||||||
],
|
],
|
||||||
components: {
|
components: {},
|
||||||
|
|
||||||
},
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
LS_IMPORT_RECENT: 'import_recent_urls', //TODO use central helper to manage all local storage keys (and maybe even access)
|
||||||
|
website_url: '',
|
||||||
|
recent_urls: [],
|
||||||
|
source_data: '',
|
||||||
|
recipe_data: undefined,
|
||||||
|
recipe_json: undefined,
|
||||||
|
recipe_tree: undefined,
|
||||||
|
recipe_images: [],
|
||||||
|
automatic: true,
|
||||||
|
error: undefined,
|
||||||
|
loading: false,
|
||||||
|
preview: false,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
let local_storage_recent = JSON.parse(window.localStorage.getItem(this.LS_IMPORT_RECENT))
|
||||||
|
this.recent_urls = local_storage_recent !== null ? local_storage_recent : []
|
||||||
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
/**
|
||||||
|
* Requests the recipe to be loaded form the source (url/data) from the server
|
||||||
|
* Updates all variables to contain what they need to render either simple preview or manual mapping mode
|
||||||
|
*/
|
||||||
|
loadRecipe: function () {
|
||||||
|
console.log(this.website_url)
|
||||||
|
if (this.website_url !== '') {
|
||||||
|
if (this.recent_urls.length > 5) {
|
||||||
|
this.recent_urls.pop()
|
||||||
|
}
|
||||||
|
if (this.recent_urls.filter(x => x === this.website_url).length === 0) {
|
||||||
|
this.recent_urls.push(this.website_url)
|
||||||
|
}
|
||||||
|
window.localStorage.setItem(this.LS_IMPORT_RECENT, JSON.stringify(this.recent_urls))
|
||||||
|
}
|
||||||
|
this.recipe_data = undefined
|
||||||
|
this.recipe_json = undefined
|
||||||
|
this.recipe_tree = undefined
|
||||||
|
this.recipe_images = []
|
||||||
|
this.error = undefined
|
||||||
|
this.loading = true
|
||||||
|
this.preview = false
|
||||||
|
axios.post(resolveDjangoUrl('api_recipe_from_source'), {
|
||||||
|
'url': this.website_url,
|
||||||
|
'data': this.source_data,
|
||||||
|
'auto': this.automatic,
|
||||||
|
'mode': this.mode
|
||||||
|
},).then((response) => {
|
||||||
|
this.recipe_json = response.data['recipe_json'];
|
||||||
|
this.recipe_tree = response.data['recipe_tree'];
|
||||||
|
this.recipe_html = response.data['recipe_html'];
|
||||||
|
this.recipe_images = response.data['images']; //todo change on backend as well after old view is deprecated
|
||||||
|
if (this.automatic) {
|
||||||
|
this.recipe_data = this.recipe_json;
|
||||||
|
this.preview = false
|
||||||
|
} else {
|
||||||
|
this.preview = true
|
||||||
|
}
|
||||||
|
this.loading = false
|
||||||
|
}).catch((err) => {
|
||||||
|
this.error = err.data
|
||||||
|
this.loading = false
|
||||||
|
console.log(err.response)
|
||||||
|
StandardToasts.makeStandardToast(StandardToasts.FAIL_FETCH, err.response.data.msg)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Clear list of recently imported recipe urls
|
||||||
|
*/
|
||||||
|
clearRecentImports: function () {
|
||||||
|
window.localStorage.setItem(this.LS_IMPORT_RECENT, JSON.stringify([]))
|
||||||
|
this.recent_urls = []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -333,6 +333,9 @@
|
|||||||
"ingredient_list": "Ingredient List",
|
"ingredient_list": "Ingredient List",
|
||||||
"explain": "Explain",
|
"explain": "Explain",
|
||||||
"filter": "Filter",
|
"filter": "Filter",
|
||||||
|
"Website": "Website",
|
||||||
|
"App": "App",
|
||||||
|
"Bookmarklet": "Bookmarklet",
|
||||||
"search_no_recipes": "Could not find any recipes!",
|
"search_no_recipes": "Could not find any recipes!",
|
||||||
"search_import_help_text": "Import a recipe from an external website or application.",
|
"search_import_help_text": "Import a recipe from an external website or application.",
|
||||||
"search_create_help_text": "Create a new recipe directly in Tandoor.",
|
"search_create_help_text": "Create a new recipe directly in Tandoor.",
|
||||||
|
22
vue/src/utils/integration.js
Normal file
22
vue/src/utils/integration.js
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
// containing all data and functions regarding the different integrations
|
||||||
|
|
||||||
|
export const INTEGRATIONS = {
|
||||||
|
DEFAULT: {name: "Tandoor", import: true, export: true},
|
||||||
|
CHEFTAP: {name: "Cheftap", import: true, export: false},
|
||||||
|
CHOWDOWN: {name: "Chowdown", import: true, export: false},
|
||||||
|
COOKBOOKAPP: {name: "CookBookApp", import: true, export: false},
|
||||||
|
COPYMETHAT: {name: "CopyMeThat", import: true, export: false},
|
||||||
|
DOMESTICA: {name: "Domestica", import: true, export: false},
|
||||||
|
MEALIE: {name: "Mealie", import: true, export: false},
|
||||||
|
MEALMASTER: {name: "Mealmaster", import: true, export: false},
|
||||||
|
NEXTCLOUD: {name: "Nextcloud Cookbook", import: true, export: false},
|
||||||
|
OPENEATS: {name: "Openeats", import: true, export: false},
|
||||||
|
PAPRIKA: {name: "Paprika", import: true, export: false},
|
||||||
|
PEPPERPLATE: {name: "Pepperplate", import: true, export: false},
|
||||||
|
PLANTOEAT: {name: "Plantoeat", import: true, export: false},
|
||||||
|
RECETTETEK: {name: "RecetteTek", import: true, export: false},
|
||||||
|
RECIPEKEEPER: {name: "Recipekeeper", import: true, export: false},
|
||||||
|
RECIPESAGE: {name: "Recipesage", import: true, export: true},
|
||||||
|
REZKONV: {name: "Rezkonv", import: true, export: false},
|
||||||
|
SAFRON: {name: "Safron", import: true, export: true},
|
||||||
|
}
|
@ -2,18 +2,18 @@
|
|||||||
* Utility functions to call bootstrap toasts
|
* Utility functions to call bootstrap toasts
|
||||||
* */
|
* */
|
||||||
import i18n from "@/i18n"
|
import i18n from "@/i18n"
|
||||||
import { frac } from "@/utils/fractions"
|
import {frac} from "@/utils/fractions"
|
||||||
/*
|
/*
|
||||||
* Utility functions to use OpenAPIs generically
|
* Utility functions to use OpenAPIs generically
|
||||||
* */
|
* */
|
||||||
import { ApiApiFactory } from "@/utils/openapi/api.ts"
|
import {ApiApiFactory} from "@/utils/openapi/api.ts"
|
||||||
import axios from "axios"
|
import axios from "axios"
|
||||||
import { BToast } from "bootstrap-vue"
|
import {BToast} from "bootstrap-vue"
|
||||||
// /*
|
// /*
|
||||||
// * Utility functions to use manipulate nested components
|
// * Utility functions to use manipulate nested components
|
||||||
// * */
|
// * */
|
||||||
import Vue from "vue"
|
import Vue from "vue"
|
||||||
import { Actions, Models } from "./models"
|
import {Actions, Models} from "./models"
|
||||||
|
|
||||||
export const ToastMixin = {
|
export const ToastMixin = {
|
||||||
name: "ToastMixin",
|
name: "ToastMixin",
|
||||||
@ -49,37 +49,37 @@ export class StandardToasts {
|
|||||||
static FAIL_MOVE = "FAIL_MOVE"
|
static FAIL_MOVE = "FAIL_MOVE"
|
||||||
static FAIL_MERGE = "FAIL_MERGE"
|
static FAIL_MERGE = "FAIL_MERGE"
|
||||||
|
|
||||||
static makeStandardToast(toast, err_details = undefined) {
|
static makeStandardToast(toast, err_details = undefined) { //TODO err_details render very ugly, improve this maybe by using a custom toast component (in conjunction with error logging maybe)
|
||||||
switch (toast) {
|
switch (toast) {
|
||||||
case StandardToasts.SUCCESS_CREATE:
|
case StandardToasts.SUCCESS_CREATE:
|
||||||
makeToast(i18n.tc("Success"), i18n.tc("success_creating_resource"), "success")
|
makeToast(i18n.tc("Success"), i18n.tc("success_creating_resource") + (err_details ? "\n" + err_details : ""), "success")
|
||||||
break
|
break
|
||||||
case StandardToasts.SUCCESS_FETCH:
|
case StandardToasts.SUCCESS_FETCH:
|
||||||
makeToast(i18n.tc("Success"), i18n.tc("success_fetching_resource"), "success")
|
makeToast(i18n.tc("Success"), i18n.tc("success_fetching_resource") + (err_details ? "\n" + err_details : ""), "success")
|
||||||
break
|
break
|
||||||
case StandardToasts.SUCCESS_UPDATE:
|
case StandardToasts.SUCCESS_UPDATE:
|
||||||
makeToast(i18n.tc("Success"), i18n.tc("success_updating_resource"), "success")
|
makeToast(i18n.tc("Success"), i18n.tc("success_updating_resource") + (err_details ? "\n" + err_details : ""), "success")
|
||||||
break
|
break
|
||||||
case StandardToasts.SUCCESS_DELETE:
|
case StandardToasts.SUCCESS_DELETE:
|
||||||
makeToast(i18n.tc("Success"), i18n.tc("success_deleting_resource"), "success")
|
makeToast(i18n.tc("Success"), i18n.tc("success_deleting_resource") + (err_details ? "\n" + err_details : ""), "success")
|
||||||
break
|
break
|
||||||
case StandardToasts.SUCCESS_MOVE:
|
case StandardToasts.SUCCESS_MOVE:
|
||||||
makeToast(i18n.tc("Success"), i18n.tc("success_moving_resource"), "success")
|
makeToast(i18n.tc("Success"), i18n.tc("success_moving_resource") + (err_details ? "\n" + err_details : ""), "success")
|
||||||
break
|
break
|
||||||
case StandardToasts.SUCCESS_MERGE:
|
case StandardToasts.SUCCESS_MERGE:
|
||||||
makeToast(i18n.tc("Success"), i18n.tc("success_merging_resource"), "success")
|
makeToast(i18n.tc("Success"), i18n.tc("success_merging_resource") + (err_details ? "\n" + err_details : ""), "success")
|
||||||
break
|
break
|
||||||
case StandardToasts.FAIL_CREATE:
|
case StandardToasts.FAIL_CREATE:
|
||||||
makeToast(i18n.tc("Failure"), i18n.tc("err_creating_resource"), "danger")
|
makeToast(i18n.tc("Failure"), i18n.tc("err_creating_resource") + (err_details ? "\n" + err_details : ""), "danger")
|
||||||
break
|
break
|
||||||
case StandardToasts.FAIL_FETCH:
|
case StandardToasts.FAIL_FETCH:
|
||||||
makeToast(i18n.tc("Failure"), i18n.tc("err_fetching_resource"), "danger")
|
makeToast(i18n.tc("Failure"), i18n.tc("err_fetching_resource") + (err_details ? "\n" + err_details : ""), "danger")
|
||||||
break
|
break
|
||||||
case StandardToasts.FAIL_UPDATE:
|
case StandardToasts.FAIL_UPDATE:
|
||||||
makeToast(i18n.tc("Failure"), i18n.tc("err_updating_resource"), "danger")
|
makeToast(i18n.tc("Failure"), i18n.tc("err_updating_resource") + (err_details ? "\n" + err_details : ""), "danger")
|
||||||
break
|
break
|
||||||
case StandardToasts.FAIL_DELETE:
|
case StandardToasts.FAIL_DELETE:
|
||||||
makeToast(i18n.tc("Failure"), i18n.tc("err_deleting_resource"), "danger")
|
makeToast(i18n.tc("Failure"), i18n.tc("err_deleting_resource") + (err_details ? "\n" + err_details : ""), "danger")
|
||||||
break
|
break
|
||||||
case StandardToasts.FAIL_MOVE:
|
case StandardToasts.FAIL_MOVE:
|
||||||
makeToast(i18n.tc("Failure"), i18n.tc("err_moving_resource") + (err_details ? "\n" + err_details : ""), "danger")
|
makeToast(i18n.tc("Failure"), i18n.tc("err_moving_resource") + (err_details ? "\n" + err_details : ""), "danger")
|
||||||
@ -234,7 +234,7 @@ export const ApiMixin = {
|
|||||||
return apiClient[func](...parameters)
|
return apiClient[func](...parameters)
|
||||||
},
|
},
|
||||||
genericGetAPI: function (url, options) {
|
genericGetAPI: function (url, options) {
|
||||||
return axios.get(resolveDjangoUrl(url), { params: options, emulateJSON: true })
|
return axios.get(resolveDjangoUrl(url), {params: options, emulateJSON: true})
|
||||||
},
|
},
|
||||||
genericPostAPI: function (url, form) {
|
genericPostAPI: function (url, form) {
|
||||||
let data = new FormData()
|
let data = new FormData()
|
||||||
@ -284,6 +284,7 @@ function formatParam(config, value, options) {
|
|||||||
}
|
}
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildParams(options, setup) {
|
function buildParams(options, setup) {
|
||||||
let config = setup?.config ?? {}
|
let config = setup?.config ?? {}
|
||||||
let params = setup?.params ?? []
|
let params = setup?.params ?? []
|
||||||
@ -311,6 +312,7 @@ function buildParams(options, setup) {
|
|||||||
})
|
})
|
||||||
return parameters
|
return parameters
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDefault(config, options) {
|
function getDefault(config, options) {
|
||||||
let value = undefined
|
let value = undefined
|
||||||
value = config?.default ?? undefined
|
value = config?.default ?? undefined
|
||||||
@ -338,11 +340,12 @@ function getDefault(config, options) {
|
|||||||
}
|
}
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getConfig(model, action) {
|
export function getConfig(model, action) {
|
||||||
let f = action.function
|
let f = action.function
|
||||||
// if not defined partialUpdate will use params from create
|
// if not defined partialUpdate will use params from create
|
||||||
if (f === "partialUpdate" && !model?.[f]?.params) {
|
if (f === "partialUpdate" && !model?.[f]?.params) {
|
||||||
model[f] = { params: [...["id"], ...model.create.params] }
|
model[f] = {params: [...["id"], ...model.create.params]}
|
||||||
}
|
}
|
||||||
|
|
||||||
let config = {
|
let config = {
|
||||||
@ -350,12 +353,12 @@ export function getConfig(model, action) {
|
|||||||
apiName: model.apiName,
|
apiName: model.apiName,
|
||||||
}
|
}
|
||||||
// spread operator merges dictionaries - last item in list takes precedence
|
// spread operator merges dictionaries - last item in list takes precedence
|
||||||
config = { ...config, ...action, ...model.model_type?.[f], ...model?.[f] }
|
config = {...config, ...action, ...model.model_type?.[f], ...model?.[f]}
|
||||||
// nested dictionaries are not merged - so merge again on any nested keys
|
// nested dictionaries are not merged - so merge again on any nested keys
|
||||||
config.config = { ...action?.config, ...model.model_type?.[f]?.config, ...model?.[f]?.config }
|
config.config = {...action?.config, ...model.model_type?.[f]?.config, ...model?.[f]?.config}
|
||||||
// look in partialUpdate again if necessary
|
// look in partialUpdate again if necessary
|
||||||
if (f === "partialUpdate" && Object.keys(config.config).length === 0) {
|
if (f === "partialUpdate" && Object.keys(config.config).length === 0) {
|
||||||
config.config = { ...model.model_type?.create?.config, ...model?.create?.config }
|
config.config = {...model.model_type?.create?.config, ...model?.create?.config}
|
||||||
}
|
}
|
||||||
config["function"] = f + config.apiName + (config?.suffix ?? "") // parens are required to force optional chaining to evaluate before concat
|
config["function"] = f + config.apiName + (config?.suffix ?? "") // parens are required to force optional chaining to evaluate before concat
|
||||||
return config
|
return config
|
||||||
@ -366,17 +369,17 @@ export function getConfig(model, action) {
|
|||||||
// * */
|
// * */
|
||||||
export function getForm(model, action, item1, item2) {
|
export function getForm(model, action, item1, item2) {
|
||||||
let f = action.function
|
let f = action.function
|
||||||
let config = { ...action?.form, ...model.model_type?.[f]?.form, ...model?.[f]?.form }
|
let config = {...action?.form, ...model.model_type?.[f]?.form, ...model?.[f]?.form}
|
||||||
// if not defined partialUpdate will use form from create
|
// if not defined partialUpdate will use form from create
|
||||||
if (f === "partialUpdate" && Object.keys(config).length == 0) {
|
if (f === "partialUpdate" && Object.keys(config).length == 0) {
|
||||||
config = { ...Actions.CREATE?.form, ...model.model_type?.["create"]?.form, ...model?.["create"]?.form }
|
config = {...Actions.CREATE?.form, ...model.model_type?.["create"]?.form, ...model?.["create"]?.form}
|
||||||
config["title"] = { ...action?.form_title, ...model.model_type?.[f]?.form_title, ...model?.[f]?.form_title }
|
config["title"] = {...action?.form_title, ...model.model_type?.[f]?.form_title, ...model?.[f]?.form_title}
|
||||||
// form functions should not be inherited
|
// form functions should not be inherited
|
||||||
if (config?.["form_function"]?.includes("Create")) {
|
if (config?.["form_function"]?.includes("Create")) {
|
||||||
delete config["form_function"]
|
delete config["form_function"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let form = { fields: [] }
|
let form = {fields: []}
|
||||||
let value = ""
|
let value = ""
|
||||||
for (const [k, v] of Object.entries(config)) {
|
for (const [k, v] of Object.entries(config)) {
|
||||||
if (v?.function) {
|
if (v?.function) {
|
||||||
@ -404,6 +407,7 @@ export function getForm(model, action, item1, item2) {
|
|||||||
}
|
}
|
||||||
return form
|
return form
|
||||||
}
|
}
|
||||||
|
|
||||||
function formTranslate(translate, model, item1, item2) {
|
function formTranslate(translate, model, item1, item2) {
|
||||||
if (typeof translate !== "object") {
|
if (typeof translate !== "object") {
|
||||||
return translate
|
return translate
|
||||||
@ -495,7 +499,7 @@ const specialCases = {
|
|||||||
let params = []
|
let params = []
|
||||||
if (action.function === "partialUpdate") {
|
if (action.function === "partialUpdate") {
|
||||||
API = GenericAPI
|
API = GenericAPI
|
||||||
params = [Models.SUPERMARKET, Actions.FETCH, { id: options.id }]
|
params = [Models.SUPERMARKET, Actions.FETCH, {id: options.id}]
|
||||||
} else if (action.function === "create") {
|
} else if (action.function === "create") {
|
||||||
API = new ApiApiFactory()[setup.function]
|
API = new ApiApiFactory()[setup.function]
|
||||||
params = buildParams(options, setup)
|
params = buildParams(options, setup)
|
||||||
@ -532,15 +536,15 @@ const specialCases = {
|
|||||||
let order = Math.max(...existing_categories.map((x) => x?.order ?? 0), ...updated_categories.map((x) => x?.order ?? 0), 0) + 1
|
let order = Math.max(...existing_categories.map((x) => x?.order ?? 0), ...updated_categories.map((x) => x?.order ?? 0), 0) + 1
|
||||||
|
|
||||||
removed_categories.forEach((x) => {
|
removed_categories.forEach((x) => {
|
||||||
promises.push(GenericAPI(Models.SHOPPING_CATEGORY_RELATION, Actions.DELETE, { id: x.id }))
|
promises.push(GenericAPI(Models.SHOPPING_CATEGORY_RELATION, Actions.DELETE, {id: x.id}))
|
||||||
})
|
})
|
||||||
let item = { supermarket: id }
|
let item = {supermarket: id}
|
||||||
added_categories.forEach((x) => {
|
added_categories.forEach((x) => {
|
||||||
item.order = x?.order ?? order
|
item.order = x?.order ?? order
|
||||||
if (!x?.order) {
|
if (!x?.order) {
|
||||||
order = order + 1
|
order = order + 1
|
||||||
}
|
}
|
||||||
item.category = { id: x.category.id, name: x.category.name }
|
item.category = {id: x.category.id, name: x.category.name}
|
||||||
promises.push(GenericAPI(Models.SHOPPING_CATEGORY_RELATION, Actions.CREATE, item))
|
promises.push(GenericAPI(Models.SHOPPING_CATEGORY_RELATION, Actions.CREATE, item))
|
||||||
})
|
})
|
||||||
changed_categories.forEach((x) => {
|
changed_categories.forEach((x) => {
|
||||||
@ -549,13 +553,13 @@ const specialCases = {
|
|||||||
if (!x?.order) {
|
if (!x?.order) {
|
||||||
order = order + 1
|
order = order + 1
|
||||||
}
|
}
|
||||||
item.category = { id: x.category.id, name: x.category.name }
|
item.category = {id: x.category.id, name: x.category.name}
|
||||||
promises.push(GenericAPI(Models.SHOPPING_CATEGORY_RELATION, Actions.UPDATE, item))
|
promises.push(GenericAPI(Models.SHOPPING_CATEGORY_RELATION, Actions.UPDATE, item))
|
||||||
})
|
})
|
||||||
|
|
||||||
return Promise.all(promises).then(() => {
|
return Promise.all(promises).then(() => {
|
||||||
// finally get and return the Supermarket which everything downstream is expecting
|
// finally get and return the Supermarket which everything downstream is expecting
|
||||||
return GenericAPI(Models.SUPERMARKET, Actions.FETCH, { id: id })
|
return GenericAPI(Models.SUPERMARKET, Actions.FETCH, {id: id})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
Reference in New Issue
Block a user