From 55a030470036501d8194ee32d24fedb4f9664ed1 Mon Sep 17 00:00:00 2001 From: smilerz Date: Wed, 24 Nov 2021 12:10:15 -0600 Subject: [PATCH] add search debug --- .gitignore | 1 + cookbook/helper/recipe_search.py | 26 +- cookbook/templates/base.html | 1 + cookbook/urls.py | 20 +- cookbook/views/api.py | 67 +- cookbook/views/views.py | 31 +- .../RecipeSearchView/RecipeSearchView.vue | 1031 +++++++++-------- vue/src/locales/en.json | 417 +++---- 8 files changed, 812 insertions(+), 782 deletions(-) diff --git a/.gitignore b/.gitignore index 5791432e..e36efacb 100644 --- a/.gitignore +++ b/.gitignore @@ -84,3 +84,4 @@ vetur.config.js cookbook/static/vue vue/webpack-stats.json cookbook/templates/sw.js +.prettierignore diff --git a/cookbook/helper/recipe_search.py b/cookbook/helper/recipe_search.py index 195cafef..6553af39 100644 --- a/cookbook/helper/recipe_search.py +++ b/cookbook/helper/recipe_search.py @@ -1,17 +1,17 @@ from collections import Counter from datetime import timedelta -from recipes import settings -from django.contrib.postgres.search import ( - SearchQuery, SearchRank, TrigramSimilarity -) +from django.contrib.postgres.search import SearchQuery, SearchRank, TrigramSimilarity from django.core.cache import caches from django.db.models import Avg, Case, Count, Func, Max, Q, Subquery, Value, When from django.db.models.functions import Coalesce from django.utils import timezone, translation +from cookbook.filters import RecipeFilter +from cookbook.helper.permission_helper import has_group_permission from cookbook.managers import DICTIONARY -from cookbook.models import Food, Keyword, ViewLog, SearchPreference +from cookbook.models import Food, Keyword, Recipe, SearchPreference, ViewLog +from recipes import settings class Round(Func): @@ -143,9 +143,9 @@ def search_recipes(request, queryset, params): # TODO add order by user settings - only do search rank and annotation if rank order is configured search_rank = ( - SearchRank('name_search_vector', search_query, cover_density=True) - + SearchRank('desc_search_vector', search_query, cover_density=True) - + SearchRank('steps__search_vector', search_query, cover_density=True) + SearchRank('name_search_vector', search_query, cover_density=True) + + SearchRank('desc_search_vector', search_query, cover_density=True) + + SearchRank('steps__search_vector', search_query, cover_density=True) ) queryset = queryset.filter(query_filter).annotate(rank=search_rank) orderby += ['-rank'] @@ -400,3 +400,13 @@ def annotated_qs(qs, root=False, fill=False): if start_depth and start_depth > 0: info['close'] = list(range(0, prev_depth - start_depth + 1)) return result + + +def old_search(request): + if has_group_permission(request.user, ('guest',)): + params = dict(request.GET) + params['internal'] = None + f = RecipeFilter(params, + queryset=Recipe.objects.filter(space=request.user.userpreference.space).all().order_by('name'), + space=request.space) + return f.qs diff --git a/cookbook/templates/base.html b/cookbook/templates/base.html index 6d7b0413..1b0229e0 100644 --- a/cookbook/templates/base.html +++ b/cookbook/templates/base.html @@ -345,6 +345,7 @@ localStorage.setItem('SCRIPT_NAME', "{% base_path request 'script' %}") localStorage.setItem('BASE_PATH', "{% base_path request 'base' %}") localStorage.setItem('STATIC_URL', "{% base_path request 'static_base' %}") + localStorage.setItem('DEBUG', "{% is_debug %}") window.addEventListener("load", () => { if ("serviceWorker" in navigator) { navigator.serviceWorker.register("{% url 'service_worker' %}", {scope: "{% base_path request 'base' %}" + '/'}).then(function (reg) { diff --git a/cookbook/urls.py b/cookbook/urls.py index 9be128ac..690eae01 100644 --- a/cookbook/urls.py +++ b/cookbook/urls.py @@ -2,17 +2,17 @@ from pydoc import locate from django.urls import include, path from django.views.generic import TemplateView -from recipes.version import VERSION_NUMBER -from rest_framework import routers, permissions +from rest_framework import permissions, routers from rest_framework.schemas import get_schema_view from cookbook.helper import dal +from recipes.settings import DEBUG +from recipes.version import VERSION_NUMBER -from .models import (Comment, Food, InviteLink, Keyword, MealPlan, Recipe, - RecipeBook, RecipeBookEntry, RecipeImport, ShoppingList, - Storage, Supermarket, SupermarketCategory, Sync, SyncLog, Unit, get_model_name, Automation, - UserFile, Step) -from .views import api, data, delete, edit, import_export, lists, new, views, telegram +from .models import (Automation, Comment, Food, InviteLink, Keyword, MealPlan, Recipe, RecipeBook, + RecipeBookEntry, RecipeImport, ShoppingList, Step, Storage, Supermarket, + SupermarketCategory, Sync, SyncLog, Unit, UserFile, get_model_name) +from .views import api, data, delete, edit, import_export, lists, new, telegram, views router = routers.DefaultRouter() router.register(r'user-name', api.UserNameViewSet, basename='username') @@ -68,8 +68,6 @@ urlpatterns = [ path('history/', views.history, name='view_history'), path('supermarket/', views.supermarket, name='view_supermarket'), path('abuse/', views.report_share_abuse, name='view_report_share_abuse'), - path('test/', views.test, name='view_test'), - path('test2/', views.test2, name='view_test2'), path('import/', import_export.import_recipe, name='view_import'), path('import-response//', import_export.import_response, name='view_import_response'), @@ -189,3 +187,7 @@ for m in vue_models: f'list/{url_name}/', c, name=f'list_{py_name}' ) ) + +if DEBUG: + urlpatterns.append(path('test/', views.test, name='view_test')) + urlpatterns.append(path('test2/', views.test2, name='view_test2')) diff --git a/cookbook/views/api.py b/cookbook/views/api.py index 026629d1..87f46d4e 100644 --- a/cookbook/views/api.py +++ b/cookbook/views/api.py @@ -2,11 +2,11 @@ import io import json import re import uuid +from collections import OrderedDict import requests from annoying.decorators import ajax_request from annoying.functions import get_object_or_None -from collections import OrderedDict from django.contrib import messages from django.contrib.auth.models import User from django.contrib.postgres.search import TrigramSimilarity @@ -15,12 +15,12 @@ from django.core.files import File from django.db.models import Case, ProtectedError, Q, Value, When from django.db.models.fields.related import ForeignObjectRel from django.http import FileResponse, HttpResponse, JsonResponse -from django_scopes import scopes_disabled -from django.shortcuts import redirect, get_object_or_404 +from django.shortcuts import get_object_or_404, redirect from django.urls import reverse from django.utils.translation import gettext as _ +from django_scopes import scopes_disabled from icalendar import Calendar, Event -from recipe_scrapers import scrape_me, WebsiteNotImplementedError, NoSchemaFoundInWildMode +from recipe_scrapers import NoSchemaFoundInWildMode, WebsiteNotImplementedError, scrape_me from rest_framework import decorators, status, viewsets from rest_framework.exceptions import APIException, PermissionDenied from rest_framework.pagination import PageNumberPagination @@ -28,41 +28,39 @@ from rest_framework.parsers import MultiPartParser from rest_framework.renderers import JSONRenderer, TemplateHTMLRenderer from rest_framework.response import Response from rest_framework.viewsets import ViewSetMixin -from treebeard.exceptions import PathOverflow, InvalidMoveToDescendant, InvalidPosition +from treebeard.exceptions import InvalidMoveToDescendant, InvalidPosition, PathOverflow from cookbook.helper.image_processing import handle_image from cookbook.helper.ingredient_parser import IngredientParser -from cookbook.helper.permission_helper import (CustomIsAdmin, CustomIsGuest, - CustomIsOwner, CustomIsShare, - CustomIsShared, CustomIsUser, +from cookbook.helper.permission_helper import (CustomIsAdmin, CustomIsGuest, CustomIsOwner, + CustomIsShare, CustomIsShared, CustomIsUser, group_required) from cookbook.helper.recipe_html_import import get_recipe_from_source - -from cookbook.helper.recipe_search import search_recipes, get_facet +from cookbook.helper.recipe_search import get_facet, old_search, search_recipes from cookbook.helper.recipe_url_import import get_from_scraper -from cookbook.models import (CookLog, Food, Ingredient, Keyword, MealPlan, - MealType, Recipe, RecipeBook, ShoppingList, - ShoppingListEntry, ShoppingListRecipe, Step, - Storage, Sync, SyncLog, Unit, UserPreference, - ViewLog, RecipeBookEntry, Supermarket, ImportLog, BookmarkletImport, SupermarketCategory, UserFile, ShareLink, SupermarketCategoryRelation, Automation) +from cookbook.models import (Automation, BookmarkletImport, CookLog, Food, ImportLog, Ingredient, + Keyword, MealPlan, MealType, Recipe, RecipeBook, RecipeBookEntry, + ShareLink, ShoppingList, ShoppingListEntry, ShoppingListRecipe, Step, + Storage, Supermarket, SupermarketCategory, SupermarketCategoryRelation, + Sync, SyncLog, Unit, UserFile, UserPreference, ViewLog) from cookbook.provider.dropbox import Dropbox from cookbook.provider.local import Local from cookbook.provider.nextcloud import Nextcloud -from cookbook.schemas import FilterSchema, RecipeSchema, TreeSchema, QueryOnlySchema -from cookbook.serializer import (FoodSerializer, IngredientSerializer, - KeywordSerializer, MealPlanSerializer, - MealTypeSerializer, RecipeBookSerializer, - RecipeImageSerializer, RecipeSerializer, - ShoppingListAutoSyncSerializer, - ShoppingListEntrySerializer, - ShoppingListRecipeSerializer, - ShoppingListSerializer, StepSerializer, - StorageSerializer, SyncLogSerializer, - SyncSerializer, UnitSerializer, - UserNameSerializer, UserPreferenceSerializer, - ViewLogSerializer, CookLogSerializer, RecipeBookEntrySerializer, - RecipeOverviewSerializer, SupermarketSerializer, ImportLogSerializer, - BookmarkletImportSerializer, SupermarketCategorySerializer, UserFileSerializer, SupermarketCategoryRelationSerializer, AutomationSerializer) +from cookbook.schemas import FilterSchema, QueryOnlySchema, RecipeSchema, TreeSchema +from cookbook.serializer import (AutomationSerializer, BookmarkletImportSerializer, + CookLogSerializer, FoodSerializer, ImportLogSerializer, + IngredientSerializer, KeywordSerializer, MealPlanSerializer, + MealTypeSerializer, RecipeBookEntrySerializer, + RecipeBookSerializer, RecipeImageSerializer, + RecipeOverviewSerializer, RecipeSerializer, + ShoppingListAutoSyncSerializer, ShoppingListEntrySerializer, + ShoppingListRecipeSerializer, ShoppingListSerializer, + StepSerializer, StorageSerializer, + SupermarketCategoryRelationSerializer, + SupermarketCategorySerializer, SupermarketSerializer, + SyncLogSerializer, SyncSerializer, UnitSerializer, + UserFileSerializer, UserNameSerializer, UserPreferenceSerializer, + ViewLogSerializer) from recipes import settings @@ -547,7 +545,16 @@ class RecipeViewSet(viewsets.ModelViewSet): return super().get_queryset() + def list(self, request, *args, **kwargs): + if self.request.GET.get('debug', False): + return JsonResponse({ + 'new': str(self.get_queryset().query), + 'old': str(old_search(request).query) + }) + return super().list(request, *args, **kwargs) + # TODO write extensive tests for permissions + def get_serializer_class(self): if self.action == 'list': return RecipeOverviewSerializer diff --git a/cookbook/views/views.py b/cookbook/views/views.py index 23fafdd8..5b37cdc5 100644 --- a/cookbook/views/views.py +++ b/cookbook/views/views.py @@ -13,7 +13,7 @@ from django.contrib.auth.password_validation import validate_password from django.core.exceptions import ValidationError from django.db.models import Avg, Q, Sum from django.http import HttpResponseRedirect, JsonResponse -from django.shortcuts import get_object_or_404, render, redirect +from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse, reverse_lazy from django.utils import timezone from django.utils.translation import gettext as _ @@ -22,16 +22,15 @@ from django_tables2 import RequestConfig from rest_framework.authtoken.models import Token from cookbook.filters import RecipeFilter -from cookbook.forms import (CommentForm, Recipe, User, - UserCreateForm, UserNameForm, UserPreference, - UserPreferenceForm, SpaceJoinForm, SpaceCreateForm, - SearchPreferenceForm) -from cookbook.helper.permission_helper import group_required, share_link_valid, has_group_permission -from cookbook.models import (Comment, CookLog, InviteLink, MealPlan, - ViewLog, ShoppingList, Space, Keyword, RecipeImport, Unit, - Food, UserFile, ShareLink, SearchPreference, SearchFields) -from cookbook.tables import (CookLogTable, RecipeTable, RecipeTableSmall, - ViewLogTable, InviteLinkTable) +from cookbook.forms import (CommentForm, Recipe, SearchPreferenceForm, SpaceCreateForm, + SpaceJoinForm, User, UserCreateForm, UserNameForm, UserPreference, + UserPreferenceForm) +from cookbook.helper.permission_helper import group_required, has_group_permission, share_link_valid +from cookbook.models import (Comment, CookLog, Food, InviteLink, Keyword, MealPlan, RecipeImport, + SearchFields, SearchPreference, ShareLink, ShoppingList, Space, Unit, + UserFile, ViewLog) +from cookbook.tables import (CookLogTable, InviteLinkTable, RecipeTable, RecipeTableSmall, + ViewLogTable) from cookbook.views.data import Object from recipes.version import BUILD_REF, VERSION_NUMBER @@ -331,10 +330,10 @@ def user_settings(request): if not sp: sp = SearchPreferenceForm(user=request.user) fields_searched = ( - len(search_form.cleaned_data['icontains']) - + len(search_form.cleaned_data['istartswith']) - + len(search_form.cleaned_data['trigram']) - + len(search_form.cleaned_data['fulltext']) + len(search_form.cleaned_data['icontains']) + + len(search_form.cleaned_data['istartswith']) + + len(search_form.cleaned_data['trigram']) + + len(search_form.cleaned_data['fulltext']) ) if fields_searched == 0: search_form.add_error(None, _('You must select at least one field to search!')) @@ -382,7 +381,7 @@ def user_settings(request): if up: preference_form = UserPreferenceForm(instance=up, space=request.space) else: - preference_form = UserPreferenceForm( space=request.space) + preference_form = UserPreferenceForm(space=request.space) fields_searched = len(sp.icontains.all()) + len(sp.istartswith.all()) + len(sp.trigram.all()) + len( sp.fulltext.all()) diff --git a/vue/src/apps/RecipeSearchView/RecipeSearchView.vue b/vue/src/apps/RecipeSearchView/RecipeSearchView.vue index 9521fd6f..e753d3a5 100644 --- a/vue/src/apps/RecipeSearchView/RecipeSearchView.vue +++ b/vue/src/apps/RecipeSearchView/RecipeSearchView.vue @@ -1,554 +1,563 @@ - + diff --git a/vue/src/locales/en.json b/vue/src/locales/en.json index 650fcb9d..f211e044 100644 --- a/vue/src/locales/en.json +++ b/vue/src/locales/en.json @@ -1,210 +1,211 @@ { - "warning_feature_beta": "This feature is currently in a BETA (testing) state. Please expect bugs and possibly breaking changes in the future (possibly loosing feature related data) when using this feature.", - "err_fetching_resource": "There was an error fetching a resource!", - "err_creating_resource": "There was an error creating a resource!", - "err_updating_resource": "There was an error updating a resource!", - "err_deleting_resource": "There was an error deleting a resource!", - "success_fetching_resource": "Successfully fetched a resource!", - "success_creating_resource": "Successfully created a resource!", - "success_updating_resource": "Successfully updated a resource!", - "success_deleting_resource": "Successfully deleted a resource!", - "file_upload_disabled": "File upload is not enabled for your space.", - "step_time_minutes": "Step time in minutes", - "confirm_delete": "Are you sure you want to delete this {object}?", - "import_running": "Import running, please wait!", - "all_fields_optional": "All fields are optional and can be left empty.", - "convert_internal": "Convert to internal recipe", - "show_only_internal": "Show only internal recipes", - "show_split_screen": "Split View", - "Log_Recipe_Cooking": "Log Recipe Cooking", - "External_Recipe_Image": "External Recipe Image", - "Add_to_Shopping": "Add to Shopping", - "Add_to_Plan": "Add to Plan", - "Step_start_time": "Step start time", - "Sort_by_new": "Sort by new", - "Table_of_Contents": "Table of Contents", - "Recipes_per_page": "Recipes per Page", - "Show_as_header": "Show as header", - "Hide_as_header": "Hide as header", - "Add_nutrition_recipe": "Add nutrition to recipe", - "Remove_nutrition_recipe": "Delete nutrition from recipe", - "Copy_template_reference": "Copy template reference", - "Save_and_View": "Save & View", - "Manage_Books": "Manage Books", - "Meal_Plan": "Meal Plan", - "Select_Book": "Select Book", - "Select_File": "Select File", - "Recipe_Image": "Recipe Image", - "Import_finished": "Import finished", - "View_Recipes": "View Recipes", - "Log_Cooking": "Log Cooking", - "New_Recipe": "New Recipe", - "Url_Import": "Url Import", - "Reset_Search": "Reset Search", - "Recently_Viewed": "Recently Viewed", - "Load_More": "Load More", - "New_Keyword": "New Keyword", - "Delete_Keyword": "Delete Keyword", - "Edit_Keyword": "Edit Keyword", - "Edit_Recipe": "Edit Recipe", - "Move_Keyword": "Move Keyword", - "Merge_Keyword": "Merge Keyword", - "Hide_Keywords": "Hide Keyword", - "Hide_Recipes": "Hide Recipes", - "Move_Up": "Move up", - "Move_Down": "Move down", - "Step_Name": "Step Name", - "Step_Type": "Step Type", - "Make_header": "Make_Header", - "Make_Ingredient": "Make_Ingredient", - "Enable_Amount": "Enable Amount", - "Disable_Amount": "Disable Amount", - "Add_Step": "Add Step", - "Keywords": "Keywords", - "Books": "Books", - "Proteins": "Proteins", - "Fats": "Fats", - "Carbohydrates": "Carbohydrates", - "Calories": "Calories", - "Energy": "Energy", - "Nutrition": "Nutrition", - "Date": "Date", - "Share": "Share", - "Automation": "Automation", - "Parameter": "Parameter", - "Export": "Export", - "Copy": "Copy", - "Rating": "Rating", - "Close": "Close", - "Cancel": "Cancel", - "Link": "Link", - "Add": "Add", - "New": "New", - "Note": "Note", - "Success": "Success", - "Failure": "Failure", - "Ingredients": "Ingredients", - "Supermarket": "Supermarket", - "Categories": "Categories", - "Category": "Category", - "Selected": "Selected", - "min": "min", - "Servings": "Servings", - "Waiting": "Waiting", - "Preparation": "Preparation", - "External": "External", - "Size": "Size", - "Files": "Files", - "File": "File", - "Edit": "Edit", - "Image": "Image", - "Delete": "Delete", - "Open": "Open", - "Ok": "Open", - "Save": "Save", - "Step": "Step", - "Search": "Search", - "Import": "Import", - "Print": "Print", - "Settings": "Settings", - "or": "or", - "and": "and", - "Information": "Information", - "Download": "Download", - "Create": "Create", - "Advanced Search Settings": "Advanced Search Settings", - "View": "View", - "Recipes": "Recipes", - "Move": "Move", - "Merge": "Merge", - "Parent": "Parent", - "delete_confirmation": "Are you sure that you want to delete {source}?", - "move_confirmation": "Move {child} to parent {parent}", - "merge_confirmation": "Replace {source} with {target}", - "create_rule": "and create automation", - "move_selection": "Select a parent {type} to move {source} to.", - "merge_selection": "Replace all occurrences of {source} with the selected {type}.", - "Root": "Root", - "Ignore_Shopping": "Ignore Shopping", - "Shopping_Category": "Shopping Category", - "Edit_Food": "Edit Food", - "Move_Food": "Move Food", - "New_Food": "New Food", - "Hide_Food": "Hide Food", - "Food_Alias": "Food Alias", - "Unit_Alias": "Unit Alias", - "Keyword_Alias": "Keyword Alias", - "Delete_Food": "Delete Food", - "No_ID": "ID not found, cannot delete.", - "Meal_Plan_Days": "Future meal plans", - "merge_title": "Merge {type}", - "move_title": "Move {type}", - "Food": "Food", - "Recipe_Book": "Recipe Book", - "del_confirmation_tree": "Are you sure that you want to delete {source} and all of it's children?", - "delete_title": "Delete {type}", - "create_title": "New {type}", - "edit_title": "Edit {type}", - "Name": "Name", - "Type": "Type", - "Description": "Description", - "Recipe": "Recipe", - "tree_root": "Root of Tree", - "Icon": "Icon", - "Unit": "Unit", - "No_Results": "No Results", - "New_Unit": "New Unit", - "Create_New_Shopping Category": "Create New Shopping Category", - "Create_New_Food": "Add New Food", - "Create_New_Keyword": "Add New Keyword", - "Create_New_Unit": "Add New Unit", - "Create_New_Meal_Type": "Add New Meal Type", - "and_up": "& Up", - "Instructions": "Instructions", - "Unrated": "Unrated", - "Automate": "Automate", - "Empty": "Empty", - "Key_Ctrl": "Ctrl", - "Key_Shift": "Shift", - "Time": "Time", - "Text": "Text", - "Shopping_list": "Shopping List", - "Create_Meal_Plan_Entry": "Create meal plan entry", - "Edit_Meal_Plan_Entry": "Edit meal plan entry", - "Title": "Title", - "Week": "Week", - "Month": "Month", - "Year": "Year", - "Planner": "Planner", - "Planner_Settings": "Planner settings", - "Period": "Period", - "Plan_Period_To_Show": "Show weeks, months or years", - "Periods": "Periods", - "Plan_Show_How_Many_Periods": "How many periods to show", - "Starting_Day": "Starting day of the week", - "Meal_Types": "Meal types", - "Meal_Type": "Meal type", - "Clone": "Clone", - "Drag_Here_To_Delete": "Drag here to delete", - "Meal_Type_Required": "Meal type is required", - "Title_or_Recipe_Required": "Title or recipe selection required", - "Color": "Color", - "New_Meal_Type": "New Meal type", - "Week_Numbers": "Week numbers", - "Show_Week_Numbers": "Show week numbers ?", - "Export_As_ICal": "Export current period to iCal format", - "Export_To_ICal": "Export .ics", - "Cannot_Add_Notes_To_Shopping": "Notes cannot be added to the shopping list", - "Added_To_Shopping_List": "Added to shopping list", - "Shopping_List_Empty": "Your shopping list is currently empty, you can add items via the context menu of a meal plan entry (right click on the card or left click the menu icon)", - "Next_Period": "Next Period", - "Previous_Period": "Previous Period", - "Current_Period": "Current Period", - "Next_Day": "Next Day", - "Previous_Day": "Previous Day", - "Coming_Soon": "Coming-Soon", - "Auto_Planner": "Auto-Planner", - "New_Cookbook": "New cookbook", - "Hide_Keyword": "Hide keywords", - "Clear": "Clear" + "warning_feature_beta": "This feature is currently in a BETA (testing) state. Please expect bugs and possibly breaking changes in the future (possibly loosing feature related data) when using this feature.", + "err_fetching_resource": "There was an error fetching a resource!", + "err_creating_resource": "There was an error creating a resource!", + "err_updating_resource": "There was an error updating a resource!", + "err_deleting_resource": "There was an error deleting a resource!", + "success_fetching_resource": "Successfully fetched a resource!", + "success_creating_resource": "Successfully created a resource!", + "success_updating_resource": "Successfully updated a resource!", + "success_deleting_resource": "Successfully deleted a resource!", + "file_upload_disabled": "File upload is not enabled for your space.", + "step_time_minutes": "Step time in minutes", + "confirm_delete": "Are you sure you want to delete this {object}?", + "import_running": "Import running, please wait!", + "all_fields_optional": "All fields are optional and can be left empty.", + "convert_internal": "Convert to internal recipe", + "show_only_internal": "Show only internal recipes", + "show_split_screen": "Split View", + "Log_Recipe_Cooking": "Log Recipe Cooking", + "External_Recipe_Image": "External Recipe Image", + "Add_to_Shopping": "Add to Shopping", + "Add_to_Plan": "Add to Plan", + "Step_start_time": "Step start time", + "Sort_by_new": "Sort by new", + "Table_of_Contents": "Table of Contents", + "Recipes_per_page": "Recipes per Page", + "Show_as_header": "Show as header", + "Hide_as_header": "Hide as header", + "Add_nutrition_recipe": "Add nutrition to recipe", + "Remove_nutrition_recipe": "Delete nutrition from recipe", + "Copy_template_reference": "Copy template reference", + "Save_and_View": "Save & View", + "Manage_Books": "Manage Books", + "Meal_Plan": "Meal Plan", + "Select_Book": "Select Book", + "Select_File": "Select File", + "Recipe_Image": "Recipe Image", + "Import_finished": "Import finished", + "View_Recipes": "View Recipes", + "Log_Cooking": "Log Cooking", + "New_Recipe": "New Recipe", + "Url_Import": "Url Import", + "Reset_Search": "Reset Search", + "Recently_Viewed": "Recently Viewed", + "Load_More": "Load More", + "New_Keyword": "New Keyword", + "Delete_Keyword": "Delete Keyword", + "Edit_Keyword": "Edit Keyword", + "Edit_Recipe": "Edit Recipe", + "Move_Keyword": "Move Keyword", + "Merge_Keyword": "Merge Keyword", + "Hide_Keywords": "Hide Keyword", + "Hide_Recipes": "Hide Recipes", + "Move_Up": "Move up", + "Move_Down": "Move down", + "Step_Name": "Step Name", + "Step_Type": "Step Type", + "Make_header": "Make_Header", + "Make_Ingredient": "Make_Ingredient", + "Enable_Amount": "Enable Amount", + "Disable_Amount": "Disable Amount", + "Add_Step": "Add Step", + "Keywords": "Keywords", + "Books": "Books", + "Proteins": "Proteins", + "Fats": "Fats", + "Carbohydrates": "Carbohydrates", + "Calories": "Calories", + "Energy": "Energy", + "Nutrition": "Nutrition", + "Date": "Date", + "Share": "Share", + "Automation": "Automation", + "Parameter": "Parameter", + "Export": "Export", + "Copy": "Copy", + "Rating": "Rating", + "Close": "Close", + "Cancel": "Cancel", + "Link": "Link", + "Add": "Add", + "New": "New", + "Note": "Note", + "Success": "Success", + "Failure": "Failure", + "Ingredients": "Ingredients", + "Supermarket": "Supermarket", + "Categories": "Categories", + "Category": "Category", + "Selected": "Selected", + "min": "min", + "Servings": "Servings", + "Waiting": "Waiting", + "Preparation": "Preparation", + "External": "External", + "Size": "Size", + "Files": "Files", + "File": "File", + "Edit": "Edit", + "Image": "Image", + "Delete": "Delete", + "Open": "Open", + "Ok": "Open", + "Save": "Save", + "Step": "Step", + "Search": "Search", + "Import": "Import", + "Print": "Print", + "Settings": "Settings", + "or": "or", + "and": "and", + "Information": "Information", + "Download": "Download", + "Create": "Create", + "Advanced Search Settings": "Advanced Search Settings", + "View": "View", + "Recipes": "Recipes", + "Move": "Move", + "Merge": "Merge", + "Parent": "Parent", + "delete_confirmation": "Are you sure that you want to delete {source}?", + "move_confirmation": "Move {child} to parent {parent}", + "merge_confirmation": "Replace {source} with {target}", + "create_rule": "and create automation", + "move_selection": "Select a parent {type} to move {source} to.", + "merge_selection": "Replace all occurrences of {source} with the selected {type}.", + "Root": "Root", + "Ignore_Shopping": "Ignore Shopping", + "Shopping_Category": "Shopping Category", + "Edit_Food": "Edit Food", + "Move_Food": "Move Food", + "New_Food": "New Food", + "Hide_Food": "Hide Food", + "Food_Alias": "Food Alias", + "Unit_Alias": "Unit Alias", + "Keyword_Alias": "Keyword Alias", + "Delete_Food": "Delete Food", + "No_ID": "ID not found, cannot delete.", + "Meal_Plan_Days": "Future meal plans", + "merge_title": "Merge {type}", + "move_title": "Move {type}", + "Food": "Food", + "Recipe_Book": "Recipe Book", + "del_confirmation_tree": "Are you sure that you want to delete {source} and all of it's children?", + "delete_title": "Delete {type}", + "create_title": "New {type}", + "edit_title": "Edit {type}", + "Name": "Name", + "Type": "Type", + "Description": "Description", + "Recipe": "Recipe", + "tree_root": "Root of Tree", + "Icon": "Icon", + "Unit": "Unit", + "No_Results": "No Results", + "New_Unit": "New Unit", + "Create_New_Shopping Category": "Create New Shopping Category", + "Create_New_Food": "Add New Food", + "Create_New_Keyword": "Add New Keyword", + "Create_New_Unit": "Add New Unit", + "Create_New_Meal_Type": "Add New Meal Type", + "and_up": "& Up", + "Instructions": "Instructions", + "Unrated": "Unrated", + "Automate": "Automate", + "Empty": "Empty", + "Key_Ctrl": "Ctrl", + "Key_Shift": "Shift", + "Time": "Time", + "Text": "Text", + "Shopping_list": "Shopping List", + "Create_Meal_Plan_Entry": "Create meal plan entry", + "Edit_Meal_Plan_Entry": "Edit meal plan entry", + "Title": "Title", + "Week": "Week", + "Month": "Month", + "Year": "Year", + "Planner": "Planner", + "Planner_Settings": "Planner settings", + "Period": "Period", + "Plan_Period_To_Show": "Show weeks, months or years", + "Periods": "Periods", + "Plan_Show_How_Many_Periods": "How many periods to show", + "Starting_Day": "Starting day of the week", + "Meal_Types": "Meal types", + "Meal_Type": "Meal type", + "Clone": "Clone", + "Drag_Here_To_Delete": "Drag here to delete", + "Meal_Type_Required": "Meal type is required", + "Title_or_Recipe_Required": "Title or recipe selection required", + "Color": "Color", + "New_Meal_Type": "New Meal type", + "Week_Numbers": "Week numbers", + "Show_Week_Numbers": "Show week numbers ?", + "Export_As_ICal": "Export current period to iCal format", + "Export_To_ICal": "Export .ics", + "Cannot_Add_Notes_To_Shopping": "Notes cannot be added to the shopping list", + "Added_To_Shopping_List": "Added to shopping list", + "Shopping_List_Empty": "Your shopping list is currently empty, you can add items via the context menu of a meal plan entry (right click on the card or left click the menu icon)", + "Next_Period": "Next Period", + "Previous_Period": "Previous Period", + "Current_Period": "Current Period", + "Next_Day": "Next Day", + "Previous_Day": "Previous Day", + "Coming_Soon": "Coming-Soon", + "Auto_Planner": "Auto-Planner", + "New_Cookbook": "New cookbook", + "Hide_Keyword": "Hide keywords", + "Clear": "Clear", + "show_sql": "Show SQL" }