diff --git a/cookbook/forms.py b/cookbook/forms.py index bacf21240..17d92865b 100644 --- a/cookbook/forms.py +++ b/cookbook/forms.py @@ -153,6 +153,20 @@ class ImportForm(forms.Form): ) +class ImportExportBase(forms.Form): + DEFAULT = 'Default' + + type = forms.ChoiceField(choices=((DEFAULT, _('Default')),)) + + +class NewImportForm(ImportExportBase): + files = forms.FileField(required=True, widget=forms.ClearableFileInput(attrs={'multiple': True})) + + +class NewExportForm(ImportExportBase): + recipes = forms.ModelMultipleChoiceField(queryset=Recipe.objects.filter(internal=True).all(), widget=MultiSelectWidget) + + class UnitMergeForm(forms.Form): prefix = 'unit' diff --git a/cookbook/integration/default.py b/cookbook/integration/default.py new file mode 100644 index 000000000..fb91377ef --- /dev/null +++ b/cookbook/integration/default.py @@ -0,0 +1,43 @@ +import json +import os +from zipfile import ZipFile + +from rest_framework.renderers import JSONRenderer + +from cookbook.integration.integration import Integration +from cookbook.serializer import RecipeExportSerializer + + +class Default(Integration): + + def do_export(self, recipes): + path = self.get_tmp_dir_path() + export_zip_obj = ZipFile(os.path.join(path, 'export.zip'), 'w') + + for r in recipes: + if r.internal: + base_path = os.path.join(path, str(r.pk)) + os.makedirs(base_path, exist_ok=True) + recipe_zip_obj = ZipFile(base_path + '.zip', 'w') + + f = open(os.path.join(path, str(r.pk), 'recipe.json'), "w", encoding="utf-8") + f.write(self.get_export(r)) + recipe_zip_obj.write(f.name) + recipe_zip_obj.write(r.image.path) + f.close() + + recipe_zip_obj.close() + export_zip_obj.write(recipe_zip_obj.filename) + + export_zip_obj.close() + return export_zip_obj.filename + + def get_recipe(self, string): + data = json.loads(string) + + return RecipeExportSerializer(data=data, context={'request': self.request}) + + def get_export(self, recipe): + export = RecipeExportSerializer(recipe).data + + return JSONRenderer().render(export).decode("utf-8") diff --git a/cookbook/integration/integration.py b/cookbook/integration/integration.py index ac96fdc61..3e853e88c 100644 --- a/cookbook/integration/integration.py +++ b/cookbook/integration/integration.py @@ -1,8 +1,36 @@ +import os +import tempfile + + class Integration: - @staticmethod - def get_recipe(string): + request = None + + def __init__(self, request): + self.request = request + + def do_export(self, recipes): raise Exception('Method not implemented in storage integration') - @staticmethod - def get_export(recipe): + def do_import(self): raise Exception('Method not implemented in storage integration') + + def get_recipe(self, string): + raise Exception('Method not implemented in storage integration') + + def get_export(self, recipe): + raise Exception('Method not implemented in storage integration') + + def get_export_file(self, recipe): + try: + with open(recipe.image.path, 'rb') as img_f: + return img_f + except: + return None + + def get_tmp_dir_path(self): + path = os.path.join(tempfile.gettempdir(), 'recipe_io', str(self.request.user.pk)) + os.makedirs(path, exist_ok=True) + return path + + def delete_temp_dir_path(self): + os.remove(self.get_tmp_dir_path()) diff --git a/cookbook/serializer.py b/cookbook/serializer.py index 5de6f01b3..f52c2c926 100644 --- a/cookbook/serializer.py +++ b/cookbook/serializer.py @@ -113,7 +113,7 @@ class KeywordSerializer(UniqueFieldsMixin, serializers.ModelSerializer): def create(self, validated_data): # since multi select tags dont have id's # duplicate names might be routed to create - obj, created = Keyword.objects.get_or_create(**validated_data) + obj, created = Keyword.objects.get_or_create(name=validated_data['name']) return obj class Meta: @@ -131,7 +131,7 @@ class UnitSerializer(UniqueFieldsMixin, serializers.ModelSerializer): def create(self, validated_data): # since multi select tags dont have id's # duplicate names might be routed to create - obj, created = Unit.objects.get_or_create(**validated_data) + obj, created = Unit.objects.get_or_create(name=validated_data['name']) return obj class Meta: @@ -145,7 +145,7 @@ class SupermarketCategorySerializer(UniqueFieldsMixin, WritableNestedModelSerial def create(self, validated_data): # since multi select tags dont have id's # duplicate names might be routed to create - obj, created = SupermarketCategory.objects.get_or_create(**validated_data) + obj, created = SupermarketCategory.objects.get_or_create(name=validated_data['name']) return obj def update(self, instance, validated_data): @@ -221,17 +221,6 @@ class StepSerializer(WritableNestedModelSerializer): ) -# used for the import export. temporary workaround until that module is finally fixed -class StepExportSerializer(WritableNestedModelSerializer): - ingredients = IngredientSerializer(many=True) - - class Meta: - model = Step - fields = ( - 'id', 'name', 'type', 'instruction', 'ingredients', 'time', 'order', 'show_as_header' - ) - - class NutritionInformationSerializer(serializers.ModelSerializer): class Meta: model = NutritionInformation @@ -270,11 +259,6 @@ class RecipeSerializer(WritableNestedModelSerializer): return super().create(validated_data) -# used for the import export. temporary workaround until that module is finally fixed -class RecipeExportSerializer(RecipeSerializer): - steps = StepExportSerializer(many=True) - - class RecipeImageSerializer(WritableNestedModelSerializer): class Meta: model = Recipe @@ -403,3 +387,72 @@ class ViewLogSerializer(serializers.ModelSerializer): class Meta: model = ViewLog fields = '__all__' + + +# Export/Import Serializers + +class KeywordExportSerializer(KeywordSerializer): + class Meta: + model = Keyword + fields = ('name', 'icon', 'description', 'created_at', 'updated_at') + + +class NutritionInformationExportSerializer(NutritionInformationSerializer): + class Meta: + model = NutritionInformation + fields = ('carbohydrates', 'fats', 'proteins', 'calories', 'source') + + +class SupermarketCategoryExportSerializer(SupermarketCategorySerializer): + class Meta: + model = SupermarketCategory + fields = ('name',) + + +class UnitExportSerializer(UnitSerializer): + class Meta: + model = Unit + fields = ('name', 'description') + + +class FoodExportSerializer(FoodSerializer): + supermarket_category = SupermarketCategoryExportSerializer(allow_null=True, required=False) + + class Meta: + model = Food + fields = ('name', 'ignore_shopping', 'supermarket_category') + + +class IngredientExportSerializer(WritableNestedModelSerializer): + food = FoodExportSerializer(allow_null=True) + unit = UnitExportSerializer(allow_null=True) + amount = CustomDecimalField() + + class Meta: + model = Ingredient + fields = ('food', 'unit', 'amount', 'note', 'order', 'is_header', 'no_amount') + + +class StepExportSerializer(WritableNestedModelSerializer): + ingredients = IngredientExportSerializer(many=True) + + class Meta: + model = Step + fields = ('name', 'type', 'instruction', 'ingredients', 'time', 'order', 'show_as_header') + + +class RecipeExportSerializer(WritableNestedModelSerializer): + nutrition = NutritionInformationSerializer(allow_null=True, required=False) + steps = StepExportSerializer(many=True) + keywords = KeywordExportSerializer(many=True) + + class Meta: + model = Recipe + fields = ( + 'name', 'description', 'keywords', 'steps', 'working_time', + 'waiting_time', 'internal', 'nutrition', 'servings', 'servings_text', + ) + + def create(self, validated_data): + validated_data['created_by'] = self.context['request'].user + return super().create(validated_data) diff --git a/cookbook/templates/test.html b/cookbook/templates/test.html index fc765357e..d133f0eaf 100644 --- a/cookbook/templates/test.html +++ b/cookbook/templates/test.html @@ -1,11 +1,26 @@ - \ No newline at end of file +{% extends "base.html" %} +{% load crispy_forms_filters %} +{% load i18n %} +{% load static %} + +{% block title %}{% trans 'Import Recipes' %}{% endblock %} + +{% block extra_head %} + {{ form.media }} +{% endblock %} + + +{% block content %} +

{% trans 'Import' %}

+
+
+
+ {% csrf_token %} + {{ form|crispy }} + +
+
+
+ +{% endblock %} \ No newline at end of file diff --git a/cookbook/templates/test2.html b/cookbook/templates/test2.html new file mode 100644 index 000000000..4133da939 --- /dev/null +++ b/cookbook/templates/test2.html @@ -0,0 +1,25 @@ +{% extends "base.html" %} +{% load crispy_forms_filters %} +{% load i18n %} +{% load static %} + +{% block title %}{% trans 'Export Recipes' %}{% endblock %} + +{% block extra_head %} + {{ form.media }} +{% endblock %} + + +{% block content %} +

{% trans 'Export' %}

+
+
+
+ {% csrf_token %} + {{ form|crispy }} + +
+
+
+{% endblock %} \ No newline at end of file diff --git a/cookbook/urls.py b/cookbook/urls.py index fcdeab881..d55d5b3f8 100644 --- a/cookbook/urls.py +++ b/cookbook/urls.py @@ -51,7 +51,8 @@ urlpatterns = [ path('shopping/latest/', views.latest_shopping_list, name='view_shopping_latest'), path('settings/', views.user_settings, name='view_settings'), path('history/', views.history, name='view_history'), - path('test/', views.test, name='view_test'), + path('test/', views.test, name='view_test'), + path('test2/', views.test2, name='view_test2'), path('import/', import_export.import_recipe, name='view_import'), path('export/', import_export.export_recipe, name='view_export'), diff --git a/cookbook/views/views.py b/cookbook/views/views.py index c9d22ad0c..083413773 100644 --- a/cookbook/views/views.py +++ b/cookbook/views/views.py @@ -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 +from django.http import HttpResponseRedirect, FileResponse, HttpResponse from django.shortcuts import get_object_or_404, render, redirect from django.urls import reverse, reverse_lazy from django.utils import timezone @@ -22,8 +22,9 @@ from rest_framework.authtoken.models import Token from cookbook.filters import RecipeFilter from cookbook.forms import (CommentForm, Recipe, RecipeBookEntryForm, User, UserCreateForm, UserNameForm, UserPreference, - UserPreferenceForm) + UserPreferenceForm, ImportForm, NewImportForm, NewExportForm) 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, @@ -485,10 +486,29 @@ def offline(request): return render(request, 'offline.html', {}) -def test(request, pk): +def test(request): if not settings.DEBUG: return HttpResponseRedirect(reverse('index')) - recipe = Recipe.objects.get(pk=pk) + if request.method == "POST": + form = NewImportForm(request.POST) + else: + form = NewImportForm() - return render(request, 'test.html', {'recipe': recipe}) + return render(request, 'test.html', {'form': form}) + + +def test2(request): + if not settings.DEBUG: + return HttpResponseRedirect(reverse('index')) + + if request.method == "POST": + form = NewExportForm(request.POST) + if form.is_valid(): + integration = Default(request) + integration.do_export(form.cleaned_data['recipes']) + return render(request, 'test2.html', {'form': form}) + else: + form = NewExportForm() + + return render(request, 'test2.html', {'form': form})