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 { } - +