diff --git a/.idea/dictionaries/vabene1111_PC.xml b/.idea/dictionaries/vabene1111_PC.xml index da49b1e4a..bb97e302a 100644 --- a/.idea/dictionaries/vabene1111_PC.xml +++ b/.idea/dictionaries/vabene1111_PC.xml @@ -7,6 +7,7 @@ gunicorn ical mealie + pepperplate safron traefik diff --git a/cookbook/forms.py b/cookbook/forms.py index 3e1e9bc57..76f8ae936 100644 --- a/cookbook/forms.py +++ b/cookbook/forms.py @@ -111,11 +111,14 @@ class ImportExportBase(forms.Form): CHOWDOWN = 'CHOWDOWN' SAFRON = 'SAFRON' CHEFTAP = 'CHEFTAP' + PEPPERPLATE = 'PEPPERPLATE' type = forms.ChoiceField(choices=( (DEFAULT, _('Default')), (PAPRIKA, 'Paprika'), (NEXTCLOUD, 'Nextcloud Cookbook'), (MEALIE, 'Mealie'), (CHOWDOWN, 'Chowdown'), (SAFRON, 'Safron'), (CHEFTAP, 'ChefTap'), + (PEPPERPLATE, 'Pepperplate'), )) + duplicates = forms.BooleanField(help_text=_('To prevent duplicates recipes with the same name as existing ones are ignored. Check this box to import everything.')) class ImportForm(ImportExportBase): diff --git a/cookbook/integration/Pepperplate.py b/cookbook/integration/Pepperplate.py new file mode 100644 index 000000000..57b4aa67f --- /dev/null +++ b/cookbook/integration/Pepperplate.py @@ -0,0 +1,58 @@ +import json +import re +from io import BytesIO +from zipfile import ZipFile + +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 Pepperplate(Integration): + + def get_recipe_from_file(self, file): + ingredient_mode = False + direction_mode = False + + ingredients = [] + directions = [] + for fl in file.readlines(): + line = fl.decode("utf-8") + if 'Title:' in line: + title = line.replace('Title:', '').replace('"', '').strip() + if 'Description:' in line: + description = line.replace('Description:', '').strip() + if 'Original URL:' in line or 'Source:' in line or 'Yield:' in line or 'Total:' in line: + if len(line.strip().split(':')[1]) > 0: + directions.append(line.strip() + '\n') + if ingredient_mode: + if len(line) > 2 and 'Instructions:' not in line: + ingredients.append(line.strip()) + if direction_mode: + if len(line) > 2: + directions.append(line.strip() + '\n') + if 'Ingredients:' in line: + ingredient_mode = True + if 'Instructions:' in line: + ingredient_mode = False + direction_mode = True + + recipe = Recipe.objects.create(name=title, description=description, created_by=self.request.user, internal=True, space=self.request.space) + + step = Step.objects.create( + instruction='\n'.join(directions) + '\n\n' + ) + + for ingredient in ingredients: + 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) + + 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 95cfbafb4..9650029da 100644 --- a/cookbook/integration/integration.py +++ b/cookbook/integration/integration.py @@ -77,9 +77,10 @@ class Integration: """ return True - def do_import(self, files, il): + def do_import(self, files, il, import_duplicates): """ Imports given files + :param import_duplicates: if true duplicates are imported as well :param files: List of in memory files :param il: Import Log object to refresh while running :return: HttpResponseRedirect to the recipe search showing all imported recipes @@ -99,15 +100,17 @@ 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 duplicate := self.is_duplicate(recipe): - ignored_recipes.append(duplicate) + if not import_duplicates: + if duplicate := self.is_duplicate(recipe): + ignored_recipes.append(duplicate) import_zip.close() else: recipe = self.get_recipe_from_file(f['file']) recipe.keywords.add(self.keyword) il.msg += f'{recipe.pk} - {recipe.name} \n' - if duplicate := self.is_duplicate(recipe): - ignored_recipes.append(duplicate) + if not import_duplicates: + if duplicate := self.is_duplicate(recipe): + ignored_recipes.append(duplicate) except BadZipFile: il.msg += 'ERROR ' + _('Importer expected a .zip file. Did you choose the correct importer type for your data ?') + '\n' diff --git a/cookbook/views/import_export.py b/cookbook/views/import_export.py index 3dd0815e0..06398d6ac 100644 --- a/cookbook/views/import_export.py +++ b/cookbook/views/import_export.py @@ -10,6 +10,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.Pepperplate import Pepperplate from cookbook.integration.cheftap import ChefTap from cookbook.integration.chowdown import Chowdown from cookbook.integration.default import Default @@ -35,6 +36,8 @@ def get_integration(request, export_type): return Safron(request, export_type) if export_type == ImportExportBase.CHEFTAP: return ChefTap(request, export_type) + if export_type == ImportExportBase.PEPPERPLATE: + return Pepperplate(request, export_type) @group_required('user') @@ -49,7 +52,7 @@ def import_recipe(request): files = [] for f in request.FILES.getlist('files'): files.append({'file': BytesIO(f.read()), 'name': f.name}) - t = threading.Thread(target=integration.do_import, args=[files, il]) + t = threading.Thread(target=integration.do_import, args=[files, il, form['duplicates']]) t.setDaemon(True) t.start() diff --git a/docs/features/import_export.md b/docs/features/import_export.md index 5f78103a5..934f72084 100644 --- a/docs/features/import_export.md +++ b/docs/features/import_export.md @@ -29,6 +29,7 @@ Overview of the capabilities of the different integrations. | Safron | ✔️ | ⌚ | ❌ | | Paprika | ✔️ | ⌚ | ✔️ | | ChefTap | ✔️ | ❌ | ❌️ | +| Pepperplate | ✔️ | ⌚ | ❌️ | ✔ = implemented, ❌ = not implemented and not possible/planned, ⌚ = not yet implemented @@ -111,6 +112,13 @@ A Paprika export contains a folder with a html representation of your recipes an The `.paprikarecipes` file is basically just a zip with gzipped contents. Simply upload the whole file and import all your recipes. +## Pepperplate +Pepperplate provides a `.zip` files contain all your recipes as `.txt` files. These files are well-structured and allow +the import of all data without loosing anything. + +Simply export the recipes from Pepperplate and upload the zip to Tandoor. Images are not included in the export and +thus cannot be imported. + ## ChefTap ChefTaps allows you to export your recipes from the app (I think). The export is a zip file containing a folder called `recipes` which in turn contains `.txt` files with your recipes.