diff --git a/cookbook/forms.py b/cookbook/forms.py
index 83ee6232..37a32638 100644
--- a/cookbook/forms.py
+++ b/cookbook/forms.py
@@ -152,13 +152,14 @@ class ImportExportBase(forms.Form):
OPENEATS = 'OPENEATS'
PLANTOEAT = 'PLANTOEAT'
COOKBOOKAPP = 'COOKBOOKAPP'
+ COPYMETHAT = 'COPYMETHAT'
type = forms.ChoiceField(choices=(
(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'), (RECIPEKEEPER, 'Recipe Keeper'),
- (PLANTOEAT, 'Plantoeat'), (COOKBOOKAPP, 'CookBookApp'),
+ (PLANTOEAT, 'Plantoeat'), (COOKBOOKAPP, 'CookBookApp'), (COPYMETHAT, 'CopyMeThat'),
))
diff --git a/cookbook/integration/copymethat.py b/cookbook/integration/copymethat.py
new file mode 100644
index 00000000..4f4a217e
--- /dev/null
+++ b/cookbook/integration/copymethat.py
@@ -0,0 +1,84 @@
+import re
+from io import BytesIO
+from zipfile import ZipFile
+
+from bs4 import BeautifulSoup
+
+from cookbook.helper.ingredient_parser import IngredientParser
+from cookbook.helper.recipe_html_import import get_recipe_from_source
+from cookbook.helper.recipe_url_import import iso_duration_to_minutes, parse_servings
+from cookbook.integration.integration import Integration
+from cookbook.models import Recipe, Step, Ingredient, Keyword
+from recipes.settings import DEBUG
+
+
+class CopyMeThat(Integration):
+
+ def import_file_name_filter(self, zip_info_object):
+ if DEBUG:
+ print("testing", zip_info_object.filename, zip_info_object.filename == 'recipes.html')
+ return zip_info_object.filename == 'recipes.html'
+
+ def get_recipe_from_file(self, file):
+ # 'file' comes is as a beautifulsoup object
+ recipe = Recipe.objects.create(name=file.find("div", {"id": "name"}).text.strip(), created_by=self.request.user, internal=True, space=self.request.space, )
+
+ for category in file.find_all("span", {"class": "recipeCategory"}):
+ keyword, created = Keyword.objects.get_or_create(name=category.text, space=self.request.space)
+ recipe.keywords.add(keyword)
+
+ try:
+ recipe.servings = parse_servings(file.find("a", {"id": "recipeYield"}).text.strip())
+ recipe.working_time = iso_duration_to_minutes(file.find("span", {"meta": "prepTime"}).text.strip())
+ recipe.waiting_time = iso_duration_to_minutes(file.find("span", {"meta": "cookTime"}).text.strip())
+ recipe.save()
+ except AttributeError:
+ pass
+
+ step = Step.objects.create(instruction='', space=self.request.space, )
+
+ ingredient_parser = IngredientParser(self.request, True)
+ for ingredient in file.find_all("li", {"class": "recipeIngredient"}):
+ if ingredient.text == "":
+ continue
+ amount, unit, ingredient, note = ingredient_parser.parse(ingredient.text.strip())
+ f = ingredient_parser.get_food(ingredient)
+ u = ingredient_parser.get_unit(unit)
+ step.ingredients.add(Ingredient.objects.create(
+ food=f, unit=u, amount=amount, note=note, space=self.request.space,
+ ))
+
+ for s in file.find_all("li", {"class": "instruction"}):
+ if s.text == "":
+ continue
+ step.instruction += s.text.strip() + ' \n\n'
+
+ for s in file.find_all("li", {"class": "recipeNote"}):
+ if s.text == "":
+ continue
+ step.instruction += s.text.strip() + ' \n\n'
+
+ try:
+ if file.find("a", {"id": "original_link"}).text != '':
+ step.instruction += "\n\nImported from: " + file.find("a", {"id": "original_link"}).text
+ step.save()
+ except AttributeError:
+ pass
+
+ 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_="recipeImage").get("src"))), filetype='.jpeg')
+ except Exception as e:
+ print(recipe.name, ': failed to import image ', str(e))
+
+ recipe.save()
+ return recipe
+
+ def split_recipe_file(self, file):
+ soup = BeautifulSoup(file, "html.parser")
+ return soup.find_all("div", {"class": "recipe"})
diff --git a/cookbook/integration/integration.py b/cookbook/integration/integration.py
index d9b80eb9..2e5ac851 100644
--- a/cookbook/integration/integration.py
+++ b/cookbook/integration/integration.py
@@ -5,6 +5,7 @@ import uuid
from io import BytesIO, StringIO
from zipfile import ZipFile, BadZipFile
+from bs4 import Tag
from django.core.exceptions import ObjectDoesNotExist
from django.core.files import File
from django.db import IntegrityError
@@ -16,7 +17,7 @@ from django_scopes import scope
from cookbook.forms import ImportExportBase
from cookbook.helper.image_processing import get_filetype, handle_image
from cookbook.models import Keyword, Recipe
-from recipes.settings import DATABASES, DEBUG
+from recipes.settings import DEBUG
class Integration:
@@ -153,9 +154,17 @@ class Integration:
file_list.append(z)
il.total_recipes += len(file_list)
+ import cookbook
+ if isinstance(self, cookbook.integration.copymethat.CopyMeThat):
+ file_list = self.split_recipe_file(BytesIO(import_zip.read('recipes.html')))
+ il.total_recipes += len(file_list)
+
for z in file_list:
try:
- recipe = self.get_recipe_from_file(BytesIO(import_zip.read(z.filename)))
+ if isinstance(z, Tag):
+ recipe = self.get_recipe_from_file(z)
+ else:
+ 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'
self.handle_duplicates(recipe, import_duplicates)
diff --git a/cookbook/templates/url_import.html b/cookbook/templates/url_import.html
index 6457f876..983f6037 100644
--- a/cookbook/templates/url_import.html
+++ b/cookbook/templates/url_import.html
@@ -76,6 +76,7 @@
+
diff --git a/cookbook/views/import_export.py b/cookbook/views/import_export.py
index 0a8e83e1..ebbef836 100644
--- a/cookbook/views/import_export.py
+++ b/cookbook/views/import_export.py
@@ -11,6 +11,7 @@ from django.utils.translation import gettext as _
from cookbook.forms import ExportForm, ImportForm, ImportExportBase
from cookbook.helper.permission_helper import group_required
from cookbook.integration.cookbookapp import CookBookApp
+from cookbook.integration.copymethat import CopyMeThat
from cookbook.integration.pepperplate import Pepperplate
from cookbook.integration.cheftap import ChefTap
from cookbook.integration.chowdown import Chowdown
@@ -65,6 +66,8 @@ def get_integration(request, export_type):
return Plantoeat(request, export_type)
if export_type == ImportExportBase.COOKBOOKAPP:
return CookBookApp(request, export_type)
+ if export_type == ImportExportBase.COPYMETHAT:
+ return CopyMeThat(request, export_type)
@group_required('user')
diff --git a/docs/features/import_export.md b/docs/features/import_export.md
index d53c30c0..9db829ec 100644
--- a/docs/features/import_export.md
+++ b/docs/features/import_export.md
@@ -37,6 +37,7 @@ Overview of the capabilities of the different integrations.
| OpenEats | ✔️ | ❌ | ⌚ |
| Plantoeat | ✔️ | ❌ | ✔ |
| CookBookApp | ✔️ | ⌚ | ✔️ |
+| CopyMeThat | ✔️ | ❌ | ✔️ |
✔ = implemented, ❌ = not implemented and not possible/planned, ⌚ = not yet implemented
@@ -218,3 +219,7 @@ Plan to eat allows you to export a text file containing all your recipes. Simply
## CookBookApp
CookBookApp can export .zip files containing .html files. Upload the entire ZIP to Tandoor to import all included recipes.
+
+## CopyMeThat
+
+CopyMeThat can export .zip files containing an `.html` file as well as a folder containing all the images. Upload the entire ZIP to Tandoor to import all included recipes.
\ No newline at end of file