From 7f8e29f1bc32702af0dd96ecd54aa81f1175b770 Mon Sep 17 00:00:00 2001 From: vabene1111 Date: Sun, 28 Mar 2021 18:58:37 +0200 Subject: [PATCH] added recipe sage and domestica imports --- cookbook/forms.py | 6 ++- cookbook/integration/domestica.py | 51 ++++++++++++++++++++++++ cookbook/integration/integration.py | 32 ++++++++------- cookbook/integration/paprika.py | 10 +---- cookbook/integration/recipesage.py | 61 +++++++++++++++++++++++++++++ cookbook/views/import_export.py | 6 +++ docs/features/import_export.md | 10 +++++ 7 files changed, 152 insertions(+), 24 deletions(-) create mode 100644 cookbook/integration/domestica.py create mode 100644 cookbook/integration/recipesage.py diff --git a/cookbook/forms.py b/cookbook/forms.py index 76f8ae93..29f3c9c2 100644 --- a/cookbook/forms.py +++ b/cookbook/forms.py @@ -112,13 +112,15 @@ class ImportExportBase(forms.Form): SAFRON = 'SAFRON' CHEFTAP = 'CHEFTAP' PEPPERPLATE = 'PEPPERPLATE' + RECIPESAGE = 'RECIPESAGE' + DOMESTICA = 'DOMESTICA' type = forms.ChoiceField(choices=( (DEFAULT, _('Default')), (PAPRIKA, 'Paprika'), (NEXTCLOUD, 'Nextcloud Cookbook'), (MEALIE, 'Mealie'), (CHOWDOWN, 'Chowdown'), (SAFRON, 'Safron'), (CHEFTAP, 'ChefTap'), - (PEPPERPLATE, 'Pepperplate'), + (PEPPERPLATE, 'Pepperplate'), (RECIPESAGE, 'Recipe Sage'), (DOMESTICA, 'Domestica'), )) - duplicates = forms.BooleanField(help_text=_('To prevent duplicates recipes with the same name as existing ones are ignored. Check this box to import everything.')) + duplicates = forms.BooleanField(help_text=_('To prevent duplicates recipes with the same name as existing ones are ignored. Check this box to import everything.'), required=False) class ImportForm(ImportExportBase): diff --git a/cookbook/integration/domestica.py b/cookbook/integration/domestica.py new file mode 100644 index 00000000..ed6ecb9e --- /dev/null +++ b/cookbook/integration/domestica.py @@ -0,0 +1,51 @@ +import base64 +from io import BytesIO + +from cookbook.helper.ingredient_parser import parse, get_food, get_unit +from cookbook.integration.integration import Integration +from cookbook.models import Recipe, Step, Ingredient + + +class Domestica(Integration): + + def get_recipe_from_file(self, file): + + recipe = Recipe.objects.create( + name=file['name'].strip(), + created_by=self.request.user, internal=True, + space=self.request.space) + + if file['servings'] != '': + recipe.servings = file['servings'] + + if file['timeCook'] != '': + recipe.waiting_time = file['timeCook'] + + if file['timePrep'] != '': + recipe.working_time = file['timePrep'] + + recipe.save() + + step = Step.objects.create( + instruction=file['directions'] + ) + + if file['source'] != '': + step.instruction += '\n' + file['source'] + + for ingredient in file['ingredients'].split('\n'): + amount, unit, ingredient, note = parse(ingredient) + f = get_food(ingredient, self.request.space) + u = get_unit(unit, self.request.space) + step.ingredients.add(Ingredient.objects.create( + food=f, unit=u, amount=amount, note=note + )) + recipe.steps.add(step) + + if file['image'] != '': + self.import_recipe_image(recipe, BytesIO(base64.b64decode(file['image'].replace('data:image/jpeg;base64,', '')))) + + return recipe + + def get_file_from_recipe(self, recipe): + raise NotImplementedError('Method not implemented in storage integration') diff --git a/cookbook/integration/integration.py b/cookbook/integration/integration.py index 9650029d..f7ad45c7 100644 --- a/cookbook/integration/integration.py +++ b/cookbook/integration/integration.py @@ -1,4 +1,5 @@ import datetime +import json import uuid from io import BytesIO, StringIO @@ -19,6 +20,7 @@ class Integration: request = None keyword = None files = None + ignored_recipes = [] def __init__(self, request, export_type): """ @@ -89,7 +91,6 @@ class Integration: self.keyword.name = _('Import') + ' ' + str(il.pk) self.keyword.save() - ignored_recipes = [] try: self.files = files for f in files: @@ -100,38 +101,41 @@ class Integration: recipe = self.get_recipe_from_file(BytesIO(import_zip.read(z.filename))) recipe.keywords.add(self.keyword) il.msg += f'{recipe.pk} - {recipe.name} \n' - if not import_duplicates: - if duplicate := self.is_duplicate(recipe): - ignored_recipes.append(duplicate) + self.handle_duplicates(recipe, import_duplicates) + import_zip.close() + elif '.json' in f['name']: + json_data = json.loads(f['file'].read().decode("utf-8")) + for d in json_data: + recipe = self.get_recipe_from_file(d) + recipe.keywords.add(self.keyword) + il.msg += f'{recipe.pk} - {recipe.name} \n' + self.handle_duplicates(recipe, import_duplicates) else: recipe = self.get_recipe_from_file(f['file']) recipe.keywords.add(self.keyword) il.msg += f'{recipe.pk} - {recipe.name} \n' - if not import_duplicates: - if duplicate := self.is_duplicate(recipe): - ignored_recipes.append(duplicate) + self.handle_duplicates(recipe, import_duplicates) except BadZipFile: il.msg += 'ERROR ' + _('Importer expected a .zip file. Did you choose the correct importer type for your data ?') + '\n' - if len(ignored_recipes) > 0: - il.msg += '\n' + _('The following recipes were ignored because they already existed:') + ' ' + ', '.join(ignored_recipes) + '\n\n' + if len(self.ignored_recipes) > 0: + il.msg += '\n' + _('The following recipes were ignored because they already existed:') + ' ' + ', '.join(self.ignored_recipes) + '\n\n' il.keyword = self.keyword il.msg += (_('Imported %s recipes.') % Recipe.objects.filter(keywords=self.keyword).count()) + '\n' il.running = False il.save() - def is_duplicate(self, recipe): + def handle_duplicates(self, recipe, import_duplicates): """ Checks if a recipe is already present, if so deletes it :param recipe: Recipe object + :param import_duplicates: if duplicates should be imported """ - if Recipe.objects.filter(space=self.request.space, name=recipe.name).count() > 1: + if Recipe.objects.filter(space=self.request.space, name=recipe.name).count() > 1 and not import_duplicates: recipe.delete() - return recipe.name - else: - return None + self.ignored_recipes.append(recipe.name) @staticmethod def import_recipe_image(recipe, image_file): diff --git a/cookbook/integration/paprika.py b/cookbook/integration/paprika.py index b9dab105..d4856ea8 100644 --- a/cookbook/integration/paprika.py +++ b/cookbook/integration/paprika.py @@ -1,17 +1,11 @@ import base64 +import gzip import json -import re from io import BytesIO -from zipfile import ZipFile - -import microdata -from bs4 import BeautifulSoup from cookbook.helper.ingredient_parser import parse, get_food, get_unit -from cookbook.helper.recipe_url_import import find_recipe_json from cookbook.integration.integration import Integration -from cookbook.models import Recipe, Step, Food, Ingredient, Unit -import gzip +from cookbook.models import Recipe, Step, Ingredient class Paprika(Integration): diff --git a/cookbook/integration/recipesage.py b/cookbook/integration/recipesage.py new file mode 100644 index 00000000..c2aa7d55 --- /dev/null +++ b/cookbook/integration/recipesage.py @@ -0,0 +1,61 @@ +import base64 +from io import BytesIO + +import requests + +from cookbook.helper.ingredient_parser import parse, get_food, get_unit +from cookbook.integration.integration import Integration +from cookbook.models import Recipe, Step, Ingredient + + +class RecipeSage(Integration): + + def get_recipe_from_file(self, file): + + recipe = Recipe.objects.create( + name=file['name'].strip(), + created_by=self.request.user, internal=True, + space=self.request.space) + + try: + if file['recipeYield'] != '': + recipe.servings = int(file['recipeYield']) + + if file['totalTime'] != '': + recipe.waiting_time = int(file['totalTime']) - int(file['timePrep']) + + if file['prepTime'] != '': + recipe.working_time = int(file['timePrep']) + + recipe.save() + except Exception as e: + print('failed to parse yield or time ', str(e)) + + ingredients_added = False + for s in file['recipeInstructions']: + step = Step.objects.create( + instruction=s['text'] + ) + if not ingredients_added: + ingredients_added = True + + for ingredient in file['recipeIngredient']: + amount, unit, ingredient, note = parse(ingredient) + f = get_food(ingredient, self.request.space) + u = get_unit(unit, self.request.space) + step.ingredients.add(Ingredient.objects.create( + food=f, unit=u, amount=amount, note=note + )) + recipe.steps.add(step) + + if len(file['image']) > 0: + try: + response = requests.get(file['image'][0]) + self.import_recipe_image(recipe, BytesIO(response.content)) + except Exception as e: + print('failed to import image ', str(e)) + + return recipe + + def get_file_from_recipe(self, recipe): + raise NotImplementedError('Method not implemented in storage integration') diff --git a/cookbook/views/import_export.py b/cookbook/views/import_export.py index 06398d6a..3de51ea3 100644 --- a/cookbook/views/import_export.py +++ b/cookbook/views/import_export.py @@ -14,9 +14,11 @@ from cookbook.integration.Pepperplate import Pepperplate from cookbook.integration.cheftap import ChefTap from cookbook.integration.chowdown import Chowdown from cookbook.integration.default import Default +from cookbook.integration.domestica import Domestica from cookbook.integration.mealie import Mealie from cookbook.integration.nextcloud_cookbook import NextcloudCookbook from cookbook.integration.paprika import Paprika +from cookbook.integration.recipesage import RecipeSage from cookbook.integration.safron import Safron from cookbook.models import Recipe, ImportLog @@ -38,6 +40,10 @@ def get_integration(request, export_type): return ChefTap(request, export_type) if export_type == ImportExportBase.PEPPERPLATE: return Pepperplate(request, export_type) + if export_type == ImportExportBase.DOMESTICA: + return Domestica(request, export_type) + if export_type == ImportExportBase.RECIPESAGE: + return RecipeSage(request, export_type) @group_required('user') diff --git a/docs/features/import_export.md b/docs/features/import_export.md index 934f7208..9c0c866d 100644 --- a/docs/features/import_export.md +++ b/docs/features/import_export.md @@ -30,6 +30,8 @@ Overview of the capabilities of the different integrations. | Paprika | ✔️ | ⌚ | ✔️ | | ChefTap | ✔️ | ❌ | ❌️ | | Pepperplate | ✔️ | ⌚ | ❌️ | +| RecipeSage | ✔️ | ⌚ | ✔️ | +| Domestica | ✔️ | ⌚ | ✔️ | ✔ = implemented, ❌ = not implemented and not possible/planned, ⌚ = not yet implemented @@ -40,6 +42,14 @@ It is maintained with new fields added and contains all data to transfer your re It is also one of the few recipe formats that is actually structured in a way that allows for easy machine readability if you want to use the data for any other purpose. +## RecipeSage +Go to Settings > Export Recipe Data and select `EXPORT AS JSON-LD (BEST)`. Then simply upload the exported file +to Tandoor. + +## Domestica +Go to Import/Export and select `Export Recipes`. Then simply upload the exported file +to Tandoor. + ## Nextcloud Importing recipes from Nextcloud cookbook is very easy and since Nextcloud Cookbook provides nice, standardized and structured information most of your recipe is going to be intact.