diff --git a/cookbook/forms.py b/cookbook/forms.py index 23171d29..f4f37e08 100644 --- a/cookbook/forms.py +++ b/cookbook/forms.py @@ -121,6 +121,7 @@ class ImportExportBase(forms.Form): SAFRON = 'SAFRON' CHEFTAP = 'CHEFTAP' PEPPERPLATE = 'PEPPERPLATE' + RECIPEKEEPER = 'RECIPEKEEPER' RECETTETEK = 'RECETTETEK' RECIPESAGE = 'RECIPESAGE' DOMESTICA = 'DOMESTICA' @@ -132,7 +133,8 @@ class ImportExportBase(forms.Form): (DEFAULT, _('Default')), (PAPRIKA, 'Paprika'), (NEXTCLOUD, 'Nextcloud Cookbook'), (MEALIE, 'Mealie'), (CHOWDOWN, 'Chowdown'), (SAFRON, 'Safron'), (CHEFTAP, 'ChefTap'), (PEPPERPLATE, 'Pepperplate'), (RECETTETEK, 'RecetteTek'), (RECIPESAGE, 'Recipe Sage'), (DOMESTICA, 'Domestica'), - (MEALMASTER, 'MealMaster'), (REZKONV, 'RezKonv'), (OPENEATS, 'Openeats'), + (MEALMASTER, 'MealMaster'), (REZKONV, 'RezKonv'), (OPENEATS, 'Openeats'), (RECIPEKEEPER, 'Recipe Keeper'), + )) diff --git a/cookbook/integration/integration.py b/cookbook/integration/integration.py index 5a8063bd..ef24372a 100644 --- a/cookbook/integration/integration.py +++ b/cookbook/integration/integration.py @@ -104,7 +104,18 @@ class Integration: try: self.files = files for f in files: - if '.zip' in f['name'] or '.paprikarecipes' in f['name']: + if 'RecipeKeeper' in f['name']: + import_zip = ZipFile(f['file']) + for z in import_zip.filelist: + if self.import_file_name_filter(z): + data_list = self.split_recipe_file(import_zip.read(z.filename).decode('utf-8')) + for d in data_list: + 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) + import_zip.close() + elif '.zip' in f['name'] or '.paprikarecipes' in f['name']: import_zip = ZipFile(f['file']) for z in import_zip.filelist: if self.import_file_name_filter(z): diff --git a/cookbook/integration/recipekeeper.py b/cookbook/integration/recipekeeper.py new file mode 100644 index 00000000..dc207916 --- /dev/null +++ b/cookbook/integration/recipekeeper.py @@ -0,0 +1,81 @@ +import re +from bs4 import BeautifulSoup +from io import BytesIO +from zipfile import ZipFile + +from django.utils.translation import gettext as _ + +from cookbook.helper.ingredient_parser import parse, get_food, get_unit +from cookbook.integration.integration import Integration +from cookbook.models import Recipe, Step, Food, Unit, Ingredient, Keyword + + +class RecipeKeeper(Integration): + + def import_file_name_filter(self, zip_info_object): + return re.match(r'^recipes.html$', zip_info_object.filename) + + def split_recipe_file(self, file): + recipe_html = BeautifulSoup(file, 'html.parser') + return recipe_html.find_all('div', class_='recipe-details') + + def get_recipe_from_file(self, file): + # 'file' comes is as a beautifulsoup object + recipe = Recipe.objects.create(name=file.find("h2", {"itemprop": "name"}).text.strip(), created_by=self.request.user, internal=True, space=self.request.space, ) + + # add 'Courses' and 'Categories' as keywords + for course in file.find_all("span", {"itemprop": "recipeCourse"}): + keyword, created = Keyword.objects.get_or_create(name=course.text, space=self.request.space) + recipe.keywords.add(keyword) + + for category in file.find_all("meta", {"itemprop": "recipeCategory"}): + keyword, created = Keyword.objects.get_or_create(name=category.get("content"), space=self.request.space) + recipe.keywords.add(keyword) + + # TODO: import prep and cook times + # Recipe Keeper uses ISO 8601 format for its duration periods. + + source_url_added = False + ingredients_added = False + for s in file.find("div", {"itemprop": "recipeDirections"}).find_all("p"): + + if s.text == "": + continue + + step = Step.objects.create( + instruction=s.text + ) + + if not source_url_added: + # If there is a source URL, add it to the first step field. + if file.find("span", {"itemprop": "recipeSource"}).text != '': + step.instruction += "\n\nImported from: " + file.find("span", {"itemprop": "recipeSource"}).text + step.save() + source_url_added = True + + if not ingredients_added: + ingredients_added = True + for ingredient in file.find("div", {"itemprop": "recipeIngredients"}).findChildren("p"): + if ingredient.text == "": + continue + amount, unit, ingredient, note = parse(ingredient.text.strip()) + 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) + + # import the Primary recipe image that is stored in the Zip + try: + for f in self.files: + if '.zip' in f['name']: + import_zip = ZipFile(f['file']) + self.import_recipe_image(recipe, BytesIO(import_zip.read(file.find("img", class_="recipe-photo").get("src")))) + except Exception as e: + pass + + 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 3482f2b7..8c907071 100644 --- a/cookbook/views/import_export.py +++ b/cookbook/views/import_export.py @@ -20,6 +20,7 @@ from cookbook.integration.mealmaster import MealMaster from cookbook.integration.nextcloud_cookbook import NextcloudCookbook from cookbook.integration.openeats import OpenEats from cookbook.integration.paprika import Paprika +from cookbook.integration.recipekeeper import RecipeKeeper from cookbook.integration.recettetek import RecetteTek from cookbook.integration.recipesage import RecipeSage from cookbook.integration.rezkonv import RezKonv @@ -46,6 +47,8 @@ def get_integration(request, export_type): return Pepperplate(request, export_type) if export_type == ImportExportBase.DOMESTICA: return Domestica(request, export_type) + if export_type == ImportExportBase.RECIPEKEEPER: + return RecipeKeeper(request, export_type) if export_type == ImportExportBase.RECETTETEK: return RecetteTek(request, export_type) if export_type == ImportExportBase.RECIPESAGE: