diff --git a/cookbook/forms.py b/cookbook/forms.py index cc8c45fa..81fa057f 100644 --- a/cookbook/forms.py +++ b/cookbook/forms.py @@ -132,9 +132,10 @@ class ShoppingForm(forms.Form): class ImportExportBase(forms.Form): - DEFAULT = 'Default' + DEFAULT = 'DEFAULT' + PAPRIKA = 'PAPRIKA' - type = forms.ChoiceField(choices=((DEFAULT, _('Default')),)) + type = forms.ChoiceField(choices=((DEFAULT, _('Default')), (PAPRIKA, _('Paprika')),)) class ImportForm(ImportExportBase): diff --git a/cookbook/helper/recipe_url_import.py b/cookbook/helper/recipe_url_import.py index 71f1649a..bb7ec81e 100644 --- a/cookbook/helper/recipe_url_import.py +++ b/cookbook/helper/recipe_url_import.py @@ -31,7 +31,7 @@ def get_from_html(html_text, url): if ('@type' in ld_json_item and ld_json_item['@type'] == 'Recipe'): - return find_recipe_json(ld_json_item, url) + return JsonResponse(find_recipe_json(ld_json_item, url)) except JSONDecodeError: return JsonResponse( { @@ -45,7 +45,7 @@ def get_from_html(html_text, url): for i in items: md_json = json.loads(i.json()) if 'schema.org/Recipe' in str(md_json['type']): - return find_recipe_json(md_json['properties'], url) + return JsonResponse(find_recipe_json(md_json['properties'], url)) return JsonResponse( { @@ -173,7 +173,8 @@ def find_recipe_json(ld_json, url): else: ld_json['recipeInstructions'] = '' - ld_json['recipeInstructions'] += '\n\n' + _('Imported from') + ' ' + url + if url != '': + ld_json['recipeInstructions'] += '\n\n' + _('Imported from') + ' ' + url if 'image' in ld_json: # check if list of images is returned, take first if so @@ -232,4 +233,4 @@ def find_recipe_json(ld_json, url): ]: ld_json.pop(key, None) - return JsonResponse(ld_json) + return ld_json diff --git a/cookbook/integration/integration.py b/cookbook/integration/integration.py index 231efdcd..014bd3d9 100644 --- a/cookbook/integration/integration.py +++ b/cookbook/integration/integration.py @@ -2,12 +2,14 @@ import datetime import uuid from io import BytesIO, StringIO -from zipfile import ZipFile +from zipfile import ZipFile, BadZipFile +from django.contrib import messages from django.core.files import File from django.http import HttpResponseRedirect, HttpResponse from django.urls import reverse - +from django.utils.formats import date_format +from django.utils.translation import gettext as _ from cookbook.models import Keyword @@ -22,8 +24,8 @@ class Integration: """ self.request = request self.keyword = Keyword.objects.create( - name=f'Import {datetime.datetime.now()}', - description=f'Imported by {request.user.get_user_name()} on {datetime.datetime.now()}', + name=f'Import {date_format(datetime.datetime.now(), "DATETIME_FORMAT")}.{datetime.datetime.now().strftime("%S")}', + description=f'Imported by {request.user.get_user_name()} at {date_format(datetime.datetime.now(), "DATETIME_FORMAT")}', icon='📥' ) @@ -67,11 +69,18 @@ class Integration: :param files: List of in memory files :return: HttpResponseRedirect to the recipe search showing all imported recipes """ - for f in files: - import_zip = ZipFile(f.file) - for z in import_zip.namelist(): - recipe = self.get_recipe_from_file(BytesIO(import_zip.read(z))) - recipe.keywords.add(self.keyword) + try: + for f in files: + if '.zip' in f.name: + import_zip = ZipFile(f.file) + for z in import_zip.namelist(): + recipe = self.get_recipe_from_file(BytesIO(import_zip.read(z))) + recipe.keywords.add(self.keyword) + else: + recipe = self.get_recipe_from_file(f.file) + recipe.keywords.add(self.keyword) + except BadZipFile: + messages.add_message(self.request, messages.ERROR, _('Importer expected a .zip file. Did you choose the correct importer type for your data ?')) return HttpResponseRedirect(reverse('view_search') + '?keywords=' + str(self.keyword.pk)) diff --git a/cookbook/integration/paprika.py b/cookbook/integration/paprika.py new file mode 100644 index 00000000..2b3a8b26 --- /dev/null +++ b/cookbook/integration/paprika.py @@ -0,0 +1,36 @@ +import json + +import microdata + +from cookbook.helper.recipe_url_import import find_recipe_json +from cookbook.integration.integration import Integration +from cookbook.models import Recipe, Step, Food, Ingredient, Unit + + +class Paprika(Integration): + + def get_file_from_recipe(self, recipe): + raise NotImplementedError('Method not implemented in storage integration') + + def get_recipe_from_file(self, file): + html_text = file.getvalue().decode("utf-8") + + items = microdata.get_items(html_text) + for i in items: + md_json = json.loads(i.json()) + if 'schema.org/Recipe' in str(md_json['type']): + recipe_json = find_recipe_json(md_json['properties'], '') + recipe = Recipe.objects.create(name=recipe_json['name'].strip(), created_by=self.request.user, internal=True) + step = Step.objects.create( + instruction=recipe_json['recipeInstructions'] + ) + + for ingredient in recipe_json['recipeIngredient']: + f, created = Food.objects.get_or_create(name=ingredient['ingredient']['text']) + u, created = Unit.objects.get_or_create(name=ingredient['unit']['text']) + step.ingredients.add(Ingredient.objects.create( + food=f, unit=u, amount=ingredient['amount'], note=ingredient['note'] + )) + + recipe.steps.add(step) + return recipe diff --git a/cookbook/views/import_export.py b/cookbook/views/import_export.py index e99d4af7..b668970a 100644 --- a/cookbook/views/import_export.py +++ b/cookbook/views/import_export.py @@ -1,15 +1,21 @@ import re +from django.contrib import messages from django.shortcuts import render +from django.utils.translation import gettext as _ -from cookbook.forms import ExportForm, ExportForm, ImportForm +from cookbook.forms import ExportForm, ImportForm, ImportExportBase from cookbook.helper.permission_helper import group_required from cookbook.integration.default import Default +from cookbook.integration.paprika import Paprika from cookbook.models import Recipe def get_integration(request, export_type): - return Default(request) + if export_type == ImportExportBase.DEFAULT: + return Default(request) + if export_type == ImportExportBase.PAPRIKA: + return Paprika(request) @group_required('user') @@ -17,8 +23,11 @@ def import_recipe(request): if request.method == "POST": form = ImportForm(request.POST, request.FILES) if form.is_valid(): - integration = Default(request) - return integration.do_import(request.FILES.getlist('files')) + try: + integration = get_integration(request, form.cleaned_data['type']) + return integration.do_import(request.FILES.getlist('files')) + except NotImplementedError: + messages.add_message(request, messages.ERROR, _('Importing is not implemented for this provider')) else: form = ImportForm() @@ -30,8 +39,12 @@ def export_recipe(request): if request.method == "POST": form = ExportForm(request.POST) if form.is_valid(): - integration = Default(request) - return integration.do_export(form.cleaned_data['recipes']) + try: + integration = get_integration(request, form.cleaned_data['type']) + return integration.do_export(form.cleaned_data['recipes']) + except NotImplementedError: + messages.add_message(request, messages.ERROR, _('Exporting is not implemented for this provider')) + else: form = ExportForm() recipe = request.GET.get('r') diff --git a/cookbook/views/views.py b/cookbook/views/views.py index 693979d3..bb122d70 100644 --- a/cookbook/views/views.py +++ b/cookbook/views/views.py @@ -1,6 +1,6 @@ import os import re -from datetime import datetime, timedelta +from datetime import datetime from uuid import UUID from django.conf import settings @@ -11,7 +11,7 @@ from django.contrib.auth.password_validation import validate_password from django.core.exceptions import ValidationError from django.db import IntegrityError from django.db.models import Avg, Q -from django.http import HttpResponseRedirect, FileResponse, HttpResponse +from django.http import HttpResponseRedirect from django.shortcuts import get_object_or_404, render, redirect from django.urls import reverse, reverse_lazy from django.utils import timezone @@ -22,9 +22,8 @@ from rest_framework.authtoken.models import Token from cookbook.filters import RecipeFilter from cookbook.forms import (CommentForm, Recipe, RecipeBookEntryForm, User, UserCreateForm, UserNameForm, UserPreference, - UserPreferenceForm, ImportForm, ImportForm, ExportForm) + UserPreferenceForm) from cookbook.helper.permission_helper import group_required, share_link_valid, has_group_permission -from cookbook.integration.default import Default from cookbook.models import (Comment, CookLog, InviteLink, MealPlan, RecipeBook, RecipeBookEntry, ViewLog, ShoppingList) from cookbook.tables import (CookLogTable, RecipeTable, RecipeTableSmall, diff --git a/docs/features/import_export.md b/docs/features/import_export.md index d9453990..2250fcb9 100644 --- a/docs/features/import_export.md +++ b/docs/features/import_export.md @@ -19,4 +19,13 @@ The default integration is the build in (and preferred) way to import and export It is maintained with new fields added and contains all data to transfer your recipes from one installation to another. 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. \ No newline at end of file +easy machine readability if you want to use the data for any other purpose. + +## Paprika +Paprika can create two types of export. The first is a proprietary `.paprikarecipes` file in some kind of binarized format. +The second one is HTML files containing at least a bit of microdata. + +If you want to import your Paprika recipes create a html export. Then import the individual recipes HTML files. +Due to the lack of structure not all fields can be imported. +Even tough images are present in the export they cannot be imported atm. This is technically possible and might be +added in the future. \ No newline at end of file