Merge pull request #1211 from TiagoRascazzi/develop

Added Saffron and PDF export format
This commit is contained in:
vabene1111 2022-01-07 10:31:13 +01:00 committed by GitHub
commit 0a6abf9688
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 180 additions and 43 deletions

View File

@ -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'),
))

View File

@ -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() ]]

View File

@ -65,46 +65,33 @@ 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')
response = HttpResponse(json.dumps(json_list), content_type='application/force-download')
response['Content-Disposition'] = 'attachment; filename="recipes.json"'
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
def import_file_name_filter(self, zip_info_object):
"""
Since zipfile.namelist() returns all files in all subdirectories this function allows filtering of files
@ -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:

View File

@ -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))

View File

@ -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"))

View File

@ -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

View File

@ -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')

View File

@ -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

View File

@ -276,4 +276,9 @@ export default {
}
</script>
<style></style>
<style>
#app > div > div{
break-inside: avoid;
}
</style>