diff --git a/cookbook/forms.py b/cookbook/forms.py
index 8cd5d68f..b5fb9866 100644
--- a/cookbook/forms.py
+++ b/cookbook/forms.py
@@ -143,7 +143,7 @@ class ImportExportBase(forms.Form):
NEXTCLOUD = 'NEXTCLOUD'
MEALIE = 'MEALIE'
CHOWDOWN = 'CHOWDOWN'
- SAFRON = 'SAFRON'
+ SAFFRON = 'SAFFRON'
CHEFTAP = 'CHEFTAP'
PEPPERPLATE = 'PEPPERPLATE'
RECIPEKEEPER = 'RECIPEKEEPER'
@@ -156,13 +156,14 @@ class ImportExportBase(forms.Form):
PLANTOEAT = 'PLANTOEAT'
COOKBOOKAPP = 'COOKBOOKAPP'
COPYMETHAT = 'COPYMETHAT'
+ PDF = 'PDF'
type = forms.ChoiceField(choices=(
(DEFAULT, _('Default')), (PAPRIKA, 'Paprika'), (NEXTCLOUD, 'Nextcloud Cookbook'),
- (MEALIE, 'Mealie'), (CHOWDOWN, 'Chowdown'), (SAFRON, 'Safron'), (CHEFTAP, 'ChefTap'),
+ (MEALIE, 'Mealie'), (CHOWDOWN, 'Chowdown'), (SAFFRON, 'Saffron'), (CHEFTAP, 'ChefTap'),
(PEPPERPLATE, 'Pepperplate'), (RECETTETEK, 'RecetteTek'), (RECIPESAGE, 'Recipe Sage'), (DOMESTICA, 'Domestica'),
(MEALMASTER, 'MealMaster'), (REZKONV, 'RezKonv'), (OPENEATS, 'Openeats'), (RECIPEKEEPER, 'Recipe Keeper'),
- (PLANTOEAT, 'Plantoeat'), (COOKBOOKAPP, 'CookBookApp'), (COPYMETHAT, 'CopyMeThat'),
+ (PLANTOEAT, 'Plantoeat'), (COOKBOOKAPP, 'CookBookApp'), (COPYMETHAT, 'CopyMeThat'), (PDF, 'PDF'),
))
diff --git a/cookbook/integration/default.py b/cookbook/integration/default.py
index 1fb16e7f..39c0bc66 100644
--- a/cookbook/integration/default.py
+++ b/cookbook/integration/default.py
@@ -1,5 +1,5 @@
import json
-from io import BytesIO
+from io import BytesIO, StringIO
from re import match
from zipfile import ZipFile
@@ -35,3 +35,28 @@ class Default(Integration):
export = RecipeExportSerializer(recipe).data
return 'recipe.json', JSONRenderer().render(export).decode("utf-8")
+
+ def get_files_from_recipes(self, recipes, cookie):
+ export_zip_stream = BytesIO()
+ export_zip_obj = ZipFile(export_zip_stream, 'w')
+
+ for r in recipes:
+ if r.internal and r.space == self.request.space:
+ recipe_zip_stream = BytesIO()
+ recipe_zip_obj = ZipFile(recipe_zip_stream, 'w')
+
+ recipe_stream = StringIO()
+ filename, data = self.get_file_from_recipe(r)
+ recipe_stream.write(data)
+ recipe_zip_obj.writestr(filename, recipe_stream.getvalue())
+ recipe_stream.close()
+ try:
+ recipe_zip_obj.writestr(f'image{get_filetype(r.image.file.name)}', r.image.file.read())
+ except ValueError:
+ pass
+
+ recipe_zip_obj.close()
+ export_zip_obj.writestr(str(r.pk) + '.zip', recipe_zip_stream.getvalue())
+ export_zip_obj.close()
+
+ return [[ 'export.zip', export_zip_stream.getvalue() ]]
\ No newline at end of file
diff --git a/cookbook/integration/integration.py b/cookbook/integration/integration.py
index cecab4a2..3b8034e3 100644
--- a/cookbook/integration/integration.py
+++ b/cookbook/integration/integration.py
@@ -65,45 +65,32 @@ class Integration:
"""
Perform the export based on a list of recipes
:param recipes: list of recipe objects
- :return: HttpResponse with a ZIP file that is directly downloaded
+ :return: HttpResponse with the file of the requested export format that is directly downloaded (When that format involve multiple files they are zipped together)
"""
- # TODO this is temporary, find a better solution for different export formats when doing other exporters
- if self.export_type != ImportExportBase.RECIPESAGE:
- export_zip_stream = BytesIO()
- export_zip_obj = ZipFile(export_zip_stream, 'w')
+ files = self.get_files_from_recipes(recipes, self.request.COOKIES)
- for r in recipes:
- if r.internal and r.space == self.request.space:
- recipe_zip_stream = BytesIO()
- recipe_zip_obj = ZipFile(recipe_zip_stream, 'w')
+ if len(files) == 1:
+ filename, file = files[0]
+ export_filename = filename
+ export_file = file
- recipe_stream = StringIO()
- filename, data = self.get_file_from_recipe(r)
- recipe_stream.write(data)
- recipe_zip_obj.writestr(filename, recipe_stream.getvalue())
- recipe_stream.close()
- try:
- recipe_zip_obj.writestr(f'image{get_filetype(r.image.file.name)}', r.image.file.read())
- except ValueError:
- pass
-
- recipe_zip_obj.close()
- export_zip_obj.writestr(str(r.pk) + '.zip', recipe_zip_stream.getvalue())
-
- export_zip_obj.close()
-
- response = HttpResponse(export_zip_stream.getvalue(), content_type='application/force-download')
- response['Content-Disposition'] = 'attachment; filename="export.zip"'
- return response
else:
- json_list = []
- for r in recipes:
- json_list.append(self.get_file_from_recipe(r))
+ export_filename = "export.zip"
+ export_stream = BytesIO()
+ export_obj = ZipFile(export_stream, 'w')
+
+ for filename, file in files:
+ export_obj.writestr(filename, file)
+
+ export_obj.close()
+ export_file = export_stream.getvalue()
+
+
+ response = HttpResponse(export_file, content_type='application/force-download')
+ response['Content-Disposition'] = 'attachment; filename="'+export_filename+'"'
+ return response
- response = HttpResponse(json.dumps(json_list), content_type='application/force-download')
- response['Content-Disposition'] = 'attachment; filename="recipes.json"'
- return response
def import_file_name_filter(self, zip_info_object):
"""
@@ -275,6 +262,17 @@ class Integration:
"""
raise NotImplementedError('Method not implemented in integration')
+
+ def get_files_from_recipes(self, recipes, cookie):
+ """
+ Takes a list of recipe object and converts it to a array containing each file.
+ Each file is represented as an array [filename, data] where data is a string of the content of the file.
+ :param recipe: Recipe object that should be converted
+ :returns:
+ [[filename, data], ...]
+ """
+ raise NotImplementedError('Method not implemented in integration')
+
@staticmethod
def handle_exception(exception, log=None, message=''):
if log:
diff --git a/cookbook/integration/pdfexport.py b/cookbook/integration/pdfexport.py
new file mode 100644
index 00000000..61571da0
--- /dev/null
+++ b/cookbook/integration/pdfexport.py
@@ -0,0 +1,62 @@
+import json
+from io import BytesIO
+from re import match
+from zipfile import ZipFile
+import asyncio
+from pyppeteer import launch
+
+from rest_framework.renderers import JSONRenderer
+
+from cookbook.helper.image_processing import get_filetype
+from cookbook.integration.integration import Integration
+from cookbook.serializer import RecipeExportSerializer
+
+import django.core.management.commands.runserver as runserver
+
+
+class PDFexport(Integration):
+
+ def get_recipe_from_file(self, file):
+ raise NotImplementedError('Method not implemented in storage integration')
+
+
+
+
+
+ async def get_files_from_recipes_async(self, recipes, cookie):
+ cmd = runserver.Command()
+
+ browser = await launch(
+ handleSIGINT=False,
+ handleSIGTERM=False,
+ handleSIGHUP=False,
+ ignoreHTTPSErrors=True
+ )
+
+ cookies = {'domain': cmd.default_addr, 'name': 'sessionid', 'value': cookie['sessionid'],}
+ options = { 'format': 'letter',
+ 'margin': {
+ 'top': '0.75in',
+ 'bottom': '0.75in',
+ 'left': '0.75in',
+ 'right': '0.75in',
+ }
+ }
+
+ page = await browser.newPage()
+ await page.emulateMedia('print')
+ await page.setCookie(cookies)
+
+ files = []
+ for recipe in recipes:
+ await page.goto('http://'+cmd.default_addr+':'+cmd.default_port+'/view/recipe/'+str(recipe.id), {'waitUntil': 'networkidle0',})
+ files.append([ recipe.name+'.pdf', await page.pdf(options) ])
+
+
+ await browser.close()
+ return files
+
+
+
+ def get_files_from_recipes(self, recipes, cookie):
+ return asyncio.run(self.get_files_from_recipes_async(recipes, cookie))
diff --git a/cookbook/integration/recipesage.py b/cookbook/integration/recipesage.py
index 9c5f70ac..0ca32194 100644
--- a/cookbook/integration/recipesage.py
+++ b/cookbook/integration/recipesage.py
@@ -88,5 +88,12 @@ class RecipeSage(Integration):
return data
+ def get_files_from_recipes(self, recipes, cookie):
+ json_list = []
+ for r in recipes:
+ json_list.append(self.get_file_from_recipe(r))
+
+ return [['export.json', json.dumps(json_list)]]
+
def split_recipe_file(self, file):
return json.loads(file.read().decode("utf-8"))
diff --git a/cookbook/integration/safron.py b/cookbook/integration/saffron.py
similarity index 68%
rename from cookbook/integration/safron.py
rename to cookbook/integration/saffron.py
index fa7a793e..16a93a0c 100644
--- a/cookbook/integration/safron.py
+++ b/cookbook/integration/saffron.py
@@ -5,7 +5,7 @@ from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Ingredient
-class Safron(Integration):
+class Saffron(Integration):
def get_recipe_from_file(self, file):
ingredient_mode = False
@@ -58,4 +58,39 @@ class Safron(Integration):
return recipe
def get_file_from_recipe(self, recipe):
- raise NotImplementedError('Method not implemented in storage integration')
+
+ data = "Title: "+recipe.name if recipe.name else ""+"\n"
+ data += "Description: "+recipe.description if recipe.description else ""+"\n"
+ data += "Source: \n"
+ data += "Original URL: \n"
+ data += "Yield: "+str(recipe.servings)+"\n"
+ data += "Cookbook: \n"
+ data += "Section: \n"
+ data += "Image: \n"
+
+ recipeInstructions = []
+ recipeIngredient = []
+ for s in recipe.steps.all():
+ if s.type != Step.TIME:
+ recipeInstructions.append(s.instruction)
+
+ for i in s.ingredients.all():
+ recipeIngredient.append(f'{float(i.amount)} {i.unit} {i.food}')
+
+ data += "Ingredients: \n"
+ for ingredient in recipeIngredient:
+ data += ingredient+"\n"
+
+ data += "Instructions: \n"
+ for instruction in recipeInstructions:
+ data += instruction+"\n"
+
+ return recipe.name+'.txt', data
+
+ def get_files_from_recipes(self, recipes, cookie):
+ files = []
+ for r in recipes:
+ filename, data = self.get_file_from_recipe(r)
+ files.append([ filename, data ])
+
+ return files
\ No newline at end of file
diff --git a/cookbook/views/import_export.py b/cookbook/views/import_export.py
index ebbef836..83a9e7c9 100644
--- a/cookbook/views/import_export.py
+++ b/cookbook/views/import_export.py
@@ -27,7 +27,8 @@ from cookbook.integration.recipekeeper import RecipeKeeper
from cookbook.integration.recettetek import RecetteTek
from cookbook.integration.recipesage import RecipeSage
from cookbook.integration.rezkonv import RezKonv
-from cookbook.integration.safron import Safron
+from cookbook.integration.saffron import Saffron
+from cookbook.integration.pdfexport import PDFexport
from cookbook.models import Recipe, ImportLog, UserPreference
@@ -42,8 +43,8 @@ def get_integration(request, export_type):
return Mealie(request, export_type)
if export_type == ImportExportBase.CHOWDOWN:
return Chowdown(request, export_type)
- if export_type == ImportExportBase.SAFRON:
- return Safron(request, export_type)
+ if export_type == ImportExportBase.SAFFRON:
+ return Saffron(request, export_type)
if export_type == ImportExportBase.CHEFTAP:
return ChefTap(request, export_type)
if export_type == ImportExportBase.PEPPERPLATE:
@@ -68,6 +69,8 @@ def get_integration(request, export_type):
return CookBookApp(request, export_type)
if export_type == ImportExportBase.COPYMETHAT:
return CopyMeThat(request, export_type)
+ if export_type == ImportExportBase.PDF:
+ return PDFexport(request, export_type)
@group_required('user')
diff --git a/requirements.txt b/requirements.txt
index 4a8a3374..f6d9633e 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -43,3 +43,4 @@ django-hCaptcha==0.1.0
python-ldap==3.4.0
django-auth-ldap==4.0.0
pytest-factoryboy==2.1.0
+pyppeteer==0.2.6
diff --git a/vue/src/apps/RecipeView/RecipeView.vue b/vue/src/apps/RecipeView/RecipeView.vue
index 0000d491..7cf1932a 100644
--- a/vue/src/apps/RecipeView/RecipeView.vue
+++ b/vue/src/apps/RecipeView/RecipeView.vue
@@ -276,4 +276,9 @@ export default {
}
-
+