diff --git a/cookbook/forms.py b/cookbook/forms.py index b21eb6ce7..4f966f00a 100644 --- a/cookbook/forms.py +++ b/cookbook/forms.py @@ -159,6 +159,7 @@ class ImportExportBase(forms.Form): PLANTOEAT = 'PLANTOEAT' COOKBOOKAPP = 'COOKBOOKAPP' COPYMETHAT = 'COPYMETHAT' + COOKMATE = 'COOKMATE' PDF = 'PDF' type = forms.ChoiceField(choices=( @@ -167,6 +168,7 @@ class ImportExportBase(forms.Form): (PEPPERPLATE, 'Pepperplate'), (RECETTETEK, 'RecetteTek'), (RECIPESAGE, 'Recipe Sage'), (DOMESTICA, 'Domestica'), (MEALMASTER, 'MealMaster'), (REZKONV, 'RezKonv'), (OPENEATS, 'Openeats'), (RECIPEKEEPER, 'Recipe Keeper'), (PLANTOEAT, 'Plantoeat'), (COOKBOOKAPP, 'CookBookApp'), (COPYMETHAT, 'CopyMeThat'), (PDF, 'PDF'), (MELARECIPES, 'Melarecipes'), + (COOKMATE, 'Cookmate') )) diff --git a/cookbook/integration/cookmate.py b/cookbook/integration/cookmate.py new file mode 100644 index 000000000..19c0e936e --- /dev/null +++ b/cookbook/integration/cookmate.py @@ -0,0 +1,77 @@ +import base64 +import json +from io import BytesIO + +from gettext import gettext as _ + +import requests +from lxml import etree + +from cookbook.helper.ingredient_parser import IngredientParser +from cookbook.helper.recipe_url_import import parse_servings, parse_time, parse_servings_text +from cookbook.integration.integration import Integration +from cookbook.models import Ingredient, Keyword, Recipe, Step + + +class Cookmate(Integration): + + def import_file_name_filter(self, zip_info_object): + return zip_info_object.filename.endswith('.xml') + + def get_files_from_recipes(self, recipes, el, cookie): + raise NotImplementedError('Method not implemented in storage integration') + + def get_recipe_from_file(self, file): + recipe_xml = file + + recipe = Recipe.objects.create( + name=recipe_xml.find('title').text.strip(), + created_by=self.request.user, internal=True, space=self.request.space) + + if recipe_xml.find('preptime') is not None: + recipe.working_time = parse_time(recipe_xml.find('preptime').text.strip()) + + if recipe_xml.find('cooktime') is not None: + recipe.waiting_time = parse_time(recipe_xml.find('cooktime').text.strip()) + + if recipe_xml.find('quantity') is not None: + recipe.servings = parse_servings(recipe_xml.find('quantity').text.strip()) + recipe.servings_text = parse_servings_text(recipe_xml.find('quantity').text.strip()) + + if recipe_xml.find('url') is not None: + recipe.source_url = recipe_xml.find('url').text.strip() + + if recipe_xml.find('description') is not None: # description is a list of
  • 's with text + if len(recipe_xml.find('description')) > 0: + recipe.description = recipe_xml.find('description')[0].text[:512] + + for step in recipe_xml.find('recipetext').getchildren(): + step = Step.objects.create( + instruction=step.text.strip(), space=self.request.space, + ) + recipe.steps.add(step) + + ingredient_parser = IngredientParser(self.request, True) + + for ingredient in recipe_xml.find('ingredient').getchildren(): + if ingredient.text.strip() != '': + amount, unit, food, note = ingredient_parser.parse(ingredient.text.strip()) + f = ingredient_parser.get_food(food) + u = ingredient_parser.get_unit(unit) + recipe.steps.first().ingredients.add(Ingredient.objects.create( + food=f, unit=u, amount=amount, note=note, original_text=ingredient.text.strip(), space=self.request.space, + )) + + if recipe_xml.find('imageurl') is not None: + try: + response = requests.get(recipe_xml.find('imageurl').text.strip()) + self.import_recipe_image(recipe, BytesIO(response.content)) + except Exception as e: + print('failed to import image ', str(e)) + + recipe.save() + + 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 65ed00aef..6493c266d 100644 --- a/cookbook/integration/integration.py +++ b/cookbook/integration/integration.py @@ -5,6 +5,8 @@ import traceback import uuid from io import BytesIO, StringIO from zipfile import BadZipFile, ZipFile + +import lxml from django.core.cache import cache import datetime @@ -16,6 +18,7 @@ from django.http import HttpResponse from django.utils.formats import date_format from django.utils.translation import gettext as _ from django_scopes import scope +from lxml import etree from cookbook.forms import ImportExportBase from cookbook.helper.image_processing import get_filetype, handle_image @@ -144,7 +147,7 @@ class Integration: il.imported_recipes += 1 il.save() import_zip.close() - elif '.zip' in f['name'] or '.paprikarecipes' in f['name']: + elif '.zip' in f['name'] or '.paprikarecipes' in f['name'] or '.mcb' in f['name']: import_zip = ZipFile(f['file']) file_list = [] for z in import_zip.filelist: @@ -157,9 +160,16 @@ class Integration: file_list = self.split_recipe_file(BytesIO(import_zip.read('recipes.html'))) il.total_recipes += len(file_list) + if isinstance(self, cookbook.integration.cookmate.Cookmate): + new_file_list = [] + for file in file_list: + new_file_list += etree.parse(BytesIO(import_zip.read(file.filename))).getroot().getchildren() + il.total_recipes = len(new_file_list) + file_list = new_file_list + for z in file_list: try: - if isinstance(z, Tag): + if not hasattr(z, 'filename'): recipe = self.get_recipe_from_file(z) else: recipe = self.get_recipe_from_file(BytesIO(import_zip.read(z.filename))) diff --git a/cookbook/views/import_export.py b/cookbook/views/import_export.py index 7875fa832..daa55d432 100644 --- a/cookbook/views/import_export.py +++ b/cookbook/views/import_export.py @@ -15,6 +15,7 @@ from cookbook.helper.recipe_search import RecipeSearch from cookbook.integration.cheftap import ChefTap from cookbook.integration.chowdown import Chowdown from cookbook.integration.cookbookapp import CookBookApp +from cookbook.integration.cookmate import Cookmate from cookbook.integration.copymethat import CopyMeThat from cookbook.integration.default import Default from cookbook.integration.domestica import Domestica @@ -77,6 +78,8 @@ def get_integration(request, export_type): return PDFexport(request, export_type) if export_type == ImportExportBase.MELARECIPES: return MelaRecipes(request, export_type) + if export_type == ImportExportBase.COOKMATE: + return Cookmate(request, export_type) @group_required('user') diff --git a/docs/features/import_export.md b/docs/features/import_export.md index 18ead2820..85c3b1229 100644 --- a/docs/features/import_export.md +++ b/docs/features/import_export.md @@ -38,7 +38,8 @@ Overview of the capabilities of the different integrations. | Plantoeat | ✔️ | ❌ | ✔ | | CookBookApp | ✔️ | ⌚ | ✔️ | | CopyMeThat | ✔️ | ❌ | ✔️ | -| CopyMeThat | ✔️ | ⌚ | ✔️ | +| Melarecipes | ✔️ | ⌚ | ✔️ | +| Cookmate | ✔️ | ⌚ | ✔️ | | PDF (experimental) | ⌚️ | ✔️ | ✔️ | ✔️ = implemented, ❌ = not implemented and not possible/planned, ⌚ = not yet implemented diff --git a/vue/src/utils/integration.js b/vue/src/utils/integration.js index 94ab94693..a2e6db1bb 100644 --- a/vue/src/utils/integration.js +++ b/vue/src/utils/integration.js @@ -5,6 +5,7 @@ export const INTEGRATIONS = [ {id: 'CHEFTAP', name: "Cheftap", import: true, export: false}, {id: 'CHOWDOWN', name: "Chowdown", import: true, export: false}, {id: 'COOKBOOKAPP', name: "CookBookApp", import: true, export: false}, + {id: 'COOKMATE', name: "Cookmate", import: true, export: false}, {id: 'COPYMETHAT', name: "CopyMeThat", import: true, export: false}, {id: 'DOMESTICA', name: "Domestica", import: true, export: false}, {id: 'MEALIE', name: "Mealie", import: true, export: false},