added recipe sage and domestica imports
This commit is contained in:
parent
738f0781b2
commit
7f8e29f1bc
@ -112,13 +112,15 @@ class ImportExportBase(forms.Form):
|
|||||||
SAFRON = 'SAFRON'
|
SAFRON = 'SAFRON'
|
||||||
CHEFTAP = 'CHEFTAP'
|
CHEFTAP = 'CHEFTAP'
|
||||||
PEPPERPLATE = 'PEPPERPLATE'
|
PEPPERPLATE = 'PEPPERPLATE'
|
||||||
|
RECIPESAGE = 'RECIPESAGE'
|
||||||
|
DOMESTICA = 'DOMESTICA'
|
||||||
|
|
||||||
type = forms.ChoiceField(choices=(
|
type = forms.ChoiceField(choices=(
|
||||||
(DEFAULT, _('Default')), (PAPRIKA, 'Paprika'), (NEXTCLOUD, 'Nextcloud Cookbook'),
|
(DEFAULT, _('Default')), (PAPRIKA, 'Paprika'), (NEXTCLOUD, 'Nextcloud Cookbook'),
|
||||||
(MEALIE, 'Mealie'), (CHOWDOWN, 'Chowdown'), (SAFRON, 'Safron'), (CHEFTAP, 'ChefTap'),
|
(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):
|
class ImportForm(ImportExportBase):
|
||||||
|
51
cookbook/integration/domestica.py
Normal file
51
cookbook/integration/domestica.py
Normal file
@ -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')
|
@ -1,4 +1,5 @@
|
|||||||
import datetime
|
import datetime
|
||||||
|
import json
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from io import BytesIO, StringIO
|
from io import BytesIO, StringIO
|
||||||
@ -19,6 +20,7 @@ class Integration:
|
|||||||
request = None
|
request = None
|
||||||
keyword = None
|
keyword = None
|
||||||
files = None
|
files = None
|
||||||
|
ignored_recipes = []
|
||||||
|
|
||||||
def __init__(self, request, export_type):
|
def __init__(self, request, export_type):
|
||||||
"""
|
"""
|
||||||
@ -89,7 +91,6 @@ class Integration:
|
|||||||
self.keyword.name = _('Import') + ' ' + str(il.pk)
|
self.keyword.name = _('Import') + ' ' + str(il.pk)
|
||||||
self.keyword.save()
|
self.keyword.save()
|
||||||
|
|
||||||
ignored_recipes = []
|
|
||||||
try:
|
try:
|
||||||
self.files = files
|
self.files = files
|
||||||
for f in files:
|
for f in files:
|
||||||
@ -100,38 +101,41 @@ class Integration:
|
|||||||
recipe = self.get_recipe_from_file(BytesIO(import_zip.read(z.filename)))
|
recipe = self.get_recipe_from_file(BytesIO(import_zip.read(z.filename)))
|
||||||
recipe.keywords.add(self.keyword)
|
recipe.keywords.add(self.keyword)
|
||||||
il.msg += f'{recipe.pk} - {recipe.name} \n'
|
il.msg += f'{recipe.pk} - {recipe.name} \n'
|
||||||
if not import_duplicates:
|
self.handle_duplicates(recipe, import_duplicates)
|
||||||
if duplicate := self.is_duplicate(recipe):
|
|
||||||
ignored_recipes.append(duplicate)
|
|
||||||
import_zip.close()
|
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:
|
else:
|
||||||
recipe = self.get_recipe_from_file(f['file'])
|
recipe = self.get_recipe_from_file(f['file'])
|
||||||
recipe.keywords.add(self.keyword)
|
recipe.keywords.add(self.keyword)
|
||||||
il.msg += f'{recipe.pk} - {recipe.name} \n'
|
il.msg += f'{recipe.pk} - {recipe.name} \n'
|
||||||
if not import_duplicates:
|
self.handle_duplicates(recipe, import_duplicates)
|
||||||
if duplicate := self.is_duplicate(recipe):
|
|
||||||
ignored_recipes.append(duplicate)
|
|
||||||
except BadZipFile:
|
except BadZipFile:
|
||||||
il.msg += 'ERROR ' + _('Importer expected a .zip file. Did you choose the correct importer type for your data ?') + '\n'
|
il.msg += 'ERROR ' + _('Importer expected a .zip file. Did you choose the correct importer type for your data ?') + '\n'
|
||||||
|
|
||||||
if len(ignored_recipes) > 0:
|
if len(self.ignored_recipes) > 0:
|
||||||
il.msg += '\n' + _('The following recipes were ignored because they already existed:') + ' ' + ', '.join(ignored_recipes) + '\n\n'
|
il.msg += '\n' + _('The following recipes were ignored because they already existed:') + ' ' + ', '.join(self.ignored_recipes) + '\n\n'
|
||||||
|
|
||||||
il.keyword = self.keyword
|
il.keyword = self.keyword
|
||||||
il.msg += (_('Imported %s recipes.') % Recipe.objects.filter(keywords=self.keyword).count()) + '\n'
|
il.msg += (_('Imported %s recipes.') % Recipe.objects.filter(keywords=self.keyword).count()) + '\n'
|
||||||
il.running = False
|
il.running = False
|
||||||
il.save()
|
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
|
Checks if a recipe is already present, if so deletes it
|
||||||
:param recipe: Recipe object
|
: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()
|
recipe.delete()
|
||||||
return recipe.name
|
self.ignored_recipes.append(recipe.name)
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def import_recipe_image(recipe, image_file):
|
def import_recipe_image(recipe, image_file):
|
||||||
|
@ -1,17 +1,11 @@
|
|||||||
import base64
|
import base64
|
||||||
|
import gzip
|
||||||
import json
|
import json
|
||||||
import re
|
|
||||||
from io import BytesIO
|
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.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.integration.integration import Integration
|
||||||
from cookbook.models import Recipe, Step, Food, Ingredient, Unit
|
from cookbook.models import Recipe, Step, Ingredient
|
||||||
import gzip
|
|
||||||
|
|
||||||
|
|
||||||
class Paprika(Integration):
|
class Paprika(Integration):
|
||||||
|
61
cookbook/integration/recipesage.py
Normal file
61
cookbook/integration/recipesage.py
Normal file
@ -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')
|
@ -14,9 +14,11 @@ from cookbook.integration.Pepperplate import Pepperplate
|
|||||||
from cookbook.integration.cheftap import ChefTap
|
from cookbook.integration.cheftap import ChefTap
|
||||||
from cookbook.integration.chowdown import Chowdown
|
from cookbook.integration.chowdown import Chowdown
|
||||||
from cookbook.integration.default import Default
|
from cookbook.integration.default import Default
|
||||||
|
from cookbook.integration.domestica import Domestica
|
||||||
from cookbook.integration.mealie import Mealie
|
from cookbook.integration.mealie import Mealie
|
||||||
from cookbook.integration.nextcloud_cookbook import NextcloudCookbook
|
from cookbook.integration.nextcloud_cookbook import NextcloudCookbook
|
||||||
from cookbook.integration.paprika import Paprika
|
from cookbook.integration.paprika import Paprika
|
||||||
|
from cookbook.integration.recipesage import RecipeSage
|
||||||
from cookbook.integration.safron import Safron
|
from cookbook.integration.safron import Safron
|
||||||
from cookbook.models import Recipe, ImportLog
|
from cookbook.models import Recipe, ImportLog
|
||||||
|
|
||||||
@ -38,6 +40,10 @@ def get_integration(request, export_type):
|
|||||||
return ChefTap(request, export_type)
|
return ChefTap(request, export_type)
|
||||||
if export_type == ImportExportBase.PEPPERPLATE:
|
if export_type == ImportExportBase.PEPPERPLATE:
|
||||||
return Pepperplate(request, export_type)
|
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')
|
@group_required('user')
|
||||||
|
@ -30,6 +30,8 @@ Overview of the capabilities of the different integrations.
|
|||||||
| Paprika | ✔️ | ⌚ | ✔️ |
|
| Paprika | ✔️ | ⌚ | ✔️ |
|
||||||
| ChefTap | ✔️ | ❌ | ❌️ |
|
| ChefTap | ✔️ | ❌ | ❌️ |
|
||||||
| Pepperplate | ✔️ | ⌚ | ❌️ |
|
| Pepperplate | ✔️ | ⌚ | ❌️ |
|
||||||
|
| RecipeSage | ✔️ | ⌚ | ✔️ |
|
||||||
|
| Domestica | ✔️ | ⌚ | ✔️ |
|
||||||
|
|
||||||
✔ = implemented, ❌ = not implemented and not possible/planned, ⌚ = not yet implemented
|
✔ = 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
|
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.
|
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
|
## Nextcloud
|
||||||
Importing recipes from Nextcloud cookbook is very easy and since Nextcloud Cookbook provides nice, standardized and
|
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.
|
structured information most of your recipe is going to be intact.
|
||||||
|
Loading…
Reference in New Issue
Block a user