From 2565ab30a4da5add16a95732dec4f7ccdfd4554e Mon Sep 17 00:00:00 2001 From: vabene1111 Date: Sat, 5 Mar 2022 15:16:58 +0100 Subject: [PATCH] lots of improvements and bookmarklet import working again --- cookbook/templates/test.html | 5 +- cookbook/templatetags/custom_tags.py | 6 +- cookbook/views/api.py | 9 +- cookbook/views/data.py | 94 +------- cookbook/views/views.py | 13 +- vue/src/apps/ImportView/ImportView.vue | 223 ++++++++---------- .../apps/ImportView/ImportViewStepEditor.vue | 151 ++++++++++++ vue/src/utils/utils.js | 21 ++ 8 files changed, 305 insertions(+), 217 deletions(-) create mode 100644 vue/src/apps/ImportView/ImportViewStepEditor.vue 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 -
-
- All - all -
-
-
-
- - {{ i.original_text }} - - -
-
- - - - - - - - - - - - - - - - - - - - -
-
+ @@ -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 @@ + + + + + \ 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 * */