Merge pull request #1211 from TiagoRascazzi/develop
Added Saffron and PDF export format
This commit is contained in:
commit
0a6abf9688
@ -143,7 +143,7 @@ class ImportExportBase(forms.Form):
|
|||||||
NEXTCLOUD = 'NEXTCLOUD'
|
NEXTCLOUD = 'NEXTCLOUD'
|
||||||
MEALIE = 'MEALIE'
|
MEALIE = 'MEALIE'
|
||||||
CHOWDOWN = 'CHOWDOWN'
|
CHOWDOWN = 'CHOWDOWN'
|
||||||
SAFRON = 'SAFRON'
|
SAFFRON = 'SAFFRON'
|
||||||
CHEFTAP = 'CHEFTAP'
|
CHEFTAP = 'CHEFTAP'
|
||||||
PEPPERPLATE = 'PEPPERPLATE'
|
PEPPERPLATE = 'PEPPERPLATE'
|
||||||
RECIPEKEEPER = 'RECIPEKEEPER'
|
RECIPEKEEPER = 'RECIPEKEEPER'
|
||||||
@ -156,13 +156,14 @@ class ImportExportBase(forms.Form):
|
|||||||
PLANTOEAT = 'PLANTOEAT'
|
PLANTOEAT = 'PLANTOEAT'
|
||||||
COOKBOOKAPP = 'COOKBOOKAPP'
|
COOKBOOKAPP = 'COOKBOOKAPP'
|
||||||
COPYMETHAT = 'COPYMETHAT'
|
COPYMETHAT = 'COPYMETHAT'
|
||||||
|
PDF = 'PDF'
|
||||||
|
|
||||||
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'), (SAFFRON, 'Saffron'), (CHEFTAP, 'ChefTap'),
|
||||||
(PEPPERPLATE, 'Pepperplate'), (RECETTETEK, 'RecetteTek'), (RECIPESAGE, 'Recipe Sage'), (DOMESTICA, 'Domestica'),
|
(PEPPERPLATE, 'Pepperplate'), (RECETTETEK, 'RecetteTek'), (RECIPESAGE, 'Recipe Sage'), (DOMESTICA, 'Domestica'),
|
||||||
(MEALMASTER, 'MealMaster'), (REZKONV, 'RezKonv'), (OPENEATS, 'Openeats'), (RECIPEKEEPER, 'Recipe Keeper'),
|
(MEALMASTER, 'MealMaster'), (REZKONV, 'RezKonv'), (OPENEATS, 'Openeats'), (RECIPEKEEPER, 'Recipe Keeper'),
|
||||||
(PLANTOEAT, 'Plantoeat'), (COOKBOOKAPP, 'CookBookApp'), (COPYMETHAT, 'CopyMeThat'),
|
(PLANTOEAT, 'Plantoeat'), (COOKBOOKAPP, 'CookBookApp'), (COPYMETHAT, 'CopyMeThat'), (PDF, 'PDF'),
|
||||||
))
|
))
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import json
|
import json
|
||||||
from io import BytesIO
|
from io import BytesIO, StringIO
|
||||||
from re import match
|
from re import match
|
||||||
from zipfile import ZipFile
|
from zipfile import ZipFile
|
||||||
|
|
||||||
@ -35,3 +35,28 @@ class Default(Integration):
|
|||||||
export = RecipeExportSerializer(recipe).data
|
export = RecipeExportSerializer(recipe).data
|
||||||
|
|
||||||
return 'recipe.json', JSONRenderer().render(export).decode("utf-8")
|
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() ]]
|
@ -65,45 +65,32 @@ class Integration:
|
|||||||
"""
|
"""
|
||||||
Perform the export based on a list of recipes
|
Perform the export based on a list of recipes
|
||||||
:param recipes: list of recipe objects
|
: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
|
files = self.get_files_from_recipes(recipes, self.request.COOKIES)
|
||||||
if self.export_type != ImportExportBase.RECIPESAGE:
|
|
||||||
export_zip_stream = BytesIO()
|
|
||||||
export_zip_obj = ZipFile(export_zip_stream, 'w')
|
|
||||||
|
|
||||||
for r in recipes:
|
if len(files) == 1:
|
||||||
if r.internal and r.space == self.request.space:
|
filename, file = files[0]
|
||||||
recipe_zip_stream = BytesIO()
|
export_filename = filename
|
||||||
recipe_zip_obj = ZipFile(recipe_zip_stream, 'w')
|
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:
|
else:
|
||||||
json_list = []
|
export_filename = "export.zip"
|
||||||
for r in recipes:
|
export_stream = BytesIO()
|
||||||
json_list.append(self.get_file_from_recipe(r))
|
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):
|
def import_file_name_filter(self, zip_info_object):
|
||||||
"""
|
"""
|
||||||
@ -275,6 +262,17 @@ class Integration:
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError('Method not implemented in 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
|
@staticmethod
|
||||||
def handle_exception(exception, log=None, message=''):
|
def handle_exception(exception, log=None, message=''):
|
||||||
if log:
|
if log:
|
||||||
|
62
cookbook/integration/pdfexport.py
Normal file
62
cookbook/integration/pdfexport.py
Normal 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))
|
@ -88,5 +88,12 @@ class RecipeSage(Integration):
|
|||||||
|
|
||||||
return data
|
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):
|
def split_recipe_file(self, file):
|
||||||
return json.loads(file.read().decode("utf-8"))
|
return json.loads(file.read().decode("utf-8"))
|
||||||
|
@ -5,7 +5,7 @@ from cookbook.integration.integration import Integration
|
|||||||
from cookbook.models import Recipe, Step, Ingredient
|
from cookbook.models import Recipe, Step, Ingredient
|
||||||
|
|
||||||
|
|
||||||
class Safron(Integration):
|
class Saffron(Integration):
|
||||||
|
|
||||||
def get_recipe_from_file(self, file):
|
def get_recipe_from_file(self, file):
|
||||||
ingredient_mode = False
|
ingredient_mode = False
|
||||||
@ -58,4 +58,39 @@ class Safron(Integration):
|
|||||||
return recipe
|
return recipe
|
||||||
|
|
||||||
def get_file_from_recipe(self, 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
|
@ -27,7 +27,8 @@ from cookbook.integration.recipekeeper import RecipeKeeper
|
|||||||
from cookbook.integration.recettetek import RecetteTek
|
from cookbook.integration.recettetek import RecetteTek
|
||||||
from cookbook.integration.recipesage import RecipeSage
|
from cookbook.integration.recipesage import RecipeSage
|
||||||
from cookbook.integration.rezkonv import RezKonv
|
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
|
from cookbook.models import Recipe, ImportLog, UserPreference
|
||||||
|
|
||||||
|
|
||||||
@ -42,8 +43,8 @@ def get_integration(request, export_type):
|
|||||||
return Mealie(request, export_type)
|
return Mealie(request, export_type)
|
||||||
if export_type == ImportExportBase.CHOWDOWN:
|
if export_type == ImportExportBase.CHOWDOWN:
|
||||||
return Chowdown(request, export_type)
|
return Chowdown(request, export_type)
|
||||||
if export_type == ImportExportBase.SAFRON:
|
if export_type == ImportExportBase.SAFFRON:
|
||||||
return Safron(request, export_type)
|
return Saffron(request, export_type)
|
||||||
if export_type == ImportExportBase.CHEFTAP:
|
if export_type == ImportExportBase.CHEFTAP:
|
||||||
return ChefTap(request, export_type)
|
return ChefTap(request, export_type)
|
||||||
if export_type == ImportExportBase.PEPPERPLATE:
|
if export_type == ImportExportBase.PEPPERPLATE:
|
||||||
@ -68,6 +69,8 @@ def get_integration(request, export_type):
|
|||||||
return CookBookApp(request, export_type)
|
return CookBookApp(request, export_type)
|
||||||
if export_type == ImportExportBase.COPYMETHAT:
|
if export_type == ImportExportBase.COPYMETHAT:
|
||||||
return CopyMeThat(request, export_type)
|
return CopyMeThat(request, export_type)
|
||||||
|
if export_type == ImportExportBase.PDF:
|
||||||
|
return PDFexport(request, export_type)
|
||||||
|
|
||||||
|
|
||||||
@group_required('user')
|
@group_required('user')
|
||||||
|
@ -43,3 +43,4 @@ django-hCaptcha==0.1.0
|
|||||||
python-ldap==3.4.0
|
python-ldap==3.4.0
|
||||||
django-auth-ldap==4.0.0
|
django-auth-ldap==4.0.0
|
||||||
pytest-factoryboy==2.1.0
|
pytest-factoryboy==2.1.0
|
||||||
|
pyppeteer==0.2.6
|
||||||
|
@ -276,4 +276,9 @@ export default {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style></style>
|
<style>
|
||||||
|
#app > div > div{
|
||||||
|
break-inside: avoid;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
Loading…
Reference in New Issue
Block a user