diff --git a/cookbook/templates/test.html b/cookbook/templates/test.html
index 629d81be..f0c69a80 100644
--- a/cookbook/templates/test.html
+++ b/cookbook/templates/test.html
@@ -3,7 +3,7 @@
{% load static %}
{% load i18n %}
{% load l10n %}
-
+{% load custom_tags %}
{% block title %}Test{% endblock %}
@@ -11,6 +11,7 @@
{% block content_fluid %}
+
@@ -27,6 +28,8 @@
{% render_bundle 'import_view' %}
diff --git a/cookbook/templatetags/custom_tags.py b/cookbook/templatetags/custom_tags.py
index 3b119cf3..f34eac8f 100644
--- a/cookbook/templatetags/custom_tags.py
+++ b/cookbook/templatetags/custom_tags.py
@@ -136,7 +136,7 @@ def bookmarklet(request):
if (api_token := Token.objects.filter(user=request.user).first()) is None:
api_token = Token.objects.create(user=request.user)
- bookmark = "javascript: \
+ bookmark = "Test"
+ return re.sub(r"[\n\t]*", "", bookmark)
@register.simple_tag
diff --git a/cookbook/views/api.py b/cookbook/views/api.py
index 1463c3f3..24139c1d 100644
--- a/cookbook/views/api.py
+++ b/cookbook/views/api.py
@@ -1085,13 +1085,20 @@ def recipe_from_source(request):
- 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:
+ - (optional) bookmarklet: id of bookmarklet import to use, overrides URL and data attributes
+ :return: JsonResponse containing the parsed json, original html,json and images
"""
request_payload = json.loads(request.body.decode('utf-8'))
url = request_payload.get('url', None)
data = request_payload.get('data', None)
+ bookmarklet = request_payload.get('bookmarklet', None)
auto = True if request_payload.get('auto', 'true') == 'true' else False
+ if bookmarklet := BookmarkletImport.objects.filter(pk=bookmarklet).first():
+ url = bookmarklet.url
+ data = bookmarklet.html
+ bookmarklet.delete()
+
# headers to use for request to external sites
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"}
diff --git a/cookbook/views/data.py b/cookbook/views/data.py
index 6362f3ef..70e7fcbf 100644
--- a/cookbook/views/data.py
+++ b/cookbook/views/data.py
@@ -16,6 +16,7 @@ from django.utils.translation import ngettext
from django_tables2 import RequestConfig
from PIL import UnidentifiedImageError
from requests.exceptions import MissingSchema
+from rest_framework.authtoken.models import Token
from cookbook.forms import BatchEditForm, SyncForm
from cookbook.helper.image_processing import handle_image
@@ -23,7 +24,7 @@ from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.helper.permission_helper import group_required, has_group_permission
from cookbook.helper.recipe_url_import import parse_cooktime
from cookbook.models import (Comment, Food, Ingredient, Keyword, Recipe, RecipeImport, Step, Sync,
- Unit, UserPreference)
+ Unit, UserPreference, BookmarkletImport)
from cookbook.tables import SyncTable
from recipes import settings
@@ -123,7 +124,6 @@ def batch_edit(request):
@group_required('user')
-@atomic
def import_url(request):
if request.space.max_recipes != 0 and Recipe.objects.filter(space=request.space).count() >= request.space.max_recipes: # TODO move to central helper function
messages.add_message(request, messages.WARNING, _('You have reached the maximum number of recipes for your space.'))
@@ -133,93 +133,15 @@ def import_url(request):
messages.add_message(request, messages.WARNING, _('You have more users than allowed in your space.'))
return HttpResponseRedirect(reverse('index'))
- if request.method == 'POST':
- data = json.loads(request.body)
- data['cookTime'] = parse_cooktime(data.get('cookTime', ''))
- data['prepTime'] = parse_cooktime(data.get('prepTime', ''))
-
- recipe = Recipe.objects.create(
- name=data['name'],
- description=data['description'],
- waiting_time=data['cookTime'],
- working_time=data['prepTime'],
- servings=data['servings'],
- internal=True,
- created_by=request.user,
- space=request.space,
- )
-
- step = Step.objects.create(
- instruction=data['recipeInstructions'], space=request.space,
- )
-
- recipe.steps.add(step)
-
- for kw in data['keywords']:
- if data['all_keywords']: # do not remove this check :) https://github.com/vabene1111/recipes/issues/645
- k, created = Keyword.objects.get_or_create(name=kw['text'], space=request.space)
- recipe.keywords.add(k)
- else:
- try:
- k = Keyword.objects.get(name=kw['text'], space=request.space)
- recipe.keywords.add(k)
- except ObjectDoesNotExist:
- pass
-
- ingredient_parser = IngredientParser(request, True)
- for ing in data['recipeIngredient']:
- original = ing.pop('original', None) or ing.pop('original_text', None)
- ingredient = Ingredient(original_text=original, space=request.space, )
-
- if food_text := ing['ingredient']['text'].strip():
- ingredient.food = ingredient_parser.get_food(food_text)
-
- if ing['unit']:
- if unit_text := ing['unit']['text'].strip():
- ingredient.unit = ingredient_parser.get_unit(unit_text)
-
- # TODO properly handle no_amount recipes
- if isinstance(ing['amount'], str):
- try:
- ingredient.amount = float(ing['amount'].replace(',', '.'))
- except ValueError:
- ingredient.no_amount = True
- pass
- elif isinstance(ing['amount'], float) \
- or isinstance(ing['amount'], int):
- ingredient.amount = ing['amount']
- ingredient.note = ing['note'].strip() if 'note' in ing else ''
-
- ingredient.save()
- step.ingredients.add(ingredient)
-
- if 'image' in data and data['image'] != '' and data['image'] is not None:
- try:
- response = requests.get(data['image'])
-
- img, filetype = handle_image(request, File(BytesIO(response.content), name='image'))
- recipe.image = File(
- img, name=f'{uuid.uuid4()}_{recipe.pk}{filetype}'
- )
- recipe.save()
- except UnidentifiedImageError as e:
- print(e)
- pass
- except MissingSchema as e:
- print(e)
- pass
- except Exception as e:
- print(e)
- pass
-
- return HttpResponse(reverse('view_recipe', args=[recipe.pk]))
+ if (api_token := Token.objects.filter(user=request.user).first()) is None:
+ api_token = Token.objects.create(user=request.user)
+ bookmarklet_import_id = -1
if 'id' in request.GET:
- context = {'bookmarklet': request.GET.get('id', '')}
- else:
- context = {}
+ if bookmarklet_import := BookmarkletImport.objects.filter(id=request.GET['id']).first():
+ bookmarklet_import_id = bookmarklet_import.pk
- return render(request, 'url_import.html', context)
+ return render(request, 'test.html', {'api_token': api_token, 'bookmarklet_import_id': bookmarklet_import_id})
class Object(object):
diff --git a/cookbook/views/views.py b/cookbook/views/views.py
index 2d298547..43e8c703 100644
--- a/cookbook/views/views.py
+++ b/cookbook/views/views.py
@@ -327,10 +327,10 @@ def user_settings(request):
if not sp:
sp = SearchPreferenceForm(user=request.user)
fields_searched = (
- len(search_form.cleaned_data['icontains'])
- + len(search_form.cleaned_data['istartswith'])
- + len(search_form.cleaned_data['trigram'])
- + len(search_form.cleaned_data['fulltext'])
+ len(search_form.cleaned_data['icontains'])
+ + len(search_form.cleaned_data['istartswith'])
+ + len(search_form.cleaned_data['trigram'])
+ + len(search_form.cleaned_data['fulltext'])
)
if fields_searched == 0:
search_form.add_error(None, _('You must select at least one field to search!'))
@@ -647,7 +647,10 @@ def test(request):
if not settings.DEBUG:
return HttpResponseRedirect(reverse('index'))
- return render(request, 'test.html', {})
+ if (api_token := Token.objects.filter(user=request.user).first()) is None:
+ api_token = Token.objects.create(user=request.user)
+
+ return render(request, 'test.html', {'api_token': api_token})
def test2(request):
diff --git a/vue/src/apps/ImportView/ImportView.vue b/vue/src/apps/ImportView/ImportView.vue
index 0a155cfe..83faa5bd 100644
--- a/vue/src/apps/ImportView/ImportView.vue
+++ b/vue/src/apps/ImportView/ImportView.vue
@@ -105,52 +105,8 @@
- Steps
-
-
-
-
- {{ i.original_text }}
-
-
-
-
-
-
-
-
- x === s),1)">
-
-
-
-
-
-
-
-
-
-
- x === s) +1,0,{ingredients:[], instruction: ''})">
-
-
-
-
-
-
+
@@ -181,12 +137,13 @@
-
+
- Import & View
- Import & Edit
- Import & start new import
+ Import & View
+ Import & Edit
+ Import & start new import
+
@@ -232,14 +189,16 @@
-
Some pages cannot be imported from their URL, the Bookmarklet can be used to import from
- some of them anyway.
+ some of them anyway.
1. Drag the following button to your bookmarks bar Bookmark Text
- 2. Open the page you want to import from
- 3. Click on the bookmark to perform the import
+ :href="makeBookmarklet()">Import into
+ Tandoor
+
+ 2. Open the page you want to import from
+ 3. Click on the bookmark to perform the import
+
@@ -259,11 +218,12 @@ import {BootstrapVue} from 'bootstrap-vue'
import 'bootstrap-vue/dist/bootstrap-vue.css'
-import {resolveDjangoUrl, ResolveUrlMixin, StandardToasts, ToastMixin} from "@/utils/utils";
+import {resolveDjangoStatic, resolveDjangoUrl, ResolveUrlMixin, StandardToasts, ToastMixin} from "@/utils/utils";
import axios from "axios";
import {ApiApiFactory} from "@/utils/openapi/api";
import draggable from "vuedraggable";
import {INTEGRATIONS} from "@/utils/integration";
+import ImportViewStepEditor from "@/apps/ImportView/ImportViewStepEditor";
Vue.use(BootstrapVue)
@@ -274,6 +234,7 @@ export default {
ToastMixin,
],
components: {
+ ImportViewStepEditor,
draggable
},
data() {
@@ -299,39 +260,67 @@ export default {
recipe_app: undefined,
import_duplicates: false,
recipe_files: [],
-
+ // Bookmarklet
+ BOOKMARKLET_CODE: window.BOOKMARKLET_CODE
}
},
mounted() {
let local_storage_recent = JSON.parse(window.localStorage.getItem(this.LS_IMPORT_RECENT))
this.recent_urls = local_storage_recent !== null ? local_storage_recent : []
this.tab_index = 0 //TODO add ability to pass open tab via get parameter
+
+ if (window.BOOKMARKLET_IMPORT_ID !== -1){
+ this.loadRecipe(window.BOOKMARKLET_IMPORT_ID)
+ }
},
methods: {
/**
* Import recipe based on the data configured by the client
+ * @param action: action to perform after import (options are: edit, view, import)
*/
- importRecipe: function () {
+ importRecipe: function (action) {
let apiFactory = new ApiApiFactory()
apiFactory.createRecipe(this.recipe_json).then(response => { // save recipe
let recipe = response.data
apiFactory.imageRecipe(response.data.id, undefined, this.recipe_json.image).then(response => { // save recipe image
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_CREATE)
- window.location = resolveDjangoUrl('edit_recipe', recipe.id)
+ this.afterImportAction(action, recipe)
}).catch(e => {
StandardToasts.makeStandardToast(StandardToasts.FAIL_UPDATE)
- window.location = resolveDjangoUrl('edit_recipe', recipe.id)
+ this.afterImportAction(action, recipe)
})
}).catch(err => {
StandardToasts.makeStandardToast(StandardToasts.FAIL_CREATE)
})
},
+ /**
+ * Action performed after URL import
+ * @param action: action to perform after import
+ * edit: edit imported recipe
+ * view: view imported recipe
+ * import: restart the importer
+ * @param recipe: recipe that was imported
+ */
+ afterImportAction: function (action, recipe) {
+ switch (action) {
+ case 'edit':
+ window.location = resolveDjangoUrl('edit_recipe', recipe.id)
+ break;
+ case 'view':
+ window.location = resolveDjangoUrl('view_recipe', recipe.id)
+ break;
+ case 'import':
+ location.reload();
+ break;
+ }
+ },
/**
* 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 () {
+ loadRecipe: function (bookmarklet) {
console.log(this.website_url)
+ // keep list of recently imported urls
if (this.website_url !== '') {
if (this.recent_urls.length > 5) {
this.recent_urls.pop()
@@ -341,6 +330,47 @@ export default {
}
window.localStorage.setItem(this.LS_IMPORT_RECENT, JSON.stringify(this.recent_urls))
}
+
+ // reset all variables
+ this.recipe_data = undefined
+ this.recipe_json = undefined
+ this.recipe_tree = undefined
+ this.recipe_images = []
+
+ // load recipe
+ let payload = {
+ 'url': this.website_url,
+ 'data': this.source_data,
+ 'auto': this.automatic,
+ 'mode': this.mode
+ }
+
+ if (bookmarklet !== undefined){
+ payload['bookmarklet'] = bookmarklet
+ }
+
+ axios.post(resolveDjangoUrl('api_recipe_from_source'), payload,).then((response) => {
+ this.recipe_json = response.data['recipe_json'];
+
+ this.$set(this.recipe_json, 'unused_keywords', this.recipe_json.keywords.filter(k => k.id === undefined))
+ this.$set(this.recipe_json, 'keywords', this.recipe_json.keywords.filter(k => k.id !== undefined))
+
+ this.recipe_tree = response.data['recipe_tree'];
+ this.recipe_html = response.data['recipe_html'];
+ this.recipe_images = response.data['recipe_images'] !== undefined ? response.data['recipe_images'] : [];
+
+ this.tab_index = 0 // open tab 0 with import wizard
+ this.collapse_visible.options = true // open options collapse
+ }).catch((err) => {
+ StandardToasts.makeStandardToast(StandardToasts.FAIL_FETCH, err.response.data.msg)
+ })
+ },
+ /**
+ * 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
+ */
+ loadBookmarkletRecipe: function () {
+
this.recipe_data = undefined
this.recipe_json = undefined
this.recipe_tree = undefined
@@ -384,72 +414,23 @@ export default {
StandardToasts.makeStandardToast(StandardToasts.FAIL_CREATE)
})
},
- /**
- * utility function used by splitAllSteps and splitStep to split a single step object into multiple step objects
- * @param step: single step
- * @param split_character: character to split steps at
- * @return array of step objects
- */
- splitStepObject: function (step, split_character) {
- let steps = []
- step.instruction.split(split_character).forEach(part => {
- if (part.trim() !== '') {
- steps.push({'instruction': part, 'ingredients': []})
- }
- })
- steps[0].ingredients = step.ingredients // put all ingredients from the original step in the ingredients of the first step of the split step list
- return steps
- },
- /**
- * Splits all steps of a given recipe at the split character (e.g. \n or \n\n)
- * @param split_character: character to split steps at
- */
- splitAllSteps: function (split_character) {
- let steps = []
- this.recipe_json.steps.forEach(step => {
- steps = steps.concat(this.splitStepObject(step, split_character))
- })
- this.recipe_json.steps = steps
- },
- /**
- * Splits the given step at the split character (e.g. \n or \n\n)
- * @param step: step ingredients to split
- * @param split_character: character to split steps at
- */
- splitStep: function (step, split_character) {
- let old_index = this.recipe_json.steps.findIndex(x => x === step)
- let new_steps = this.splitStepObject(step, split_character)
- this.recipe_json.steps.splice(old_index, 1, ...new_steps)
- },
- /**
- * Merge all steps of a given recipe into one
- */
- mergeAllSteps: function () {
- let step = {'instruction': '', 'ingredients': []}
- this.recipe_json.steps.forEach(s => {
- step.instruction += s.instruction + '\n'
- step.ingredients = step.ingredients.concat(s.ingredients)
- })
- this.recipe_json.steps = [step]
- },
- /**
- * Merge two steps (the given and next one)
- */
- mergeStep: function (step) {
- let step_index = this.recipe_json.steps.findIndex(x => x === step)
- let removed_steps = this.recipe_json.steps.splice(step_index, 2)
-
- this.recipe_json.steps.splice(step_index, 0, {
- 'instruction': removed_steps.flatMap(x => x.instruction).join('\n'),
- 'ingredients': removed_steps.flatMap(x => x.ingredients)
- })
- },
/**
* Clear list of recently imported recipe urls
*/
clearRecentImports: function () {
window.localStorage.setItem(this.LS_IMPORT_RECENT, JSON.stringify([]))
this.recent_urls = []
+ },
+ makeBookmarklet: function () {
+ return 'javascript:(function(){' +
+ 'if(window.bookmarkletTandoor!==undefined){' +
+ 'bookmarkletTandoor();' +
+ '} else {' +
+ `localStorage.setItem("importURL", "${localStorage.getItem('BASE_PATH')}${this.resolveDjangoUrl('api:bookmarkletimport-list')}");` +
+ `localStorage.setItem("redirectURL", "${localStorage.getItem('BASE_PATH')}${this.resolveDjangoUrl('data_import_url')}");` +
+ `localStorage.setItem("token", "${window.API_TOKEN}");` +
+ `document.body.appendChild(document.createElement("script")).src="${localStorage.getItem('BASE_PATH')}${resolveDjangoStatic('/js/bookmarklet.js')}?r="+Math.floor(Math.random()*999999999)}` +
+ `})()`
}
}
}
diff --git a/vue/src/apps/ImportView/ImportViewStepEditor.vue b/vue/src/apps/ImportView/ImportViewStepEditor.vue
new file mode 100644
index 00000000..f54002d2
--- /dev/null
+++ b/vue/src/apps/ImportView/ImportViewStepEditor.vue
@@ -0,0 +1,151 @@
+
+
+ Steps
+
+
+
+
+ {{ i.original_text }}
+
+
+
+
+
+
+
+
+ x === s),1)">
+
+
+
+
+
+
+
+
+
+
+ x === s) +1,0,{ingredients:[], instruction: ''})">
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/vue/src/utils/utils.js b/vue/src/utils/utils.js
index 5aad3a25..aff4751c 100644
--- a/vue/src/utils/utils.js
+++ b/vue/src/utils/utils.js
@@ -145,6 +145,27 @@ export function resolveDjangoUrl(url, params = null) {
}
}
+/*
+ * Utility functions to use djangos static files
+ * */
+
+export const StaticMixin = {
+ methods: {
+ /**
+ * access django static files from javascript
+ * @param {string} param path to static file
+ */
+ resolveDjangoStatic: function (param) {
+ return resolveDjangoStatic(param)
+ },
+ },
+}
+
+export function resolveDjangoStatic(param) {
+ let url = localStorage.getItem('STATIC_URL') + param
+ return url.replace('//','/') //replace // with / in case param started with / which resulted in // after the static base url
+}
+
/*
* other utilities
* */