From 9c873127a58d0fde1b658cef42dee64d267ad2bb Mon Sep 17 00:00:00 2001 From: Tiago Rascazzi Date: Sat, 8 Jan 2022 11:07:33 -0500 Subject: [PATCH 01/30] Added loading page --- .directory | 3 + .env.template | 5 +- cookbook/forms.py | 2 + cookbook/integration/default.py | 9 +- cookbook/integration/integration.py | 75 ++- cookbook/integration/pdfexport.py | 32 +- cookbook/integration/recipesage.py | 6 +- cookbook/integration/saffron.py | 6 +- cookbook/migrations/0164_exportlog.py | 32 + .../0165_exportlog_cache_duration.py | 18 + .../0166_exportlog_possibly_not_expired.py | 18 + cookbook/models.py | 19 + cookbook/serializer.py | 16 +- cookbook/static/css/app.min.css | 7 + cookbook/templates/export.html | 42 +- cookbook/templates/export_response.html | 32 + cookbook/urls.py | 5 +- cookbook/views/api.py | 15 +- cookbook/views/import_export.py | 46 +- recipes/settings.py | 2 + .../ExportResponseView/ExportResponseView.vue | 145 +++++ vue/src/apps/ExportResponseView/main.js | 18 + vue/src/apps/ExportView/ExportView.vue | 167 +++++ vue/src/apps/ExportView/main.js | 18 + vue/src/utils/openapi/api.ts | 575 ++++++++++++++++-- vue/vue.config.js | 8 + 26 files changed, 1212 insertions(+), 109 deletions(-) create mode 100644 .directory create mode 100644 cookbook/migrations/0164_exportlog.py create mode 100644 cookbook/migrations/0165_exportlog_cache_duration.py create mode 100644 cookbook/migrations/0166_exportlog_possibly_not_expired.py create mode 100644 cookbook/templates/export_response.html create mode 100644 vue/src/apps/ExportResponseView/ExportResponseView.vue create mode 100644 vue/src/apps/ExportResponseView/main.js create mode 100644 vue/src/apps/ExportView/ExportView.vue create mode 100644 vue/src/apps/ExportView/main.js diff --git a/.directory b/.directory new file mode 100644 index 000000000..280ecee95 --- /dev/null +++ b/.directory @@ -0,0 +1,3 @@ +[Dolphin] +Timestamp=2022,1,7,19,23,46.14 +Version=4 diff --git a/.env.template b/.env.template index aa13a7a96..018354383 100644 --- a/.env.template +++ b/.env.template @@ -148,4 +148,7 @@ REVERSE_PROXY_AUTH=0 # Enables exporting PDF (see export docs) # Disabled by default, uncomment to enable -# ENABLE_PDF_EXPORT=1 \ No newline at end of file +# ENABLE_PDF_EXPORT=1 + +# Duration to keep the cached export file +EXPORT_FILE_CACHE_DURATION=600 \ No newline at end of file diff --git a/cookbook/forms.py b/cookbook/forms.py index c7a03706f..31f3850f2 100644 --- a/cookbook/forms.py +++ b/cookbook/forms.py @@ -167,6 +167,7 @@ class ImportExportBase(forms.Form): )) +#TODO deprecated class ImportForm(ImportExportBase): files = forms.FileField(required=True, widget=forms.ClearableFileInput(attrs={'multiple': True})) duplicates = forms.BooleanField(help_text=_( @@ -174,6 +175,7 @@ class ImportForm(ImportExportBase): required=False) +#TODO deprecated class ExportForm(ImportExportBase): recipes = forms.ModelMultipleChoiceField(widget=MultiSelectWidget, queryset=Recipe.objects.none(), required=False) all = forms.BooleanField(required=False) diff --git a/cookbook/integration/default.py b/cookbook/integration/default.py index 39c0bc666..6a2b34102 100644 --- a/cookbook/integration/default.py +++ b/cookbook/integration/default.py @@ -32,11 +32,12 @@ class Default(Integration): return None def get_file_from_recipe(self, recipe): + export = RecipeExportSerializer(recipe).data return 'recipe.json', JSONRenderer().render(export).decode("utf-8") - def get_files_from_recipes(self, recipes, cookie): + def get_files_from_recipes(self, recipes, el, cookie): export_zip_stream = BytesIO() export_zip_obj = ZipFile(export_zip_stream, 'w') @@ -50,6 +51,7 @@ class Default(Integration): 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: @@ -57,6 +59,11 @@ class Default(Integration): recipe_zip_obj.close() export_zip_obj.writestr(str(r.pk) + '.zip', recipe_zip_stream.getvalue()) + + el.exported_recipes += 1 + el.msg += self.recipe_processed_msg(r) + el.save() + 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 3b8034e37..11524a382 100644 --- a/cookbook/integration/integration.py +++ b/cookbook/integration/integration.py @@ -1,9 +1,12 @@ +import time import datetime import json import traceback import uuid from io import BytesIO, StringIO from zipfile import BadZipFile, ZipFile +from django.core.cache import cache + from bs4 import Tag from django.core.exceptions import ObjectDoesNotExist @@ -18,6 +21,7 @@ from cookbook.forms import ImportExportBase from cookbook.helper.image_processing import get_filetype, handle_image from cookbook.models import Keyword, Recipe from recipes.settings import DEBUG +from recipes.settings import EXPORT_FILE_CACHE_DURATION class Integration: @@ -61,35 +65,37 @@ class Integration: space=request.space ) - def do_export(self, recipes): - """ - Perform the export based on a list of recipes - :param recipes: list of recipe objects - :return: HttpResponse with the file of the requested export format that is directly downloaded (When that format involve multiple files they are zipped together) - """ - - files = self.get_files_from_recipes(recipes, self.request.COOKIES) - - if len(files) == 1: - filename, file = files[0] - export_filename = filename - export_file = file - - else: - 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 + def do_export(self, recipes, el): + + with scope(space=self.request.space): + el.total_recipes = len(recipes) + el.cache_duration = EXPORT_FILE_CACHE_DURATION + el.save() + + files = self.get_files_from_recipes(recipes, el, self.request.COOKIES) + + if len(files) == 1: + filename, file = files[0] + export_filename = filename + export_file = file + + else: + 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() + + + cache.set('export_file_'+str(el.pk), {'filename': export_filename, 'file': export_file}, EXPORT_FILE_CACHE_DURATION) + el.running = False + el.save() def import_file_name_filter(self, zip_info_object): @@ -128,7 +134,7 @@ class Integration: for d in data_list: recipe = self.get_recipe_from_file(d) recipe.keywords.add(self.keyword) - il.msg += f'{recipe.pk} - {recipe.name} \n' + il.msg += self.recipe_processed_msg(recipe) self.handle_duplicates(recipe, import_duplicates) il.imported_recipes += 1 il.save() @@ -153,7 +159,7 @@ class Integration: else: recipe = self.get_recipe_from_file(BytesIO(import_zip.read(z.filename))) recipe.keywords.add(self.keyword) - il.msg += f'{recipe.pk} - {recipe.name} \n' + il.msg += self.recipe_processed_msg(recipe) self.handle_duplicates(recipe, import_duplicates) il.imported_recipes += 1 il.save() @@ -168,7 +174,7 @@ class Integration: try: recipe = self.get_recipe_from_file(d) recipe.keywords.add(self.keyword) - il.msg += f'{recipe.pk} - {recipe.name} \n' + il.msg += self.recipe_processed_msg(recipe) self.handle_duplicates(recipe, import_duplicates) il.imported_recipes += 1 il.save() @@ -185,7 +191,7 @@ class Integration: try: recipe = self.get_recipe_from_file(d) recipe.keywords.add(self.keyword) - il.msg += f'{recipe.pk} - {recipe.name} \n' + il.msg += self.recipe_processed_msg(recipe) self.handle_duplicates(recipe, import_duplicates) il.imported_recipes += 1 il.save() @@ -195,7 +201,7 @@ class Integration: else: recipe = self.get_recipe_from_file(f['file']) recipe.keywords.add(self.keyword) - il.msg += f'{recipe.pk} - {recipe.name} \n' + il.msg += self.recipe_processed_msg(recipe) self.handle_duplicates(recipe, import_duplicates) except BadZipFile: il.msg += 'ERROR ' + _( @@ -263,7 +269,7 @@ class Integration: raise NotImplementedError('Method not implemented in integration') - def get_files_from_recipes(self, recipes, cookie): + def get_files_from_recipes(self, recipes, el, 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. @@ -282,3 +288,6 @@ class Integration: log.msg += exception.msg if DEBUG: traceback.print_exc() + + def recipe_processed_msg(self, recipe): + return f'{recipe.pk} - {recipe.name} \n' diff --git a/cookbook/integration/pdfexport.py b/cookbook/integration/pdfexport.py index b982f24d1..9f500480a 100644 --- a/cookbook/integration/pdfexport.py +++ b/cookbook/integration/pdfexport.py @@ -11,22 +11,25 @@ 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 +from cookbook.models import ExportLog +from asgiref.sync import sync_to_async +import django.core.management.commands.runserver as runserver +import logging 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): + async def get_files_from_recipes_async(self, recipes, el, cookie): cmd = runserver.Command() browser = await launch( handleSIGINT=False, handleSIGTERM=False, handleSIGHUP=False, - ignoreHTTPSErrors=True + ignoreHTTPSErrors=True, ) cookies = {'domain': cmd.default_addr, 'name': 'sessionid', 'value': cookie['sessionid'], } @@ -39,17 +42,28 @@ class PDFexport(Integration): } } - 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', }) + + page = await browser.newPage() + await page.emulateMedia('print') + await page.setCookie(cookies) + + await page.goto('http://'+cmd.default_addr+':'+cmd.default_port+'/view/recipe/'+str(recipe.id), {'waitUntil': 'domcontentloaded'}) + await page.waitForSelector('h3'); + files.append([recipe.name + '.pdf', await page.pdf(options)]) + await page.close(); + + el.exported_recipes += 1 + el.msg += self.recipe_processed_msg(recipe) + await sync_to_async(el.save, thread_sensitive=True)() + await browser.close() return files - def get_files_from_recipes(self, recipes, cookie): - return asyncio.run(self.get_files_from_recipes_async(recipes, cookie)) + + def get_files_from_recipes(self, recipes, el, cookie): + return asyncio.run(self.get_files_from_recipes_async(recipes, el, cookie)) diff --git a/cookbook/integration/recipesage.py b/cookbook/integration/recipesage.py index 0ca32194d..93dee8858 100644 --- a/cookbook/integration/recipesage.py +++ b/cookbook/integration/recipesage.py @@ -88,11 +88,15 @@ class RecipeSage(Integration): return data - def get_files_from_recipes(self, recipes, cookie): + def get_files_from_recipes(self, recipes, el, cookie): json_list = [] for r in recipes: json_list.append(self.get_file_from_recipe(r)) + el.exported_recipes += 1 + el.msg += self.recipe_processed_msg(r) + el.save() + return [['export.json', json.dumps(json_list)]] def split_recipe_file(self, file): diff --git a/cookbook/integration/saffron.py b/cookbook/integration/saffron.py index 16a93a0ce..63342b2bb 100644 --- a/cookbook/integration/saffron.py +++ b/cookbook/integration/saffron.py @@ -87,10 +87,14 @@ class Saffron(Integration): return recipe.name+'.txt', data - def get_files_from_recipes(self, recipes, cookie): + def get_files_from_recipes(self, recipes, el, cookie): files = [] for r in recipes: filename, data = self.get_file_from_recipe(r) files.append([ filename, data ]) + el.exported_recipes += 1 + el.msg += self.recipe_processed_msg(r) + el.save() + return files \ No newline at end of file diff --git a/cookbook/migrations/0164_exportlog.py b/cookbook/migrations/0164_exportlog.py new file mode 100644 index 000000000..952c9372f --- /dev/null +++ b/cookbook/migrations/0164_exportlog.py @@ -0,0 +1,32 @@ +# Generated by Django 3.2.11 on 2022-01-07 20:29 + +import cookbook.models +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('cookbook', '0163_auto_20220105_0758'), + ] + + operations = [ + migrations.CreateModel( + name='ExportLog', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('type', models.CharField(max_length=32)), + ('running', models.BooleanField(default=True)), + ('msg', models.TextField(default='')), + ('total_recipes', models.IntegerField(default=0)), + ('exported_recipes', models.IntegerField(default=0)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ('space', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cookbook.space')), + ], + bases=(models.Model, cookbook.models.PermissionModelMixin), + ), + ] diff --git a/cookbook/migrations/0165_exportlog_cache_duration.py b/cookbook/migrations/0165_exportlog_cache_duration.py new file mode 100644 index 000000000..8005a4b2d --- /dev/null +++ b/cookbook/migrations/0165_exportlog_cache_duration.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.11 on 2022-01-08 00:10 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('cookbook', '0164_exportlog'), + ] + + operations = [ + migrations.AddField( + model_name='exportlog', + name='cache_duration', + field=models.IntegerField(default=0), + ), + ] diff --git a/cookbook/migrations/0166_exportlog_possibly_not_expired.py b/cookbook/migrations/0166_exportlog_possibly_not_expired.py new file mode 100644 index 000000000..15e19e13b --- /dev/null +++ b/cookbook/migrations/0166_exportlog_possibly_not_expired.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.11 on 2022-01-08 00:43 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('cookbook', '0165_exportlog_cache_duration'), + ] + + operations = [ + migrations.AddField( + model_name='exportlog', + name='possibly_not_expired', + field=models.BooleanField(default=True), + ), + ] diff --git a/cookbook/models.py b/cookbook/models.py index cfe4ab814..df9880a82 100644 --- a/cookbook/models.py +++ b/cookbook/models.py @@ -992,6 +992,25 @@ class ImportLog(models.Model, PermissionModelMixin): def __str__(self): return f"{self.created_at}:{self.type}" +class ExportLog(models.Model, PermissionModelMixin): + type = models.CharField(max_length=32) + running = models.BooleanField(default=True) + msg = models.TextField(default="") + + total_recipes = models.IntegerField(default=0) + exported_recipes = models.IntegerField(default=0) + cache_duration = models.IntegerField(default=0) + possibly_not_expired = models.BooleanField(default=True) + + created_at = models.DateTimeField(auto_now_add=True) + created_by = models.ForeignKey(User, on_delete=models.CASCADE) + + objects = ScopedManager(space='space') + space = models.ForeignKey(Space, on_delete=models.CASCADE) + + def __str__(self): + return f"{self.created_at}:{self.type}" + class BookmarkletImport(ExportModelOperationsMixin('bookmarklet_import'), models.Model, PermissionModelMixin): html = models.TextField() diff --git a/cookbook/serializer.py b/cookbook/serializer.py index ac6238681..4d980087e 100644 --- a/cookbook/serializer.py +++ b/cookbook/serializer.py @@ -14,7 +14,7 @@ from rest_framework.fields import empty from cookbook.helper.shopping_helper import list_from_recipe from cookbook.models import (Automation, BookmarkletImport, Comment, CookLog, Food, - FoodInheritField, ImportLog, Ingredient, Keyword, MealPlan, MealType, + FoodInheritField, ImportLog, ExportLog, Ingredient, Keyword, MealPlan, MealType, NutritionInformation, Recipe, RecipeBook, RecipeBookEntry, RecipeImport, ShareLink, ShoppingList, ShoppingListEntry, ShoppingListRecipe, Step, Storage, Supermarket, SupermarketCategory, @@ -841,6 +841,20 @@ class ImportLogSerializer(serializers.ModelSerializer): read_only_fields = ('created_by',) +class ExportLogSerializer(serializers.ModelSerializer): + + def create(self, validated_data): + validated_data['created_by'] = self.context['request'].user + validated_data['space'] = self.context['request'].space + return super().create(validated_data) + + class Meta: + model = ExportLog + fields = ('id', 'type', 'msg', 'running', 'total_recipes', 'exported_recipes', 'cache_duration', 'possibly_not_expired', 'created_by', 'created_at') + read_only_fields = ('created_by',) + + + class AutomationSerializer(serializers.ModelSerializer): def create(self, validated_data): diff --git a/cookbook/static/css/app.min.css b/cookbook/static/css/app.min.css index 7c680a783..a100e2c28 100644 --- a/cookbook/static/css/app.min.css +++ b/cookbook/static/css/app.min.css @@ -1140,3 +1140,10 @@ min-width: 28rem; } } + +@media print{ + #switcher{ + display: none; + } + +} \ No newline at end of file diff --git a/cookbook/templates/export.html b/cookbook/templates/export.html index 4133da939..ca5148235 100644 --- a/cookbook/templates/export.html +++ b/cookbook/templates/export.html @@ -1,25 +1,33 @@ {% extends "base.html" %} -{% load crispy_forms_filters %} -{% load i18n %} +{% load render_bundle from webpack_loader %} {% load static %} +{% load i18n %} +{% load l10n %} + {% block title %}{% trans 'Export Recipes' %}{% endblock %} -{% block extra_head %} - {{ form.media }} + +{% block content %} +
+ +
+{% endblock %} + + +{% block script %} + {% if debug %} + + {% else %} + + {% endif %} + + + + {% render_bundle 'export_view' %} {% endblock %} -{% block content %} -

{% trans 'Export' %}

-
-
-
- {% csrf_token %} - {{ form|crispy }} - -
-
-
-{% endblock %} \ No newline at end of file diff --git a/cookbook/templates/export_response.html b/cookbook/templates/export_response.html new file mode 100644 index 000000000..1f438632a --- /dev/null +++ b/cookbook/templates/export_response.html @@ -0,0 +1,32 @@ +{% extends "base.html" %} +{% load render_bundle from webpack_loader %} +{% load static %} +{% load i18n %} +{% load l10n %} + +{% block title %}{% trans 'Export' %}{% endblock %} + +{% block content %} + +
+ +
+ + +{% endblock %} + + +{% block script %} + {% if debug %} + + {% else %} + + {% endif %} + + + + {% render_bundle 'export_response_view' %} +{% endblock %} \ No newline at end of file diff --git a/cookbook/urls.py b/cookbook/urls.py index 68ac160b1..ac7dd838c 100644 --- a/cookbook/urls.py +++ b/cookbook/urls.py @@ -21,6 +21,7 @@ router.register(r'cook-log', api.CookLogViewSet) router.register(r'food', api.FoodViewSet) router.register(r'food-inherit-field', api.FoodInheritFieldViewSet) router.register(r'import-log', api.ImportLogViewSet) +router.register(r'export-log', api.ExportLogViewSet) router.register(r'ingredient', api.IngredientViewSet) router.register(r'keyword', api.KeywordViewSet) router.register(r'meal-plan', api.MealPlanViewSet) @@ -72,8 +73,10 @@ urlpatterns = [ path('abuse/', views.report_share_abuse, name='view_report_share_abuse'), path('import/', import_export.import_recipe, name='view_import'), - path('import-response//', import_export.import_response, name='view_import_response'), + path('import-response//', import_export.import_response, name='view_import_response'),\ path('export/', import_export.export_recipe, name='view_export'), + path('export-response//', import_export.export_response, name='view_export_response'), + path('export-file//', import_export.export_file, name='view_export_file'), path('view/recipe/', views.recipe_view, name='view_recipe'), path('view/recipe//', views.recipe_view, name='view_recipe'), diff --git a/cookbook/views/api.py b/cookbook/views/api.py index 86f976602..3ef9db87c 100644 --- a/cookbook/views/api.py +++ b/cookbook/views/api.py @@ -40,7 +40,7 @@ from cookbook.helper.recipe_search import get_facet, old_search, search_recipes from cookbook.helper.recipe_url_import import get_from_scraper from cookbook.helper.shopping_helper import list_from_recipe, shopping_helper from cookbook.models import (Automation, BookmarkletImport, CookLog, Food, FoodInheritField, - ImportLog, Ingredient, Keyword, MealPlan, MealType, Recipe, RecipeBook, + ImportLog, ExportLog, Ingredient, Keyword, MealPlan, MealType, Recipe, RecipeBook, RecipeBookEntry, ShareLink, ShoppingList, ShoppingListEntry, ShoppingListRecipe, Step, Storage, Supermarket, SupermarketCategory, SupermarketCategoryRelation, Sync, SyncLog, Unit, UserFile, @@ -51,7 +51,7 @@ from cookbook.provider.nextcloud import Nextcloud from cookbook.schemas import FilterSchema, QueryParam, QueryParamAutoSchema, TreeSchema from cookbook.serializer import (AutomationSerializer, BookmarkletImportSerializer, CookLogSerializer, FoodInheritFieldSerializer, FoodSerializer, - FoodShoppingUpdateSerializer, ImportLogSerializer, + FoodShoppingUpdateSerializer, ImportLogSerializer, ExportLogSerializer, IngredientSerializer, KeywordSerializer, MealPlanSerializer, MealTypeSerializer, RecipeBookEntrySerializer, RecipeBookSerializer, RecipeImageSerializer, @@ -799,6 +799,17 @@ class ImportLogViewSet(viewsets.ModelViewSet): return self.queryset.filter(space=self.request.space) +class ExportLogViewSet(viewsets.ModelViewSet): + queryset = ExportLog.objects + serializer_class = ExportLogSerializer + permission_classes = [CustomIsUser] + pagination_class = DefaultPagination + + def get_queryset(self): + return self.queryset.filter(space=self.request.space) + + + class BookmarkletImportViewSet(viewsets.ModelViewSet): queryset = BookmarkletImport.objects serializer_class = BookmarkletImportSerializer diff --git a/cookbook/views/import_export.py b/cookbook/views/import_export.py index 9fd810c17..c1530b000 100644 --- a/cookbook/views/import_export.py +++ b/cookbook/views/import_export.py @@ -1,9 +1,10 @@ import re import threading from io import BytesIO +from django.core.cache import cache from django.contrib import messages -from django.http import HttpResponseRedirect, JsonResponse +from django.http import HttpResponse, HttpResponseRedirect, JsonResponse from django.shortcuts import render from django.urls import reverse from django.utils.translation import gettext as _ @@ -29,7 +30,7 @@ from cookbook.integration.recipesage import RecipeSage from cookbook.integration.rezkonv import RezKonv 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, ExportLog, UserPreference from recipes import settings @@ -123,13 +124,23 @@ def export_recipe(request): if form.cleaned_data['all']: recipes = Recipe.objects.filter(space=request.space, internal=True).all() - if form.cleaned_data['type'] == ImportExportBase.PDF and not settings.ENABLE_PDF_EXPORT: - messages.add_message(request, messages.ERROR, _('The PDF Exporter is not enabled on this instance as it is still in an experimental state.')) - return render(request, 'export.html', {'form': form}) integration = get_integration(request, form.cleaned_data['type']) - return integration.do_export(recipes) + + el = ExportLog.objects.create(type=form.cleaned_data['type'], created_by=request.user, space=request.space) + + t = threading.Thread(target=integration.do_export, args=[recipes, el]) + t.setDaemon(True) + t.start() + + return JsonResponse({'export_id': el.pk}) except NotImplementedError: - messages.add_message(request, messages.ERROR, _('Exporting is not implemented for this provider')) + return JsonResponse( + { + 'error': True, + 'msg': _('Importing is not implemented for this provider') + }, + status=400 + ) else: form = ExportForm(space=request.space) @@ -145,3 +156,24 @@ def export_recipe(request): @group_required('user') def import_response(request, pk): return render(request, 'import_response.html', {'pk': pk}) + +@group_required('user') +def export_response(request, pk): + return render(request, 'export_response.html', {'pk': pk}) + + +@group_required('user') +def export_file(request, pk): + + cacheData = cache.get('export_file_'+str(pk)) + + if cacheData is None: + el = ExportLog.objects.get(pk=pk) + el.possibly_not_expired = False; + el.save() + return render(request, 'export_response.html', {'pk': pk}) + + response = HttpResponse(cacheData['file'], content_type='application/force-download') + response['Content-Disposition'] = 'attachment; filename="'+cacheData['filename']+'"' + return response + diff --git a/recipes/settings.py b/recipes/settings.py index 9b9dacbb1..9f0915332 100644 --- a/recipes/settings.py +++ b/recipes/settings.py @@ -138,6 +138,7 @@ ENABLE_SIGNUP = bool(int(os.getenv('ENABLE_SIGNUP', False))) ENABLE_METRICS = bool(int(os.getenv('ENABLE_METRICS', False))) ENABLE_PDF_EXPORT = bool(int(os.getenv('ENABLE_PDF_EXPORT', False))) +EXPORT_FILE_CACHE_DURATION = int(os.getenv('EXPORT_FILE_CACHE_DURATION', 600)) MIDDLEWARE = [ 'corsheaders.middleware.CorsMiddleware', @@ -425,3 +426,4 @@ EMAIL_USE_TLS = bool(int(os.getenv('EMAIL_USE_TLS', False))) EMAIL_USE_SSL = bool(int(os.getenv('EMAIL_USE_SSL', False))) DEFAULT_FROM_EMAIL = os.getenv('DEFAULT_FROM_EMAIL', 'webmaster@localhost') ACCOUNT_EMAIL_SUBJECT_PREFIX = os.getenv('ACCOUNT_EMAIL_SUBJECT_PREFIX', '[Tandoor Recipes] ') # allauth sender prefix + diff --git a/vue/src/apps/ExportResponseView/ExportResponseView.vue b/vue/src/apps/ExportResponseView/ExportResponseView.vue new file mode 100644 index 000000000..913f4366d --- /dev/null +++ b/vue/src/apps/ExportResponseView/ExportResponseView.vue @@ -0,0 +1,145 @@ + + + + + diff --git a/vue/src/apps/ExportResponseView/main.js b/vue/src/apps/ExportResponseView/main.js new file mode 100644 index 000000000..220ac3be4 --- /dev/null +++ b/vue/src/apps/ExportResponseView/main.js @@ -0,0 +1,18 @@ +import Vue from 'vue' +import App from './ExportResponseView.vue' +import i18n from '@/i18n' + +Vue.config.productionTip = false + +// TODO move this and other default stuff to centralized JS file (verify nothing breaks) +let publicPath = localStorage.STATIC_URL + 'vue/' +if (process.env.NODE_ENV === 'development') { + publicPath = 'http://localhost:8080/' +} +export default __webpack_public_path__ = publicPath // eslint-disable-line + + +new Vue({ + i18n, + render: h => h(App), +}).$mount('#app') diff --git a/vue/src/apps/ExportView/ExportView.vue b/vue/src/apps/ExportView/ExportView.vue new file mode 100644 index 000000000..a9bbaceb7 --- /dev/null +++ b/vue/src/apps/ExportView/ExportView.vue @@ -0,0 +1,167 @@ + + + + + + + diff --git a/vue/src/apps/ExportView/main.js b/vue/src/apps/ExportView/main.js new file mode 100644 index 000000000..8c8af8e52 --- /dev/null +++ b/vue/src/apps/ExportView/main.js @@ -0,0 +1,18 @@ +import Vue from 'vue' +import App from './ExportView.vue' +import i18n from '@/i18n' + +Vue.config.productionTip = false + +// TODO move this and other default stuff to centralized JS file (verify nothing breaks) +let publicPath = localStorage.STATIC_URL + 'vue/' +if (process.env.NODE_ENV === 'development') { + publicPath = 'http://localhost:8080/' +} +export default __webpack_public_path__ = publicPath // eslint-disable-line + + +new Vue({ + i18n, + render: h => h(App), +}).$mount('#app') diff --git a/vue/src/utils/openapi/api.ts b/vue/src/utils/openapi/api.ts index 8a8c1cfff..0466eeb71 100644 --- a/vue/src/utils/openapi/api.ts +++ b/vue/src/utils/openapi/api.ts @@ -173,6 +173,73 @@ export interface CookLog { */ created_at?: string; } +/** + * + * @export + * @interface ExportLog + */ +export interface ExportLog { + /** + * + * @type {number} + * @memberof ExportLog + */ + id?: number; + /** + * + * @type {string} + * @memberof ExportLog + */ + type: string; + /** + * + * @type {string} + * @memberof ExportLog + */ + msg?: string; + /** + * + * @type {boolean} + * @memberof ExportLog + */ + running?: boolean; + /** + * + * @type {number} + * @memberof ExportLog + */ + total_recipes?: number; + /** + * + * @type {number} + * @memberof ExportLog + */ + exported_recipes?: number; + /** + * + * @type {number} + * @memberof ExportLog + */ + cache_duration?: number; + /** + * + * @type {boolean} + * @memberof ExportLog + */ + possibly_not_expired?: boolean; + /** + * + * @type {string} + * @memberof ExportLog + */ + created_by?: string; + /** + * + * @type {string} + * @memberof ExportLog + */ + created_at?: string; +} /** * * @export @@ -211,10 +278,10 @@ export interface Food { recipe?: FoodRecipe | null; /** * - * @type {boolean} + * @type {string} * @memberof Food */ - food_onhand?: boolean; + food_onhand?: string | null; /** * * @type {FoodSupermarketCategory} @@ -607,10 +674,10 @@ export interface IngredientFood { recipe?: FoodRecipe | null; /** * - * @type {boolean} + * @type {string} * @memberof IngredientFood */ - food_onhand?: boolean; + food_onhand?: string | null; /** * * @type {FoodSupermarketCategory} @@ -704,6 +771,37 @@ export interface InlineResponse2001 { */ results?: Array; } +/** + * + * @export + * @interface InlineResponse20010 + */ +export interface InlineResponse20010 { + /** + * + * @type {number} + * @memberof InlineResponse20010 + */ + count?: number; + /** + * + * @type {string} + * @memberof InlineResponse20010 + */ + next?: string | null; + /** + * + * @type {string} + * @memberof InlineResponse20010 + */ + previous?: string | null; + /** + * + * @type {Array} + * @memberof InlineResponse20010 + */ + results?: Array; +} /** * * @export @@ -761,10 +859,10 @@ export interface InlineResponse2003 { previous?: string | null; /** * - * @type {Array} + * @type {Array} * @memberof InlineResponse2003 */ - results?: Array; + results?: Array; } /** * @@ -792,10 +890,10 @@ export interface InlineResponse2004 { previous?: string | null; /** * - * @type {Array} + * @type {Array} * @memberof InlineResponse2004 */ - results?: Array; + results?: Array; } /** * @@ -823,10 +921,10 @@ export interface InlineResponse2005 { previous?: string | null; /** * - * @type {Array} + * @type {Array} * @memberof InlineResponse2005 */ - results?: Array; + results?: Array; } /** * @@ -854,10 +952,10 @@ export interface InlineResponse2006 { previous?: string | null; /** * - * @type {Array} + * @type {Array} * @memberof InlineResponse2006 */ - results?: Array; + results?: Array; } /** * @@ -885,10 +983,10 @@ export interface InlineResponse2007 { previous?: string | null; /** * - * @type {Array} + * @type {Array} * @memberof InlineResponse2007 */ - results?: Array; + results?: Array; } /** * @@ -916,10 +1014,10 @@ export interface InlineResponse2008 { previous?: string | null; /** * - * @type {Array} + * @type {Array} * @memberof InlineResponse2008 */ - results?: Array; + results?: Array; } /** * @@ -947,10 +1045,10 @@ export interface InlineResponse2009 { previous?: string | null; /** * - * @type {Array} + * @type {Array} * @memberof InlineResponse2009 */ - results?: Array; + results?: Array; } /** * @@ -1182,7 +1280,7 @@ export interface MealPlanRecipe { * @type {any} * @memberof MealPlanRecipe */ - image?: any; + image?: any | null; /** * * @type {Array} @@ -1372,7 +1470,7 @@ export interface Recipe { * @type {any} * @memberof Recipe */ - image?: any; + image?: any | null; /** * * @type {Array} @@ -1770,7 +1868,7 @@ export interface RecipeOverview { * @type {any} * @memberof RecipeOverview */ - image?: any; + image?: any | null; /** * * @type {Array} @@ -3054,6 +3152,12 @@ export interface UserPreference { * @memberof UserPreference */ filter_to_supermarket?: boolean; + /** + * + * @type {boolean} + * @memberof UserPreference + */ + shopping_add_onhand?: boolean; } /** @@ -3237,6 +3341,39 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration) options: localVarRequestOptions, }; }, + /** + * + * @param {ExportLog} [exportLog] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + createExportLog: async (exportLog?: ExportLog, options: any = {}): Promise => { + const localVarPath = `/api/export-log/`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter, options.query); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(exportLog, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * * @param {Food} [food] @@ -4075,6 +4212,39 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration) + setSearchParams(localVarUrlObj, localVarQueryParameter, options.query); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @param {string} id A unique integer value identifying this export log. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + destroyExportLog: async (id: string, options: any = {}): Promise => { + // verify required parameter 'id' is not null or undefined + assertParamExists('destroyExportLog', 'id', id) + const localVarPath = `/api/export-log/{id}/` + .replace(`{${"id"}}`, encodeURIComponent(String(id))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + + setSearchParams(localVarUrlObj, localVarQueryParameter, options.query); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; @@ -4940,6 +5110,45 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration) + setSearchParams(localVarUrlObj, localVarQueryParameter, options.query); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @param {number} [page] A page number within the paginated result set. + * @param {number} [pageSize] Number of results to return per page. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + listExportLogs: async (page?: number, pageSize?: number, options: any = {}): Promise => { + const localVarPath = `/api/export-log/`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + if (page !== undefined) { + localVarQueryParameter['page'] = page; + } + + if (pageSize !== undefined) { + localVarQueryParameter['page_size'] = pageSize; + } + + + setSearchParams(localVarUrlObj, localVarQueryParameter, options.query); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; @@ -6200,6 +6409,43 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration) options: localVarRequestOptions, }; }, + /** + * + * @param {string} id A unique integer value identifying this export log. + * @param {ExportLog} [exportLog] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + partialUpdateExportLog: async (id: string, exportLog?: ExportLog, options: any = {}): Promise => { + // verify required parameter 'id' is not null or undefined + assertParamExists('partialUpdateExportLog', 'id', id) + const localVarPath = `/api/export-log/{id}/` + .replace(`{${"id"}}`, encodeURIComponent(String(id))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter, options.query); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(exportLog, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * * @param {string} id A unique integer value identifying this food. @@ -7159,6 +7405,39 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration) + setSearchParams(localVarUrlObj, localVarQueryParameter, options.query); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @param {string} id A unique integer value identifying this export log. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + retrieveExportLog: async (id: string, options: any = {}): Promise => { + // verify required parameter 'id' is not null or undefined + assertParamExists('retrieveExportLog', 'id', id) + const localVarPath = `/api/export-log/{id}/` + .replace(`{${"id"}}`, encodeURIComponent(String(id))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + + setSearchParams(localVarUrlObj, localVarQueryParameter, options.query); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; @@ -8178,6 +8457,43 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration) options: localVarRequestOptions, }; }, + /** + * + * @param {string} id A unique integer value identifying this export log. + * @param {ExportLog} [exportLog] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updateExportLog: async (id: string, exportLog?: ExportLog, options: any = {}): Promise => { + // verify required parameter 'id' is not null or undefined + assertParamExists('updateExportLog', 'id', id) + const localVarPath = `/api/export-log/{id}/` + .replace(`{${"id"}}`, encodeURIComponent(String(id))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter, options.query); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(exportLog, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * * @param {string} id A unique integer value identifying this food. @@ -9054,6 +9370,16 @@ export const ApiApiFp = function(configuration?: Configuration) { const localVarAxiosArgs = await localVarAxiosParamCreator.createCookLog(cookLog, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, + /** + * + * @param {ExportLog} [exportLog] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async createExportLog(exportLog?: ExportLog, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.createExportLog(exportLog, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, /** * * @param {Food} [food] @@ -9307,6 +9633,16 @@ export const ApiApiFp = function(configuration?: Configuration) { const localVarAxiosArgs = await localVarAxiosParamCreator.destroyCookLog(id, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, + /** + * + * @param {string} id A unique integer value identifying this export log. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async destroyExportLog(id: string, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.destroyExportLog(id, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, /** * * @param {string} id A unique integer value identifying this food. @@ -9567,6 +9903,17 @@ export const ApiApiFp = function(configuration?: Configuration) { const localVarAxiosArgs = await localVarAxiosParamCreator.listCookLogs(page, pageSize, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, + /** + * + * @param {number} [page] A page number within the paginated result set. + * @param {number} [pageSize] Number of results to return per page. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async listExportLogs(page?: number, pageSize?: number, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.listExportLogs(page, pageSize, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, /** * * @param {*} [options] Override http request option. @@ -9620,7 +9967,7 @@ export const ApiApiFp = function(configuration?: Configuration) { * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async listKeywords(query?: string, root?: number, tree?: number, page?: number, pageSize?: number, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + async listKeywords(query?: string, root?: number, tree?: number, page?: number, pageSize?: number, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.listKeywords(query, root, tree, page, pageSize, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, @@ -9679,7 +10026,7 @@ export const ApiApiFp = function(configuration?: Configuration) { * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async listRecipes(query?: string, keywords?: number, foods?: number, units?: number, rating?: number, books?: string, keywordsOr?: string, foodsOr?: string, booksOr?: string, internal?: string, random?: string, _new?: string, page?: number, pageSize?: number, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + async listRecipes(query?: string, keywords?: number, foods?: number, units?: number, rating?: number, books?: string, keywordsOr?: string, foodsOr?: string, booksOr?: string, internal?: string, random?: string, _new?: string, page?: number, pageSize?: number, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.listRecipes(query, keywords, foods, units, rating, books, keywordsOr, foodsOr, booksOr, internal, random, _new, page, pageSize, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, @@ -9722,7 +10069,7 @@ export const ApiApiFp = function(configuration?: Configuration) { * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async listSteps(recipe?: number, query?: string, page?: number, pageSize?: number, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + async listSteps(recipe?: number, query?: string, page?: number, pageSize?: number, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.listSteps(recipe, query, page, pageSize, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, @@ -9742,7 +10089,7 @@ export const ApiApiFp = function(configuration?: Configuration) { * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async listSupermarketCategoryRelations(page?: number, pageSize?: number, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + async listSupermarketCategoryRelations(page?: number, pageSize?: number, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.listSupermarketCategoryRelations(page, pageSize, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, @@ -9771,7 +10118,7 @@ export const ApiApiFp = function(configuration?: Configuration) { * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async listSyncLogs(page?: number, pageSize?: number, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + async listSyncLogs(page?: number, pageSize?: number, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.listSyncLogs(page, pageSize, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, @@ -9792,7 +10139,7 @@ export const ApiApiFp = function(configuration?: Configuration) { * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async listUnits(query?: string, page?: number, pageSize?: number, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + async listUnits(query?: string, page?: number, pageSize?: number, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.listUnits(query, page, pageSize, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, @@ -9830,7 +10177,7 @@ export const ApiApiFp = function(configuration?: Configuration) { * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async listViewLogs(page?: number, pageSize?: number, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + async listViewLogs(page?: number, pageSize?: number, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { const localVarAxiosArgs = await localVarAxiosParamCreator.listViewLogs(page, pageSize, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, @@ -9927,6 +10274,17 @@ export const ApiApiFp = function(configuration?: Configuration) { const localVarAxiosArgs = await localVarAxiosParamCreator.partialUpdateCookLog(id, cookLog, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, + /** + * + * @param {string} id A unique integer value identifying this export log. + * @param {ExportLog} [exportLog] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async partialUpdateExportLog(id: string, exportLog?: ExportLog, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.partialUpdateExportLog(id, exportLog, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, /** * * @param {string} id A unique integer value identifying this food. @@ -10212,6 +10570,16 @@ export const ApiApiFp = function(configuration?: Configuration) { const localVarAxiosArgs = await localVarAxiosParamCreator.retrieveCookLog(id, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, + /** + * + * @param {string} id A unique integer value identifying this export log. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async retrieveExportLog(id: string, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.retrieveExportLog(id, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, /** * * @param {string} id A unique integer value identifying this food. @@ -10517,6 +10885,17 @@ export const ApiApiFp = function(configuration?: Configuration) { const localVarAxiosArgs = await localVarAxiosParamCreator.updateCookLog(id, cookLog, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, + /** + * + * @param {string} id A unique integer value identifying this export log. + * @param {ExportLog} [exportLog] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async updateExportLog(id: string, exportLog?: ExportLog, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.updateExportLog(id, exportLog, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, /** * * @param {string} id A unique integer value identifying this food. @@ -10799,6 +11178,15 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?: createCookLog(cookLog?: CookLog, options?: any): AxiosPromise { return localVarFp.createCookLog(cookLog, options).then((request) => request(axios, basePath)); }, + /** + * + * @param {ExportLog} [exportLog] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + createExportLog(exportLog?: ExportLog, options?: any): AxiosPromise { + return localVarFp.createExportLog(exportLog, options).then((request) => request(axios, basePath)); + }, /** * * @param {Food} [food] @@ -11027,6 +11415,15 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?: destroyCookLog(id: string, options?: any): AxiosPromise { return localVarFp.destroyCookLog(id, options).then((request) => request(axios, basePath)); }, + /** + * + * @param {string} id A unique integer value identifying this export log. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + destroyExportLog(id: string, options?: any): AxiosPromise { + return localVarFp.destroyExportLog(id, options).then((request) => request(axios, basePath)); + }, /** * * @param {string} id A unique integer value identifying this food. @@ -11261,6 +11658,16 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?: listCookLogs(page?: number, pageSize?: number, options?: any): AxiosPromise { return localVarFp.listCookLogs(page, pageSize, options).then((request) => request(axios, basePath)); }, + /** + * + * @param {number} [page] A page number within the paginated result set. + * @param {number} [pageSize] Number of results to return per page. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + listExportLogs(page?: number, pageSize?: number, options?: any): AxiosPromise { + return localVarFp.listExportLogs(page, pageSize, options).then((request) => request(axios, basePath)); + }, /** * * @param {*} [options] Override http request option. @@ -11310,7 +11717,7 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?: * @param {*} [options] Override http request option. * @throws {RequiredError} */ - listKeywords(query?: string, root?: number, tree?: number, page?: number, pageSize?: number, options?: any): AxiosPromise { + listKeywords(query?: string, root?: number, tree?: number, page?: number, pageSize?: number, options?: any): AxiosPromise { return localVarFp.listKeywords(query, root, tree, page, pageSize, options).then((request) => request(axios, basePath)); }, /** @@ -11364,7 +11771,7 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?: * @param {*} [options] Override http request option. * @throws {RequiredError} */ - listRecipes(query?: string, keywords?: number, foods?: number, units?: number, rating?: number, books?: string, keywordsOr?: string, foodsOr?: string, booksOr?: string, internal?: string, random?: string, _new?: string, page?: number, pageSize?: number, options?: any): AxiosPromise { + listRecipes(query?: string, keywords?: number, foods?: number, units?: number, rating?: number, books?: string, keywordsOr?: string, foodsOr?: string, booksOr?: string, internal?: string, random?: string, _new?: string, page?: number, pageSize?: number, options?: any): AxiosPromise { return localVarFp.listRecipes(query, keywords, foods, units, rating, books, keywordsOr, foodsOr, booksOr, internal, random, _new, page, pageSize, options).then((request) => request(axios, basePath)); }, /** @@ -11403,7 +11810,7 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?: * @param {*} [options] Override http request option. * @throws {RequiredError} */ - listSteps(recipe?: number, query?: string, page?: number, pageSize?: number, options?: any): AxiosPromise { + listSteps(recipe?: number, query?: string, page?: number, pageSize?: number, options?: any): AxiosPromise { return localVarFp.listSteps(recipe, query, page, pageSize, options).then((request) => request(axios, basePath)); }, /** @@ -11421,7 +11828,7 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?: * @param {*} [options] Override http request option. * @throws {RequiredError} */ - listSupermarketCategoryRelations(page?: number, pageSize?: number, options?: any): AxiosPromise { + listSupermarketCategoryRelations(page?: number, pageSize?: number, options?: any): AxiosPromise { return localVarFp.listSupermarketCategoryRelations(page, pageSize, options).then((request) => request(axios, basePath)); }, /** @@ -11447,7 +11854,7 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?: * @param {*} [options] Override http request option. * @throws {RequiredError} */ - listSyncLogs(page?: number, pageSize?: number, options?: any): AxiosPromise { + listSyncLogs(page?: number, pageSize?: number, options?: any): AxiosPromise { return localVarFp.listSyncLogs(page, pageSize, options).then((request) => request(axios, basePath)); }, /** @@ -11466,7 +11873,7 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?: * @param {*} [options] Override http request option. * @throws {RequiredError} */ - listUnits(query?: string, page?: number, pageSize?: number, options?: any): AxiosPromise { + listUnits(query?: string, page?: number, pageSize?: number, options?: any): AxiosPromise { return localVarFp.listUnits(query, page, pageSize, options).then((request) => request(axios, basePath)); }, /** @@ -11500,7 +11907,7 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?: * @param {*} [options] Override http request option. * @throws {RequiredError} */ - listViewLogs(page?: number, pageSize?: number, options?: any): AxiosPromise { + listViewLogs(page?: number, pageSize?: number, options?: any): AxiosPromise { return localVarFp.listViewLogs(page, pageSize, options).then((request) => request(axios, basePath)); }, /** @@ -11588,6 +11995,16 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?: partialUpdateCookLog(id: string, cookLog?: CookLog, options?: any): AxiosPromise { return localVarFp.partialUpdateCookLog(id, cookLog, options).then((request) => request(axios, basePath)); }, + /** + * + * @param {string} id A unique integer value identifying this export log. + * @param {ExportLog} [exportLog] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + partialUpdateExportLog(id: string, exportLog?: ExportLog, options?: any): AxiosPromise { + return localVarFp.partialUpdateExportLog(id, exportLog, options).then((request) => request(axios, basePath)); + }, /** * * @param {string} id A unique integer value identifying this food. @@ -11847,6 +12264,15 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?: retrieveCookLog(id: string, options?: any): AxiosPromise { return localVarFp.retrieveCookLog(id, options).then((request) => request(axios, basePath)); }, + /** + * + * @param {string} id A unique integer value identifying this export log. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + retrieveExportLog(id: string, options?: any): AxiosPromise { + return localVarFp.retrieveExportLog(id, options).then((request) => request(axios, basePath)); + }, /** * * @param {string} id A unique integer value identifying this food. @@ -12122,6 +12548,16 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?: updateCookLog(id: string, cookLog?: CookLog, options?: any): AxiosPromise { return localVarFp.updateCookLog(id, cookLog, options).then((request) => request(axios, basePath)); }, + /** + * + * @param {string} id A unique integer value identifying this export log. + * @param {ExportLog} [exportLog] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + updateExportLog(id: string, exportLog?: ExportLog, options?: any): AxiosPromise { + return localVarFp.updateExportLog(id, exportLog, options).then((request) => request(axios, basePath)); + }, /** * * @param {string} id A unique integer value identifying this food. @@ -12388,6 +12824,17 @@ export class ApiApi extends BaseAPI { return ApiApiFp(this.configuration).createCookLog(cookLog, options).then((request) => request(this.axios, this.basePath)); } + /** + * + * @param {ExportLog} [exportLog] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof ApiApi + */ + public createExportLog(exportLog?: ExportLog, options?: any) { + return ApiApiFp(this.configuration).createExportLog(exportLog, options).then((request) => request(this.axios, this.basePath)); + } + /** * * @param {Food} [food] @@ -12666,6 +13113,17 @@ export class ApiApi extends BaseAPI { return ApiApiFp(this.configuration).destroyCookLog(id, options).then((request) => request(this.axios, this.basePath)); } + /** + * + * @param {string} id A unique integer value identifying this export log. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof ApiApi + */ + public destroyExportLog(id: string, options?: any) { + return ApiApiFp(this.configuration).destroyExportLog(id, options).then((request) => request(this.axios, this.basePath)); + } + /** * * @param {string} id A unique integer value identifying this food. @@ -12952,6 +13410,18 @@ export class ApiApi extends BaseAPI { return ApiApiFp(this.configuration).listCookLogs(page, pageSize, options).then((request) => request(this.axios, this.basePath)); } + /** + * + * @param {number} [page] A page number within the paginated result set. + * @param {number} [pageSize] Number of results to return per page. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof ApiApi + */ + public listExportLogs(page?: number, pageSize?: number, options?: any) { + return ApiApiFp(this.configuration).listExportLogs(page, pageSize, options).then((request) => request(this.axios, this.basePath)); + } + /** * * @param {*} [options] Override http request option. @@ -13345,6 +13815,18 @@ export class ApiApi extends BaseAPI { return ApiApiFp(this.configuration).partialUpdateCookLog(id, cookLog, options).then((request) => request(this.axios, this.basePath)); } + /** + * + * @param {string} id A unique integer value identifying this export log. + * @param {ExportLog} [exportLog] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof ApiApi + */ + public partialUpdateExportLog(id: string, exportLog?: ExportLog, options?: any) { + return ApiApiFp(this.configuration).partialUpdateExportLog(id, exportLog, options).then((request) => request(this.axios, this.basePath)); + } + /** * * @param {string} id A unique integer value identifying this food. @@ -13656,6 +14138,17 @@ export class ApiApi extends BaseAPI { return ApiApiFp(this.configuration).retrieveCookLog(id, options).then((request) => request(this.axios, this.basePath)); } + /** + * + * @param {string} id A unique integer value identifying this export log. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof ApiApi + */ + public retrieveExportLog(id: string, options?: any) { + return ApiApiFp(this.configuration).retrieveExportLog(id, options).then((request) => request(this.axios, this.basePath)); + } + /** * * @param {string} id A unique integer value identifying this food. @@ -13991,6 +14484,18 @@ export class ApiApi extends BaseAPI { return ApiApiFp(this.configuration).updateCookLog(id, cookLog, options).then((request) => request(this.axios, this.basePath)); } + /** + * + * @param {string} id A unique integer value identifying this export log. + * @param {ExportLog} [exportLog] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof ApiApi + */ + public updateExportLog(id: string, exportLog?: ExportLog, options?: any) { + return ApiApiFp(this.configuration).updateExportLog(id, exportLog, options).then((request) => request(this.axios, this.basePath)); + } + /** * * @param {string} id A unique integer value identifying this food. diff --git a/vue/vue.config.js b/vue/vue.config.js index 01460e044..a94390ce8 100644 --- a/vue/vue.config.js +++ b/vue/vue.config.js @@ -17,6 +17,14 @@ const pages = { entry: "./src/apps/ImportResponseView/main.js", chunks: ["chunk-vendors"], }, + export_response_view: { + entry: "./src/apps/ExportResponseView/main.js", + chunks: ["chunk-vendors"], + }, + export_view: { + entry: "./src/apps/ExportView/main.js", + chunks: ["chunk-vendors"], + }, supermarket_view: { entry: "./src/apps/SupermarketView/main.js", chunks: ["chunk-vendors"], From 08e6833c129544e15b5c244c354057b82fa3aae8 Mon Sep 17 00:00:00 2001 From: TiagoRascazzi <47132532+TiagoRascazzi@users.noreply.github.com> Date: Sat, 8 Jan 2022 11:10:48 -0500 Subject: [PATCH 02/30] Removed comment --- cookbook/forms.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/cookbook/forms.py b/cookbook/forms.py index 31f3850f2..c7a03706f 100644 --- a/cookbook/forms.py +++ b/cookbook/forms.py @@ -167,7 +167,6 @@ class ImportExportBase(forms.Form): )) -#TODO deprecated class ImportForm(ImportExportBase): files = forms.FileField(required=True, widget=forms.ClearableFileInput(attrs={'multiple': True})) duplicates = forms.BooleanField(help_text=_( @@ -175,7 +174,6 @@ class ImportForm(ImportExportBase): required=False) -#TODO deprecated class ExportForm(ImportExportBase): recipes = forms.ModelMultipleChoiceField(widget=MultiSelectWidget, queryset=Recipe.objects.none(), required=False) all = forms.BooleanField(required=False) From 33d1022a738c5b1843cee1e00807dc63e5288fef Mon Sep 17 00:00:00 2001 From: Tiago Rascazzi Date: Sat, 8 Jan 2022 12:30:42 -0500 Subject: [PATCH 03/30] Increase number of result for multiselect in export --- vue/src/apps/ExportView/ExportView.vue | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/vue/src/apps/ExportView/ExportView.vue b/vue/src/apps/ExportView/ExportView.vue index a9bbaceb7..160cbe9e2 100644 --- a/vue/src/apps/ExportView/ExportView.vue +++ b/vue/src/apps/ExportView/ExportView.vue @@ -115,7 +115,9 @@ export default { let apiFactory = new ApiApiFactory() this.recipes_loading = true - apiFactory.listRecipes(query).then((response) => { + + let maxResultLenght = 1000 + apiFactory.listRecipes(query, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, 1, maxResultLenght).then((response) => { this.recipes = response.data.results; this.recipes_loading = false }).catch((err) => { From 7c48c13dce07d8590e64bb1ba92570c64658a2ab Mon Sep 17 00:00:00 2001 From: Tiago Rascazzi Date: Sat, 8 Jan 2022 13:37:26 -0500 Subject: [PATCH 04/30] Added export from url args --- cookbook/templates/export.html | 2 +- cookbook/views/import_export.py | 6 ++---- vue/src/apps/ExportView/ExportView.vue | 24 +++++++++++++++++++++--- 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/cookbook/templates/export.html b/cookbook/templates/export.html index ca5148235..787fc81c0 100644 --- a/cookbook/templates/export.html +++ b/cookbook/templates/export.html @@ -24,7 +24,7 @@ {% render_bundle 'export_view' %} diff --git a/cookbook/views/import_export.py b/cookbook/views/import_export.py index c1530b000..6e0a7b3ed 100644 --- a/cookbook/views/import_export.py +++ b/cookbook/views/import_export.py @@ -143,14 +143,12 @@ def export_recipe(request): ) else: - form = ExportForm(space=request.space) recipe = request.GET.get('r') if recipe: if re.match(r'^([0-9])+$', recipe): - if recipe := Recipe.objects.filter(pk=int(recipe), space=request.space).first(): - form = ExportForm(initial={'recipes': recipe}, space=request.space) + recipe = Recipe.objects.filter(pk=int(recipe), space=request.space).first() - return render(request, 'export.html', {'form': form}) + return render(request, 'export.html', {'pk': recipe.pk}) @group_required('user') diff --git a/vue/src/apps/ExportView/ExportView.vue b/vue/src/apps/ExportView/ExportView.vue index 160cbe9e2..efe0538be 100644 --- a/vue/src/apps/ExportView/ExportView.vue +++ b/vue/src/apps/ExportView/ExportView.vue @@ -93,8 +93,7 @@ export default { components: {Multiselect}, data() { return { - import_id: window.EXPORT_ID, - import_info: undefined, + export_id: window.EXPORT_ID, loading: false, disabled_multiselect: false, @@ -107,11 +106,29 @@ export default { }, mounted() { - this.searchRecipes('') + this.insertRequested() + }, methods: { + insertRequested: function(){ + + let apiFactory = new ApiApiFactory() + + this.recipes_loading = true + + apiFactory.retrieveRecipe(this.export_id).then((response) => { + this.recipes_loading = false + this.recipe_list.push(response.data) + + }).catch((err) => { + console.log(err) + StandardToasts.makeStandardToast(StandardToasts.FAIL_FETCH) + }).then(e => this.searchRecipes('')) + }, + searchRecipes: function (query) { + let apiFactory = new ApiApiFactory() this.recipes_loading = true @@ -120,6 +137,7 @@ export default { apiFactory.listRecipes(query, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, 1, maxResultLenght).then((response) => { this.recipes = response.data.results; this.recipes_loading = false + }).catch((err) => { console.log(err) StandardToasts.makeStandardToast(StandardToasts.FAIL_FETCH) From 16b357e11e512b1e55e119fb7117bef357db0e8c Mon Sep 17 00:00:00 2001 From: Tiago Rascazzi Date: Sat, 8 Jan 2022 14:44:28 -0500 Subject: [PATCH 05/30] Added printReady selector --- cookbook/integration/pdfexport.py | 2 +- cookbook/views/import_export.py | 5 +++-- vue/src/apps/ExportView/ExportView.vue | 7 ++++--- vue/src/apps/RecipeView/RecipeView.vue | 13 ++++++++++++- 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/cookbook/integration/pdfexport.py b/cookbook/integration/pdfexport.py index 9f500480a..20c948a3e 100644 --- a/cookbook/integration/pdfexport.py +++ b/cookbook/integration/pdfexport.py @@ -51,7 +51,7 @@ class PDFexport(Integration): await page.setCookie(cookies) await page.goto('http://'+cmd.default_addr+':'+cmd.default_port+'/view/recipe/'+str(recipe.id), {'waitUntil': 'domcontentloaded'}) - await page.waitForSelector('h3'); + await page.waitForSelector('#printReady'); files.append([recipe.name + '.pdf', await page.pdf(options)]) await page.close(); diff --git a/cookbook/views/import_export.py b/cookbook/views/import_export.py index 6e0a7b3ed..9d8fec206 100644 --- a/cookbook/views/import_export.py +++ b/cookbook/views/import_export.py @@ -143,12 +143,13 @@ def export_recipe(request): ) else: + pk = '' recipe = request.GET.get('r') if recipe: if re.match(r'^([0-9])+$', recipe): - recipe = Recipe.objects.filter(pk=int(recipe), space=request.space).first() + pk = Recipe.objects.filter(pk=int(recipe), space=request.space).first().pk - return render(request, 'export.html', {'pk': recipe.pk}) + return render(request, 'export.html', {'pk': pk}) @group_required('user') diff --git a/vue/src/apps/ExportView/ExportView.vue b/vue/src/apps/ExportView/ExportView.vue index efe0538be..5db26da29 100644 --- a/vue/src/apps/ExportView/ExportView.vue +++ b/vue/src/apps/ExportView/ExportView.vue @@ -105,9 +105,10 @@ export default { } }, mounted() { - - this.insertRequested() - + if(this.export_id) + this.insertRequested() + else + this.searchRecipes('') }, methods: { diff --git a/vue/src/apps/RecipeView/RecipeView.vue b/vue/src/apps/RecipeView/RecipeView.vue index 7cf1932a9..f4c25cd36 100644 --- a/vue/src/apps/RecipeView/RecipeView.vue +++ b/vue/src/apps/RecipeView/RecipeView.vue @@ -107,7 +107,7 @@
- +
@@ -246,6 +246,9 @@ export default { this.start_time = moment().format("yyyy-MM-DDTHH:mm") } + + if(recipe.image === null) this.printReady() + this.recipe = this.rootrecipe = recipe this.servings = this.servings_cache[this.rootrecipe.id] = recipe.servings this.loading = false @@ -272,6 +275,14 @@ export default { this.servings = this.servings_cache?.[e.id] ?? e.servings } }, + printReady: function(){ + const template = document.createElement("template"); + template.id = "printReady"; + document.body.appendChild(template); + }, + onImgLoad: function(){ + this.printReady() + }, }, } From fef9bcb1e115dbd9b8b53e2f115ef53d2a07d285 Mon Sep 17 00:00:00 2001 From: Tiago Rascazzi Date: Tue, 11 Jan 2022 15:44:10 -0500 Subject: [PATCH 06/30] Added date to filename --- cookbook/integration/default.py | 4 ++-- cookbook/integration/integration.py | 21 +++++++++++++-------- cookbook/integration/pdfexport.py | 2 +- cookbook/integration/recipesage.py | 4 ++-- cookbook/integration/saffron.py | 2 +- 5 files changed, 19 insertions(+), 14 deletions(-) diff --git a/cookbook/integration/default.py b/cookbook/integration/default.py index 6a2b34102..da72dd71c 100644 --- a/cookbook/integration/default.py +++ b/cookbook/integration/default.py @@ -61,9 +61,9 @@ class Default(Integration): export_zip_obj.writestr(str(r.pk) + '.zip', recipe_zip_stream.getvalue()) el.exported_recipes += 1 - el.msg += self.recipe_processed_msg(r) + el.msg += self.get_recipe_processed_msg(r) el.save() export_zip_obj.close() - return [[ 'export.zip', export_zip_stream.getvalue() ]] \ No newline at end of file + return [[ self.get_export_file_name(), export_zip_stream.getvalue() ]] \ No newline at end of file diff --git a/cookbook/integration/integration.py b/cookbook/integration/integration.py index 11524a382..9048f4130 100644 --- a/cookbook/integration/integration.py +++ b/cookbook/integration/integration.py @@ -6,7 +6,7 @@ import uuid from io import BytesIO, StringIO from zipfile import BadZipFile, ZipFile from django.core.cache import cache - +import datetime from bs4 import Tag from django.core.exceptions import ObjectDoesNotExist @@ -82,7 +82,8 @@ class Integration: export_file = file else: - export_filename = "export.zip" + #zip the files if there is more then one file + export_filename = self.get_export_file_name() export_stream = BytesIO() export_obj = ZipFile(export_stream, 'w') @@ -134,7 +135,7 @@ class Integration: for d in data_list: recipe = self.get_recipe_from_file(d) recipe.keywords.add(self.keyword) - il.msg += self.recipe_processed_msg(recipe) + il.msg += self.get_recipe_processed_msg(recipe) self.handle_duplicates(recipe, import_duplicates) il.imported_recipes += 1 il.save() @@ -159,7 +160,7 @@ class Integration: else: recipe = self.get_recipe_from_file(BytesIO(import_zip.read(z.filename))) recipe.keywords.add(self.keyword) - il.msg += self.recipe_processed_msg(recipe) + il.msg += self.get_recipe_processed_msg(recipe) self.handle_duplicates(recipe, import_duplicates) il.imported_recipes += 1 il.save() @@ -174,7 +175,7 @@ class Integration: try: recipe = self.get_recipe_from_file(d) recipe.keywords.add(self.keyword) - il.msg += self.recipe_processed_msg(recipe) + il.msg += self.get_recipe_processed_msg(recipe) self.handle_duplicates(recipe, import_duplicates) il.imported_recipes += 1 il.save() @@ -191,7 +192,7 @@ class Integration: try: recipe = self.get_recipe_from_file(d) recipe.keywords.add(self.keyword) - il.msg += self.recipe_processed_msg(recipe) + il.msg += self.get_recipe_processed_msg(recipe) self.handle_duplicates(recipe, import_duplicates) il.imported_recipes += 1 il.save() @@ -201,7 +202,7 @@ class Integration: else: recipe = self.get_recipe_from_file(f['file']) recipe.keywords.add(self.keyword) - il.msg += self.recipe_processed_msg(recipe) + il.msg += self.get_recipe_processed_msg(recipe) self.handle_duplicates(recipe, import_duplicates) except BadZipFile: il.msg += 'ERROR ' + _( @@ -289,5 +290,9 @@ class Integration: if DEBUG: traceback.print_exc() - def recipe_processed_msg(self, recipe): + + def get_export_file_name(self, format='zip'): + return "export_{}.{}".format(datetime.datetime.now().strftime("%Y-%m-%d"), format) + + def get_recipe_processed_msg(self, recipe): return f'{recipe.pk} - {recipe.name} \n' diff --git a/cookbook/integration/pdfexport.py b/cookbook/integration/pdfexport.py index 20c948a3e..fca782473 100644 --- a/cookbook/integration/pdfexport.py +++ b/cookbook/integration/pdfexport.py @@ -57,7 +57,7 @@ class PDFexport(Integration): await page.close(); el.exported_recipes += 1 - el.msg += self.recipe_processed_msg(recipe) + el.msg += self.get_recipe_processed_msg(recipe) await sync_to_async(el.save, thread_sensitive=True)() diff --git a/cookbook/integration/recipesage.py b/cookbook/integration/recipesage.py index 93dee8858..0bc6704be 100644 --- a/cookbook/integration/recipesage.py +++ b/cookbook/integration/recipesage.py @@ -94,10 +94,10 @@ class RecipeSage(Integration): json_list.append(self.get_file_from_recipe(r)) el.exported_recipes += 1 - el.msg += self.recipe_processed_msg(r) + el.msg += self.get_recipe_processed_msg(r) el.save() - return [['export.json', json.dumps(json_list)]] + return [[self.get_export_file_name('json'), json.dumps(json_list)]] def split_recipe_file(self, file): return json.loads(file.read().decode("utf-8")) diff --git a/cookbook/integration/saffron.py b/cookbook/integration/saffron.py index 63342b2bb..058f2a8f7 100644 --- a/cookbook/integration/saffron.py +++ b/cookbook/integration/saffron.py @@ -94,7 +94,7 @@ class Saffron(Integration): files.append([ filename, data ]) el.exported_recipes += 1 - el.msg += self.recipe_processed_msg(r) + el.msg += self.get_recipe_processed_msg(r) el.save() return files \ No newline at end of file From 74e88218d5007e76d98938c5e5e0b62cc8e5d355 Mon Sep 17 00:00:00 2001 From: Ben Scobie Date: Tue, 1 Feb 2022 21:05:04 +0000 Subject: [PATCH 07/30] Keep screen awake when viewing recipe --- vue/src/apps/RecipeView/RecipeView.vue | 30 ++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/vue/src/apps/RecipeView/RecipeView.vue b/vue/src/apps/RecipeView/RecipeView.vue index 49f152406..80e0be8b7 100644 --- a/vue/src/apps/RecipeView/RecipeView.vue +++ b/vue/src/apps/RecipeView/RecipeView.vue @@ -208,6 +208,7 @@ export default { servings_cache: {}, start_time: "", share_uid: window.SHARE_UID, + wake_lock: null, } }, watch: { @@ -218,8 +219,37 @@ export default { mounted() { this.loadRecipe(window.RECIPE_ID) this.$i18n.locale = window.CUSTOM_LOCALE + this.requestWakeLock() + }, + beforeUnmount() { + this.destroyWakeLock() }, methods: { + requestWakeLock: async function() { + if ('wakeLock' in navigator) { + try { + this.wake_lock = await navigator.wakeLock.request('screen') + document.addEventListener('visibilitychange', this.visibilityChange) + } catch (err) { + console.log(err) + } + } + }, + destroyWakeLock: function() { + if (this.wake_lock != null) { + this.wake_lock.release() + .then(() => { + this.wake_lock = null + }); + } + + document.removeEventListener('visibilitychange', this.visibilityChange) + }, + visibilityChange: async function() { + if (this.wake_lock != null && document.visibilityState === 'visible') { + await this.requestWakeLock() + } + }, loadRecipe: function (recipe_id) { apiLoadRecipe(recipe_id).then((recipe) => { if (window.USER_SERVINGS !== 0) { From d45e3b8e60e1c03bc4bc2fdecbe35d2b47c0d55d Mon Sep 17 00:00:00 2001 From: vabene1111 Date: Thu, 3 Feb 2022 18:00:02 +0100 Subject: [PATCH 08/30] some tweaks for new exporter --- .directory | 3 -- .env.template | 4 +- .../0165_exportlog_cache_duration.py | 18 --------- .../0166_exportlog_possibly_not_expired.py | 18 --------- .../{0164_exportlog.py => 0169_exportlog.py} | 6 ++- cookbook/tests/other/test_export.py | 25 ++++++++++++ cookbook/urls.py | 2 +- cookbook/views/import_export.py | 20 +++++----- docs/features/import_export.md | 38 +++++++++---------- vue/src/apps/ExportView/ExportView.vue | 28 ++++---------- 10 files changed, 69 insertions(+), 93 deletions(-) delete mode 100644 .directory delete mode 100644 cookbook/migrations/0165_exportlog_cache_duration.py delete mode 100644 cookbook/migrations/0166_exportlog_possibly_not_expired.py rename cookbook/migrations/{0164_exportlog.py => 0169_exportlog.py} (83%) create mode 100644 cookbook/tests/other/test_export.py diff --git a/.directory b/.directory deleted file mode 100644 index 280ecee95..000000000 --- a/.directory +++ /dev/null @@ -1,3 +0,0 @@ -[Dolphin] -Timestamp=2022,1,7,19,23,46.14 -Version=4 diff --git a/.env.template b/.env.template index e825cbbfe..8a9ccc480 100644 --- a/.env.template +++ b/.env.template @@ -152,6 +152,6 @@ REVERSE_PROXY_AUTH=0 # Disabled by default, uncomment to enable # ENABLE_PDF_EXPORT=1 -# Duration to keep the cached export file (in seconds) -EXPORT_FILE_CACHE_DURATION=300 +# Recipe exports are cached for a certain time by default, adjust time if needed +# EXPORT_FILE_CACHE_DURATION=600 diff --git a/cookbook/migrations/0165_exportlog_cache_duration.py b/cookbook/migrations/0165_exportlog_cache_duration.py deleted file mode 100644 index 8005a4b2d..000000000 --- a/cookbook/migrations/0165_exportlog_cache_duration.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.2.11 on 2022-01-08 00:10 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('cookbook', '0164_exportlog'), - ] - - operations = [ - migrations.AddField( - model_name='exportlog', - name='cache_duration', - field=models.IntegerField(default=0), - ), - ] diff --git a/cookbook/migrations/0166_exportlog_possibly_not_expired.py b/cookbook/migrations/0166_exportlog_possibly_not_expired.py deleted file mode 100644 index 15e19e13b..000000000 --- a/cookbook/migrations/0166_exportlog_possibly_not_expired.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.2.11 on 2022-01-08 00:43 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('cookbook', '0165_exportlog_cache_duration'), - ] - - operations = [ - migrations.AddField( - model_name='exportlog', - name='possibly_not_expired', - field=models.BooleanField(default=True), - ), - ] diff --git a/cookbook/migrations/0164_exportlog.py b/cookbook/migrations/0169_exportlog.py similarity index 83% rename from cookbook/migrations/0164_exportlog.py rename to cookbook/migrations/0169_exportlog.py index 952c9372f..0fa6c20eb 100644 --- a/cookbook/migrations/0164_exportlog.py +++ b/cookbook/migrations/0169_exportlog.py @@ -1,4 +1,4 @@ -# Generated by Django 3.2.11 on 2022-01-07 20:29 +# Generated by Django 3.2.11 on 2022-02-03 15:03 import cookbook.models from django.conf import settings @@ -10,7 +10,7 @@ class Migration(migrations.Migration): dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('cookbook', '0163_auto_20220105_0758'), + ('cookbook', '0168_add_unit_searchfields'), ] operations = [ @@ -23,6 +23,8 @@ class Migration(migrations.Migration): ('msg', models.TextField(default='')), ('total_recipes', models.IntegerField(default=0)), ('exported_recipes', models.IntegerField(default=0)), + ('cache_duration', models.IntegerField(default=0)), + ('possibly_not_expired', models.BooleanField(default=True)), ('created_at', models.DateTimeField(auto_now_add=True)), ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), ('space', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cookbook.space')), diff --git a/cookbook/tests/other/test_export.py b/cookbook/tests/other/test_export.py new file mode 100644 index 000000000..995c0cce6 --- /dev/null +++ b/cookbook/tests/other/test_export.py @@ -0,0 +1,25 @@ +import pytest +from django.contrib import auth +from django.urls import reverse + +from cookbook.forms import ImportExportBase +from cookbook.helper.ingredient_parser import IngredientParser +from cookbook.models import ExportLog + + +@pytest.fixture +def obj_1(space_1, u1_s1): + return ExportLog.objects.create(type=ImportExportBase.DEFAULT, running=False, created_by=auth.get_user(u1_s1), space=space_1, exported_recipes=10, total_recipes=10) + + +@pytest.mark.parametrize("arg", [ + ['a_u', 302], + ['g1_s1', 302], + ['u1_s1', 200], + ['a1_s1', 200], + ['u1_s2', 404], + ['a1_s2', 404], +]) +def test_export_file_cache(arg, request, obj_1): + c = request.getfixturevalue(arg[0]) + assert c.get(reverse('view_export_file', args=[obj_1.pk])).status_code == arg[1] diff --git a/cookbook/urls.py b/cookbook/urls.py index ac7dd838c..bd3849898 100644 --- a/cookbook/urls.py +++ b/cookbook/urls.py @@ -73,7 +73,7 @@ urlpatterns = [ path('abuse/', views.report_share_abuse, name='view_report_share_abuse'), path('import/', import_export.import_recipe, name='view_import'), - path('import-response//', import_export.import_response, name='view_import_response'),\ + path('import-response//', import_export.import_response, name='view_import_response'), path('export/', import_export.export_recipe, name='view_export'), path('export-response//', import_export.export_response, name='view_export_response'), path('export-file//', import_export.export_file, name='view_export_file'), diff --git a/cookbook/views/import_export.py b/cookbook/views/import_export.py index 9d8fec206..1974c18da 100644 --- a/cookbook/views/import_export.py +++ b/cookbook/views/import_export.py @@ -5,7 +5,7 @@ from django.core.cache import cache from django.contrib import messages from django.http import HttpResponse, HttpResponseRedirect, JsonResponse -from django.shortcuts import render +from django.shortcuts import render, get_object_or_404 from django.urls import reverse from django.utils.translation import gettext as _ @@ -125,7 +125,10 @@ def export_recipe(request): recipes = Recipe.objects.filter(space=request.space, internal=True).all() integration = get_integration(request, form.cleaned_data['type']) - + + if form.cleaned_data['type'] == ImportExportBase.PDF and not settings.ENABLE_PDF_EXPORT: + return JsonResponse({'error': _('The PDF Exporter is not enabled on this instance as it is still in an experimental state.')}) + el = ExportLog.objects.create(type=form.cleaned_data['type'], created_by=request.user, space=request.space) t = threading.Thread(target=integration.do_export, args=[recipes, el]) @@ -141,7 +144,6 @@ def export_recipe(request): }, status=400 ) - else: pk = '' recipe = request.GET.get('r') @@ -156,6 +158,7 @@ def export_recipe(request): def import_response(request, pk): return render(request, 'import_response.html', {'pk': pk}) + @group_required('user') def export_response(request, pk): return render(request, 'export_response.html', {'pk': pk}) @@ -163,16 +166,15 @@ def export_response(request, pk): @group_required('user') def export_file(request, pk): - - cacheData = cache.get('export_file_'+str(pk)) + el = get_object_or_404(ExportLog, pk=pk, space=request.space) + + cacheData = cache.get(f'export_file_{el.pk}') if cacheData is None: - el = ExportLog.objects.get(pk=pk) - el.possibly_not_expired = False; + el.possibly_not_expired = False el.save() return render(request, 'export_response.html', {'pk': pk}) response = HttpResponse(cacheData['file'], content_type='application/force-download') - response['Content-Disposition'] = 'attachment; filename="'+cacheData['filename']+'"' + response['Content-Disposition'] = 'attachment; filename="' + cacheData['filename'] + '"' return response - diff --git a/docs/features/import_export.md b/docs/features/import_export.md index d551ccedd..98424158d 100644 --- a/docs/features/import_export.md +++ b/docs/features/import_export.md @@ -21,26 +21,26 @@ if your favorite one is missing. Overview of the capabilities of the different integrations. | Integration | Import | Export | Images | -|--------------------| ------ | ------ | ------ | -| Default | ✔️ | ✔️ | ✔️ | -| Nextcloud | ✔️ | ⌚ | ✔️ | -| Mealie | ✔️ | ⌚ | ✔️ | -| Chowdown | ✔️ | ⌚ | ✔️ | -| Safron | ✔️ | ✔ | ❌ | -| Paprika | ✔️ | ⌚ | ✔️ | -| ChefTap | ✔️ | ❌ | ❌ | -| Pepperplate | ✔️ | ⌚ | ❌ | -| RecipeSage | ✔️ | ✔️ | ✔️ | -| Domestica | ✔️ | ⌚ | ✔️ | -| MealMaster | ✔️ | ❌ | ❌ | -| RezKonv | ✔️ | ❌ | ❌ | -| OpenEats | ✔️ | ❌ | ⌚ | -| Plantoeat | ✔️ | ❌ | ✔ | -| CookBookApp | ✔️ | ⌚ | ✔️ | -| CopyMeThat | ✔️ | ❌ | ✔️ | -| PDF (experimental) | ⌚️ | ✔ | ✔️ | +|--------------------| ------ | -- | ------ | +| Default | ✔️ | ✔️ | ✔️ | +| Nextcloud | ✔️ | ⌚ | ✔️ | +| Mealie | ✔️ | ⌚ | ✔️ | +| Chowdown | ✔️ | ⌚ | ✔️ | +| Safron | ✔️ | ✔️ | ❌ | +| Paprika | ✔️ | ⌚ | ✔️ | +| ChefTap | ✔️ | ❌ | ❌ | +| Pepperplate | ✔️ | ⌚ | ❌ | +| RecipeSage | ✔️ | ✔️ | ✔️ | +| Domestica | ✔️ | ⌚ | ✔️ | +| MealMaster | ✔️ | ❌ | ❌ | +| RezKonv | ✔️ | ❌ | ❌ | +| OpenEats | ✔️ | ❌ | ⌚ | +| Plantoeat | ✔️ | ❌ | ✔ | +| CookBookApp | ✔️ | ⌚ | ✔️ | +| CopyMeThat | ✔️ | ❌ | ✔️ | +| PDF (experimental) | ⌚️ | ✔️ | ✔️ | -✔ = implemented, ❌ = not implemented and not possible/planned, ⌚ = not yet implemented +✔️ = implemented, ❌ = not implemented and not possible/planned, ⌚ = not yet implemented ## Default The default integration is the built in (and preferred) way to import and export recipes. diff --git a/vue/src/apps/ExportView/ExportView.vue b/vue/src/apps/ExportView/ExportView.vue index 5db26da29..ca55b7206 100644 --- a/vue/src/apps/ExportView/ExportView.vue +++ b/vue/src/apps/ExportView/ExportView.vue @@ -9,24 +9,9 @@
@@ -53,8 +38,6 @@ @search-change="searchRecipes"> - -
@@ -164,14 +147,17 @@ export default { axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded'; axios.post(resolveDjangoUrl('view_export',), formData).then((response) => { - - window.location.href = resolveDjangoUrl('view_export_response', response.data['export_id']) + if (response.data['error'] !== undefined){ + makeToast(this.$t("Error"), response.data['error'],"warning") + }else{ + window.location.href = resolveDjangoUrl('view_export_response', response.data['export_id']) + } }).catch((err) => { this.error = err.data this.loading = false console.log(err) - makeToast(this.$t("Error"), this.$t("There was an error loading a resource!"), "danger") + makeToast(this.$t("Error"), this.$t("There was an error loading a resource!"), "warning") }) }, From 7f427c2d1f5935af775e9398cb99f53daeb58085 Mon Sep 17 00:00:00 2001 From: vabene1111 Date: Fri, 4 Feb 2022 16:51:16 +0100 Subject: [PATCH 09/30] validate 0 servings in frontend --- vue/src/apps/RecipeEditView/RecipeEditView.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vue/src/apps/RecipeEditView/RecipeEditView.vue b/vue/src/apps/RecipeEditView/RecipeEditView.vue index 72378251d..547c2fbfb 100644 --- a/vue/src/apps/RecipeEditView/RecipeEditView.vue +++ b/vue/src/apps/RecipeEditView/RecipeEditView.vue @@ -736,8 +736,8 @@ export default { } this.recipe.servings = Math.floor(this.recipe.servings) // temporary fix until a proper framework for frontend input validation is established - if (this.recipe.servings === "" || isNaN(this.recipe.servings)) { - this.recipe.servings = 0 + if (this.recipe.servings === "" || isNaN(this.recipe.servings) || this.recipe.servings===0 ) { + this.recipe.servings = 1 } apiFactory From fd026154d8d70b8d75171e59c1d6aa17f3a3f1e5 Mon Sep 17 00:00:00 2001 From: vabene1111 Date: Fri, 4 Feb 2022 17:11:35 +0100 Subject: [PATCH 10/30] changed step header default and fixed ingredient rendering error --- vue/src/apps/RecipeEditView/RecipeEditView.vue | 2 +- vue/src/components/IngredientsCard.vue | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/vue/src/apps/RecipeEditView/RecipeEditView.vue b/vue/src/apps/RecipeEditView/RecipeEditView.vue index 547c2fbfb..4521f1b67 100644 --- a/vue/src/apps/RecipeEditView/RecipeEditView.vue +++ b/vue/src/apps/RecipeEditView/RecipeEditView.vue @@ -791,7 +791,7 @@ export default { let empty_step = { instruction: "", ingredients: [], - show_as_header: true, + show_as_header: false, time_visible: false, ingredients_visible: true, instruction_visible: true, diff --git a/vue/src/components/IngredientsCard.vue b/vue/src/components/IngredientsCard.vue index d1a8307d3..1036d6fa5 100644 --- a/vue/src/components/IngredientsCard.vue +++ b/vue/src/components/IngredientsCard.vue @@ -132,6 +132,7 @@ export default { let ingredient_list = this.steps .map((x) => x.ingredients) .flat() + .filter((x) => (x.food !== null && x.food !== undefined)) .map((x) => x.food.id) let params = { @@ -231,7 +232,7 @@ export default { ...i, shop: checked, shopping_status: shopping_status, // possible values: true, false, null - category: i.food.supermarket_category?.name, + category: i.food?.supermarket_category?.name, shopping_list: shopping.map((x) => { return { mealplan: x?.recipe_mealplan?.name, From 262a1f0064f8632b828f0bdfc8be2862b587196d Mon Sep 17 00:00:00 2001 From: Tomasz Klimczak Date: Thu, 3 Feb 2022 18:28:25 +0000 Subject: [PATCH 11/30] Translated using Weblate (Polish) Currently translated at 99.6% (297 of 298 strings) Translation: Tandoor/Recipes Frontend Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/pl/ --- vue/src/locales/pl.json | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/vue/src/locales/pl.json b/vue/src/locales/pl.json index 9d2b49df3..eb1243d4c 100644 --- a/vue/src/locales/pl.json +++ b/vue/src/locales/pl.json @@ -284,5 +284,18 @@ "related_recipes": "Powiązane przepisy", "today_recipes": "Dzisiejsze przepisy", "Search Settings": "Ustawienia wyszukiwania", - "Pin": "Pin" + "Pin": "Pin", + "left_handed_help": "Zoptymalizuje interfejs użytkownika do użytku lewą ręką.", + "food_recipe_help": "Powiązanie tutaj przepisu będzie skutkowało połączenie przepisu z każdym innym przepisem, który używa tego jedzenia", + "Foods": "Żywność", + "view_recipe": "Zobacz przepis", + "left_handed": "Tryb dla leworęcznych", + "OnHand_help": "Żywność jest w spiżarni i nie zostanie automatycznie dodana do listy zakupów.", + "ignore_shopping_help": "Nigdy nie dodawaj żywności do listy zakupów (np. wody)", + "shopping_category_help": "Z supermarketów można zamawiać i filtrować według kategorii zakupów zgodnie z układem alejek.", + "review_shopping": "Przejrzyj wpisy zakupów przed zapisaniem", + "sql_debug": "Debugowanie SQL", + "remember_search": "Zapamiętaj wyszukiwanie", + "remember_hours": "Godziny do zapamiętania", + "tree_select": "Użyj drzewa wyboru" } From d2a64093819bcd6c2dfbd5599b68342517351ad6 Mon Sep 17 00:00:00 2001 From: Kristof Mattei <864376+kristof-mattei@users.noreply.github.com> Date: Fri, 4 Feb 2022 14:02:50 -0800 Subject: [PATCH 12/30] Fixed parsing error, `}` should've been included --- cookbook/templates/settings.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cookbook/templates/settings.html b/cookbook/templates/settings.html index 2d9d15834..3e2314625 100644 --- a/cookbook/templates/settings.html +++ b/cookbook/templates/settings.html @@ -261,7 +261,8 @@ { $('#div_id_shopping-mealplan_autoexclude_onhand').hide(); $('#div_id_shopping-mealplan_autoinclude_related').hide(); - } {% endcomment %} + } } + {% endcomment %} {% endblock %} From 3b946e512c4788b56717e1015c07942b47074157 Mon Sep 17 00:00:00 2001 From: Kristof Mattei <864376+kristof-mattei@users.noreply.github.com> Date: Fri, 4 Feb 2022 14:04:05 -0800 Subject: [PATCH 13/30] Aligned formatting. --- cookbook/templates/settings.html | 36 ++++++++++++++++---------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/cookbook/templates/settings.html b/cookbook/templates/settings.html index 3e2314625..ef2d78433 100644 --- a/cookbook/templates/settings.html +++ b/cookbook/templates/settings.html @@ -217,13 +217,13 @@
From 80965c546229628ebd15cf29cd194203328b1108 Mon Sep 17 00:00:00 2001 From: Kai Schlachter Date: Sat, 5 Feb 2022 13:14:04 +0100 Subject: [PATCH 14/30] Added docker compose file for IPv6 native --- .../docker/ipv6_plain/docker-compose.yml | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 docs/install/docker/ipv6_plain/docker-compose.yml diff --git a/docs/install/docker/ipv6_plain/docker-compose.yml b/docs/install/docker/ipv6_plain/docker-compose.yml new file mode 100644 index 000000000..a65c54f72 --- /dev/null +++ b/docs/install/docker/ipv6_plain/docker-compose.yml @@ -0,0 +1,62 @@ +version: "2.4" +services: + db_recipes: + restart: always + image: postgres:11-alpine + volumes: + - ${POSTGRES_DATA_DIR:-./postgresql}:/var/lib/postgresql/data + env_file: + - ./.env + healthcheck: + test: ["CMD-SHELL", "psql -U $$POSTGRES_USER -d $$POSTGRES_DB --list || exit 1"] + interval: 4s + timeout: 1s + retries: 12 + networks: + tandoor: + ipv6_address: ${IPV6_PREFIX:?NO_IPV6_PREFIX}::2 + web_recipes: + image: vabene1111/recipes + restart: always + env_file: + - ./.env + volumes: + - staticfiles:/opt/recipes/staticfiles + - nginx_config:/opt/recipes/nginx/conf.d + - ${MEDIA_FILES_DIR:-./mediafiles}:/opt/recipes/mediafiles + depends_on: + db_recipes: + condition: service_healthy + networks: + tandoor: + ipv6_address: ${IPV6_PREFIX:?NO_IPV6_PREFIX}::3 + + nginx_recipes: + image: nginx:mainline-alpine + restart: always + ports: + - 80:80 + env_file: + - ./.env + depends_on: + - web_recipes + volumes: + - nginx_config:/etc/nginx/conf.d:ro + - staticfiles:/static + - ${MEDIA_FILES_DIR:-./mediafiles}:/media + networks: + tandoor: + ipv6_address: ${IPV6_PREFIX:?NO_IPV6_PREFIX}::4 +volumes: + nginx_config: + staticfiles: + +networks: + tandoor: + enable_ipv6: true + name: ${NETWORK_NAME:-tandoor} + driver: bridge + ipam: + driver: default + config: + - subnet: ${IPV6_PREFIX:?NO_IPV6_PREFIX}::/${IPV6_PREFIX_LENGTH:?NO_IPV6_PREFIX_LENGTH} \ No newline at end of file From 1c27f2f9b18961a6d3ecb9512038128f8b27d5fc Mon Sep 17 00:00:00 2001 From: Dirk Date: Sat, 5 Feb 2022 21:02:59 +0000 Subject: [PATCH 15/30] Translated using Weblate (German) Currently translated at 99.6% (297 of 298 strings) Translation: Tandoor/Recipes Frontend Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/de/ --- vue/src/locales/de.json | 61 ++++++++++++++++++++++------------------- 1 file changed, 33 insertions(+), 28 deletions(-) diff --git a/vue/src/locales/de.json b/vue/src/locales/de.json index 058848769..bf9538038 100644 --- a/vue/src/locales/de.json +++ b/vue/src/locales/de.json @@ -1,19 +1,19 @@ { "Import": "Importieren", "import_running": "Import läuft, bitte warten!", - "Import_finished": "Import fertig", + "Import_finished": "Import abgeschlossen", "View_Recipes": "Rezepte Ansehen", "Information": "Information", "all_fields_optional": "Alle Felder sind optional und können leer gelassen werden.", - "convert_internal": "Zu internem Rezept wandeln", + "convert_internal": "Zu internem Rezept umwandeln", "Log_Recipe_Cooking": "Kochen protokollieren", - "External_Recipe_Image": "Externes Rezept Bild", + "External_Recipe_Image": "Externes Rezeptbild", "Add_to_Book": "Zu Buch hinzufügen", "Add_to_Shopping": "Zu Einkaufsliste hinzufügen", - "Add_to_Plan": "Zu Plan hinzufügen", + "Add_to_Plan": "Zur Planung hinzufügen", "Step_start_time": "Schritt Startzeit", - "Select_Book": "Buch wählen", - "Recipe_Image": "Rezept Bild", + "Select_Book": "Buch auswählen", + "Recipe_Image": "Rezeptbild", "Log_Cooking": "Kochen protokollieren", "Proteins": "Proteine", "Fats": "Fette", @@ -61,17 +61,17 @@ "success_fetching_resource": "Ressource erfolgreich abgerufen!", "Download": "Herunterladen", "Success": "Erfolgreich", - "err_fetching_resource": "Ein Fehler trat während dem Abrufen einer Ressource auf!", - "err_creating_resource": "Ein Fehler trat während dem Erstellen einer Ressource auf!", - "err_updating_resource": "Ein Fehler trat während dem Aktualisieren einer Ressource auf!", + "err_fetching_resource": "Beim Abrufen einer Ressource ist ein Fehler aufgetreten!", + "err_creating_resource": "Beim Erstellen einer Ressource ist ein Fehler aufgetreten!", + "err_updating_resource": "Beim Aktualisieren einer Ressource ist ein Fehler aufgetreten!", "success_creating_resource": "Ressource erfolgreich erstellt!", "success_updating_resource": "Ressource erfolgreich aktualisiert!", "File": "Datei", "Delete": "Löschen", - "err_deleting_resource": "Ein Fehler trat während dem Löschen einer Ressource auf!", + "err_deleting_resource": "Beim Löschen einer Ressource ist ein Fehler aufgetreten!", "Cancel": "Abbrechen", "success_deleting_resource": "Ressource erfolgreich gelöscht!", - "Load_More": "Mehr laden", + "Load_More": "Weitere laden", "Ok": "Öffnen", "Link": "Link", "Key_Ctrl": "Strg", @@ -87,28 +87,28 @@ "Text": "Text", "Icon": "Icon", "Automation": "Automatisierung", - "Ignore_Shopping": "Einkauf Ignorieren", + "Ignore_Shopping": "Einkauf ignorieren", "Parameter": "Parameter", - "Sort_by_new": "Sortieren nach neueste", - "Shopping_Category": "Einkauf Kategorie", - "Edit_Food": "Essen bearbeiten", - "Move_Food": "Essen verschieben", - "New_Food": "Neues Essen", - "Hide_Food": "Essen verbergen", - "Food_Alias": "Essen Alias", + "Sort_by_new": "Nach Neueste sortieren", + "Shopping_Category": "Einkaufskategorie", + "Edit_Food": "Lebensmittel bearbeiten", + "Move_Food": "Lebensmittel verschieben", + "New_Food": "Neues Lebensmittel", + "Hide_Food": "Lebensmittel verbergen", + "Food_Alias": "Lebensmittel Alias", "Unit_Alias": "Einheit Alias", "Keyword_Alias": "Schlagwort Alias", - "Delete_Food": "Essen löschen", + "Delete_Food": "Lebensmittel löschen", "No_ID": "Nr. nicht gefunden, Objekt kann nicht gelöscht werden", "create_rule": "und erstelle Automatisierung", "Table_of_Contents": "Inhaltsverzeichnis", "merge_title": "Zusammenführen {type}", "del_confirmation_tree": "Sicher das {source} und alle untergeordneten Objekte gelöscht werden soll?", - "warning_feature_beta": "Diese Funktion ist aktuell in einer BETA (Test) Phase. Fehler sind zu erwarten und Änderungen in der Zukunft können die Funktionsweise möglicherweise Verändern oder Daten die mit dieser Funktion zusammen hängen entfernen.", + "warning_feature_beta": "Diese Funktion ist aktuell in einer BETA (Test) Phase. Es ist sowohl mit Fehlern, als auch mit zukünftigen Änderungen der Funktionsweise zu rechnen, wodurch es bei Verwendung entsprechender Funktionen zu Datenverlust kommen kann.", "Edit_Keyword": "Schlagwort bearbeiten", "Move_Keyword": "Schlagwort verschieben", "Merge_Keyword": "Schlagworte zusammenführen", - "Hide_Keywords": "Schlagworte verstecken", + "Hide_Keywords": "Schlagwort verstecken", "Meal_Plan_Days": "Zukünftige Pläne", "Description": "Beschreibung", "Create_New_Shopping Category": "Erstelle neue Einkaufs Kategorie", @@ -117,7 +117,7 @@ "and_up": "& Hoch", "Unrated": "Unbewertet", "Shopping_list": "Einkaufsliste", - "step_time_minutes": "Schritt Zeit in Minuten", + "step_time_minutes": "Schritt Dauer in Minuten", "Save_and_View": "Speichern & Ansehen", "Edit_Recipe": "Rezept bearbeiten", "Hide_Recipes": "Rezepte verstecken", @@ -232,10 +232,10 @@ "AddToShopping": "Zur Einkaufsliste hinzufügen", "FoodOnHand": "Sie haben {food} vorrätig.", "DeleteShoppingConfirm": "Möchten Sie wirklich alle {food} von der Einkaufsliste zu entfernen?", - "err_moving_resource": "Während des Verschiebens einer Resource ist ein Fehler aufgetreten!", - "err_merging_resource": "Beim Zusammenführen einer Ressource ist ein Fehler aufgetreten!", + "err_moving_resource": "Beim Verschieben einer Resource ist ein Fehler aufgetreten!", + "err_merging_resource": "Beim Zusammenführen einer Ressource trat ein Fehler auf!", "success_moving_resource": "Ressource wurde erfolgreich verschoben!", - "success_merging_resource": "Ressource wurde erfolgreich zusammengeführt!", + "success_merging_resource": "Zusammenführung einer Ressource war erfolgreich!", "Shopping_Categories": "Einkaufskategorien", "Added_on": "Hinzugefügt am", "IngredientInShopping": "Diese Zutat befindet sich in Ihrer Einkaufsliste.", @@ -252,7 +252,7 @@ "Pin": "Pin", "mark_complete": "Vollständig markieren", "shopping_add_onhand_desc": "Markiere Lebensmittel als \"Vorrätig\", wenn von der Einkaufsliste abgehakt wurden.", - "left_handed": "Linkshändermodus", + "left_handed": "Linkshänder-Modus", "left_handed_help": "Optimiert die Benutzeroberfläche für die Bedienung mit der linken Hand.", "FoodInherit": "Lebensmittel vererbbare Felder", "SupermarketCategoriesOnly": "Nur Supermarkt Kategorien", @@ -291,5 +291,10 @@ "remember_search": "Suchbegriff merken", "remember_hours": "Stunden zu erinnern", "tree_select": "Baum-Auswahl verwenden", - "CountMore": "...+{count} weitere" + "CountMore": "...+{count} weitere", + "ignore_shopping_help": "Lebensmittel nie zur Einkaufsliste hinzufügen (z.B. Wasser).", + "OnHand_help": "Lebensmittel ist \"Vorrätig\" und wird nicht automatisch zur Einkaufsliste hinzugefügt.", + "shopping_category_help": "Supermärkte können nach Einkaufskategorien geordnet und gefiltert werden, je nachdem, wie die Gänge angeordnet sind.", + "Foods": "Lebensmittel", + "food_recipe_help": "Wird ein Rezept hier verknüpft, wird diese Verknüpfung in allen anderen Rezepten übernommen, die dieses Lebensmittel beinhaltet" } From bc8131ac563537fb30935eb3af239fdc8ff2c478 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dersim=20=C3=96zdag?= Date: Sat, 5 Feb 2022 21:00:36 +0000 Subject: [PATCH 16/30] Translated using Weblate (German) Currently translated at 99.6% (297 of 298 strings) Translation: Tandoor/Recipes Frontend Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/de/ --- vue/src/locales/de.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/vue/src/locales/de.json b/vue/src/locales/de.json index bf9538038..c074872d2 100644 --- a/vue/src/locales/de.json +++ b/vue/src/locales/de.json @@ -296,5 +296,7 @@ "OnHand_help": "Lebensmittel ist \"Vorrätig\" und wird nicht automatisch zur Einkaufsliste hinzugefügt.", "shopping_category_help": "Supermärkte können nach Einkaufskategorien geordnet und gefiltert werden, je nachdem, wie die Gänge angeordnet sind.", "Foods": "Lebensmittel", - "food_recipe_help": "Wird ein Rezept hier verknüpft, wird diese Verknüpfung in allen anderen Rezepten übernommen, die dieses Lebensmittel beinhaltet" + "food_recipe_help": "Wird ein Rezept hier verknüpft, wird diese Verknüpfung in allen anderen Rezepten übernommen, die dieses Lebensmittel beinhaltet", + "review_shopping": "Überprüfe die Einkaufsliste vor dem Speichern", + "view_recipe": "Rezept anschauen" } From 72d29cc88a2a95bbfb4552bb9ebd79a2f6e17e78 Mon Sep 17 00:00:00 2001 From: "A. L" Date: Sat, 5 Feb 2022 20:45:32 +0000 Subject: [PATCH 17/30] Translated using Weblate (German) Currently translated at 99.6% (297 of 298 strings) Translation: Tandoor/Recipes Frontend Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/de/ --- vue/src/locales/de.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vue/src/locales/de.json b/vue/src/locales/de.json index c074872d2..59ddb64da 100644 --- a/vue/src/locales/de.json +++ b/vue/src/locales/de.json @@ -99,7 +99,7 @@ "Unit_Alias": "Einheit Alias", "Keyword_Alias": "Schlagwort Alias", "Delete_Food": "Lebensmittel löschen", - "No_ID": "Nr. nicht gefunden, Objekt kann nicht gelöscht werden", + "No_ID": "ID nicht gefunden. Objekt kann nicht gelöscht werden.", "create_rule": "und erstelle Automatisierung", "Table_of_Contents": "Inhaltsverzeichnis", "merge_title": "Zusammenführen {type}", From 65003175ce6cd23ae6ce0650a4d64b9f2a134cd7 Mon Sep 17 00:00:00 2001 From: Dirk Date: Sat, 5 Feb 2022 21:06:23 +0000 Subject: [PATCH 18/30] Translated using Weblate (German) Currently translated at 99.6% (297 of 298 strings) Translation: Tandoor/Recipes Frontend Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/de/ --- vue/src/locales/de.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vue/src/locales/de.json b/vue/src/locales/de.json index 59ddb64da..0d16e681e 100644 --- a/vue/src/locales/de.json +++ b/vue/src/locales/de.json @@ -99,7 +99,7 @@ "Unit_Alias": "Einheit Alias", "Keyword_Alias": "Schlagwort Alias", "Delete_Food": "Lebensmittel löschen", - "No_ID": "ID nicht gefunden. Objekt kann nicht gelöscht werden.", + "No_ID": "ID nicht gefunden und kann nicht gelöscht werden.", "create_rule": "und erstelle Automatisierung", "Table_of_Contents": "Inhaltsverzeichnis", "merge_title": "Zusammenführen {type}", From 8dffc58ca6a55c257f51a33ed76e0fbffda526e5 Mon Sep 17 00:00:00 2001 From: Christian Date: Sun, 6 Feb 2022 18:28:55 +0000 Subject: [PATCH 19/30] Translated using Weblate (German) Currently translated at 96.6% (549 of 568 strings) Translation: Tandoor/Recipes Backend Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/de/ --- cookbook/locale/de/LC_MESSAGES/django.po | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/cookbook/locale/de/LC_MESSAGES/django.po b/cookbook/locale/de/LC_MESSAGES/django.po index 8df1fccde..c3b7ca97d 100644 --- a/cookbook/locale/de/LC_MESSAGES/django.po +++ b/cookbook/locale/de/LC_MESSAGES/django.po @@ -15,8 +15,8 @@ msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2022-01-18 14:52+0100\n" -"PO-Revision-Date: 2022-02-02 15:31+0000\n" -"Last-Translator: Sven \n" +"PO-Revision-Date: 2022-02-06 21:31+0000\n" +"Last-Translator: Christian \n" "Language-Team: German \n" "Language: de\n" @@ -415,6 +415,7 @@ msgstr "" #: .\cookbook\forms.py:501 msgid "Days of recent shopping list entries to display." msgstr "" +"Tage der letzten Einträge in der Einkaufsliste, die angezeigt werden sollen." #: .\cookbook\forms.py:502 msgid "Mark food 'On Hand' when checked off shopping list." @@ -478,7 +479,7 @@ msgstr "Automatisch als vorrätig markieren" #: .\cookbook\forms.py:528 msgid "Reset Food Inheritance" -msgstr "" +msgstr "Lebensmittelvererbung zurücksetzen" #: .\cookbook\forms.py:529 msgid "Reset all food to inherit the fields configured." From 2f045e6e0d5bfe529b95b31219af6c53fe621206 Mon Sep 17 00:00:00 2001 From: David Laukamp Date: Sun, 6 Feb 2022 21:18:21 +0000 Subject: [PATCH 20/30] Translated using Weblate (German) Currently translated at 96.6% (549 of 568 strings) Translation: Tandoor/Recipes Backend Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/de/ --- cookbook/locale/de/LC_MESSAGES/django.po | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cookbook/locale/de/LC_MESSAGES/django.po b/cookbook/locale/de/LC_MESSAGES/django.po index c3b7ca97d..92205ee14 100644 --- a/cookbook/locale/de/LC_MESSAGES/django.po +++ b/cookbook/locale/de/LC_MESSAGES/django.po @@ -16,7 +16,7 @@ msgstr "" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2022-01-18 14:52+0100\n" "PO-Revision-Date: 2022-02-06 21:31+0000\n" -"Last-Translator: Christian \n" +"Last-Translator: David Laukamp \n" "Language-Team: German \n" "Language: de\n" @@ -484,6 +484,7 @@ msgstr "Lebensmittelvererbung zurücksetzen" #: .\cookbook\forms.py:529 msgid "Reset all food to inherit the fields configured." msgstr "" +"Alle Lebensmittel zurücksetzen, um die konfigurierten Felder zu übernehmen." #: .\cookbook\forms.py:541 msgid "Fields on food that should be inherited by default." From 2b6e365f0b81f8fc341957e596c47a46fa573b47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marion=20K=C3=A4mpfer?= Date: Sun, 6 Feb 2022 16:11:52 +0000 Subject: [PATCH 21/30] Translated using Weblate (French) Currently translated at 91.0% (517 of 568 strings) Translation: Tandoor/Recipes Backend Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/fr/ --- cookbook/locale/fr/LC_MESSAGES/django.po | 32 +++++++++++------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/cookbook/locale/fr/LC_MESSAGES/django.po b/cookbook/locale/fr/LC_MESSAGES/django.po index 42bc4c54d..5ca60a34e 100644 --- a/cookbook/locale/fr/LC_MESSAGES/django.po +++ b/cookbook/locale/fr/LC_MESSAGES/django.po @@ -14,16 +14,16 @@ msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2022-01-18 14:52+0100\n" -"PO-Revision-Date: 2022-01-16 07:06+0000\n" -"Last-Translator: Josselin du PLESSIS \n" -"Language-Team: French \n" +"PO-Revision-Date: 2022-02-06 21:31+0000\n" +"Last-Translator: Marion Kämpfer \n" +"Language-Team: French \n" "Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=n > 1;\n" -"X-Generator: Weblate 4.8\n" +"X-Generator: Weblate 4.10.1\n" #: .\cookbook\filters.py:23 .\cookbook\templates\forms\ingredients.html:34 #: .\cookbook\templates\space.html:50 .\cookbook\templates\stats.html:28 @@ -123,10 +123,8 @@ msgstr "" "nouvellement créés seront partagés par défaut." #: .\cookbook\forms.py:80 -#, fuzzy -#| msgid "Try the new shopping list" msgid "Users with whom to share shopping lists." -msgstr "Essayer la nouvelle liste de courses" +msgstr "Utilisateurs avec lesquels partager des listes de courses." #: .\cookbook\forms.py:82 msgid "Show recently viewed recipes on search page." @@ -419,6 +417,8 @@ msgstr "" #: .\cookbook\forms.py:500 msgid "Filter shopping list to only include supermarket categories." msgstr "" +"Filtrer la liste de courses pour n’inclure que des catégories de " +"supermarchés." #: .\cookbook\forms.py:501 msgid "Days of recent shopping list entries to display." @@ -430,17 +430,15 @@ msgstr "" #: .\cookbook\forms.py:503 msgid "Delimiter to use for CSV exports." -msgstr "" +msgstr "Caractère de séparation à utiliser pour les exportations CSV." #: .\cookbook\forms.py:504 msgid "Prefix to add when copying list to the clipboard." -msgstr "" +msgstr "Préfixe à utiliser pour copier la liste dans le presse-papiers." #: .\cookbook\forms.py:508 -#, fuzzy -#| msgid "New Shopping List" msgid "Share Shopping List" -msgstr "Nouvelle liste de courses" +msgstr "Partager la liste de courses" #: .\cookbook\forms.py:509 msgid "Autosync" @@ -448,7 +446,7 @@ msgstr "" #: .\cookbook\forms.py:510 msgid "Auto Add Meal Plan" -msgstr "" +msgstr "Ajouter le menu de la semaine automatiquement" #: .\cookbook\forms.py:511 msgid "Exclude On Hand" @@ -474,7 +472,7 @@ msgstr "" #: .\cookbook\forms.py:516 msgid "CSV Delimiter" -msgstr "" +msgstr "Caractère de séparation CSV" #: .\cookbook\forms.py:517 .\cookbook\templates\shopping_list.html:322 msgid "List Prefix" @@ -742,7 +740,7 @@ msgstr "Alias de mot-clé" #: .\cookbook\serializer.py:175 msgid "A user is required" -msgstr "" +msgstr "Un utilisateur est requis" #: .\cookbook\serializer.py:195 msgid "File uploads are not enabled for this Space." @@ -754,7 +752,7 @@ msgstr "Vous avez atteint votre limite de téléversement de fichiers." #: .\cookbook\serializer.py:962 msgid "Existing shopping list to update" -msgstr "" +msgstr "Liste de courses existante à mettre à jour" #: .\cookbook\serializer.py:964 msgid "" From 6068496240a754f75c77aaa440d611e47f4c2a0c Mon Sep 17 00:00:00 2001 From: Dirk Date: Sat, 5 Feb 2022 21:07:01 +0000 Subject: [PATCH 22/30] Translated using Weblate (German) Currently translated at 100.0% (298 of 298 strings) Translation: Tandoor/Recipes Frontend Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/de/ --- vue/src/locales/de.json | 58 ++++++++++++++++++++--------------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/vue/src/locales/de.json b/vue/src/locales/de.json index 0d16e681e..25a1b8f87 100644 --- a/vue/src/locales/de.json +++ b/vue/src/locales/de.json @@ -75,12 +75,12 @@ "Ok": "Öffnen", "Link": "Link", "Key_Ctrl": "Strg", - "move_title": "Verschieben {type}", - "Food": "Essen", + "move_title": "{type} verschieben", + "Food": "Lebensmittel", "Recipe_Book": "Kochbuch", - "delete_title": "Löschen {type}", - "create_title": "Neu {type}", - "edit_title": "Bearbeiten {type}", + "delete_title": "Lösche {type}", + "create_title": "{type} erstellen", + "edit_title": "{type} bearbeiten", "Name": "Name", "Empty": "Leer", "Key_Shift": "Umschalttaste", @@ -102,16 +102,16 @@ "No_ID": "ID nicht gefunden und kann nicht gelöscht werden.", "create_rule": "und erstelle Automatisierung", "Table_of_Contents": "Inhaltsverzeichnis", - "merge_title": "Zusammenführen {type}", - "del_confirmation_tree": "Sicher das {source} und alle untergeordneten Objekte gelöscht werden soll?", + "merge_title": "{type} zusammenführen", + "del_confirmation_tree": "Sicher, dass {source} und alle untergeordneten Objekte gelöscht werden sollen?", "warning_feature_beta": "Diese Funktion ist aktuell in einer BETA (Test) Phase. Es ist sowohl mit Fehlern, als auch mit zukünftigen Änderungen der Funktionsweise zu rechnen, wodurch es bei Verwendung entsprechender Funktionen zu Datenverlust kommen kann.", "Edit_Keyword": "Schlagwort bearbeiten", "Move_Keyword": "Schlagwort verschieben", "Merge_Keyword": "Schlagworte zusammenführen", "Hide_Keywords": "Schlagwort verstecken", - "Meal_Plan_Days": "Zukünftige Pläne", + "Meal_Plan_Days": "Zukünftige Essenspläne", "Description": "Beschreibung", - "Create_New_Shopping Category": "Erstelle neue Einkaufs Kategorie", + "Create_New_Shopping Category": "Neue Einkaufskategorie erstellen", "Automate": "Automatisieren", "Type": "Typ", "and_up": "& Hoch", @@ -128,7 +128,7 @@ "Copy_template_reference": "Template Referenz kopieren", "Step_Type": "Schritt Typ", "Make_Header": "In Überschrift wandeln", - "Make_Ingredient": "In Zutat wandeln", + "Make_Ingredient": "In Zutat umwandeln", "Enable_Amount": "Menge aktivieren", "Disable_Amount": "Menge deaktivieren", "Add_Step": "Schritt hinzufügen", @@ -152,9 +152,9 @@ "Unit": "Einheit", "No_Results": "Keine Ergebnisse", "New_Unit": "Neue Einheit", - "Create_New_Food": "Neues Essen", - "Create_New_Keyword": "Neues Schlagwort", - "Create_New_Unit": "Neue Einheit", + "Create_New_Food": "Neues Lebensmittel hinzufügen", + "Create_New_Keyword": "Neues Schlagwort hinzufügen", + "Create_New_Unit": "Neue Einheit hinzufügen", "Instructions": "Anleitung", "Time": "Zeit", "New_Keyword": "Neues Schlagwort", @@ -169,7 +169,7 @@ "Week": "Woche", "Month": "Monat", "Year": "Jahr", - "Drag_Here_To_Delete": "Ziehen zum Löschen", + "Drag_Here_To_Delete": "Hierher ziehen zum Löschen", "Select_File": "Datei auswählen", "Image": "Bild", "Planner": "Planer", @@ -180,7 +180,7 @@ "Meal_Type_Required": "Mahlzeitentyp ist erforderlich", "Remove_nutrition_recipe": "Nährwerte aus Rezept löschen", "Add_nutrition_recipe": "Nährwerte zu Rezept hinzufügen", - "Title_or_Recipe_Required": "Titel oder Rezept benötigt", + "Title_or_Recipe_Required": "Auswahl von Titel oder Rezept erforderlich", "Next_Day": "Tag vor", "Previous_Day": "Tag zurück", "Edit_Meal_Plan_Entry": "Eintrag bearbeiten", @@ -189,19 +189,19 @@ "Color": "Farbe", "New_Meal_Type": "Neue Mahlzeit", "Periods": "Zeiträume", - "Plan_Show_How_Many_Periods": "Wie viele Zeiträume angezeigt werden", + "Plan_Show_How_Many_Periods": "Anzahl der anzuzeigenden Zeiträume", "Starting_Day": "Wochenbeginn am", "Meal_Type": "Mahlzeit", "Meal_Types": "Mahlzeiten", "Export_As_ICal": "Aktuellen Zeitraum im iCal Format exportieren", "Week_Numbers": "Kalenderwochen", - "Show_Week_Numbers": "Kalenderwochen anzeigen ?", + "Show_Week_Numbers": "Kalenderwochen anzeigen?", "Added_To_Shopping_List": "Zur Einkaufsliste hinzugefügt", "Export_To_ICal": "Export als .ics", "Cannot_Add_Notes_To_Shopping": "Notizen können nicht auf die Einkaufsliste gesetzt werden", - "Shopping_List_Empty": "Deine Einkaufsliste ist aktuell leer, Einträge können via dem Kontextmenü hinzugefügt werden (Rechtsklick auf einen Eintrag oder Klick auf das Menü-Icon)", - "Next_Period": "Zeitraum vor", - "Previous_Period": "Zeitraum zurück", + "Shopping_List_Empty": "Deine Einkaufsliste ist aktuell leer. Einträge können über das Kontextmenü hinzugefügt werden (Rechtsklick auf einen Eintrag oder Klick auf das Menü-Icon)", + "Next_Period": "nächster Zeitraum", + "Previous_Period": "voriger Zeitraum", "Current_Period": "Aktueller Zeitraum", "New_Cookbook": "Neues Kochbuch", "Coming_Soon": "Bald verfügbar", @@ -212,7 +212,7 @@ "IgnoreThis": "{food} nicht automatisch zur Einkaufsliste hinzufügen", "shopping_auto_sync": "Automatische Synchronisierung", "shopping_share_desc": "Benutzer sehen all Einträge, die du zur Einkaufsliste hinzufügst. Sie müssen dich hinzufügen, damit du Ihre Einträge sehen kannst.", - "IgnoredFood": "{food} beim nächsten Einkauf ignorieren.", + "IgnoredFood": "{food} nicht für Einkauf geplant.", "Add_Servings_to_Shopping": "{servings} Portionen zum Einkauf hinzufügen", "Inherit": "Vererben", "InheritFields": "Feldwerte vererben", @@ -228,23 +228,23 @@ "mealplan_autoexclude_onhand": "Ignoriere vorhandene Zutaten", "mealplan_autoinclude_related": "Füge verwandte Rezepte hinzu", "default_delay": "Standard Zeit des Verzögerns", - "Added_by": "Hinzugefügt von", + "Added_by": "Hinzugefügt durch", "AddToShopping": "Zur Einkaufsliste hinzufügen", "FoodOnHand": "Sie haben {food} vorrätig.", - "DeleteShoppingConfirm": "Möchten Sie wirklich alle {food} von der Einkaufsliste zu entfernen?", + "DeleteShoppingConfirm": "Möchten Sie wirklich alle {food} von der Einkaufsliste entfernen?", "err_moving_resource": "Beim Verschieben einer Resource ist ein Fehler aufgetreten!", "err_merging_resource": "Beim Zusammenführen einer Ressource trat ein Fehler auf!", "success_moving_resource": "Ressource wurde erfolgreich verschoben!", "success_merging_resource": "Zusammenführung einer Ressource war erfolgreich!", "Shopping_Categories": "Einkaufskategorien", "Added_on": "Hinzugefügt am", - "IngredientInShopping": "Diese Zutat befindet sich in Ihrer Einkaufsliste.", - "NotInShopping": "{food} ist nicht in Ihrer Einkaufsliste.", + "IngredientInShopping": "Diese Zutat befindet sich auf Ihrer Einkaufsliste.", + "NotInShopping": "{food} befindet sich nicht auf Ihrer Einkaufsliste.", "OnHand": "Aktuell vorrätig", - "FoodNotOnHand": "Sie haben kein {food} vorrätig.", - "Undefined": "nicht definiert", - "AddFoodToShopping": "{food} zur Einkaufsliste hinzufügen", - "RemoveFoodFromShopping": "{food} von der Einkaufsliste entfernen", + "FoodNotOnHand": "Sie habe {food} nicht vorrätig.", + "Undefined": "undefiniert", + "AddFoodToShopping": "Fügen Sie {food} zur Einkaufsliste hinzu", + "RemoveFoodFromShopping": "{food} von der Einkaufsliste löschen", "Search Settings": "Sucheinstellungen", "shopping_auto_sync_desc": "Bei 0 wird Auto-Sync deaktiviert. Beim Betrachten einer Einkaufsliste wird die Liste alle gesetzten Sekunden aktualisiert, um mögliche Änderungen anderer zu zeigen. Nützlich, wenn mehrere Personen einkaufen und mobile Daten nutzen.", "MoveCategory": "Verschieben nach: ", From 7dcc38b5b20bb059dc7f931ee8025ae597fd4acf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dersim=20=C3=96zdag?= Date: Sat, 5 Feb 2022 21:14:50 +0000 Subject: [PATCH 23/30] Translated using Weblate (German) Currently translated at 100.0% (298 of 298 strings) Translation: Tandoor/Recipes Frontend Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/de/ --- vue/src/locales/de.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vue/src/locales/de.json b/vue/src/locales/de.json index 25a1b8f87..80787e9e0 100644 --- a/vue/src/locales/de.json +++ b/vue/src/locales/de.json @@ -292,7 +292,7 @@ "remember_hours": "Stunden zu erinnern", "tree_select": "Baum-Auswahl verwenden", "CountMore": "...+{count} weitere", - "ignore_shopping_help": "Lebensmittel nie zur Einkaufsliste hinzufügen (z.B. Wasser).", + "ignore_shopping_help": "Füge Zutat nie zur Einkaufsliste hinzu (z.B. Wasser)", "OnHand_help": "Lebensmittel ist \"Vorrätig\" und wird nicht automatisch zur Einkaufsliste hinzugefügt.", "shopping_category_help": "Supermärkte können nach Einkaufskategorien geordnet und gefiltert werden, je nachdem, wie die Gänge angeordnet sind.", "Foods": "Lebensmittel", From cec74d77ec67006bb8071da53fdb1a8907b8ca67 Mon Sep 17 00:00:00 2001 From: besendorf Date: Sun, 6 Feb 2022 22:50:48 +0100 Subject: [PATCH 24/30] add instructions for pwa for Android/Chrome browsers fixes #1349 --- docs/faq.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/faq.md b/docs/faq.md index b09ff43c2..731ed1c90 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -10,6 +10,9 @@ Tandoor can be installed as a progressive web app (PWA) on mobile and desktop de #### Safari (iPhone/iPad) Open Tandoor, click Safari's share button, select `Add to Home Screen` +### Chrome/Chromium +Open Tandoor, click the `add Tandoor to the home screen` message that pops up at the bottom of the screen + ### Desktop browsers #### Google Chrome @@ -82,4 +85,4 @@ To create a superuser you need to 1. execute into the container using `docker-compose exec web_recipes sh` 2. activate the virtual environment `source venv/bin/activate` -3. run `python manage.py createsuperuser` and follow the steps shown. \ No newline at end of file +3. run `python manage.py createsuperuser` and follow the steps shown. From 39ca3ac1ad1650f311181b82e0c2b7338963daea Mon Sep 17 00:00:00 2001 From: vabene1111 Date: Mon, 7 Feb 2022 13:08:18 +0100 Subject: [PATCH 25/30] no system page for non superuser --- cookbook/views/views.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cookbook/views/views.py b/cookbook/views/views.py index ad69bbdf8..9aa54560a 100644 --- a/cookbook/views/views.py +++ b/cookbook/views/views.py @@ -446,6 +446,9 @@ def history(request): @group_required('admin') def system(request): + if not request.user.is_superuser: + return HttpResponseRedirect(reverse('index')) + postgres = False if ( settings.DATABASES['default']['ENGINE'] == 'django.db.backends.postgresql_psycopg2' # noqa: E501 or settings.DATABASES['default']['ENGINE'] == 'django.db.backends.postgresql' # noqa: E501 From 5cc81d977f16b2f9ae8ac0bc7d77e12b130ac1c2 Mon Sep 17 00:00:00 2001 From: vabene1111 Date: Mon, 7 Feb 2022 14:41:00 +0100 Subject: [PATCH 26/30] updated translations --- cookbook/locale/de/LC_MESSAGES/django.mo | Bin 59896 -> 64623 bytes cookbook/locale/fr/LC_MESSAGES/django.mo | Bin 63560 -> 64480 bytes cookbook/locale/sl/LC_MESSAGES/django.mo | Bin 1707 -> 8453 bytes 3 files changed, 0 insertions(+), 0 deletions(-) diff --git a/cookbook/locale/de/LC_MESSAGES/django.mo b/cookbook/locale/de/LC_MESSAGES/django.mo index 77c8a4f366e840a6d992bbfda1c53a59139cf037..2154725698ddba7c44c43dae95a7b488eb4006ec 100644 GIT binary patch delta 16107 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 delta 11496 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 N^4|@-++|&CH^IwSxU+i#1 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 O*N!|sFzeR-{Qm<8n}DwX delta 11190 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 ZGR@h-NVlJT7U(^BEo+!J?}I(T{|7-*Y4rdA diff --git a/cookbook/locale/sl/LC_MESSAGES/django.mo b/cookbook/locale/sl/LC_MESSAGES/django.mo index b8819830557a1694e051e007e3c0d7ceb5fe3886..cc60fc1ad543e0558666e8ca92ea665ce3057e6a 100644 GIT binary patch literal 8453 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 literal 1707 zcmZvb%Wqpn6o&^$d6;L*v%EAF0tulT+es7!rvdVCi<^h7*iBJcjeSq-v9D*YX2vcF z70ZeRRp|oMMHdu_1riI?B@0-PN?pQ=UB!wO5*z*ie#iHQuwZm&zB%(a=gc|h-a9e% zg+O}+{bls2`-FHC{1fE%`Tar^!0X^K@Fw^O_;oIS2R;P(Huy031Na#DGx#9*OU~cG zry$=0r@+5Jw)Z#q40!wjAsz+a09nsW&P9;zuH`%jvOkSn{t#rn3S@f@Wczy{>$#Hi zTFx6F`+F;wzXk8&t+zqG_c4sIo=De~opr9nKN^#k!wG=iG3fPN2Vt&arqNon!ys#&6>GT!u+Z zd3qcjA)eG3zaMFzw38U&+9Wny4`MlvWi=Wqoro1{2VSf=QiEOPgIGB)+)-dbEI_+JuF7{$WneIq+No?4zHezFBIz6$4TicUcq8@s1q2VZ1 zP6cg^Z0RA1e0Q7Jj#P4G)D=x~V{N(~lMu3r?iyJi1Pe zn2c6Np6-Q^R*J=ttD)(jOl+;2j%i#>vq$)1zF5p6?W4BpAPsTRC&eS>73q`0W^J>2 z^y2b#sjzBIKuV(4UW%s#`Y06BL}{}qJ3Zw*g{AYYGtt7)Id)~2TvRn38*?z_LR*JI zBN;iBL}#oU`ieB^MD$B@i!z%onb*su($Z2{zAH--Co*vcV);m%m6tCcZp~q923xa> zg%6^$gi_4nc!Ao8D9O3$g@RbKvFabz`nu_BWci}%2R8sxr^UMELRaqJNna!Tdc03T zNisGlQQL1jedm@IJ9KcBTwfiQ{Fd@ag%;vqI9QJ-YMVW$hHWM4sol2@7AoQ&w&9A; From 0dbb9457a1ffafda4fe6e848e40cbc03d316034e Mon Sep 17 00:00:00 2001 From: vabene1111 Date: Mon, 7 Feb 2022 14:41:36 +0100 Subject: [PATCH 27/30] fixed test --- cookbook/tests/views/test_views_general.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cookbook/tests/views/test_views_general.py b/cookbook/tests/views/test_views_general.py index fd282edb8..ab1be6742 100644 --- a/cookbook/tests/views/test_views_general.py +++ b/cookbook/tests/views/test_views_general.py @@ -81,7 +81,7 @@ def test_history(arg, request, ext_recipe_1_s1): ['a_u', 302], ['g1_s1', 302], ['u1_s1', 302], - ['a1_s1', 200], + ['a1_s1', 302], ]) def test_system(arg, request, ext_recipe_1_s1): c = request.getfixturevalue(arg[0]) From f37790a24a0a774994294e24ad32b079de9a0875 Mon Sep 17 00:00:00 2001 From: vabene1111 Date: Mon, 7 Feb 2022 14:48:52 +0100 Subject: [PATCH 28/30] changed to vue 2 lifecycle hook --- vue/src/apps/RecipeView/RecipeView.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vue/src/apps/RecipeView/RecipeView.vue b/vue/src/apps/RecipeView/RecipeView.vue index e36294fae..6b09657f9 100644 --- a/vue/src/apps/RecipeView/RecipeView.vue +++ b/vue/src/apps/RecipeView/RecipeView.vue @@ -221,7 +221,7 @@ export default { this.$i18n.locale = window.CUSTOM_LOCALE this.requestWakeLock() }, - beforeUnmount() { + beforeDestroy() { this.destroyWakeLock() }, methods: { From 969df37e28fc8059a006ad3255f4172245dad3b4 Mon Sep 17 00:00:00 2001 From: vabene1111 Date: Mon, 7 Feb 2022 15:43:06 +0100 Subject: [PATCH 29/30] fixed sharing and added additional tests --- cookbook/serializer.py | 16 +++++++++------- cookbook/tests/api/test_api_recipe.py | 17 ++++++++++++++++- cookbook/views/api.py | 6 ++++-- vue/src/apps/RecipeView/RecipeView.vue | 2 +- 4 files changed, 30 insertions(+), 11 deletions(-) diff --git a/cookbook/serializer.py b/cookbook/serializer.py index 9f86906f2..9ffbbabfb 100644 --- a/cookbook/serializer.py +++ b/cookbook/serializer.py @@ -91,7 +91,10 @@ class CustomOnHandField(serializers.Field): if request := self.context.get('request', None): shared_users = getattr(request, '_shared_users', None) if shared_users is None: - shared_users = [x.id for x in list(self.context['request'].user.get_shopping_share())] + [self.context['request'].user.id] + try: + shared_users = [x.id for x in list(self.context['request'].user.get_shopping_share())] + [self.context['request'].user.id] + except AttributeError: # Anonymous users (using share links) don't have shared users + shared_users = [] return obj.onhand_users.filter(id__in=shared_users).exists() def to_internal_value(self, data): @@ -686,11 +689,11 @@ class ShoppingListRecipeSerializer(serializers.ModelSerializer): value = Decimal(value) value = value.quantize(Decimal(1)) if value == value.to_integral() else value.normalize() # strips trailing zero return ( - obj.name - or getattr(obj.mealplan, 'title', None) - or (d := getattr(obj.mealplan, 'date', None)) and ': '.join([obj.mealplan.recipe.name, str(d)]) - or obj.recipe.name - ) + f' ({value:.2g})' + obj.name + or getattr(obj.mealplan, 'title', None) + or (d := getattr(obj.mealplan, 'date', None)) and ': '.join([obj.mealplan.recipe.name, str(d)]) + or obj.recipe.name + ) + f' ({value:.2g})' def update(self, instance, validated_data): # TODO remove once old shopping list @@ -863,7 +866,6 @@ class ExportLogSerializer(serializers.ModelSerializer): read_only_fields = ('created_by',) - class AutomationSerializer(serializers.ModelSerializer): def create(self, validated_data): diff --git a/cookbook/tests/api/test_api_recipe.py b/cookbook/tests/api/test_api_recipe.py index 21c621c32..2ff97669e 100644 --- a/cookbook/tests/api/test_api_recipe.py +++ b/cookbook/tests/api/test_api_recipe.py @@ -4,7 +4,7 @@ import pytest from django.urls import reverse from django_scopes import scopes_disabled -from cookbook.models import Recipe +from cookbook.models import Recipe, ShareLink from cookbook.tests.conftest import get_random_json_recipe, validate_recipe LIST_URL = 'api:recipe-list' @@ -38,6 +38,21 @@ def test_list_space(recipe_1_s1, u1_s1, u1_s2, space_2): assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)['results']) == 1 +def test_share_permission(recipe_1_s1, u1_s1, u1_s2, a_u): + assert u1_s1.get(reverse(DETAIL_URL, args=[recipe_1_s1.pk])).status_code == 200 + assert u1_s2.get(reverse(DETAIL_URL, args=[recipe_1_s1.pk])).status_code == 404 + + with scopes_disabled(): + r = u1_s1.get(reverse('new_share_link', kwargs={'pk': recipe_1_s1.pk})) + assert r.status_code == 302 + r = u1_s2.get(reverse('new_share_link', kwargs={'pk': recipe_1_s1.pk})) + assert r.status_code == 404 + share = ShareLink.objects.filter(recipe=recipe_1_s1).first() + assert a_u.get(reverse(DETAIL_URL, args=[recipe_1_s1.pk]) + f'?share={share.uuid}').status_code == 200 + assert u1_s1.get(reverse(DETAIL_URL, args=[recipe_1_s1.pk]) + f'?share={share.uuid}').status_code == 200 + assert u1_s2.get(reverse(DETAIL_URL, args=[recipe_1_s1.pk]) + f'?share={share.uuid}').status_code == 404 # TODO fix in https://github.com/TandoorRecipes/recipes/issues/1238 + + @pytest.mark.parametrize("arg", [ ['a_u', 403], ['g1_s1', 200], diff --git a/cookbook/views/api.py b/cookbook/views/api.py index 7f13bca48..fc0eb9f9a 100644 --- a/cookbook/views/api.py +++ b/cookbook/views/api.py @@ -648,11 +648,13 @@ class RecipeViewSet(viewsets.ModelViewSet): schema = QueryParamAutoSchema() def get_queryset(self): + share = self.request.query_params.get('share', None) + if self.detail: - self.queryset = self.queryset.filter(space=self.request.space) + if not share: + self.queryset = self.queryset.filter(space=self.request.space) return super().get_queryset() - share = self.request.query_params.get('share', None) if not (share and self.detail): self.queryset = self.queryset.filter(space=self.request.space) diff --git a/vue/src/apps/RecipeView/RecipeView.vue b/vue/src/apps/RecipeView/RecipeView.vue index 6b09657f9..e36294fae 100644 --- a/vue/src/apps/RecipeView/RecipeView.vue +++ b/vue/src/apps/RecipeView/RecipeView.vue @@ -221,7 +221,7 @@ export default { this.$i18n.locale = window.CUSTOM_LOCALE this.requestWakeLock() }, - beforeDestroy() { + beforeUnmount() { this.destroyWakeLock() }, methods: { From c92c3e7d85f80c924b38d4a08e1da6af5e90e158 Mon Sep 17 00:00:00 2001 From: vabene1111 Date: Mon, 7 Feb 2022 17:43:57 +0100 Subject: [PATCH 30/30] improved comment formatting --- cookbook/templates/recipe_view.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cookbook/templates/recipe_view.html b/cookbook/templates/recipe_view.html index 70ec627e8..1a55fde18 100644 --- a/cookbook/templates/recipe_view.html +++ b/cookbook/templates/recipe_view.html @@ -26,7 +26,7 @@ {{ c.updated_at }} {% trans 'by' %} {{ c.created_by.username }}
- {{ c.text }} + {{ c.text | urlize |linebreaks }}