From 689eb426ea5b0e53d2c0e4cc643915d0187c9832 Mon Sep 17 00:00:00 2001 From: AquaticLava Date: Sun, 4 Sep 2022 16:30:36 -0600 Subject: [PATCH 01/71] method for asynchronous generation of meals. start of menu for auto planner. delete method deletes all records for testing the auto planner. --- vue/src/apps/MealPlanView/MealPlanView.vue | 60 ++++- vue/src/components/AutoMealPlanModal.vue | 246 +++++++++++++++++++++ 2 files changed, 302 insertions(+), 4 deletions(-) create mode 100644 vue/src/components/AutoMealPlanModal.vue diff --git a/vue/src/apps/MealPlanView/MealPlanView.vue b/vue/src/apps/MealPlanView/MealPlanView.vue index 0866a293f3..14f2785838 100644 --- a/vue/src/apps/MealPlanView/MealPlanView.vue +++ b/vue/src/apps/MealPlanView/MealPlanView.vue @@ -208,6 +208,13 @@ @delete-entry="deleteEntry" @reload-meal-types="refreshMealTypes" > +
-
@@ -272,6 +278,8 @@ import VueCookies from "vue-cookies" import {ApiMixin, StandardToasts, ResolveUrlMixin} from "@/utils/utils" import {CalendarView, CalendarMathMixin} from "vue-simple-calendar/src/components/bundle" import {ApiApiFactory} from "@/utils/openapi/api" +import axios from "axios"; +import AutoMealPlanModal from "@/components/AutoMealPlanModal"; const {makeToast} = require("@/utils/utils") @@ -284,6 +292,7 @@ let SETTINGS_COOKIE_NAME = "mealplan_settings" export default { name: "MealPlanView", components: { + AutoMealPlanModal, MealPlanEditModal, MealPlanCard, CalendarView, @@ -546,7 +555,7 @@ export default { }, deleteEntry(data) { this.plan_entries.forEach((entry, index, list) => { - if (entry.id === data.id) { + //if (entry.id === data.id) {//todo:remove block! let apiClient = new ApiApiFactory() apiClient @@ -557,7 +566,7 @@ export default { .catch((err) => { StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err) }) - } + //} }) }, entryClick(data) { @@ -634,6 +643,49 @@ export default { entry: plan_entry, } }, + createAutoPlan(date) { + this.$bvModal.show(`autoplan-modal`) + }, + async autoPlanThread(date,dateOffset,i,servings){ + + let apiClient = new ApiApiFactory() + let currentEntry = Object.assign({}, this.options.entryEditing) + currentEntry.date = moment(date).add(dateOffset,"d").format("YYYY-MM-DD") + currentEntry.servings = servings + await Promise.all([ + currentEntry.recipe = await this.randomRecipe(i+3).then((result)=>{return result}), + currentEntry.shared = await apiClient.listUserPreferences().then((result) => {return result.data[0].plan_share}), + currentEntry.meal_type = await this.getMealType(i+2).then((result)=>{return result}) + ]) + currentEntry.title = currentEntry.recipe.name + this.createEntry(currentEntry) + }, + autoPlan(data, servings){ + // ["breakfast","lunch","dinner"] + // meal types: 4,3,2 + //meal keywords: 5,4,3 + for (let i = 0; i < 3; i++) { + for (let dateOffset = 0; dateOffset < 7; dateOffset++) { + this.autoPlanThread(data,dateOffset,i,servings) + } + } + }, + randomRecipe(keyword) { + let url = `/api/recipe/?query=&keywords_or=${keyword}` + return axios.get(url).then((response) => { + let result = response.data + let count = result.count + return result.results[Math.floor(Math.random() * count)] + }).catch((err) => { + + }) + }, + getMealType(id) { + let url = `/api/meal-type/${id}` + return axios.get(url).then((response) => { + return response.data + }) + } }, directives: { hover: { diff --git a/vue/src/components/AutoMealPlanModal.vue b/vue/src/components/AutoMealPlanModal.vue new file mode 100644 index 0000000000..1138e67b77 --- /dev/null +++ b/vue/src/components/AutoMealPlanModal.vue @@ -0,0 +1,246 @@ + + + + + From 31f34253540164458f9f95e4b70ca6fc619f5c8a Mon Sep 17 00:00:00 2001 From: AquaticLava Date: Thu, 5 Jan 2023 16:25:42 -0700 Subject: [PATCH 02/71] Menu for auto planner, menu sets auto planner settings. delete method no longer deletes all records for testing the auto planner. --- vue/src/apps/MealPlanView/MealPlanView.vue | 59 ++-- vue/src/components/AutoMealPlanModal.vue | 324 ++++++++------------- 2 files changed, 160 insertions(+), 223 deletions(-) diff --git a/vue/src/apps/MealPlanView/MealPlanView.vue b/vue/src/apps/MealPlanView/MealPlanView.vue index 14f2785838..97a4b85133 100644 --- a/vue/src/apps/MealPlanView/MealPlanView.vue +++ b/vue/src/apps/MealPlanView/MealPlanView.vue @@ -209,11 +209,9 @@ @reload-meal-types="refreshMealTypes" > @@ -305,6 +303,14 @@ export default { mixins: [CalendarMathMixin, ApiMixin, ResolveUrlMixin], data: function () { return { + AutoPlan: { + meal_types: [], + keywords: [[]], + servings: 1, + date: Date.now(), + startDay: null, + endDay: null + }, showDate: new Date(), plan_entries: [], recipe_viewed: {}, @@ -555,7 +561,7 @@ export default { }, deleteEntry(data) { this.plan_entries.forEach((entry, index, list) => { - //if (entry.id === data.id) {//todo:remove block! + if (entry.id === data.id) { let apiClient = new ApiApiFactory() apiClient @@ -566,7 +572,7 @@ export default { .catch((err) => { StandardToasts.makeStandardToast(this, StandardToasts.FAIL_UPDATE, err) }) - //} + } }) }, entryClick(data) { @@ -643,35 +649,44 @@ export default { entry: plan_entry, } }, - createAutoPlan(date) { + createAutoPlan() { this.$bvModal.show(`autoplan-modal`) }, - async autoPlanThread(date,dateOffset,i,servings){ + async autoPlanThread(date,dateOffset,meal_type,keywords,servings,mealTypesKey){ let apiClient = new ApiApiFactory() let currentEntry = Object.assign({}, this.options.entryEditing) currentEntry.date = moment(date).add(dateOffset,"d").format("YYYY-MM-DD") currentEntry.servings = servings await Promise.all([ - currentEntry.recipe = await this.randomRecipe(i+3).then((result)=>{return result}), + currentEntry.recipe = await this.randomRecipe(keywords[mealTypesKey]).then((result)=>{return result}), currentEntry.shared = await apiClient.listUserPreferences().then((result) => {return result.data[0].plan_share}), - currentEntry.meal_type = await this.getMealType(i+2).then((result)=>{return result}) + currentEntry.meal_type = await this.getMealType(meal_type[mealTypesKey].id).then((result)=>{return result}) ]) currentEntry.title = currentEntry.recipe.name this.createEntry(currentEntry) }, - autoPlan(data, servings){ - // ["breakfast","lunch","dinner"] - // meal types: 4,3,2 - //meal keywords: 5,4,3 - for (let i = 0; i < 3; i++) { - for (let dateOffset = 0; dateOffset < 7; dateOffset++) { - this.autoPlanThread(data,dateOffset,i,servings) - } - } - }, - randomRecipe(keyword) { - let url = `/api/recipe/?query=&keywords_or=${keyword}` + doAutoPlan(autoPlan){ + console.log(autoPlan) + let dayInMilliseconds = (86400000) + console.log(autoPlan.startDay) + console.log(autoPlan.endDay) + console.log(autoPlan.endDay - autoPlan.startDay) + console.log(((autoPlan.endDay - autoPlan.startDay)/dayInMilliseconds) + 1) + let numberOfDays = ((autoPlan.endDay - autoPlan.startDay)/dayInMilliseconds) + 1 + + for (const mealTypesKey in autoPlan.meal_types) { + for (let dateOffset = 0; dateOffset < numberOfDays; dateOffset++) { + this.autoPlanThread(autoPlan.date, dateOffset, autoPlan.meal_types, autoPlan.keywords, autoPlan.servings, mealTypesKey) + } + } + }, + randomRecipe(keywords) { + let url = "/api/recipe/?query=" + for (const keywordsKey in keywords) { + let keyword = keywords[keywordsKey] + url += `&keywords_and=${keyword.id}` + } return axios.get(url).then((response) => { let result = response.data let count = result.count diff --git a/vue/src/components/AutoMealPlanModal.vue b/vue/src/components/AutoMealPlanModal.vue index 1138e67b77..339427aa7f 100644 --- a/vue/src/components/AutoMealPlanModal.vue +++ b/vue/src/components/AutoMealPlanModal.vue @@ -1,245 +1,167 @@ From 4a390b58246c8f09d5b95c5b352c8319ef31ae49 Mon Sep 17 00:00:00 2001 From: AquaticLava Date: Sun, 8 Jan 2023 12:01:59 -0700 Subject: [PATCH 03/71] removed logging --- vue/src/apps/MealPlanView/MealPlanView.vue | 5 ----- 1 file changed, 5 deletions(-) diff --git a/vue/src/apps/MealPlanView/MealPlanView.vue b/vue/src/apps/MealPlanView/MealPlanView.vue index 97a4b85133..e4a88470a7 100644 --- a/vue/src/apps/MealPlanView/MealPlanView.vue +++ b/vue/src/apps/MealPlanView/MealPlanView.vue @@ -667,12 +667,7 @@ export default { this.createEntry(currentEntry) }, doAutoPlan(autoPlan){ - console.log(autoPlan) let dayInMilliseconds = (86400000) - console.log(autoPlan.startDay) - console.log(autoPlan.endDay) - console.log(autoPlan.endDay - autoPlan.startDay) - console.log(((autoPlan.endDay - autoPlan.startDay)/dayInMilliseconds) + 1) let numberOfDays = ((autoPlan.endDay - autoPlan.startDay)/dayInMilliseconds) + 1 for (const mealTypesKey in autoPlan.meal_types) { From 4fb5ce550e6666fcc98986edb7825cb181a32ae2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 1 May 2023 00:58:35 +0000 Subject: [PATCH 04/71] Bump bleach from 5.0.1 to 6.0.0 Bumps [bleach](https://github.com/mozilla/bleach) from 5.0.1 to 6.0.0. - [Release notes](https://github.com/mozilla/bleach/releases) - [Changelog](https://github.com/mozilla/bleach/blob/main/CHANGES) - [Commits](https://github.com/mozilla/bleach/compare/v5.0.1...v6.0.0) --- updated-dependencies: - dependency-name: bleach dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index b96fc67f09..20704db38c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ djangorestframework==3.14.0 drf-writable-nested==0.7.0 django-oauth-toolkit==2.2.0 django-debug-toolbar==3.8.1 -bleach==5.0.1 +bleach==6.0.0 bleach-allowlist==1.0.3 gunicorn==20.1.0 lxml==4.9.2 From 6c9227faac059fa0c7b9b77a54ace3356d95fc9d Mon Sep 17 00:00:00 2001 From: AquaticLava Date: Thu, 18 May 2023 11:14:59 -0600 Subject: [PATCH 05/71] fixed formatting and minor bug causeing the start of the period to always be the current day. --- .idea/recipes.iml | 2 +- vue/src/apps/MealPlanView/MealPlanView.vue | 95 ++++++++++++---------- 2 files changed, 51 insertions(+), 46 deletions(-) diff --git a/.idea/recipes.iml b/.idea/recipes.iml index 8a0e59c8e7..1b96c9d802 100644 --- a/.idea/recipes.iml +++ b/.idea/recipes.iml @@ -18,7 +18,7 @@ - + diff --git a/vue/src/apps/MealPlanView/MealPlanView.vue b/vue/src/apps/MealPlanView/MealPlanView.vue index 7652c4bd79..c0d027a595 100644 --- a/vue/src/apps/MealPlanView/MealPlanView.vue +++ b/vue/src/apps/MealPlanView/MealPlanView.vue @@ -290,6 +290,9 @@ + {{ $t("Export_To_ICal") }} @@ -302,11 +305,7 @@ {{ $t("Create") }} -
- -
+ @@ -677,53 +676,59 @@ export default { this.$bvModal.show(`id_meal_plan_edit_modal`) }) - } + }, createAutoPlan() { this.$bvModal.show(`autoplan-modal`) }, - async autoPlanThread(date,dateOffset,meal_type,keywords,servings,mealTypesKey){ - - let apiClient = new ApiApiFactory() - let currentEntry = Object.assign({}, this.options.entryEditing) - currentEntry.date = moment(date).add(dateOffset,"d").format("YYYY-MM-DD") - currentEntry.servings = servings - await Promise.all([ - currentEntry.recipe = await this.randomRecipe(keywords[mealTypesKey]).then((result)=>{return result}), - currentEntry.shared = await apiClient.listUserPreferences().then((result) => {return result.data[0].plan_share}), - currentEntry.meal_type = await this.getMealType(meal_type[mealTypesKey].id).then((result)=>{return result}) - ]) - currentEntry.title = currentEntry.recipe.name - this.createEntry(currentEntry) - }, - doAutoPlan(autoPlan){ - let dayInMilliseconds = (86400000) - let numberOfDays = ((autoPlan.endDay - autoPlan.startDay)/dayInMilliseconds) + 1 - - for (const mealTypesKey in autoPlan.meal_types) { - for (let dateOffset = 0; dateOffset < numberOfDays; dateOffset++) { - this.autoPlanThread(autoPlan.date, dateOffset, autoPlan.meal_types, autoPlan.keywords, autoPlan.servings, mealTypesKey) - } - } + async autoPlanThread(date, dateOffset, meal_type, keywords, servings, mealTypesKey) { + + let apiClient = new ApiApiFactory() + let currentEntry = Object.assign({}, this.options.entryEditing) + currentEntry.date = moment(date).add(dateOffset, "d").format("YYYY-MM-DD") + currentEntry.servings = servings + await Promise.all([ + currentEntry.recipe = await this.randomRecipe(keywords[mealTypesKey]).then((result) => { + return result + }), + currentEntry.shared = await apiClient.listUserPreferences().then((result) => { + return result.data[0].plan_share + }), + currentEntry.meal_type = await this.getMealType(meal_type[mealTypesKey].id).then((result) => { + return result + }) + ]) + currentEntry.title = currentEntry.recipe.name + this.createEntry(currentEntry) + }, + doAutoPlan(autoPlan) { + let dayInMilliseconds = (86400000) + let numberOfDays = ((autoPlan.endDay - autoPlan.startDay) / dayInMilliseconds) + 1 + + for (const mealTypesKey in autoPlan.meal_types) { + for (let dateOffset = 0; dateOffset < numberOfDays; dateOffset++) { + this.autoPlanThread(autoPlan.startDay, dateOffset, autoPlan.meal_types, autoPlan.keywords, autoPlan.servings, mealTypesKey) + } + } }, randomRecipe(keywords) { - let url = "/api/recipe/?query=" - for (const keywordsKey in keywords) { - let keyword = keywords[keywordsKey] - url += `&keywords_and=${keyword.id}` - } - return axios.get(url).then((response) => { - let result = response.data - let count = result.count - return result.results[Math.floor(Math.random() * count)] - }).catch((err) => { - - }) + let url = "/api/recipe/?query=" + for (const keywordsKey in keywords) { + let keyword = keywords[keywordsKey] + url += `&keywords_and=${keyword.id}` + } + return axios.get(url).then((response) => { + let result = response.data + let count = result.count + return result.results[Math.floor(Math.random() * count)] + }).catch((err) => { + + }) }, getMealType(id) { - let url = `/api/meal-type/${id}` - return axios.get(url).then((response) => { - return response.data - }) + let url = `/api/meal-type/${id}` + return axios.get(url).then((response) => { + return response.data + }) } }, directives: { From ee38d93e3b83f334adf907b7fe1cd615b1c2f4a3 Mon Sep 17 00:00:00 2001 From: AquaticLava Date: Wed, 21 Jun 2023 19:31:49 -0600 Subject: [PATCH 06/71] Created auto meal plan api endpoint. --- cookbook/serializer.py | 11 ++++++++ cookbook/urls.py | 1 + cookbook/views/api.py | 57 +++++++++++++++++++++++++++++++++++++++++- 3 files changed, 68 insertions(+), 1 deletion(-) diff --git a/cookbook/serializer.py b/cookbook/serializer.py index 54ef438759..505f4e8baa 100644 --- a/cookbook/serializer.py +++ b/cookbook/serializer.py @@ -1,3 +1,4 @@ +import random import traceback import uuid from datetime import datetime, timedelta @@ -979,6 +980,16 @@ class Meta: read_only_fields = ('created_by',) +class AutoMealPlanSerializer(serializers.Serializer): + + start_date = serializers.DateField() + end_date = serializers.DateField() + meal_type_id = serializers.IntegerField() + keywords = KeywordSerializer(many=True) + servings = CustomDecimalField() + shared = UserSerializer(many=True, required=False, allow_null=True) + + class ShoppingListRecipeSerializer(serializers.ModelSerializer): name = serializers.SerializerMethodField('get_name') # should this be done at the front end? recipe_name = serializers.ReadOnlyField(source='recipe.name') diff --git a/cookbook/urls.py b/cookbook/urls.py index 123a5083bf..f7e087e218 100644 --- a/cookbook/urls.py +++ b/cookbook/urls.py @@ -36,6 +36,7 @@ def extend(self, r): router.register(r'invite-link', api.InviteLinkViewSet) router.register(r'keyword', api.KeywordViewSet) router.register(r'meal-plan', api.MealPlanViewSet) +router.register(r'auto-plan', api.AutoPlanViewSet, basename='auto-plan') router.register(r'meal-type', api.MealTypeViewSet) router.register(r'recipe', api.RecipeViewSet) router.register(r'recipe-book', api.RecipeBookViewSet) diff --git a/cookbook/views/api.py b/cookbook/views/api.py index 9e428e8ff3..7491017418 100644 --- a/cookbook/views/api.py +++ b/cookbook/views/api.py @@ -1,7 +1,9 @@ +import datetime import io import json import mimetypes import pathlib +import random import re import threading import traceback @@ -25,6 +27,7 @@ from django.db.models import Case, Count, Exists, OuterRef, ProtectedError, Q, Subquery, Value, When, Avg, Max from django.db.models.fields.related import ForeignObjectRel from django.db.models.functions import Coalesce, Lower +from django.db.models.signals import post_save from django.http import FileResponse, HttpResponse, JsonResponse from django.shortcuts import get_object_or_404, redirect from django.urls import reverse @@ -94,7 +97,8 @@ SyncLogSerializer, SyncSerializer, UnitSerializer, UserFileSerializer, UserSerializer, UserPreferenceSerializer, UserSpaceSerializer, ViewLogSerializer, AccessTokenSerializer, FoodSimpleSerializer, - RecipeExportSerializer, UnitConversionSerializer, PropertyTypeSerializer, PropertySerializer) + RecipeExportSerializer, UnitConversionSerializer, PropertyTypeSerializer, + PropertySerializer, AutoMealPlanSerializer) from cookbook.views.import_export import get_integration from recipes import settings @@ -661,6 +665,57 @@ def get_queryset(self): return queryset +class AutoPlanViewSet(viewsets.ViewSet): + def create(self, request): + serializer = AutoMealPlanSerializer(data=request.data) + + if serializer.is_valid(): + keywords = serializer.validated_data['keywords'] + start_date = serializer.validated_data['start_date'] + end_date = serializer.validated_data['end_date'] + meal_type = MealType.objects.get(pk=serializer.validated_data['meal_type_id']) + servings = serializer.validated_data['servings'] + + days = (end_date - start_date).days + 1 + recipes = Recipe.objects.all() + meal_plans = list() + + for keyword in keywords: + recipes = recipes.filter(keywords__name=keyword['name']) + + recipes = recipes.order_by('?')[:days] + recipes = list(recipes) + + for i in range(0, days): + day = start_date + datetime.timedelta(i) + recipe = random.choice(recipes) + args = {'recipe': recipe, 'servings': servings, 'title': recipe.name, + 'created_by': request.user, + 'meal_type': meal_type, + 'note': '', 'date': day, 'space': request.space} + + m = MealPlan(**args) + meal_plans.append(m) + + MealPlan.objects.bulk_create(meal_plans) + for m in meal_plans: + if request.data.get('addshopping', False) and request.data.get('recipe', None): + SLR = RecipeShoppingEditor(user=request.user, space=request.space) + SLR.create(mealplan=m, servings=servings) + + else: + post_save.send( + sender=m.__class__, + instance=m, + created=True, + update_fields=None, + ) + + return Response(serializer.data) + + return Response(serializer.errors, 400) + + class MealTypeViewSet(viewsets.ModelViewSet): """ returns list of meal types created by the From 9756b7b65320bef16535ce447e90c882dac02647 Mon Sep 17 00:00:00 2001 From: AquaticLava Date: Wed, 21 Jun 2023 19:32:54 -0600 Subject: [PATCH 07/71] regenerated open api file --- vue/src/utils/openapi/api.ts | 63 ++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/vue/src/utils/openapi/api.ts b/vue/src/utils/openapi/api.ts index 70d6a625e1..42b44d979b 100644 --- a/vue/src/utils/openapi/api.ts +++ b/vue/src/utils/openapi/api.ts @@ -4355,6 +4355,39 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration) options: localVarRequestOptions, }; }, + /** + * + * @param {any} [body] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + createAutoPlanViewSet: async (body?: any, options: any = {}): Promise => { + const localVarPath = `/api/auto-plan/`; + // 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(body, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, /** * * @param {Automation} [automation] @@ -12199,6 +12232,16 @@ export const ApiApiFp = function(configuration?: Configuration) { const localVarAxiosArgs = await localVarAxiosParamCreator.createAccessToken(accessToken, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, + /** + * + * @param {any} [body] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async createAutoPlanViewSet(body?: any, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.createAutoPlanViewSet(body, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, /** * * @param {Automation} [automation] @@ -14529,6 +14572,15 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?: createAccessToken(accessToken?: AccessToken, options?: any): AxiosPromise { return localVarFp.createAccessToken(accessToken, options).then((request) => request(axios, basePath)); }, + /** + * + * @param {any} [body] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + createAutoPlanViewSet(body?: any, options?: any): AxiosPromise { + return localVarFp.createAutoPlanViewSet(body, options).then((request) => request(axios, basePath)); + }, /** * * @param {Automation} [automation] @@ -16642,6 +16694,17 @@ export class ApiApi extends BaseAPI { return ApiApiFp(this.configuration).createAccessToken(accessToken, options).then((request) => request(this.axios, this.basePath)); } + /** + * + * @param {any} [body] + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof ApiApi + */ + public createAutoPlanViewSet(body?: any, options?: any) { + return ApiApiFp(this.configuration).createAutoPlanViewSet(body, options).then((request) => request(this.axios, this.basePath)); + } + /** * * @param {Automation} [automation] From ac17b84a7ae7a086e0b9866207661258418fb669 Mon Sep 17 00:00:00 2001 From: AquaticLava Date: Wed, 21 Jun 2023 19:35:48 -0600 Subject: [PATCH 08/71] updated auto meal plan to start at the current day, and exclude a meal plan if it has no keywords. Added debug buttons to help with testing. --- vue/src/apps/MealPlanView/MealPlanView.vue | 73 +++++++++------------- vue/src/components/AutoMealPlanModal.vue | 9 ++- 2 files changed, 33 insertions(+), 49 deletions(-) diff --git a/vue/src/apps/MealPlanView/MealPlanView.vue b/vue/src/apps/MealPlanView/MealPlanView.vue index c0d027a595..9e34cb7189 100644 --- a/vue/src/apps/MealPlanView/MealPlanView.vue +++ b/vue/src/apps/MealPlanView/MealPlanView.vue @@ -293,6 +293,12 @@ + + {{ $t("Export_To_ICal") }} @@ -680,55 +686,34 @@ export default { createAutoPlan() { this.$bvModal.show(`autoplan-modal`) }, - async autoPlanThread(date, dateOffset, meal_type, keywords, servings, mealTypesKey) { - + async autoPlanThread(autoPlan, mealTypeIndex) { let apiClient = new ApiApiFactory() - let currentEntry = Object.assign({}, this.options.entryEditing) - currentEntry.date = moment(date).add(dateOffset, "d").format("YYYY-MM-DD") - currentEntry.servings = servings - await Promise.all([ - currentEntry.recipe = await this.randomRecipe(keywords[mealTypesKey]).then((result) => { - return result - }), - currentEntry.shared = await apiClient.listUserPreferences().then((result) => { - return result.data[0].plan_share - }), - currentEntry.meal_type = await this.getMealType(meal_type[mealTypesKey].id).then((result) => { - return result - }) - ]) - currentEntry.title = currentEntry.recipe.name - this.createEntry(currentEntry) - }, - doAutoPlan(autoPlan) { - let dayInMilliseconds = (86400000) - let numberOfDays = ((autoPlan.endDay - autoPlan.startDay) / dayInMilliseconds) + 1 - - for (const mealTypesKey in autoPlan.meal_types) { - for (let dateOffset = 0; dateOffset < numberOfDays; dateOffset++) { - this.autoPlanThread(autoPlan.startDay, dateOffset, autoPlan.meal_types, autoPlan.keywords, autoPlan.servings, mealTypesKey) - } + let data = { + "start_date" : moment(autoPlan.startDay).format("YYYY-MM-DD"), + "end_date" : moment(autoPlan.endDay).format("YYYY-MM-DD"), + "meal_type_id" : autoPlan.meal_types[mealTypeIndex].id, + "keywords" : autoPlan.keywords[mealTypeIndex], + "servings" : autoPlan.servings, + "shared" : autoPlan.shared } + await apiClient.createAutoPlanViewSet(data) + }, - randomRecipe(keywords) { - let url = "/api/recipe/?query=" - for (const keywordsKey in keywords) { - let keyword = keywords[keywordsKey] - url += `&keywords_and=${keyword.id}` + async doAutoPlan(autoPlan) { + for (let i = 0; i < autoPlan.meal_types.length; i++) { + if (autoPlan.keywords[i].length === 0) continue + await this.autoPlanThread(autoPlan, i) } - return axios.get(url).then((response) => { - let result = response.data - let count = result.count - return result.results[Math.floor(Math.random() * count)] - }).catch((err) => { - - }) + this.refreshEntries() }, - getMealType(id) { - let url = `/api/meal-type/${id}` - return axios.get(url).then((response) => { - return response.data - }) + refreshEntries(){//todo Remove method + let date = this.current_period + useMealPlanStore().refreshFromAPI(moment(date.periodStart).format("YYYY-MM-DD"), moment(date.periodEnd).format("YYYY-MM-DD")) + }, + deleteAll(){//todo Remove method, only used in debugging + for (let i = 0; i < useMealPlanStore().plan_list.length; i++) { + useMealPlanStore().deleteObject(useMealPlanStore().plan_list[i]) + } } }, directives: { diff --git a/vue/src/components/AutoMealPlanModal.vue b/vue/src/components/AutoMealPlanModal.vue index 339427aa7f..2d8084b33e 100644 --- a/vue/src/components/AutoMealPlanModal.vue +++ b/vue/src/components/AutoMealPlanModal.vue @@ -21,7 +21,7 @@ :initial_selection="AutoPlan.keywords[meal_type]" :parent_variable="`${k}`" :model="Models.KEYWORD" - :placeholder="$t('Keywords')" + :placeholder="$t('Keywords, leave blank to exclude meal type')" :limit="50" /> @@ -35,9 +35,9 @@
- +
- +
{{ $t("Start Day") }} @@ -102,7 +102,7 @@ export default { this.refreshMealTypes() this.AutoPlan.servings = 1 - this.AutoPlan.startDay = this.current_period.periodStart + this.AutoPlan.startDay = new Date() this.AutoPlan.endDay = this.current_period.periodEnd }, sortMealTypes() { @@ -148,7 +148,6 @@ export default { }, updateStartDay(date){ this.AutoPlan.startDay = date - console.log(date) }, updateEndDay(date){ this.AutoPlan.endDay = date From c2def3eb9d96f6d25bc9af824686a6ccd2917e53 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 1 Jul 2023 00:09:18 +0000 Subject: [PATCH 09/71] Bump typescript from 4.9.5 to 5.1.6 in /vue Bumps [typescript](https://github.com/Microsoft/TypeScript) from 4.9.5 to 5.1.6. - [Release notes](https://github.com/Microsoft/TypeScript/releases) - [Commits](https://github.com/Microsoft/TypeScript/commits) --- updated-dependencies: - dependency-name: typescript dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- vue/package.json | 2 +- vue/yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/vue/package.json b/vue/package.json index c91f3e058e..3f83338c3c 100644 --- a/vue/package.json +++ b/vue/package.json @@ -61,7 +61,7 @@ "babel-eslint": "^10.1.0", "eslint": "^7.28.0", "eslint-plugin-vue": "^8.7.1", - "typescript": "~4.9.3", + "typescript": "~5.1.6", "vue-cli-plugin-i18n": "^2.3.2", "webpack-bundle-tracker": "1.8.1", "workbox-background-sync": "^6.5.4", diff --git a/vue/yarn.lock b/vue/yarn.lock index 9556054870..182239dec6 100644 --- a/vue/yarn.lock +++ b/vue/yarn.lock @@ -10879,10 +10879,10 @@ typescript@~4.5.5: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.5.tgz#d8c953832d28924a9e3d37c73d729c846c5896f3" integrity sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA== -typescript@~4.9.3: - version "4.9.5" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" - integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== +typescript@~5.1.6: + version "5.1.6" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.1.6.tgz#02f8ac202b6dad2c0dd5e0913745b47a37998274" + integrity sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA== unbox-primitive@^1.0.2: version "1.0.2" From 0f5e53526e5abcd4d96bf0e559e39468937b80a0 Mon Sep 17 00:00:00 2001 From: smilerz Date: Mon, 24 Apr 2023 10:59:48 -0500 Subject: [PATCH 10/71] add NEVER_UNIT automation --- cookbook/helper/ingredient_parser.py | 57 +++++++++- .../0189_alter_automation_type_and_more.py | 23 ++++ cookbook/models.py | 8 +- docs/features/automation.md | 67 +++++++----- vue/src/locales/en.json | 5 +- vue/src/utils/models.js | 100 +++++++++--------- 6 files changed, 178 insertions(+), 82 deletions(-) create mode 100644 cookbook/migrations/0189_alter_automation_type_and_more.py diff --git a/cookbook/helper/ingredient_parser.py b/cookbook/helper/ingredient_parser.py index 8ecf299b96..f1596f83cc 100644 --- a/cookbook/helper/ingredient_parser.py +++ b/cookbook/helper/ingredient_parser.py @@ -4,7 +4,7 @@ from django.core.cache import caches -from cookbook.models import Unit, Food, Automation, Ingredient +from cookbook.models import Automation, Food, Ingredient, Unit class IngredientParser: @@ -12,6 +12,7 @@ class IngredientParser: ignore_rules = False food_aliases = {} unit_aliases = {} + never_unit = {} def __init__(self, request, cache_mode, ignore_automations=False): """ @@ -40,9 +41,19 @@ def __init__(self, request, cache_mode, ignore_automations=False): for a in Automation.objects.filter(space=self.request.space, disabled=False, type=Automation.UNIT_ALIAS).only('param_1', 'param_2').order_by('order').all(): self.unit_aliases[a.param_1] = a.param_2 caches['default'].set(UNIT_CACHE_KEY, self.unit_aliases, 30) + + NEVER_UNIT_CACHE_KEY = f'automation_never_unit_{self.request.space.pk}' + if c := caches['default'].get(NEVER_UNIT_CACHE_KEY, None): + self.never_unit = c + caches['default'].touch(NEVER_UNIT_CACHE_KEY, 30) + else: + for a in Automation.objects.filter(space=self.request.space, disabled=False, type=Automation.NEVER_UNIT).only('param_1', 'param_2').order_by('order').all(): + self.never_unit[a.param_1] = a.param_2 + caches['default'].set(NEVER_UNIT_CACHE_KEY, self.never_unit, 30) else: self.food_aliases = {} self.unit_aliases = {} + self.never_unit = {} def apply_food_automation(self, food): """ @@ -205,6 +216,49 @@ def parse_food(self, tokens): food, note = self.parse_food_with_comma(tokens) return food, note + def apply_never_unit_automations(self, tokens): + """ + Moves a string that should never be treated as a unit to next token and optionally replaced with default unit + e.g. NEVER_UNIT: param1: egg, param2: None would modify ['1', 'egg', 'white'] to ['1', '', 'egg', 'white'] + or NEVER_UNIT: param1: egg, param2: pcs would modify ['1', 'egg', 'yolk'] to ['1', 'pcs', 'egg', 'yolk'] + :param1 string: string that should never be considered a unit, will be moved to token[2] + :param2 (optional) unit as string: will insert unit string into token[1] + :return: unit as string (possibly changed by automation) + """ + + if self.ignore_rules: + return tokens + + new_unit = None + alt_unit = self.apply_unit_automation(tokens[1]) + never_unit = False + if self.never_unit: + try: + new_unit = self.never_unit[tokens[1]] + never_unit = True + except KeyError: + return tokens + + else: + if automation := Automation.objects.filter(space=self.request.space, type=Automation.UNIT_ALIAS, param_1__in=[tokens[1], alt_unit], disabled=False).order_by('order').first(): + new_unit = automation.param_2 + never_unit = True + + if never_unit: + tokens.insert(1, new_unit) + + return tokens + + def parse_tokens(self, tokens): + """ + parser that applies automations to unmodified tokens + """ + + if self.ignore_rules: + return tokens + + return self.apply_never_unit_automations(tokens) + def parse(self, ingredient): """ Main parsing function, takes an ingredient string (e.g. '1 l Water') and extracts amount, unit, food, ... @@ -257,6 +311,7 @@ def parse(self, ingredient): # three arguments if it already has a unit there can't be # a fraction for the amount if len(tokens) > 2: + tokens = self.parse_tokens(tokens) try: if unit is not None: # a unit is already found, no need to try the second argument for a fraction diff --git a/cookbook/migrations/0189_alter_automation_type_and_more.py b/cookbook/migrations/0189_alter_automation_type_and_more.py new file mode 100644 index 0000000000..62d8f776a8 --- /dev/null +++ b/cookbook/migrations/0189_alter_automation_type_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 4.1.7 on 2023-04-24 15:00 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('cookbook', '0188_space_no_sharing_limit'), + ] + + operations = [ + migrations.AlterField( + model_name='automation', + name='type', + field=models.CharField(choices=[('FOOD_ALIAS', 'Food Alias'), ('UNIT_ALIAS', 'Unit Alias'), ('KEYWORD_ALIAS', 'Keyword Alias'), ('DESCRIPTION_REPLACE', 'Description Replace'), ('INSTRUCTION_REPLACE', 'Instruction Replace'), ('NEVER_UNIT', 'Never Unit')], max_length=128), + ), + migrations.AlterField( + model_name='userpreference', + name='use_fractions', + field=models.BooleanField(default=True), + ), + ] diff --git a/cookbook/models.py b/cookbook/models.py index 5a93102421..8b6667b6c7 100644 --- a/cookbook/models.py +++ b/cookbook/models.py @@ -5,7 +5,6 @@ from datetime import date, timedelta import oauth2_provider.models -from PIL import Image from annoying.fields import AutoOneToOneField from django.contrib import auth from django.contrib.auth.models import Group, User @@ -14,13 +13,14 @@ from django.core.files.uploadedfile import InMemoryUploadedFile, UploadedFile from django.core.validators import MinLengthValidator from django.db import IntegrityError, models -from django.db.models import Index, ProtectedError, Q, Avg, Max +from django.db.models import Avg, Index, Max, ProtectedError, Q from django.db.models.fields.related import ManyToManyField from django.db.models.functions import Substr from django.utils import timezone from django.utils.translation import gettext as _ from django_prometheus.models import ExportModelOperationsMixin from django_scopes import ScopedManager, scopes_disabled +from PIL import Image from treebeard.mp_tree import MP_Node, MP_NodeManager from recipes.settings import (COMMENT_PREF_DEFAULT, FRACTION_PREF_DEFAULT, KJ_PREF_DEFAULT, @@ -1314,10 +1314,12 @@ class Automation(ExportModelOperationsMixin('automations'), models.Model, Permis KEYWORD_ALIAS = 'KEYWORD_ALIAS' DESCRIPTION_REPLACE = 'DESCRIPTION_REPLACE' INSTRUCTION_REPLACE = 'INSTRUCTION_REPLACE' + NEVER_UNIT = 'NEVER_UNIT' type = models.CharField(max_length=128, choices=((FOOD_ALIAS, _('Food Alias')), (UNIT_ALIAS, _('Unit Alias')), (KEYWORD_ALIAS, _('Keyword Alias')), - (DESCRIPTION_REPLACE, _('Description Replace')), (INSTRUCTION_REPLACE, _('Instruction Replace')),)) + (DESCRIPTION_REPLACE, _('Description Replace')), (INSTRUCTION_REPLACE, _('Instruction Replace')), + (NEVER_UNIT, _('Never Unit')),)) name = models.CharField(max_length=128, default='') description = models.TextField(blank=True, null=True) diff --git a/docs/features/automation.md b/docs/features/automation.md index 66f9fefdb3..bd76dec99e 100644 --- a/docs/features/automation.md +++ b/docs/features/automation.md @@ -1,39 +1,41 @@ !!! warning - Automations are currently in a beta stage. They work pretty stable but if I encounter any - issues while working on them, I might change how they work breaking existing automations. - I will try to avoid this and am pretty confident it won't happen. +Automations are currently in a beta stage. They work pretty stable but if I encounter any +issues while working on them, I might change how they work breaking existing automations. +I will try to avoid this and am pretty confident it won't happen. - -Automations allow Tandoor to automatically perform certain tasks, especially when importing recipes, that +Automations allow Tandoor to automatically perform certain tasks, especially when importing recipes, that would otherwise have to be done manually. Currently, the following automations are supported. ## Unit, Food, Keyword Alias + Foods, Units and Keywords can have automations that automatically replace them with another object -to allow aliasing them. +to allow aliasing them. This helps to add consistency to the naming of objects, for example to always use the singular form -for the main name if a plural form is configured. +for the main name if a plural form is configured. -These automations are best created by dragging and dropping Foods, Units or Keywords in their respective -views and creating the automation there. +These automations are best created by dragging and dropping Foods, Units or Keywords in their respective +views and creating the automation there. You can also create them manually by setting the following -- **Parameter 1**: name of food/unit/keyword to match -- **Parameter 2**: name of food/unit/keyword to replace matched food with + +- **Parameter 1**: name of food/unit/keyword to match +- **Parameter 2**: name of food/unit/keyword to replace matched food with These rules are processed whenever you are importing recipes from websites or other apps and when using the simple ingredient input (shopping, recipe editor, ...). ## Description Replace -This automation is a bit more complicated than the alis rules. It is run when importing a recipe + +This automation is a bit more complicated than the alias rules. It is run when importing a recipe from a website. It uses Regular Expressions (RegEx) to determine if a description should be altered, what exactly to remove -and what to replace it with. +and what to replace it with. -- **Parameter 1**: pattern of which sites to match (e.g. `.*.chefkoch.de.*`, `.*`) -- **Parameter 2**: pattern of what to replace (e.g. `.*`) -- **Parameter 3**: value to replace matched occurrence of parameter 2 with. Only one occurrence of the pattern is replaced. +- **Parameter 1**: pattern of which sites to match (e.g. `.*.chefkoch.de.*`, `.*`) +- **Parameter 2**: pattern of what to replace (e.g. `.*`) +- **Parameter 3**: value to replace matched occurrence of parameter 2 with. Only one occurrence of the pattern is replaced. To replace the description the python [re.sub](https://docs.python.org/2/library/re.html#re.sub) function is used like this `re.sub(, , , count=1)` @@ -41,24 +43,41 @@ like this `re.sub(, , , count=1)` To test out your patterns and learn about RegEx you can use [regexr.com](https://regexr.com/) !!! info - In order to prevent denial of service attacks on the RegEx engine the number of replace automations - and the length of the inputs that are processed are limited. Those limits should never be reached - during normal usage. +In order to prevent denial of service attacks on the RegEx engine the number of replace automations +and the length of the inputs that are processed are limited. Those limits should never be reached +during normal usage. ## Instruction Replace + This works just like the Description Replace automation but runs against all instruction texts -in all steps of a recipe during import. +in all steps of a recipe during import. Also instead of just replacing a single occurrence of the matched pattern it will replace all. +## Never Unit + +Some ingredients have a pattern of AMOUNT and FOOD, if the food has multiple words (e.g. egg yolk) this can cause Tandoor +to detect the word "egg" as a unit. This automation will detect the word 'egg' as something that should never be considered +a unit. + +You can also create them manually by setting the following + +- **Parameter 1**: string to detect +- **Parameter 2**: Optional: unit to insert into ingredient (e.g. 1 whole 'egg yolk' instead of 1 'egg yolk') + +These rules are processed whenever you are importing recipes from websites or other apps +and when using the simple ingredient input (shopping, recipe editor, ...). + # Order -If the Automation type allows for more than one rule to be executed (for example description replace) -the rules are processed in ascending order (ordered by the *order* property of the automation). -The default order is always 1000 to make it easier to add automations before and after other automations. + +If the Automation type allows for more than one rule to be executed (for example description replace) +the rules are processed in ascending order (ordered by the _order_ property of the automation). +The default order is always 1000 to make it easier to add automations before and after other automations. Example: + 1. Rule ABC (order 1000) replaces `everything` with `abc` 2. Rule DEF (order 2000) replaces `everything` with `def` 3. Rule XYZ (order 500) replaces `everything` with `xyz` -After processing rules XYZ, then ABC and then DEF the description will have the value `def` \ No newline at end of file +After processing rules XYZ, then ABC and then DEF the description will have the value `def` diff --git a/vue/src/locales/en.json b/vue/src/locales/en.json index 4bf26e2273..38ba5dc332 100644 --- a/vue/src/locales/en.json +++ b/vue/src/locales/en.json @@ -517,6 +517,7 @@ "Use_Plural_Food_Always": "Use plural form for food always", "Use_Plural_Food_Simple": "Use plural form for food dynamically", "plural_usage_info": "Use the plural form for units and food inside this space.", - "Create Recipe": "Create Recipe", - "Import Recipe": "Import Recipe" + "Create Recipe": "Create Recipe", + "Import Recipe": "Import Recipe", + "Never_Unit": "Never Unit" } diff --git a/vue/src/utils/models.js b/vue/src/utils/models.js index 9a1eb35459..f36ac9951e 100644 --- a/vue/src/utils/models.js +++ b/vue/src/utils/models.js @@ -23,7 +23,7 @@ export class Models { false: undefined, }, }, - tree: {default: undefined}, + tree: { default: undefined }, }, }, delete: { @@ -50,7 +50,7 @@ export class Models { type: "lookup", field: "target", list: "self", - sticky_options: [{id: 0, name: "tree_root"}], + sticky_options: [{ id: 0, name: "tree_root" }], }, }, }, @@ -71,7 +71,7 @@ export class Models { food_onhand: true, shopping: true, }, - tags: [{field: "supermarket_category", label: "name", color: "info"}], + tags: [{ field: "supermarket_category", label: "name", color: "info" }], // REQUIRED: unordered array of fields that can be set during create create: { // if not defined partialUpdate will use the same parameters, prepending 'id' @@ -177,7 +177,7 @@ export class Models { field: "substitute_siblings", label: "substitute_siblings", // form.label always translated in utils.getForm() help_text: "substitute_siblings_help", // form.help_text always translated - condition: {field: "parent", value: true, condition: "field_exists"}, + condition: { field: "parent", value: true, condition: "field_exists" }, }, substitute_children: { form_field: true, @@ -186,7 +186,7 @@ export class Models { field: "substitute_children", label: "substitute_children", help_text: "substitute_children_help", - condition: {field: "numchild", value: 0, condition: "gt"}, + condition: { field: "numchild", value: 0, condition: "gt" }, }, inherit_fields: { form_field: true, @@ -196,7 +196,7 @@ export class Models { field: "inherit_fields", list: "FOOD_INHERIT_FIELDS", label: "InheritFields", - condition: {field: "food_children_exist", value: true, condition: "preference_equals"}, + condition: { field: "food_children_exist", value: true, condition: "preference_equals" }, help_text: "InheritFields_help", }, child_inherit_fields: { @@ -207,7 +207,7 @@ export class Models { field: "child_inherit_fields", list: "FOOD_INHERIT_FIELDS", label: "ChildInheritFields", // form.label always translated in utils.getForm() - condition: {field: "numchild", value: 0, condition: "gt"}, + condition: { field: "numchild", value: 0, condition: "gt" }, help_text: "ChildInheritFields_help", // form.help_text always translated }, reset_inherit: { @@ -217,7 +217,7 @@ export class Models { field: "reset_inherit", label: "reset_children", help_text: "reset_children_help", - condition: {field: "numchild", value: 0, condition: "gt"}, + condition: { field: "numchild", value: 0, condition: "gt" }, }, form_function: "FoodCreateDefault", }, @@ -281,7 +281,7 @@ export class Models { apiName: "Unit", paginated: true, create: { - params: [["name", "plural_name", "description", "base_unit","open_data_slug",]], + params: [["name", "plural_name", "description", "base_unit", "open_data_slug"]], form: { show_help: true, name: { @@ -311,24 +311,24 @@ export class Models { form_field: true, type: "choice", options: [ - {value: "g", text: "g"}, - {value: "kg", text: "kg"}, - {value: "ounce", text: "ounce"}, - {value: "pound", text: "pound"}, - {value: "ml", text: "ml"}, - {value: "l", text: "l"}, - {value: "fluid_ounce", text: "fluid_ounce"}, - {value: "pint", text: "pint"}, - {value: "quart", text: "quart"}, - {value: "gallon", text: "gallon"}, - {value: "tbsp", text: "tbsp"}, - {value: "tsp", text: "tsp"}, - {value: "imperial_fluid_ounce", text: "imperial_fluid_ounce"}, - {value: "imperial_pint", text: "imperial_pint"}, - {value: "imperial_quart", text: "imperial_quart"}, - {value: "imperial_gallon", text: "imperial_gallon"}, - {value: "imperial_tbsp", text: "imperial_tbsp"}, - {value: "imperial_tsp", text: "imperial_tsp"}, + { value: "g", text: "g" }, + { value: "kg", text: "kg" }, + { value: "ounce", text: "ounce" }, + { value: "pound", text: "pound" }, + { value: "ml", text: "ml" }, + { value: "l", text: "l" }, + { value: "fluid_ounce", text: "fluid_ounce" }, + { value: "pint", text: "pint" }, + { value: "quart", text: "quart" }, + { value: "gallon", text: "gallon" }, + { value: "tbsp", text: "tbsp" }, + { value: "tsp", text: "tsp" }, + { value: "imperial_fluid_ounce", text: "imperial_fluid_ounce" }, + { value: "imperial_pint", text: "imperial_pint" }, + { value: "imperial_quart", text: "imperial_quart" }, + { value: "imperial_gallon", text: "imperial_gallon" }, + { value: "imperial_tbsp", text: "imperial_tbsp" }, + { value: "imperial_tsp", text: "imperial_tsp" }, ], field: "base_unit", label: "Base Unit", @@ -470,7 +470,7 @@ export class Models { static SUPERMARKET = { name: "Supermarket", apiName: "Supermarket", - ordered_tags: [{field: "category_to_supermarket", label: "category::name", color: "info"}], + ordered_tags: [{ field: "category_to_supermarket", label: "category::name", color: "info" }], create: { params: [["name", "description", "category_to_supermarket"]], form: { @@ -553,11 +553,11 @@ export class Models { form_field: true, type: "choice", options: [ - {value: "FOOD_ALIAS", text: "Food_Alias"}, - {value: "UNIT_ALIAS", text: "Unit_Alias"}, - {value: "KEYWORD_ALIAS", text: "Keyword_Alias"}, - {value: "DESCRIPTION_REPLACE", text: "Description_Replace"}, - {value: "INSTRUCTION_REPLACE", text: "Instruction_Replace"}, + { value: "FOOD_ALIAS", text: "Food_Alias" }, + { value: "UNIT_ALIAS", text: "Unit_Alias" }, + { value: "KEYWORD_ALIAS", text: "Keyword_Alias" }, + { value: "DESCRIPTION_REPLACE", text: "Description_Replace" }, + { value: "INSTRUCTION_REPLACE", text: "Instruction_Replace" }, ], field: "type", label: "Type", @@ -625,9 +625,8 @@ export class Models { label: "Disabled", placeholder: "", }, - form_function: "AutomationOrderDefault" + form_function: "AutomationOrderDefault", }, - }, } @@ -641,7 +640,7 @@ export class Models { }, }, create: { - params: [['food', 'base_amount', 'base_unit', 'converted_amount', 'converted_unit', 'open_data_slug']], + params: [["food", "base_amount", "base_unit", "converted_amount", "converted_unit", "open_data_slug"]], form: { show_help: true, // TODO add proper help texts for everything @@ -695,9 +694,7 @@ export class Models { help_text: "open_data_help_text", optional: true, }, - }, - }, } @@ -711,7 +708,7 @@ export class Models { }, }, create: { - params: [['name', 'icon', 'unit', 'description']], + params: [["name", "icon", "unit", "description"]], form: { show_help: true, name: { @@ -755,7 +752,6 @@ export class Models { optional: true, }, }, - }, } @@ -843,7 +839,7 @@ export class Models { params: ["filter_list"], }, create: { - params: [["name",]], + params: [["name"]], form: { name: { form_field: true, @@ -1008,7 +1004,7 @@ export class Actions { }, ], }, - ok_label: {function: "translate", phrase: "Save"}, + ok_label: { function: "translate", phrase: "Save" }, }, } static UPDATE = { @@ -1043,7 +1039,7 @@ export class Actions { }, ], }, - ok_label: {function: "translate", phrase: "Delete"}, + ok_label: { function: "translate", phrase: "Delete" }, instruction: { form_field: true, type: "instruction", @@ -1070,17 +1066,17 @@ export class Actions { suffix: "s", params: ["query", "page", "pageSize", "options"], config: { - query: {default: undefined}, - page: {default: 1}, - pageSize: {default: 25}, + query: { default: undefined }, + page: { default: 1 }, + pageSize: { default: 25 }, }, } static MERGE = { function: "merge", params: ["source", "target"], config: { - source: {type: "string"}, - target: {type: "string"}, + source: { type: "string" }, + target: { type: "string" }, }, form: { title: { @@ -1095,7 +1091,7 @@ export class Actions { }, ], }, - ok_label: {function: "translate", phrase: "Merge"}, + ok_label: { function: "translate", phrase: "Merge" }, instruction: { form_field: true, type: "instruction", @@ -1129,8 +1125,8 @@ export class Actions { function: "move", params: ["source", "target"], config: { - source: {type: "string"}, - target: {type: "string"}, + source: { type: "string" }, + target: { type: "string" }, }, form: { title: { @@ -1145,7 +1141,7 @@ export class Actions { }, ], }, - ok_label: {function: "translate", phrase: "Move"}, + ok_label: { function: "translate", phrase: "Move" }, instruction: { form_field: true, type: "instruction", From 814f4157db724434f48314a4997dc84141c18ef1 Mon Sep 17 00:00:00 2001 From: smilerz Date: Mon, 24 Apr 2023 11:58:21 -0500 Subject: [PATCH 11/71] create Transpose Words automation --- cookbook/helper/ingredient_parser.py | 43 ++++++++++++++++--- .../0189_alter_automation_type_and_more.py | 4 +- cookbook/models.py | 3 +- docs/features/automation.md | 11 +++++ vue/src/locales/en.json | 3 +- vue/src/utils/models.js | 2 + 6 files changed, 56 insertions(+), 10 deletions(-) diff --git a/cookbook/helper/ingredient_parser.py b/cookbook/helper/ingredient_parser.py index f1596f83cc..dc0d94f89c 100644 --- a/cookbook/helper/ingredient_parser.py +++ b/cookbook/helper/ingredient_parser.py @@ -13,6 +13,7 @@ class IngredientParser: food_aliases = {} unit_aliases = {} never_unit = {} + transpose_words = {} def __init__(self, request, cache_mode, ignore_automations=False): """ @@ -50,10 +51,22 @@ def __init__(self, request, cache_mode, ignore_automations=False): for a in Automation.objects.filter(space=self.request.space, disabled=False, type=Automation.NEVER_UNIT).only('param_1', 'param_2').order_by('order').all(): self.never_unit[a.param_1] = a.param_2 caches['default'].set(NEVER_UNIT_CACHE_KEY, self.never_unit, 30) + + TRANSPOSE_WORDS_CACHE_KEY = f'automation_transpose_words_{self.request.space.pk}' + if c := caches['default'].get(TRANSPOSE_WORDS_CACHE_KEY, None): + self.transpose_words = c + caches['default'].touch(TRANSPOSE_WORDS_CACHE_KEY, 30) + else: + i = 0 + for a in Automation.objects.filter(space=self.request.space, disabled=False, type=Automation.TRANSPOSE_WORDS).only('param_1', 'param_2').order_by('order').all(): + self.never_unit[i] = [a.param_1, a.param_2] + i += 1 + caches['default'].set(TRANSPOSE_WORDS_CACHE_KEY, self.transpose_words, 30) else: self.food_aliases = {} self.unit_aliases = {} self.never_unit = {} + self.transpose_words = {} def apply_food_automation(self, food): """ @@ -83,7 +96,7 @@ def apply_unit_automation(self, unit): if self.ignore_rules: return unit else: - if self.unit_aliases: + if self.transpose_words: try: return self.unit_aliases[unit] except KeyError: @@ -249,15 +262,31 @@ def apply_never_unit_automations(self, tokens): return tokens - def parse_tokens(self, tokens): + def apply_transpose_words_automations(self, ingredient): """ - parser that applies automations to unmodified tokens + If two words (param_1 & param_2) are detected in sequence, swap their position in the ingredient string + :param 1: first word to detect + :param 2: second word to detect + return: new ingredient string """ + #################################################### + #################################################### + #################################################### + #################################################### if self.ignore_rules: - return tokens + return ingredient + + else: + if self.transpose_words: + for rule in self.transpose_words: + ingredient = re.sub(rf"\b({rule[0]}) ({rule[1]})\b", r"\2 \1", ingredient) + + else: + for rule in Automation.objects.filter(space=self.request.space, type=Automation.TRANSPOSE_WORDS, disabled=False).order_by('order'): + ingredient = re.sub(rf"\b({rule.param_1}) ({rule.param_2})\b", r"\2 \1", ingredient) - return self.apply_never_unit_automations(tokens) + return ingredient def parse(self, ingredient): """ @@ -275,6 +304,8 @@ def parse(self, ingredient): if len(ingredient) == 0: raise ValueError('string to parse cannot be empty') + ingredient = self.apply_transpose_words_automations(ingredient) + # some people/languages put amount and unit at the end of the ingredient string # if something like this is detected move it to the beginning so the parser can handle it if len(ingredient) < 1000 and re.search(r'^([^\W\d_])+(.)*[1-9](\d)*\s*([^\W\d_])+', ingredient): @@ -311,7 +342,7 @@ def parse(self, ingredient): # three arguments if it already has a unit there can't be # a fraction for the amount if len(tokens) > 2: - tokens = self.parse_tokens(tokens) + tokens = self.apply_never_unit_automations(tokens) try: if unit is not None: # a unit is already found, no need to try the second argument for a fraction diff --git a/cookbook/migrations/0189_alter_automation_type_and_more.py b/cookbook/migrations/0189_alter_automation_type_and_more.py index 62d8f776a8..bf997ab6d7 100644 --- a/cookbook/migrations/0189_alter_automation_type_and_more.py +++ b/cookbook/migrations/0189_alter_automation_type_and_more.py @@ -1,4 +1,4 @@ -# Generated by Django 4.1.7 on 2023-04-24 15:00 +# Generated by Django 4.1.7 on 2023-04-24 16:22 from django.db import migrations, models @@ -13,7 +13,7 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='automation', name='type', - field=models.CharField(choices=[('FOOD_ALIAS', 'Food Alias'), ('UNIT_ALIAS', 'Unit Alias'), ('KEYWORD_ALIAS', 'Keyword Alias'), ('DESCRIPTION_REPLACE', 'Description Replace'), ('INSTRUCTION_REPLACE', 'Instruction Replace'), ('NEVER_UNIT', 'Never Unit')], max_length=128), + field=models.CharField(choices=[('FOOD_ALIAS', 'Food Alias'), ('UNIT_ALIAS', 'Unit Alias'), ('KEYWORD_ALIAS', 'Keyword Alias'), ('DESCRIPTION_REPLACE', 'Description Replace'), ('INSTRUCTION_REPLACE', 'Instruction Replace'), ('NEVER_UNIT', 'Never Unit'), ('TRANSPOSE_WORDS', 'Transpose Words')], max_length=128), ), migrations.AlterField( model_name='userpreference', diff --git a/cookbook/models.py b/cookbook/models.py index 8b6667b6c7..4f3e86c999 100644 --- a/cookbook/models.py +++ b/cookbook/models.py @@ -1315,11 +1315,12 @@ class Automation(ExportModelOperationsMixin('automations'), models.Model, Permis DESCRIPTION_REPLACE = 'DESCRIPTION_REPLACE' INSTRUCTION_REPLACE = 'INSTRUCTION_REPLACE' NEVER_UNIT = 'NEVER_UNIT' + TRANSPOSE_WORDS = 'TRANSPOSE_WORDS' type = models.CharField(max_length=128, choices=((FOOD_ALIAS, _('Food Alias')), (UNIT_ALIAS, _('Unit Alias')), (KEYWORD_ALIAS, _('Keyword Alias')), (DESCRIPTION_REPLACE, _('Description Replace')), (INSTRUCTION_REPLACE, _('Instruction Replace')), - (NEVER_UNIT, _('Never Unit')),)) + (NEVER_UNIT, _('Never Unit')), (TRANSPOSE_WORDS, _('Transpose Words')),)) name = models.CharField(max_length=128, default='') description = models.TextField(blank=True, null=True) diff --git a/docs/features/automation.md b/docs/features/automation.md index bd76dec99e..eaef3de019 100644 --- a/docs/features/automation.md +++ b/docs/features/automation.md @@ -68,6 +68,17 @@ You can also create them manually by setting the following These rules are processed whenever you are importing recipes from websites or other apps and when using the simple ingredient input (shopping, recipe editor, ...). +## Transpose Words + +Some recipes list the food before the units for some foods (garlic cloves). This automation will transpose 2 words in an +ingredient so "garlic cloves" will automatically become "cloves garlic" + +- **Parameter 1**: first word to detect +- **Parameter 2**: second word to detect + +These rules are processed whenever you are importing recipes from websites or other apps +and when using the simple ingredient input (shopping, recipe editor, ...). + # Order If the Automation type allows for more than one rule to be executed (for example description replace) diff --git a/vue/src/locales/en.json b/vue/src/locales/en.json index 38ba5dc332..9fbc56e08b 100644 --- a/vue/src/locales/en.json +++ b/vue/src/locales/en.json @@ -519,5 +519,6 @@ "plural_usage_info": "Use the plural form for units and food inside this space.", "Create Recipe": "Create Recipe", "Import Recipe": "Import Recipe", - "Never_Unit": "Never Unit" + "Never_Unit": "Never Unit", + "Transpose_Words": "Transpose Words" } diff --git a/vue/src/utils/models.js b/vue/src/utils/models.js index f36ac9951e..a4911ce914 100644 --- a/vue/src/utils/models.js +++ b/vue/src/utils/models.js @@ -558,6 +558,8 @@ export class Models { { value: "KEYWORD_ALIAS", text: "Keyword_Alias" }, { value: "DESCRIPTION_REPLACE", text: "Description_Replace" }, { value: "INSTRUCTION_REPLACE", text: "Instruction_Replace" }, + { value: "NEVER_UNIT", text: "Never_Unit" }, + { value: "TRANSPOSE_WORDS", text: "Transpose_Words" }, ], field: "type", label: "Type", From 6880c0a96787a07f3242d9e678821a06e8e3ff36 Mon Sep 17 00:00:00 2001 From: smilerz Date: Tue, 25 Apr 2023 12:38:14 -0500 Subject: [PATCH 12/71] filtered automations to tokens present --- cookbook/helper/ingredient_parser.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/cookbook/helper/ingredient_parser.py b/cookbook/helper/ingredient_parser.py index dc0d94f89c..6f6b7af5c1 100644 --- a/cookbook/helper/ingredient_parser.py +++ b/cookbook/helper/ingredient_parser.py @@ -3,6 +3,7 @@ import unicodedata from django.core.cache import caches +from django.db.models import Q from cookbook.models import Automation, Food, Ingredient, Unit @@ -59,7 +60,7 @@ def __init__(self, request, cache_mode, ignore_automations=False): else: i = 0 for a in Automation.objects.filter(space=self.request.space, disabled=False, type=Automation.TRANSPOSE_WORDS).only('param_1', 'param_2').order_by('order').all(): - self.never_unit[i] = [a.param_1, a.param_2] + self.transpose_words[i] = [a.param_1, a.param_2] i += 1 caches['default'].set(TRANSPOSE_WORDS_CACHE_KEY, self.transpose_words, 30) else: @@ -270,22 +271,21 @@ def apply_transpose_words_automations(self, ingredient): return: new ingredient string """ - #################################################### - #################################################### - #################################################### - #################################################### if self.ignore_rules: return ingredient else: + tokens = ingredient.replace(',',' ').split() if self.transpose_words: - for rule in self.transpose_words: - ingredient = re.sub(rf"\b({rule[0]}) ({rule[1]})\b", r"\2 \1", ingredient) - + filtered_rules = {} + for key, value in self.transpose_words.items(): + if value[0] in tokens and value[1] in tokens: + filtered_rules[key] = value + for k, v in filtered_rules.items(): + ingredient = re.sub(rf"\b({v[0]})\W*({v[1]})\b", r"\2 \1", ingredient) else: - for rule in Automation.objects.filter(space=self.request.space, type=Automation.TRANSPOSE_WORDS, disabled=False).order_by('order'): - ingredient = re.sub(rf"\b({rule.param_1}) ({rule.param_2})\b", r"\2 \1", ingredient) - + for rule in Automation.objects.filter(space=self.request.space, type=Automation.TRANSPOSE_WORDS, disabled=False).filter(Q(Q(param_1__in=tokens) | Q(param_2__in=tokens))).order_by('order'): + ingredient = re.sub(rf"\b({v[0]})\W*({v[1]})\b", r"\2 \1", ingredient) return ingredient def parse(self, ingredient): @@ -304,8 +304,6 @@ def parse(self, ingredient): if len(ingredient) == 0: raise ValueError('string to parse cannot be empty') - ingredient = self.apply_transpose_words_automations(ingredient) - # some people/languages put amount and unit at the end of the ingredient string # if something like this is detected move it to the beginning so the parser can handle it if len(ingredient) < 1000 and re.search(r'^([^\W\d_])+(.)*[1-9](\d)*\s*([^\W\d_])+', ingredient): @@ -330,6 +328,8 @@ def parse(self, ingredient): if re.match('([0-9])+([A-z])+\s', ingredient): ingredient = re.sub(r'(?<=([a-z])|\d)(?=(?(1)\d|[a-z]))', ' ', ingredient) + ingredient = self.apply_transpose_words_automations(ingredient) + tokens = ingredient.split() # split at each space into tokens if len(tokens) == 1: # there only is one argument, that must be the food From 7f33f82b60ad464e8c657fbf6ad17c265a9a157b Mon Sep 17 00:00:00 2001 From: smilerz Date: Mon, 1 May 2023 17:00:11 -0500 Subject: [PATCH 13/71] fixed defect in NEVER_UNIT automation --- cookbook/helper/ingredient_parser.py | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/cookbook/helper/ingredient_parser.py b/cookbook/helper/ingredient_parser.py index 6f6b7af5c1..21864c4315 100644 --- a/cookbook/helper/ingredient_parser.py +++ b/cookbook/helper/ingredient_parser.py @@ -158,10 +158,10 @@ def parse_amount(self, x): end = 0 while (end < len(x) and (x[end] in string.digits or ( - (x[end] == '.' or x[end] == ',' or x[end] == '/') - and end + 1 < len(x) - and x[end + 1] in string.digits - ))): + (x[end] == '.' or x[end] == ',' or x[end] == '/') + and end + 1 < len(x) + and x[end + 1] in string.digits + ))): end += 1 if end > 0: if "/" in x[:end]: @@ -185,7 +185,8 @@ def parse_amount(self, x): if unit is not None and unit.strip() == '': unit = None - if unit is not None and (unit.startswith('(') or unit.startswith('-')): # i dont know any unit that starts with ( or - so its likely an alternative like 1L (500ml) Water or 2-3 + if unit is not None and (unit.startswith('(') or unit.startswith( + '-')): # i dont know any unit that starts with ( or - so its likely an alternative like 1L (500ml) Water or 2-3 unit = None note = x return amount, unit, note @@ -254,7 +255,8 @@ def apply_never_unit_automations(self, tokens): return tokens else: - if automation := Automation.objects.filter(space=self.request.space, type=Automation.UNIT_ALIAS, param_1__in=[tokens[1], alt_unit], disabled=False).order_by('order').first(): + if automation := Automation.objects.filter(space=self.request.space, type=Automation.NEVER_UNIT, param_1__in=[ + tokens[1], alt_unit], disabled=False).order_by('order').first(): new_unit = automation.param_2 never_unit = True @@ -275,7 +277,7 @@ def apply_transpose_words_automations(self, ingredient): return ingredient else: - tokens = ingredient.replace(',',' ').split() + tokens = ingredient.replace(',', ' ').split() if self.transpose_words: filtered_rules = {} for key, value in self.transpose_words.items(): @@ -284,7 +286,8 @@ def apply_transpose_words_automations(self, ingredient): for k, v in filtered_rules.items(): ingredient = re.sub(rf"\b({v[0]})\W*({v[1]})\b", r"\2 \1", ingredient) else: - for rule in Automation.objects.filter(space=self.request.space, type=Automation.TRANSPOSE_WORDS, disabled=False).filter(Q(Q(param_1__in=tokens) | Q(param_2__in=tokens))).order_by('order'): + for rule in Automation.objects.filter(space=self.request.space, type=Automation.TRANSPOSE_WORDS, disabled=False).filter( + Q(Q(param_1__in=tokens) | Q(param_2__in=tokens))).order_by('order'): ingredient = re.sub(rf"\b({v[0]})\W*({v[1]})\b", r"\2 \1", ingredient) return ingredient @@ -313,8 +316,8 @@ def parse(self, ingredient): # if the string contains parenthesis early on remove it and place it at the end # because its likely some kind of note - if re.match('(.){1,6}\s\((.[^\(\)])+\)\s', ingredient): - match = re.search('\((.[^\(])+\)', ingredient) + if re.match('(.){1,6}\\s\\((.[^\\(\\)])+\\)\\s', ingredient): + match = re.search('\\((.[^\\(])+\\)', ingredient) ingredient = ingredient[:match.start()] + ingredient[match.end():] + ' ' + ingredient[match.start():match.end()] # leading spaces before commas result in extra tokens, clean them out @@ -322,10 +325,10 @@ def parse(self, ingredient): # handle "(from) - (to)" amounts by using the minimum amount and adding the range to the description # "10.5 - 200 g XYZ" => "100 g XYZ (10.5 - 200)" - ingredient = re.sub("^(\d+|\d+[\\.,]\d+) - (\d+|\d+[\\.,]\d+) (.*)", "\\1 \\3 (\\1 - \\2)", ingredient) + ingredient = re.sub("^(\\d+|\\d+[\\.,]\\d+) - (\\d+|\\d+[\\.,]\\d+) (.*)", "\\1 \\3 (\\1 - \\2)", ingredient) # if amount and unit are connected add space in between - if re.match('([0-9])+([A-z])+\s', ingredient): + if re.match('([0-9])+([A-z])+\\s', ingredient): ingredient = re.sub(r'(?<=([a-z])|\d)(?=(?(1)\d|[a-z]))', ' ', ingredient) ingredient = self.apply_transpose_words_automations(ingredient) From 19f1225249f8281bc94d6533902ed68ed92de4c8 Mon Sep 17 00:00:00 2001 From: smilerz Date: Tue, 2 May 2023 16:50:13 -0500 Subject: [PATCH 14/71] make automation parameters case insensitive on search --- cookbook/helper/ingredient_parser.py | 34 ++++++++------- cookbook/helper/recipe_url_import.py | 64 +++++++++++++++++----------- 2 files changed, 57 insertions(+), 41 deletions(-) diff --git a/cookbook/helper/ingredient_parser.py b/cookbook/helper/ingredient_parser.py index 21864c4315..b5bdbf67ce 100644 --- a/cookbook/helper/ingredient_parser.py +++ b/cookbook/helper/ingredient_parser.py @@ -4,6 +4,7 @@ from django.core.cache import caches from django.db.models import Q +from django.db.models.functions import Lower from cookbook.models import Automation, Food, Ingredient, Unit @@ -32,7 +33,7 @@ def __init__(self, request, cache_mode, ignore_automations=False): caches['default'].touch(FOOD_CACHE_KEY, 30) else: for a in Automation.objects.filter(space=self.request.space, disabled=False, type=Automation.FOOD_ALIAS).only('param_1', 'param_2').order_by('order').all(): - self.food_aliases[a.param_1] = a.param_2 + self.food_aliases[a.param_1.lower()] = a.param_2 caches['default'].set(FOOD_CACHE_KEY, self.food_aliases, 30) UNIT_CACHE_KEY = f'automation_unit_alias_{self.request.space.pk}' @@ -41,7 +42,7 @@ def __init__(self, request, cache_mode, ignore_automations=False): caches['default'].touch(UNIT_CACHE_KEY, 30) else: for a in Automation.objects.filter(space=self.request.space, disabled=False, type=Automation.UNIT_ALIAS).only('param_1', 'param_2').order_by('order').all(): - self.unit_aliases[a.param_1] = a.param_2 + self.unit_aliases[a.param_1.lower()] = a.param_2 caches['default'].set(UNIT_CACHE_KEY, self.unit_aliases, 30) NEVER_UNIT_CACHE_KEY = f'automation_never_unit_{self.request.space.pk}' @@ -50,7 +51,7 @@ def __init__(self, request, cache_mode, ignore_automations=False): caches['default'].touch(NEVER_UNIT_CACHE_KEY, 30) else: for a in Automation.objects.filter(space=self.request.space, disabled=False, type=Automation.NEVER_UNIT).only('param_1', 'param_2').order_by('order').all(): - self.never_unit[a.param_1] = a.param_2 + self.never_unit[a.param_1.lower()] = a.param_2 caches['default'].set(NEVER_UNIT_CACHE_KEY, self.never_unit, 30) TRANSPOSE_WORDS_CACHE_KEY = f'automation_transpose_words_{self.request.space.pk}' @@ -60,7 +61,7 @@ def __init__(self, request, cache_mode, ignore_automations=False): else: i = 0 for a in Automation.objects.filter(space=self.request.space, disabled=False, type=Automation.TRANSPOSE_WORDS).only('param_1', 'param_2').order_by('order').all(): - self.transpose_words[i] = [a.param_1, a.param_2] + self.transpose_words[i] = [a.param_1.lower(), a.param_2.lower()] i += 1 caches['default'].set(TRANSPOSE_WORDS_CACHE_KEY, self.transpose_words, 30) else: @@ -80,11 +81,11 @@ def apply_food_automation(self, food): else: if self.food_aliases: try: - return self.food_aliases[food] + return self.food_aliases[food.lower()] except KeyError: return food else: - if automation := Automation.objects.filter(space=self.request.space, type=Automation.FOOD_ALIAS, param_1=food, disabled=False).order_by('order').first(): + if automation := Automation.objects.filter(space=self.request.space, type=Automation.FOOD_ALIAS, param_1__iexact=food, disabled=False).order_by('order').first(): return automation.param_2 return food @@ -99,11 +100,11 @@ def apply_unit_automation(self, unit): else: if self.transpose_words: try: - return self.unit_aliases[unit] + return self.unit_aliases[unit.lower()] except KeyError: return unit else: - if automation := Automation.objects.filter(space=self.request.space, type=Automation.UNIT_ALIAS, param_1=unit, disabled=False).order_by('order').first(): + if automation := Automation.objects.filter(space=self.request.space, type=Automation.UNIT_ALIAS, param_1__iexact=unit, disabled=False).order_by('order').first(): return automation.param_2 return unit @@ -249,14 +250,14 @@ def apply_never_unit_automations(self, tokens): never_unit = False if self.never_unit: try: - new_unit = self.never_unit[tokens[1]] + new_unit = self.never_unit[tokens[1].lower()] never_unit = True except KeyError: return tokens else: - if automation := Automation.objects.filter(space=self.request.space, type=Automation.NEVER_UNIT, param_1__in=[ - tokens[1], alt_unit], disabled=False).order_by('order').first(): + if automation := Automation.objects.annotate(param_1_lower=Lower('param_1')).filter(space=self.request.space, type=Automation.NEVER_UNIT, param_1_lower__in=[ + tokens[1].lower(), alt_unit.lower()], disabled=False).order_by('order').first(): new_unit = automation.param_2 never_unit = True @@ -277,18 +278,19 @@ def apply_transpose_words_automations(self, ingredient): return ingredient else: - tokens = ingredient.replace(',', ' ').split() + tokens = [x.lower() for x in ingredient.replace(',', ' ').split()] if self.transpose_words: filtered_rules = {} for key, value in self.transpose_words.items(): if value[0] in tokens and value[1] in tokens: filtered_rules[key] = value for k, v in filtered_rules.items(): - ingredient = re.sub(rf"\b({v[0]})\W*({v[1]})\b", r"\2 \1", ingredient) + ingredient = re.sub(rf"\b({v[0]})\W*({v[1]})\b", r"\2 \1", ingredient, flags=re.IGNORECASE) else: - for rule in Automation.objects.filter(space=self.request.space, type=Automation.TRANSPOSE_WORDS, disabled=False).filter( - Q(Q(param_1__in=tokens) | Q(param_2__in=tokens))).order_by('order'): - ingredient = re.sub(rf"\b({v[0]})\W*({v[1]})\b", r"\2 \1", ingredient) + for rule in Automation.objects.filter(space=self.request.space, type=Automation.TRANSPOSE_WORDS, disabled=False) \ + .annotate(param_1_lower=Lower('param_1'), param_2_lower=Lower('param_2')) \ + .filter(Q(Q(param_1_lower__in=tokens) | Q(param_2_lower__in=tokens))).order_by('order'): + ingredient = re.sub(rf"\b({v[0]})\W*({v[1]})\b", r"\2 \1", ingredient, flags=re.IGNORECASE) return ingredient def parse(self, ingredient): diff --git a/cookbook/helper/recipe_url_import.py b/cookbook/helper/recipe_url_import.py index a15df5e8e6..fea830c82c 100644 --- a/cookbook/helper/recipe_url_import.py +++ b/cookbook/helper/recipe_url_import.py @@ -15,7 +15,6 @@ from cookbook.helper.ingredient_parser import IngredientParser from cookbook.models import Automation, Keyword, PropertyType - # from unicodedata import decomposition @@ -51,7 +50,8 @@ def get_from_scraper(scrape, request): recipe_json['internal'] = True try: - servings = scrape.schema.data.get('recipeYield') or 1 # dont use scrape.yields() as this will always return "x servings" or "x items", should be improved in scrapers directly + # dont use scrape.yields() as this will always return "x servings" or "x items", should be improved in scrapers directly + servings = scrape.schema.data.get('recipeYield') or 1 except Exception: servings = 1 @@ -156,7 +156,14 @@ def get_from_scraper(scrape, request): parsed_description = parse_description(description) # TODO notify user about limit if reached # limits exist to limit the attack surface for dos style attacks - automations = Automation.objects.filter(type=Automation.DESCRIPTION_REPLACE, space=request.space, disabled=False).only('param_1', 'param_2', 'param_3').all().order_by('order')[:512] + automations = Automation.objects.filter( + type=Automation.DESCRIPTION_REPLACE, + space=request.space, + disabled=False).only( + 'param_1', + 'param_2', + 'param_3').all().order_by('order')[ + :512] for a in automations: if re.match(a.param_1, (recipe_json['source_url'])[:512]): parsed_description = re.sub(a.param_2, a.param_3, parsed_description, count=1) @@ -206,7 +213,14 @@ def get_from_scraper(scrape, request): pass if 'source_url' in recipe_json and recipe_json['source_url']: - automations = Automation.objects.filter(type=Automation.INSTRUCTION_REPLACE, space=request.space, disabled=False).only('param_1', 'param_2', 'param_3').order_by('order').all()[:512] + automations = Automation.objects.filter( + type=Automation.INSTRUCTION_REPLACE, + space=request.space, + disabled=False).only( + 'param_1', + 'param_2', + 'param_3').order_by('order').all()[ + :512] for a in automations: if re.match(a.param_1, (recipe_json['source_url'])[:512]): for s in recipe_json['steps']: @@ -272,7 +286,7 @@ def get_from_youtube_scraper(url, request): def parse_name(name): - if type(name) == list: + if isinstance(name, list): try: name = name[0] except Exception: @@ -316,16 +330,16 @@ def parse_instructions(instructions): """ instruction_list = [] - if type(instructions) == list: + if isinstance(instructions, list): for i in instructions: - if type(i) == str: + if isinstance(i, str): instruction_list.append(clean_instruction_string(i)) else: if 'text' in i: instruction_list.append(clean_instruction_string(i['text'])) elif 'itemListElement' in i: for ile in i['itemListElement']: - if type(ile) == str: + if isinstance(ile, str): instruction_list.append(clean_instruction_string(ile)) elif 'text' in ile: instruction_list.append(clean_instruction_string(ile['text'])) @@ -341,13 +355,13 @@ def parse_image(image): # check if list of images is returned, take first if so if not image: return None - if type(image) == list: + if isinstance(image, list): for pic in image: - if (type(pic) == str) and (pic[:4] == 'http'): + if (isinstance(pic, str)) and (pic[:4] == 'http'): image = pic elif 'url' in pic: image = pic['url'] - elif type(image) == dict: + elif isinstance(image, dict): if 'url' in image: image = image['url'] @@ -358,12 +372,12 @@ def parse_image(image): def parse_servings(servings): - if type(servings) == str: + if isinstance(servings, str): try: servings = int(re.search(r'\d+', servings).group()) except AttributeError: servings = 1 - elif type(servings) == list: + elif isinstance(servings, list): try: servings = int(re.findall(r'\b\d+\b', servings[0])[0]) except KeyError: @@ -372,12 +386,12 @@ def parse_servings(servings): def parse_servings_text(servings): - if type(servings) == str: + if isinstance(servings, str): try: - servings = re.sub("\d+", '', servings).strip() + servings = re.sub("\\d+", '', servings).strip() except Exception: servings = '' - if type(servings) == list: + if isinstance(servings, list): try: servings = parse_servings_text(servings[1]) except Exception: @@ -394,7 +408,7 @@ def parse_time(recipe_time): recipe_time = round(iso_parse_duration(recipe_time).seconds / 60) except ISO8601Error: try: - if (type(recipe_time) == list and len(recipe_time) > 0): + if (isinstance(recipe_time, list) and len(recipe_time) > 0): recipe_time = recipe_time[0] recipe_time = round(parse_duration(recipe_time).seconds / 60) except AttributeError: @@ -413,7 +427,7 @@ def parse_keywords(keyword_json, space): caches['default'].touch(KEYWORD_CACHE_KEY, 30) else: for a in Automation.objects.filter(space=space, disabled=False, type=Automation.KEYWORD_ALIAS).only('param_1', 'param_2').order_by('order').all(): - keyword_aliases[a.param_1] = a.param_2 + keyword_aliases[a.param_1.lower()] = a.param_2 caches['default'].set(KEYWORD_CACHE_KEY, keyword_aliases, 30) # keywords as list @@ -424,7 +438,7 @@ def parse_keywords(keyword_json, space): if len(kw) != 0: if keyword_aliases: try: - kw = keyword_aliases[kw] + kw = keyword_aliases[kw.lower()] except KeyError: pass if k := Keyword.objects.filter(name=kw, space=space).first(): @@ -438,15 +452,15 @@ def parse_keywords(keyword_json, space): def listify_keywords(keyword_list): # keywords as string try: - if type(keyword_list[0]) == dict: + if isinstance(keyword_list[0], dict): return keyword_list except (KeyError, IndexError): pass - if type(keyword_list) == str: + if isinstance(keyword_list, str): keyword_list = keyword_list.split(',') # keywords as string in list - if (type(keyword_list) == list and len(keyword_list) == 1 and ',' in keyword_list[0]): + if (isinstance(keyword_list, list) and len(keyword_list) == 1 and ',' in keyword_list[0]): keyword_list = keyword_list[0].split(',') return [x.strip() for x in keyword_list] @@ -500,13 +514,13 @@ def get_images_from_soup(soup, url): def clean_dict(input_dict, key): - if type(input_dict) == dict: + if isinstance(input_dict, dict): for x in list(input_dict): if x == key: del input_dict[x] - elif type(input_dict[x]) == dict: + elif isinstance(input_dict[x], dict): input_dict[x] = clean_dict(input_dict[x], key) - elif type(input_dict[x]) == list: + elif isinstance(input_dict[x], list): temp_list = [] for e in input_dict[x]: temp_list.append(clean_dict(e, key)) From 312cd077d0fc3766368f9db3514bd5b64421ce4f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Aug 2023 00:15:02 +0000 Subject: [PATCH 15/71] Bump eslint from 7.32.0 to 8.46.0 in /vue Bumps [eslint](https://github.com/eslint/eslint) from 7.32.0 to 8.46.0. - [Release notes](https://github.com/eslint/eslint/releases) - [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md) - [Commits](https://github.com/eslint/eslint/compare/v7.32.0...v8.46.0) --- updated-dependencies: - dependency-name: eslint dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- vue/package.json | 2 +- vue/yarn.lock | 294 ++++++++++++++++++----------------------------- 2 files changed, 114 insertions(+), 182 deletions(-) diff --git a/vue/package.json b/vue/package.json index c91f3e058e..a8b5d4f7d7 100644 --- a/vue/package.json +++ b/vue/package.json @@ -59,7 +59,7 @@ "@vue/compiler-sfc": "^3.2.45", "@vue/eslint-config-typescript": "^10.0.0", "babel-eslint": "^10.1.0", - "eslint": "^7.28.0", + "eslint": "^8.46.0", "eslint-plugin-vue": "^8.7.1", "typescript": "~4.9.3", "vue-cli-plugin-i18n": "^2.3.2", diff --git a/vue/yarn.lock b/vue/yarn.lock index f67bea6caa..e8dafaefea 100644 --- a/vue/yarn.lock +++ b/vue/yarn.lock @@ -2,6 +2,11 @@ # yarn lockfile v1 +"@aashutoshrathi/word-wrap@^1.2.3": + version "1.2.6" + resolved "https://registry.yarnpkg.com/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz#bd9154aec9983f77b3a034ecaa015c2e4201f6cf" + integrity sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA== + "@achrinza/node-ipc@^9.2.5": version "9.2.7" resolved "https://registry.yarnpkg.com/@achrinza/node-ipc/-/node-ipc-9.2.7.tgz#cc418f9218d24d9b87f32207e5d6e71c64e241f8" @@ -143,13 +148,6 @@ dependencies: xss "^1.0.8" -"@babel/code-frame@7.12.11": - version "7.12.11" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f" - integrity sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw== - dependencies: - "@babel/highlight" "^7.10.4" - "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.22.5", "@babel/code-frame@^7.5.5", "@babel/code-frame@^7.8.3": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.5.tgz#234d98e1551960604f1246e6475891a570ad5658" @@ -400,7 +398,7 @@ "@babel/traverse" "^7.22.5" "@babel/types" "^7.22.5" -"@babel/highlight@^7.10.4", "@babel/highlight@^7.22.5": +"@babel/highlight@^7.22.5": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.5.tgz#aa6c05c5407a67ebce408162b7ede789b4d22031" integrity sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw== @@ -1243,26 +1241,31 @@ dependencies: eslint-visitor-keys "^3.3.0" -"@eslint-community/regexpp@^4.4.0": - version "4.5.1" - resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.5.1.tgz#cdd35dce4fa1a89a4fd42b1599eb35b3af408884" - integrity sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ== +"@eslint-community/regexpp@^4.4.0", "@eslint-community/regexpp@^4.6.1": + version "4.6.2" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.6.2.tgz#1816b5f6948029c5eaacb0703b850ee0cb37d8f8" + integrity sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw== -"@eslint/eslintrc@^0.4.3": - version "0.4.3" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.3.tgz#9e42981ef035beb3dd49add17acb96e8ff6f394c" - integrity sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw== +"@eslint/eslintrc@^2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.1.tgz#18d635e24ad35f7276e8a49d135c7d3ca6a46f93" + integrity sha512-9t7ZA7NGGK8ckelF0PQCfcxIUzs1Md5rrO6U/c+FIQNanea5UZC0wqKXH4vHBccmu4ZJgZ2idtPeW7+Q2npOEA== dependencies: ajv "^6.12.4" - debug "^4.1.1" - espree "^7.3.0" - globals "^13.9.0" - ignore "^4.0.6" + debug "^4.3.2" + espree "^9.6.0" + globals "^13.19.0" + ignore "^5.2.0" import-fresh "^3.2.1" - js-yaml "^3.13.1" - minimatch "^3.0.4" + js-yaml "^4.1.0" + minimatch "^3.1.2" strip-json-comments "^3.1.1" +"@eslint/js@^8.46.0": + version "8.46.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.46.0.tgz#3f7802972e8b6fe3f88ed1aabc74ec596c456db6" + integrity sha512-a8TLtmPi8xzPkCbp/OGFUo5yhRkHM2Ko9kOWP4znJr0WAhWyThaw3PnwX4vOTWOAMsV2uRt32PPDcEz63esSaA== + "@graphql-tools/merge@8.3.1": version "8.3.1" resolved "https://registry.yarnpkg.com/@graphql-tools/merge/-/merge-8.3.1.tgz#06121942ad28982a14635dbc87b5d488a041d722" @@ -1373,16 +1376,21 @@ dependencies: "@hapi/hoek" "^9.0.0" -"@humanwhocodes/config-array@^0.5.0": - version "0.5.0" - resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.5.0.tgz#1407967d4c6eecd7388f83acf1eaf4d0c6e58ef9" - integrity sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg== +"@humanwhocodes/config-array@^0.11.10": + version "0.11.10" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.10.tgz#5a3ffe32cc9306365fb3fd572596cd602d5e12d2" + integrity sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ== dependencies: - "@humanwhocodes/object-schema" "^1.2.0" + "@humanwhocodes/object-schema" "^1.2.1" debug "^4.1.1" - minimatch "^3.0.4" + minimatch "^3.0.5" + +"@humanwhocodes/module-importer@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" + integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== -"@humanwhocodes/object-schema@^1.2.0": +"@humanwhocodes/object-schema@^1.2.1": version "1.2.1" resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== @@ -1477,7 +1485,7 @@ resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== -"@nodelib/fs.walk@^1.2.3": +"@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8": version "1.2.8" resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== @@ -2805,7 +2813,7 @@ acorn-import-assertions@^1.9.0: resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz#507276249d684797c84e0734ef84860334cfb1ac" integrity sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA== -acorn-jsx@^5.3.1, acorn-jsx@^5.3.2: +acorn-jsx@^5.3.2: version "5.3.2" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== @@ -2820,16 +2828,21 @@ acorn@^6.4.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.2.tgz#35866fd710528e92de10cf06016498e47e39e1e6" integrity sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ== -acorn@^7.1.0, acorn@^7.4.0: +acorn@^7.1.0: version "7.4.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== -acorn@^8.0.4, acorn@^8.0.5, acorn@^8.7.1, acorn@^8.8.0, acorn@^8.8.2: +acorn@^8.0.4, acorn@^8.0.5, acorn@^8.7.1, acorn@^8.8.2: version "8.9.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.9.0.tgz#78a16e3b2bcc198c10822786fa6679e245db5b59" integrity sha512-jaVNAFBHNLXspO543WnNNPZFRtavh3skAkITqD0/2aeMkKZTN+254PyhwxFYrk3vQ1xfY+2wbesJMs/JC8/PwQ== +acorn@^8.9.0: + version "8.10.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5" + integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw== + address@^1.1.2: version "1.2.2" resolved "https://registry.yarnpkg.com/address/-/address-1.2.2.tgz#2b5248dac5485a6390532c6a517fda2e3faac89e" @@ -2867,7 +2880,7 @@ ajv-keywords@^5.1.0: dependencies: fast-deep-equal "^3.1.3" -ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.2, ajv@^6.12.4, ajv@^6.12.5: +ajv@^6.1.0, ajv@^6.10.2, ajv@^6.12.2, ajv@^6.12.4, ajv@^6.12.5: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== @@ -2877,7 +2890,7 @@ ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.2, ajv@^6.12.4, ajv@^6.12.5: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ajv@^8.0.0, ajv@^8.0.1, ajv@^8.6.0, ajv@^8.9.0: +ajv@^8.0.0, ajv@^8.6.0, ajv@^8.9.0: version "8.12.0" resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.12.0.tgz#d1a0527323e22f53562c567c00991577dfbe19d1" integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA== @@ -2894,11 +2907,6 @@ ansi-align@^3.0.0: dependencies: string-width "^4.1.0" -ansi-colors@^4.1.1: - version "4.1.3" - resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b" - integrity sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw== - ansi-escapes@^3.0.0: version "3.2.0" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b" @@ -3173,11 +3181,6 @@ ast-types@0.14.2: dependencies: tslib "^2.0.1" -astral-regex@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" - integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== - async-each@^1.0.1: version "1.0.6" resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.6.tgz#52f1d9403818c179b7561e11a5d1b77eb2160e77" @@ -4694,7 +4697,7 @@ debug@^3.2.7: dependencies: ms "^2.1.1" -debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.0, debug@^4.3.2, debug@^4.3.4: +debug@^4.1.0, debug@^4.1.1, debug@^4.3.0, debug@^4.3.2, debug@^4.3.4: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -5132,13 +5135,6 @@ enhanced-resolve@^5.0.0, enhanced-resolve@^5.15.0: graceful-fs "^4.2.4" tapable "^2.2.0" -enquirer@^2.3.5: - version "2.3.6" - resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d" - integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg== - dependencies: - ansi-colors "^4.1.1" - entities@^2.0.0, entities@^2.0.3: version "2.2.0" resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" @@ -5286,21 +5282,14 @@ eslint-scope@^4.0.3: esrecurse "^4.1.0" estraverse "^4.1.1" -eslint-scope@^7.0.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.0.tgz#f21ebdafda02352f103634b96dd47d9f81ca117b" - integrity sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw== +eslint-scope@^7.0.0, eslint-scope@^7.2.2: + version "7.2.2" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f" + integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg== dependencies: esrecurse "^4.3.0" estraverse "^5.2.0" -eslint-utils@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-2.1.0.tgz#d2de5e03424e707dc10c74068ddedae708741b27" - integrity sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg== - dependencies: - eslint-visitor-keys "^1.1.0" - eslint-utils@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-3.0.0.tgz#8aebaface7345bb33559db0a1f13a1d2d48c3672" @@ -5308,7 +5297,7 @@ eslint-utils@^3.0.0: dependencies: eslint-visitor-keys "^2.0.0" -eslint-visitor-keys@^1.0.0, eslint-visitor-keys@^1.1.0, eslint-visitor-keys@^1.3.0: +eslint-visitor-keys@^1.0.0: version "1.3.0" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e" integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ== @@ -5318,10 +5307,10 @@ eslint-visitor-keys@^2.0.0, eslint-visitor-keys@^2.1.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303" integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== -eslint-visitor-keys@^3.1.0, eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1: - version "3.4.1" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz#c22c48f48942d08ca824cc526211ae400478a994" - integrity sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA== +eslint-visitor-keys@^3.1.0, eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.2: + version "3.4.2" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.2.tgz#8c2095440eca8c933bedcadf16fefa44dbe9ba5f" + integrity sha512-8drBzUEyZ2llkpCA67iYrgEssKDUu68V8ChqqOfFupIaG/LCVPUT+CoGJpT77zJprs4T/W7p07LP7zAIMuweVw== eslint-webpack-plugin@^3.1.0: version "3.2.0" @@ -5334,67 +5323,55 @@ eslint-webpack-plugin@^3.1.0: normalize-path "^3.0.0" schema-utils "^4.0.0" -eslint@^7.28.0: - version "7.32.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.32.0.tgz#c6d328a14be3fb08c8d1d21e12c02fdb7a2a812d" - integrity sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA== +eslint@^8.46.0: + version "8.46.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.46.0.tgz#a06a0ff6974e53e643acc42d1dcf2e7f797b3552" + integrity sha512-cIO74PvbW0qU8e0mIvk5IV3ToWdCq5FYG6gWPHHkx6gNdjlbAYvtfHmlCMXxjcoVaIdwy/IAt3+mDkZkfvb2Dg== dependencies: - "@babel/code-frame" "7.12.11" - "@eslint/eslintrc" "^0.4.3" - "@humanwhocodes/config-array" "^0.5.0" - ajv "^6.10.0" + "@eslint-community/eslint-utils" "^4.2.0" + "@eslint-community/regexpp" "^4.6.1" + "@eslint/eslintrc" "^2.1.1" + "@eslint/js" "^8.46.0" + "@humanwhocodes/config-array" "^0.11.10" + "@humanwhocodes/module-importer" "^1.0.1" + "@nodelib/fs.walk" "^1.2.8" + ajv "^6.12.4" chalk "^4.0.0" cross-spawn "^7.0.2" - debug "^4.0.1" + debug "^4.3.2" doctrine "^3.0.0" - enquirer "^2.3.5" escape-string-regexp "^4.0.0" - eslint-scope "^5.1.1" - eslint-utils "^2.1.0" - eslint-visitor-keys "^2.0.0" - espree "^7.3.1" - esquery "^1.4.0" + eslint-scope "^7.2.2" + eslint-visitor-keys "^3.4.2" + espree "^9.6.1" + esquery "^1.4.2" esutils "^2.0.2" fast-deep-equal "^3.1.3" file-entry-cache "^6.0.1" - functional-red-black-tree "^1.0.1" - glob-parent "^5.1.2" - globals "^13.6.0" - ignore "^4.0.6" - import-fresh "^3.0.0" + find-up "^5.0.0" + glob-parent "^6.0.2" + globals "^13.19.0" + graphemer "^1.4.0" + ignore "^5.2.0" imurmurhash "^0.1.4" is-glob "^4.0.0" - js-yaml "^3.13.1" + is-path-inside "^3.0.3" + js-yaml "^4.1.0" json-stable-stringify-without-jsonify "^1.0.1" levn "^0.4.1" lodash.merge "^4.6.2" - minimatch "^3.0.4" + minimatch "^3.1.2" natural-compare "^1.4.0" - optionator "^0.9.1" - progress "^2.0.0" - regexpp "^3.1.0" - semver "^7.2.1" - strip-ansi "^6.0.0" - strip-json-comments "^3.1.0" - table "^6.0.9" + optionator "^0.9.3" + strip-ansi "^6.0.1" text-table "^0.2.0" - v8-compile-cache "^2.0.3" - -espree@^7.3.0, espree@^7.3.1: - version "7.3.1" - resolved "https://registry.yarnpkg.com/espree/-/espree-7.3.1.tgz#f2df330b752c6f55019f8bd89b7660039c1bbbb6" - integrity sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g== - dependencies: - acorn "^7.4.0" - acorn-jsx "^5.3.1" - eslint-visitor-keys "^1.3.0" -espree@^9.0.0: - version "9.5.2" - resolved "https://registry.yarnpkg.com/espree/-/espree-9.5.2.tgz#e994e7dc33a082a7a82dceaf12883a829353215b" - integrity sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw== +espree@^9.0.0, espree@^9.6.0, espree@^9.6.1: + version "9.6.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" + integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ== dependencies: - acorn "^8.8.0" + acorn "^8.9.0" acorn-jsx "^5.3.2" eslint-visitor-keys "^3.4.1" @@ -5403,7 +5380,7 @@ esprima@^4.0.0, esprima@~4.0.0: resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== -esquery@^1.4.0: +esquery@^1.4.0, esquery@^1.4.2: version "1.5.0" resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.5.0.tgz#6ce17738de8577694edd7361c57182ac8cb0db0b" integrity sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg== @@ -6041,11 +6018,6 @@ function.prototype.name@^1.1.5: es-abstract "^1.19.0" functions-have-names "^1.2.2" -functional-red-black-tree@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" - integrity sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g== - functions-have-names@^1.2.2, functions-have-names@^1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" @@ -6158,7 +6130,7 @@ glob-parent@^5.1.2, glob-parent@~5.1.2: dependencies: is-glob "^4.0.1" -glob-parent@^6.0.1: +glob-parent@^6.0.1, glob-parent@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== @@ -6194,7 +6166,7 @@ globals@^11.1.0: resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== -globals@^13.6.0, globals@^13.9.0: +globals@^13.19.0: version "13.20.0" resolved "https://registry.yarnpkg.com/globals/-/globals-13.20.0.tgz#ea276a1e508ffd4f1612888f9d1bad1e2717bf82" integrity sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ== @@ -6272,6 +6244,11 @@ grapheme-splitter@^1.0.4: resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e" integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== +graphemer@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" + integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== + graphql-subscriptions@^1.2.0: version "1.2.1" resolved "https://registry.yarnpkg.com/graphql-subscriptions/-/graphql-subscriptions-1.2.1.tgz#2142b2d729661ddf967b7388f7cf1dd4cf2e061d" @@ -6642,17 +6619,12 @@ iferr@^0.1.5: resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501" integrity sha512-DUNFN5j7Tln0D+TxzloUjKB+CtVu6myn0JEFak6dG18mNt9YkQ6lzGCdafwofISZ1lLF3xRHJ98VKy9ynkcFaA== -ignore@^4.0.6: - version "4.0.6" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" - integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== - ignore@^5.2.0: version "5.2.4" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== -import-fresh@^3.0.0, import-fresh@^3.1.0, import-fresh@^3.2.1: +import-fresh@^3.1.0, import-fresh@^3.2.1: version "3.3.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== @@ -7016,6 +6988,11 @@ is-object@^1.0.1: resolved "https://registry.yarnpkg.com/is-object/-/is-object-1.0.2.tgz#a56552e1c665c9e950b4a025461da87e72f86fcf" integrity sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA== +is-path-inside@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" + integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== + is-plain-obj@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" @@ -7261,7 +7238,7 @@ js-yaml@^3.13.1, js-yaml@^3.14.0, js-yaml@^3.14.1: argparse "^1.0.7" esprima "^4.0.0" -js-yaml@^4.0.0: +js-yaml@^4.0.0, js-yaml@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== @@ -7610,11 +7587,6 @@ lodash.topairs@^4.3.0: resolved "https://registry.yarnpkg.com/lodash.topairs/-/lodash.topairs-4.3.0.tgz#3b6deaa37d60fb116713c46c5f17ea190ec48d64" integrity sha512-qrRMbykBSEGdOgQLJJqVSdPWMD7Q+GJJ5jMRfQYb+LTLsw3tYVIabnCzRqTJb2WTo17PG5gNzXuFaZgYH/9SAQ== -lodash.truncate@^4.4.2: - version "4.4.2" - resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193" - integrity sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw== - lodash.uniq@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" @@ -7927,7 +7899,7 @@ minimalistic-crypto-utils@^1.0.1: resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" integrity sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg== -minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2: +minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== @@ -8358,17 +8330,17 @@ opener@^1.5.2: resolved "https://registry.yarnpkg.com/opener/-/opener-1.5.2.tgz#5d37e1f35077b9dcac4301372271afdeb2a13598" integrity sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A== -optionator@^0.9.1: - version "0.9.1" - resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" - integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== +optionator@^0.9.3: + version "0.9.3" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.3.tgz#007397d44ed1872fdc6ed31360190f81814e2c64" + integrity sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg== dependencies: + "@aashutoshrathi/word-wrap" "^1.2.3" deep-is "^0.1.3" fast-levenshtein "^2.0.6" levn "^0.4.1" prelude-ls "^1.2.1" type-check "^0.4.0" - word-wrap "^1.2.3" ora@^5.3.0, ora@^5.4.1: version "5.4.1" @@ -9086,11 +9058,6 @@ progress-webpack-plugin@^1.0.12: figures "^2.0.0" log-update "^2.3.0" -progress@^2.0.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" - integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== - promise-inflight@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" @@ -9389,11 +9356,6 @@ regexp.prototype.flags@^1.4.3: define-properties "^1.2.0" functions-have-names "^1.2.3" -regexpp@^3.1.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" - integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== - regexpu-core@^5.3.1: version "5.3.2" resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-5.3.2.tgz#11a2b06884f3527aec3e93dbbf4a3b958a95546b" @@ -9778,7 +9740,7 @@ semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.2.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8: +semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8: version "7.5.4" resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== @@ -9955,15 +9917,6 @@ slash@^3.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== -slice-ansi@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-4.0.0.tgz#500e8dd0fd55b05815086255b3195adf2a45fe6b" - integrity sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ== - dependencies: - ansi-styles "^4.0.0" - astral-regex "^2.0.0" - is-fullwidth-code-point "^3.0.0" - snapdragon-node@^2.0.1: version "2.1.1" resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" @@ -10391,7 +10344,7 @@ strip-indent@^2.0.0: resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-2.0.0.tgz#5ef8db295d01e6ed6cbf7aab96998d7822527b68" integrity sha512-RsSNPLpq6YUL7QYy44RnPVTn/lcVZtb48Uof3X5JLbF4zD/Gs7ZFDv2HWol+leoQN2mT86LAzSshGfkTlSOpsA== -strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: +strip-json-comments@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== @@ -10488,17 +10441,6 @@ symbol-observable@^1.0.4: resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ== -table@^6.0.9: - version "6.8.1" - resolved "https://registry.yarnpkg.com/table/-/table-6.8.1.tgz#ea2b71359fe03b017a5fbc296204471158080bdf" - integrity sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA== - dependencies: - ajv "^8.0.1" - lodash.truncate "^4.4.2" - slice-ansi "^4.0.0" - string-width "^4.2.3" - strip-ansi "^6.0.1" - tapable@^1.0.0, tapable@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2" @@ -11082,11 +11024,6 @@ uuid@^9.0.0: resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.0.tgz#592f550650024a38ceb0c562f2f6aa435761efb5" integrity sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg== -v8-compile-cache@^2.0.3: - version "2.3.0" - resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" - integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== - validate-npm-package-license@^3.0.1: version "3.0.4" resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" @@ -11622,11 +11559,6 @@ wildcard@^2.0.0: resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.1.tgz#5ab10d02487198954836b6349f74fff961e10f67" integrity sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ== -word-wrap@^1.2.3: - version "1.2.4" - resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.4.tgz#cb4b50ec9aca570abd1f52f33cd45b6c61739a9f" - integrity sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA== - workbox-background-sync@6.6.1, workbox-background-sync@^6.5.4: version "6.6.1" resolved "https://registry.yarnpkg.com/workbox-background-sync/-/workbox-background-sync-6.6.1.tgz#08d603a33717ce663e718c30cc336f74909aff2f" From 7f274192158802109611b4ac318ae42cd90c1541 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Aug 2023 00:28:28 +0000 Subject: [PATCH 16/71] Bump lxml from 4.9.2 to 4.9.3 Bumps [lxml](https://github.com/lxml/lxml) from 4.9.2 to 4.9.3. - [Release notes](https://github.com/lxml/lxml/releases) - [Changelog](https://github.com/lxml/lxml/blob/master/CHANGES.txt) - [Commits](https://github.com/lxml/lxml/compare/lxml-4.9.2...lxml-4.9.3) --- updated-dependencies: - dependency-name: lxml dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index d178b0cb94..a86b1aadd2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ django-debug-toolbar==3.8.1 bleach==5.0.1 bleach-allowlist==1.0.3 gunicorn==20.1.0 -lxml==4.9.2 +lxml==4.9.3 Markdown==3.4.3 Pillow==9.4.0 psycopg2-binary==2.9.5 From cb5b51bde3fc5b8e3eb0bf01d1468a4656416c2d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Aug 2023 00:28:49 +0000 Subject: [PATCH 17/71] Bump django-auth-ldap from 4.2.0 to 4.4.0 Bumps [django-auth-ldap](https://github.com/django-auth-ldap/django-auth-ldap) from 4.2.0 to 4.4.0. - [Release notes](https://github.com/django-auth-ldap/django-auth-ldap/releases) - [Changelog](https://github.com/django-auth-ldap/django-auth-ldap/blob/master/docs/changes.rst) - [Commits](https://github.com/django-auth-ldap/django-auth-ldap/compare/4.2.0...4.4.0) --- updated-dependencies: - dependency-name: django-auth-ldap dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index d178b0cb94..80ec600246 100644 --- a/requirements.txt +++ b/requirements.txt @@ -41,7 +41,7 @@ boto3==1.26.41 django-prometheus==2.2.0 django-hCaptcha==0.2.0 python-ldap==3.4.3 -django-auth-ldap==4.2.0 +django-auth-ldap==4.4.0 pytest-factoryboy==2.5.1 pyppeteer==1.0.2 validators==0.20.0 From df684f591a5bafc5c1cc0551a31b515747f2292f Mon Sep 17 00:00:00 2001 From: AquaticLava Date: Tue, 1 Aug 2023 17:02:05 -0600 Subject: [PATCH 18/71] added share functionality. changed random recipe selection to prevent repeating duplicate choices. --- cookbook/views/api.py | 10 +++++++++- vue/src/apps/MealPlanView/MealPlanView.vue | 3 ++- vue/src/components/AutoMealPlanModal.vue | 22 +++++++++++++++++++++- 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/cookbook/views/api.py b/cookbook/views/api.py index 7491017418..7ebe76c53a 100644 --- a/cookbook/views/api.py +++ b/cookbook/views/api.py @@ -675,6 +675,11 @@ def create(self, request): end_date = serializer.validated_data['end_date'] meal_type = MealType.objects.get(pk=serializer.validated_data['meal_type_id']) servings = serializer.validated_data['servings'] + shared = serializer.get_initial().get('shared', None) + shared_pks = list() + if shared is not None: + for i in range(len(shared)): + shared_pks.append(shared[i]['id']) days = (end_date - start_date).days + 1 recipes = Recipe.objects.all() @@ -688,7 +693,7 @@ def create(self, request): for i in range(0, days): day = start_date + datetime.timedelta(i) - recipe = random.choice(recipes) + recipe = recipes[i % len(recipes)] args = {'recipe': recipe, 'servings': servings, 'title': recipe.name, 'created_by': request.user, 'meal_type': meal_type, @@ -698,7 +703,10 @@ def create(self, request): meal_plans.append(m) MealPlan.objects.bulk_create(meal_plans) + for m in meal_plans: + m.shared.set(shared_pks) + if request.data.get('addshopping', False) and request.data.get('recipe', None): SLR = RecipeShoppingEditor(user=request.user, space=request.space) SLR.create(mealplan=m, servings=servings) diff --git a/vue/src/apps/MealPlanView/MealPlanView.vue b/vue/src/apps/MealPlanView/MealPlanView.vue index 9e34cb7189..4c5d357914 100644 --- a/vue/src/apps/MealPlanView/MealPlanView.vue +++ b/vue/src/apps/MealPlanView/MealPlanView.vue @@ -371,7 +371,8 @@ export default { servings: 1, date: Date.now(), startDay: null, - endDay: null + endDay: null, + shared: [] }, showDate: new Date(), plan_entries: [], diff --git a/vue/src/components/AutoMealPlanModal.vue b/vue/src/components/AutoMealPlanModal.vue index 2d8084b33e..c559f32dc4 100644 --- a/vue/src/components/AutoMealPlanModal.vue +++ b/vue/src/components/AutoMealPlanModal.vue @@ -32,6 +32,21 @@ {{ $t("Servings") }}
+ + + {{ $t("Share") }} +
@@ -61,6 +76,7 @@ import Vue from "vue" import {BootstrapVue} from "bootstrap-vue" import GenericMultiselect from "@/components/GenericMultiselect" import {ApiMixin} from "@/utils/utils" +import {useUserPreferenceStore} from "@/stores/UserPreferenceStore"; const { ApiApiFactory } = require("@/utils/openapi/api") const { StandardToasts } = require("@/utils/utils") @@ -89,7 +105,8 @@ export default { servings: 1, date: Date.now(), startDay: null, - endDay: null + endDay: null, + shared: [] } } }, @@ -104,6 +121,9 @@ export default { this.AutoPlan.servings = 1 this.AutoPlan.startDay = new Date() this.AutoPlan.endDay = this.current_period.periodEnd + useUserPreferenceStore().getData().then(userPreference => { + this.AutoPlan.shared = userPreference.plan_share + }) }, sortMealTypes() { this.meal_types.forEach(function (element, index) { From ecd828008e49fbecb4733c7d9cdfff573e693485 Mon Sep 17 00:00:00 2001 From: AquaticLava Date: Tue, 1 Aug 2023 21:52:59 -0600 Subject: [PATCH 19/71] added auto shopping functionality. fixed bug when there are no matching recipes --- cookbook/serializer.py | 1 + cookbook/views/api.py | 4 ++- vue/src/apps/MealPlanView/MealPlanView.vue | 6 ++-- vue/src/components/AutoMealPlanModal.vue | 38 ++++++++++++++++++++-- 4 files changed, 43 insertions(+), 6 deletions(-) diff --git a/cookbook/serializer.py b/cookbook/serializer.py index 505f4e8baa..15121d17e8 100644 --- a/cookbook/serializer.py +++ b/cookbook/serializer.py @@ -988,6 +988,7 @@ class AutoMealPlanSerializer(serializers.Serializer): keywords = KeywordSerializer(many=True) servings = CustomDecimalField() shared = UserSerializer(many=True, required=False, allow_null=True) + addshopping = serializers.BooleanField() class ShoppingListRecipeSerializer(serializers.ModelSerializer): diff --git a/cookbook/views/api.py b/cookbook/views/api.py index 7ebe76c53a..b3a8c76251 100644 --- a/cookbook/views/api.py +++ b/cookbook/views/api.py @@ -688,6 +688,8 @@ def create(self, request): for keyword in keywords: recipes = recipes.filter(keywords__name=keyword['name']) + if len(recipes) == 0: + return Response(serializer.data) recipes = recipes.order_by('?')[:days] recipes = list(recipes) @@ -707,7 +709,7 @@ def create(self, request): for m in meal_plans: m.shared.set(shared_pks) - if request.data.get('addshopping', False) and request.data.get('recipe', None): + if request.data.get('addshopping', False): SLR = RecipeShoppingEditor(user=request.user, space=request.space) SLR.create(mealplan=m, servings=servings) diff --git a/vue/src/apps/MealPlanView/MealPlanView.vue b/vue/src/apps/MealPlanView/MealPlanView.vue index 4c5d357914..2763ac0d61 100644 --- a/vue/src/apps/MealPlanView/MealPlanView.vue +++ b/vue/src/apps/MealPlanView/MealPlanView.vue @@ -372,7 +372,8 @@ export default { date: Date.now(), startDay: null, endDay: null, - shared: [] + shared: [], + addshopping: false }, showDate: new Date(), plan_entries: [], @@ -695,7 +696,8 @@ export default { "meal_type_id" : autoPlan.meal_types[mealTypeIndex].id, "keywords" : autoPlan.keywords[mealTypeIndex], "servings" : autoPlan.servings, - "shared" : autoPlan.shared + "shared" : autoPlan.shared, + "addshopping": autoPlan.addshopping } await apiClient.createAutoPlanViewSet(data) diff --git a/vue/src/components/AutoMealPlanModal.vue b/vue/src/components/AutoMealPlanModal.vue index c559f32dc4..a2997c5611 100644 --- a/vue/src/components/AutoMealPlanModal.vue +++ b/vue/src/components/AutoMealPlanModal.vue @@ -47,6 +47,12 @@ > {{ $t("Share") }} + + + {{ + $t("AddToShopping") + }} +
@@ -77,11 +83,14 @@ import {BootstrapVue} from "bootstrap-vue" import GenericMultiselect from "@/components/GenericMultiselect" import {ApiMixin} from "@/utils/utils" import {useUserPreferenceStore} from "@/stores/UserPreferenceStore"; +import VueCookies from "vue-cookies"; const { ApiApiFactory } = require("@/utils/openapi/api") const { StandardToasts } = require("@/utils/utils") Vue.use(BootstrapVue) +Vue.use(VueCookies) +let MEALPLAN_COOKIE_NAME = "mealplan_settings" export default { name: "AutoMealPlanModal", @@ -106,16 +115,38 @@ export default { date: Date.now(), startDay: null, endDay: null, - shared: [] - } + shared: [], + addshopping: false + }, + mealplan_settings: { + addshopping: false, + } } }, - mounted: function () {}, + watch: { + mealplan_settings: { + handler(newVal) { + this.$cookies.set(MEALPLAN_COOKIE_NAME, this.mealplan_settings) + }, + deep: true, + }, + }, + mounted: function () { + useUserPreferenceStore().updateIfStaleOrEmpty() + }, + computed: { + autoMealPlan: function () { + return useUserPreferenceStore().getStaleData()?.mealplan_autoadd_shopping + }, + }, methods: { genericSelectChanged: function (obj) { this.AutoPlan.keywords[obj.var] = obj.val }, showModal() { + if (this.$cookies.isKey(MEALPLAN_COOKIE_NAME)) { + this.mealplan_settings = Object.assign({}, this.mealplan_settings, this.$cookies.get(MEALPLAN_COOKIE_NAME)) + } this.refreshMealTypes() this.AutoPlan.servings = 1 @@ -164,6 +195,7 @@ export default { }, createPlan() { this.$bvModal.hide(`autoplan-modal`) + this.AutoPlan.addshopping = this.mealplan_settings.addshopping this.$emit("create-plan", this.AutoPlan) }, updateStartDay(date){ From 6d84c718fd0e472fa3e66b7ff51900bbb602f079 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 5 Aug 2023 07:46:02 +0000 Subject: [PATCH 20/71] Bump django-cleanup from 7.0.0 to 8.0.0 Bumps [django-cleanup](https://github.com/un1t/django-cleanup) from 7.0.0 to 8.0.0. - [Changelog](https://github.com/un1t/django-cleanup/blob/master/CHANGELOG.md) - [Commits](https://github.com/un1t/django-cleanup/compare/7.0.0...8.0.0) --- updated-dependencies: - dependency-name: django-cleanup dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f1a17f7528..57486d0648 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ Django==4.1.10 cryptography===41.0.3 django-annoying==0.10.6 django-autocomplete-light==3.9.4 -django-cleanup==7.0.0 +django-cleanup==8.0.0 django-crispy-forms==1.14.0 django-tables2==2.5.3 djangorestframework==3.14.0 From 3a8ea4b4c9282a1b3fcffd189e449fbdae1a5fe5 Mon Sep 17 00:00:00 2001 From: smilerz Date: Thu, 10 Aug 2023 08:33:02 -0500 Subject: [PATCH 21/71] fix incorrect variable in apply_transpose_words_automations --- cookbook/helper/ingredient_parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cookbook/helper/ingredient_parser.py b/cookbook/helper/ingredient_parser.py index b5bdbf67ce..57b70f44c3 100644 --- a/cookbook/helper/ingredient_parser.py +++ b/cookbook/helper/ingredient_parser.py @@ -290,7 +290,7 @@ def apply_transpose_words_automations(self, ingredient): for rule in Automation.objects.filter(space=self.request.space, type=Automation.TRANSPOSE_WORDS, disabled=False) \ .annotate(param_1_lower=Lower('param_1'), param_2_lower=Lower('param_2')) \ .filter(Q(Q(param_1_lower__in=tokens) | Q(param_2_lower__in=tokens))).order_by('order'): - ingredient = re.sub(rf"\b({v[0]})\W*({v[1]})\b", r"\2 \1", ingredient, flags=re.IGNORECASE) + ingredient = re.sub(rf"\b({rule.param_1})\W*({rule.param_1})\b", r"\2 \1", ingredient, flags=re.IGNORECASE) return ingredient def parse(self, ingredient): From c0d67dbc581fcf73dc3333fe340b09a9ec3b296b Mon Sep 17 00:00:00 2001 From: smilerz Date: Mon, 24 Apr 2023 10:59:48 -0500 Subject: [PATCH 22/71] add NEVER_UNIT automation --- cookbook/helper/ingredient_parser.py | 57 +++++++++- .../0189_alter_automation_type_and_more.py | 23 ++++ cookbook/models.py | 8 +- docs/features/automation.md | 67 +++++++----- vue/src/locales/en.json | 5 +- vue/src/utils/models.js | 100 +++++++++--------- 6 files changed, 178 insertions(+), 82 deletions(-) create mode 100644 cookbook/migrations/0189_alter_automation_type_and_more.py diff --git a/cookbook/helper/ingredient_parser.py b/cookbook/helper/ingredient_parser.py index 8ecf299b96..f1596f83cc 100644 --- a/cookbook/helper/ingredient_parser.py +++ b/cookbook/helper/ingredient_parser.py @@ -4,7 +4,7 @@ from django.core.cache import caches -from cookbook.models import Unit, Food, Automation, Ingredient +from cookbook.models import Automation, Food, Ingredient, Unit class IngredientParser: @@ -12,6 +12,7 @@ class IngredientParser: ignore_rules = False food_aliases = {} unit_aliases = {} + never_unit = {} def __init__(self, request, cache_mode, ignore_automations=False): """ @@ -40,9 +41,19 @@ def __init__(self, request, cache_mode, ignore_automations=False): for a in Automation.objects.filter(space=self.request.space, disabled=False, type=Automation.UNIT_ALIAS).only('param_1', 'param_2').order_by('order').all(): self.unit_aliases[a.param_1] = a.param_2 caches['default'].set(UNIT_CACHE_KEY, self.unit_aliases, 30) + + NEVER_UNIT_CACHE_KEY = f'automation_never_unit_{self.request.space.pk}' + if c := caches['default'].get(NEVER_UNIT_CACHE_KEY, None): + self.never_unit = c + caches['default'].touch(NEVER_UNIT_CACHE_KEY, 30) + else: + for a in Automation.objects.filter(space=self.request.space, disabled=False, type=Automation.NEVER_UNIT).only('param_1', 'param_2').order_by('order').all(): + self.never_unit[a.param_1] = a.param_2 + caches['default'].set(NEVER_UNIT_CACHE_KEY, self.never_unit, 30) else: self.food_aliases = {} self.unit_aliases = {} + self.never_unit = {} def apply_food_automation(self, food): """ @@ -205,6 +216,49 @@ def parse_food(self, tokens): food, note = self.parse_food_with_comma(tokens) return food, note + def apply_never_unit_automations(self, tokens): + """ + Moves a string that should never be treated as a unit to next token and optionally replaced with default unit + e.g. NEVER_UNIT: param1: egg, param2: None would modify ['1', 'egg', 'white'] to ['1', '', 'egg', 'white'] + or NEVER_UNIT: param1: egg, param2: pcs would modify ['1', 'egg', 'yolk'] to ['1', 'pcs', 'egg', 'yolk'] + :param1 string: string that should never be considered a unit, will be moved to token[2] + :param2 (optional) unit as string: will insert unit string into token[1] + :return: unit as string (possibly changed by automation) + """ + + if self.ignore_rules: + return tokens + + new_unit = None + alt_unit = self.apply_unit_automation(tokens[1]) + never_unit = False + if self.never_unit: + try: + new_unit = self.never_unit[tokens[1]] + never_unit = True + except KeyError: + return tokens + + else: + if automation := Automation.objects.filter(space=self.request.space, type=Automation.UNIT_ALIAS, param_1__in=[tokens[1], alt_unit], disabled=False).order_by('order').first(): + new_unit = automation.param_2 + never_unit = True + + if never_unit: + tokens.insert(1, new_unit) + + return tokens + + def parse_tokens(self, tokens): + """ + parser that applies automations to unmodified tokens + """ + + if self.ignore_rules: + return tokens + + return self.apply_never_unit_automations(tokens) + def parse(self, ingredient): """ Main parsing function, takes an ingredient string (e.g. '1 l Water') and extracts amount, unit, food, ... @@ -257,6 +311,7 @@ def parse(self, ingredient): # three arguments if it already has a unit there can't be # a fraction for the amount if len(tokens) > 2: + tokens = self.parse_tokens(tokens) try: if unit is not None: # a unit is already found, no need to try the second argument for a fraction diff --git a/cookbook/migrations/0189_alter_automation_type_and_more.py b/cookbook/migrations/0189_alter_automation_type_and_more.py new file mode 100644 index 0000000000..62d8f776a8 --- /dev/null +++ b/cookbook/migrations/0189_alter_automation_type_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 4.1.7 on 2023-04-24 15:00 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('cookbook', '0188_space_no_sharing_limit'), + ] + + operations = [ + migrations.AlterField( + model_name='automation', + name='type', + field=models.CharField(choices=[('FOOD_ALIAS', 'Food Alias'), ('UNIT_ALIAS', 'Unit Alias'), ('KEYWORD_ALIAS', 'Keyword Alias'), ('DESCRIPTION_REPLACE', 'Description Replace'), ('INSTRUCTION_REPLACE', 'Instruction Replace'), ('NEVER_UNIT', 'Never Unit')], max_length=128), + ), + migrations.AlterField( + model_name='userpreference', + name='use_fractions', + field=models.BooleanField(default=True), + ), + ] diff --git a/cookbook/models.py b/cookbook/models.py index f4369c7049..93d1e7b391 100644 --- a/cookbook/models.py +++ b/cookbook/models.py @@ -5,7 +5,6 @@ from datetime import date, timedelta import oauth2_provider.models -from PIL import Image from annoying.fields import AutoOneToOneField from django.contrib import auth from django.contrib.auth.models import Group, User @@ -14,13 +13,14 @@ from django.core.files.uploadedfile import InMemoryUploadedFile, UploadedFile from django.core.validators import MinLengthValidator from django.db import IntegrityError, models -from django.db.models import Index, ProtectedError, Q, Avg, Max +from django.db.models import Avg, Index, Max, ProtectedError, Q from django.db.models.fields.related import ManyToManyField from django.db.models.functions import Substr from django.utils import timezone from django.utils.translation import gettext as _ from django_prometheus.models import ExportModelOperationsMixin from django_scopes import ScopedManager, scopes_disabled +from PIL import Image from treebeard.mp_tree import MP_Node, MP_NodeManager from recipes.settings import (COMMENT_PREF_DEFAULT, FRACTION_PREF_DEFAULT, KJ_PREF_DEFAULT, @@ -1315,10 +1315,12 @@ class Automation(ExportModelOperationsMixin('automations'), models.Model, Permis KEYWORD_ALIAS = 'KEYWORD_ALIAS' DESCRIPTION_REPLACE = 'DESCRIPTION_REPLACE' INSTRUCTION_REPLACE = 'INSTRUCTION_REPLACE' + NEVER_UNIT = 'NEVER_UNIT' type = models.CharField(max_length=128, choices=((FOOD_ALIAS, _('Food Alias')), (UNIT_ALIAS, _('Unit Alias')), (KEYWORD_ALIAS, _('Keyword Alias')), - (DESCRIPTION_REPLACE, _('Description Replace')), (INSTRUCTION_REPLACE, _('Instruction Replace')),)) + (DESCRIPTION_REPLACE, _('Description Replace')), (INSTRUCTION_REPLACE, _('Instruction Replace')), + (NEVER_UNIT, _('Never Unit')),)) name = models.CharField(max_length=128, default='') description = models.TextField(blank=True, null=True) diff --git a/docs/features/automation.md b/docs/features/automation.md index 66f9fefdb3..bd76dec99e 100644 --- a/docs/features/automation.md +++ b/docs/features/automation.md @@ -1,39 +1,41 @@ !!! warning - Automations are currently in a beta stage. They work pretty stable but if I encounter any - issues while working on them, I might change how they work breaking existing automations. - I will try to avoid this and am pretty confident it won't happen. +Automations are currently in a beta stage. They work pretty stable but if I encounter any +issues while working on them, I might change how they work breaking existing automations. +I will try to avoid this and am pretty confident it won't happen. - -Automations allow Tandoor to automatically perform certain tasks, especially when importing recipes, that +Automations allow Tandoor to automatically perform certain tasks, especially when importing recipes, that would otherwise have to be done manually. Currently, the following automations are supported. ## Unit, Food, Keyword Alias + Foods, Units and Keywords can have automations that automatically replace them with another object -to allow aliasing them. +to allow aliasing them. This helps to add consistency to the naming of objects, for example to always use the singular form -for the main name if a plural form is configured. +for the main name if a plural form is configured. -These automations are best created by dragging and dropping Foods, Units or Keywords in their respective -views and creating the automation there. +These automations are best created by dragging and dropping Foods, Units or Keywords in their respective +views and creating the automation there. You can also create them manually by setting the following -- **Parameter 1**: name of food/unit/keyword to match -- **Parameter 2**: name of food/unit/keyword to replace matched food with + +- **Parameter 1**: name of food/unit/keyword to match +- **Parameter 2**: name of food/unit/keyword to replace matched food with These rules are processed whenever you are importing recipes from websites or other apps and when using the simple ingredient input (shopping, recipe editor, ...). ## Description Replace -This automation is a bit more complicated than the alis rules. It is run when importing a recipe + +This automation is a bit more complicated than the alias rules. It is run when importing a recipe from a website. It uses Regular Expressions (RegEx) to determine if a description should be altered, what exactly to remove -and what to replace it with. +and what to replace it with. -- **Parameter 1**: pattern of which sites to match (e.g. `.*.chefkoch.de.*`, `.*`) -- **Parameter 2**: pattern of what to replace (e.g. `.*`) -- **Parameter 3**: value to replace matched occurrence of parameter 2 with. Only one occurrence of the pattern is replaced. +- **Parameter 1**: pattern of which sites to match (e.g. `.*.chefkoch.de.*`, `.*`) +- **Parameter 2**: pattern of what to replace (e.g. `.*`) +- **Parameter 3**: value to replace matched occurrence of parameter 2 with. Only one occurrence of the pattern is replaced. To replace the description the python [re.sub](https://docs.python.org/2/library/re.html#re.sub) function is used like this `re.sub(, , , count=1)` @@ -41,24 +43,41 @@ like this `re.sub(, , , count=1)` To test out your patterns and learn about RegEx you can use [regexr.com](https://regexr.com/) !!! info - In order to prevent denial of service attacks on the RegEx engine the number of replace automations - and the length of the inputs that are processed are limited. Those limits should never be reached - during normal usage. +In order to prevent denial of service attacks on the RegEx engine the number of replace automations +and the length of the inputs that are processed are limited. Those limits should never be reached +during normal usage. ## Instruction Replace + This works just like the Description Replace automation but runs against all instruction texts -in all steps of a recipe during import. +in all steps of a recipe during import. Also instead of just replacing a single occurrence of the matched pattern it will replace all. +## Never Unit + +Some ingredients have a pattern of AMOUNT and FOOD, if the food has multiple words (e.g. egg yolk) this can cause Tandoor +to detect the word "egg" as a unit. This automation will detect the word 'egg' as something that should never be considered +a unit. + +You can also create them manually by setting the following + +- **Parameter 1**: string to detect +- **Parameter 2**: Optional: unit to insert into ingredient (e.g. 1 whole 'egg yolk' instead of 1 'egg yolk') + +These rules are processed whenever you are importing recipes from websites or other apps +and when using the simple ingredient input (shopping, recipe editor, ...). + # Order -If the Automation type allows for more than one rule to be executed (for example description replace) -the rules are processed in ascending order (ordered by the *order* property of the automation). -The default order is always 1000 to make it easier to add automations before and after other automations. + +If the Automation type allows for more than one rule to be executed (for example description replace) +the rules are processed in ascending order (ordered by the _order_ property of the automation). +The default order is always 1000 to make it easier to add automations before and after other automations. Example: + 1. Rule ABC (order 1000) replaces `everything` with `abc` 2. Rule DEF (order 2000) replaces `everything` with `def` 3. Rule XYZ (order 500) replaces `everything` with `xyz` -After processing rules XYZ, then ABC and then DEF the description will have the value `def` \ No newline at end of file +After processing rules XYZ, then ABC and then DEF the description will have the value `def` diff --git a/vue/src/locales/en.json b/vue/src/locales/en.json index 9a664fdcc5..d9b683ede9 100644 --- a/vue/src/locales/en.json +++ b/vue/src/locales/en.json @@ -518,6 +518,7 @@ "Use_Plural_Food_Always": "Use plural form for food always", "Use_Plural_Food_Simple": "Use plural form for food dynamically", "plural_usage_info": "Use the plural form for units and food inside this space.", - "Create Recipe": "Create Recipe", - "Import Recipe": "Import Recipe" + "Create Recipe": "Create Recipe", + "Import Recipe": "Import Recipe", + "Never_Unit": "Never Unit" } diff --git a/vue/src/utils/models.js b/vue/src/utils/models.js index 9a1eb35459..f36ac9951e 100644 --- a/vue/src/utils/models.js +++ b/vue/src/utils/models.js @@ -23,7 +23,7 @@ export class Models { false: undefined, }, }, - tree: {default: undefined}, + tree: { default: undefined }, }, }, delete: { @@ -50,7 +50,7 @@ export class Models { type: "lookup", field: "target", list: "self", - sticky_options: [{id: 0, name: "tree_root"}], + sticky_options: [{ id: 0, name: "tree_root" }], }, }, }, @@ -71,7 +71,7 @@ export class Models { food_onhand: true, shopping: true, }, - tags: [{field: "supermarket_category", label: "name", color: "info"}], + tags: [{ field: "supermarket_category", label: "name", color: "info" }], // REQUIRED: unordered array of fields that can be set during create create: { // if not defined partialUpdate will use the same parameters, prepending 'id' @@ -177,7 +177,7 @@ export class Models { field: "substitute_siblings", label: "substitute_siblings", // form.label always translated in utils.getForm() help_text: "substitute_siblings_help", // form.help_text always translated - condition: {field: "parent", value: true, condition: "field_exists"}, + condition: { field: "parent", value: true, condition: "field_exists" }, }, substitute_children: { form_field: true, @@ -186,7 +186,7 @@ export class Models { field: "substitute_children", label: "substitute_children", help_text: "substitute_children_help", - condition: {field: "numchild", value: 0, condition: "gt"}, + condition: { field: "numchild", value: 0, condition: "gt" }, }, inherit_fields: { form_field: true, @@ -196,7 +196,7 @@ export class Models { field: "inherit_fields", list: "FOOD_INHERIT_FIELDS", label: "InheritFields", - condition: {field: "food_children_exist", value: true, condition: "preference_equals"}, + condition: { field: "food_children_exist", value: true, condition: "preference_equals" }, help_text: "InheritFields_help", }, child_inherit_fields: { @@ -207,7 +207,7 @@ export class Models { field: "child_inherit_fields", list: "FOOD_INHERIT_FIELDS", label: "ChildInheritFields", // form.label always translated in utils.getForm() - condition: {field: "numchild", value: 0, condition: "gt"}, + condition: { field: "numchild", value: 0, condition: "gt" }, help_text: "ChildInheritFields_help", // form.help_text always translated }, reset_inherit: { @@ -217,7 +217,7 @@ export class Models { field: "reset_inherit", label: "reset_children", help_text: "reset_children_help", - condition: {field: "numchild", value: 0, condition: "gt"}, + condition: { field: "numchild", value: 0, condition: "gt" }, }, form_function: "FoodCreateDefault", }, @@ -281,7 +281,7 @@ export class Models { apiName: "Unit", paginated: true, create: { - params: [["name", "plural_name", "description", "base_unit","open_data_slug",]], + params: [["name", "plural_name", "description", "base_unit", "open_data_slug"]], form: { show_help: true, name: { @@ -311,24 +311,24 @@ export class Models { form_field: true, type: "choice", options: [ - {value: "g", text: "g"}, - {value: "kg", text: "kg"}, - {value: "ounce", text: "ounce"}, - {value: "pound", text: "pound"}, - {value: "ml", text: "ml"}, - {value: "l", text: "l"}, - {value: "fluid_ounce", text: "fluid_ounce"}, - {value: "pint", text: "pint"}, - {value: "quart", text: "quart"}, - {value: "gallon", text: "gallon"}, - {value: "tbsp", text: "tbsp"}, - {value: "tsp", text: "tsp"}, - {value: "imperial_fluid_ounce", text: "imperial_fluid_ounce"}, - {value: "imperial_pint", text: "imperial_pint"}, - {value: "imperial_quart", text: "imperial_quart"}, - {value: "imperial_gallon", text: "imperial_gallon"}, - {value: "imperial_tbsp", text: "imperial_tbsp"}, - {value: "imperial_tsp", text: "imperial_tsp"}, + { value: "g", text: "g" }, + { value: "kg", text: "kg" }, + { value: "ounce", text: "ounce" }, + { value: "pound", text: "pound" }, + { value: "ml", text: "ml" }, + { value: "l", text: "l" }, + { value: "fluid_ounce", text: "fluid_ounce" }, + { value: "pint", text: "pint" }, + { value: "quart", text: "quart" }, + { value: "gallon", text: "gallon" }, + { value: "tbsp", text: "tbsp" }, + { value: "tsp", text: "tsp" }, + { value: "imperial_fluid_ounce", text: "imperial_fluid_ounce" }, + { value: "imperial_pint", text: "imperial_pint" }, + { value: "imperial_quart", text: "imperial_quart" }, + { value: "imperial_gallon", text: "imperial_gallon" }, + { value: "imperial_tbsp", text: "imperial_tbsp" }, + { value: "imperial_tsp", text: "imperial_tsp" }, ], field: "base_unit", label: "Base Unit", @@ -470,7 +470,7 @@ export class Models { static SUPERMARKET = { name: "Supermarket", apiName: "Supermarket", - ordered_tags: [{field: "category_to_supermarket", label: "category::name", color: "info"}], + ordered_tags: [{ field: "category_to_supermarket", label: "category::name", color: "info" }], create: { params: [["name", "description", "category_to_supermarket"]], form: { @@ -553,11 +553,11 @@ export class Models { form_field: true, type: "choice", options: [ - {value: "FOOD_ALIAS", text: "Food_Alias"}, - {value: "UNIT_ALIAS", text: "Unit_Alias"}, - {value: "KEYWORD_ALIAS", text: "Keyword_Alias"}, - {value: "DESCRIPTION_REPLACE", text: "Description_Replace"}, - {value: "INSTRUCTION_REPLACE", text: "Instruction_Replace"}, + { value: "FOOD_ALIAS", text: "Food_Alias" }, + { value: "UNIT_ALIAS", text: "Unit_Alias" }, + { value: "KEYWORD_ALIAS", text: "Keyword_Alias" }, + { value: "DESCRIPTION_REPLACE", text: "Description_Replace" }, + { value: "INSTRUCTION_REPLACE", text: "Instruction_Replace" }, ], field: "type", label: "Type", @@ -625,9 +625,8 @@ export class Models { label: "Disabled", placeholder: "", }, - form_function: "AutomationOrderDefault" + form_function: "AutomationOrderDefault", }, - }, } @@ -641,7 +640,7 @@ export class Models { }, }, create: { - params: [['food', 'base_amount', 'base_unit', 'converted_amount', 'converted_unit', 'open_data_slug']], + params: [["food", "base_amount", "base_unit", "converted_amount", "converted_unit", "open_data_slug"]], form: { show_help: true, // TODO add proper help texts for everything @@ -695,9 +694,7 @@ export class Models { help_text: "open_data_help_text", optional: true, }, - }, - }, } @@ -711,7 +708,7 @@ export class Models { }, }, create: { - params: [['name', 'icon', 'unit', 'description']], + params: [["name", "icon", "unit", "description"]], form: { show_help: true, name: { @@ -755,7 +752,6 @@ export class Models { optional: true, }, }, - }, } @@ -843,7 +839,7 @@ export class Models { params: ["filter_list"], }, create: { - params: [["name",]], + params: [["name"]], form: { name: { form_field: true, @@ -1008,7 +1004,7 @@ export class Actions { }, ], }, - ok_label: {function: "translate", phrase: "Save"}, + ok_label: { function: "translate", phrase: "Save" }, }, } static UPDATE = { @@ -1043,7 +1039,7 @@ export class Actions { }, ], }, - ok_label: {function: "translate", phrase: "Delete"}, + ok_label: { function: "translate", phrase: "Delete" }, instruction: { form_field: true, type: "instruction", @@ -1070,17 +1066,17 @@ export class Actions { suffix: "s", params: ["query", "page", "pageSize", "options"], config: { - query: {default: undefined}, - page: {default: 1}, - pageSize: {default: 25}, + query: { default: undefined }, + page: { default: 1 }, + pageSize: { default: 25 }, }, } static MERGE = { function: "merge", params: ["source", "target"], config: { - source: {type: "string"}, - target: {type: "string"}, + source: { type: "string" }, + target: { type: "string" }, }, form: { title: { @@ -1095,7 +1091,7 @@ export class Actions { }, ], }, - ok_label: {function: "translate", phrase: "Merge"}, + ok_label: { function: "translate", phrase: "Merge" }, instruction: { form_field: true, type: "instruction", @@ -1129,8 +1125,8 @@ export class Actions { function: "move", params: ["source", "target"], config: { - source: {type: "string"}, - target: {type: "string"}, + source: { type: "string" }, + target: { type: "string" }, }, form: { title: { @@ -1145,7 +1141,7 @@ export class Actions { }, ], }, - ok_label: {function: "translate", phrase: "Move"}, + ok_label: { function: "translate", phrase: "Move" }, instruction: { form_field: true, type: "instruction", From d83b0484d8ecefe2ffc3b7172ad199e3d1fece86 Mon Sep 17 00:00:00 2001 From: smilerz Date: Mon, 24 Apr 2023 11:58:21 -0500 Subject: [PATCH 23/71] create Transpose Words automation --- cookbook/helper/ingredient_parser.py | 43 ++++++++++++++++--- .../0189_alter_automation_type_and_more.py | 4 +- cookbook/models.py | 3 +- docs/features/automation.md | 11 +++++ vue/src/locales/en.json | 3 +- vue/src/utils/models.js | 2 + 6 files changed, 56 insertions(+), 10 deletions(-) diff --git a/cookbook/helper/ingredient_parser.py b/cookbook/helper/ingredient_parser.py index f1596f83cc..dc0d94f89c 100644 --- a/cookbook/helper/ingredient_parser.py +++ b/cookbook/helper/ingredient_parser.py @@ -13,6 +13,7 @@ class IngredientParser: food_aliases = {} unit_aliases = {} never_unit = {} + transpose_words = {} def __init__(self, request, cache_mode, ignore_automations=False): """ @@ -50,10 +51,22 @@ def __init__(self, request, cache_mode, ignore_automations=False): for a in Automation.objects.filter(space=self.request.space, disabled=False, type=Automation.NEVER_UNIT).only('param_1', 'param_2').order_by('order').all(): self.never_unit[a.param_1] = a.param_2 caches['default'].set(NEVER_UNIT_CACHE_KEY, self.never_unit, 30) + + TRANSPOSE_WORDS_CACHE_KEY = f'automation_transpose_words_{self.request.space.pk}' + if c := caches['default'].get(TRANSPOSE_WORDS_CACHE_KEY, None): + self.transpose_words = c + caches['default'].touch(TRANSPOSE_WORDS_CACHE_KEY, 30) + else: + i = 0 + for a in Automation.objects.filter(space=self.request.space, disabled=False, type=Automation.TRANSPOSE_WORDS).only('param_1', 'param_2').order_by('order').all(): + self.never_unit[i] = [a.param_1, a.param_2] + i += 1 + caches['default'].set(TRANSPOSE_WORDS_CACHE_KEY, self.transpose_words, 30) else: self.food_aliases = {} self.unit_aliases = {} self.never_unit = {} + self.transpose_words = {} def apply_food_automation(self, food): """ @@ -83,7 +96,7 @@ def apply_unit_automation(self, unit): if self.ignore_rules: return unit else: - if self.unit_aliases: + if self.transpose_words: try: return self.unit_aliases[unit] except KeyError: @@ -249,15 +262,31 @@ def apply_never_unit_automations(self, tokens): return tokens - def parse_tokens(self, tokens): + def apply_transpose_words_automations(self, ingredient): """ - parser that applies automations to unmodified tokens + If two words (param_1 & param_2) are detected in sequence, swap their position in the ingredient string + :param 1: first word to detect + :param 2: second word to detect + return: new ingredient string """ + #################################################### + #################################################### + #################################################### + #################################################### if self.ignore_rules: - return tokens + return ingredient + + else: + if self.transpose_words: + for rule in self.transpose_words: + ingredient = re.sub(rf"\b({rule[0]}) ({rule[1]})\b", r"\2 \1", ingredient) + + else: + for rule in Automation.objects.filter(space=self.request.space, type=Automation.TRANSPOSE_WORDS, disabled=False).order_by('order'): + ingredient = re.sub(rf"\b({rule.param_1}) ({rule.param_2})\b", r"\2 \1", ingredient) - return self.apply_never_unit_automations(tokens) + return ingredient def parse(self, ingredient): """ @@ -275,6 +304,8 @@ def parse(self, ingredient): if len(ingredient) == 0: raise ValueError('string to parse cannot be empty') + ingredient = self.apply_transpose_words_automations(ingredient) + # some people/languages put amount and unit at the end of the ingredient string # if something like this is detected move it to the beginning so the parser can handle it if len(ingredient) < 1000 and re.search(r'^([^\W\d_])+(.)*[1-9](\d)*\s*([^\W\d_])+', ingredient): @@ -311,7 +342,7 @@ def parse(self, ingredient): # three arguments if it already has a unit there can't be # a fraction for the amount if len(tokens) > 2: - tokens = self.parse_tokens(tokens) + tokens = self.apply_never_unit_automations(tokens) try: if unit is not None: # a unit is already found, no need to try the second argument for a fraction diff --git a/cookbook/migrations/0189_alter_automation_type_and_more.py b/cookbook/migrations/0189_alter_automation_type_and_more.py index 62d8f776a8..bf997ab6d7 100644 --- a/cookbook/migrations/0189_alter_automation_type_and_more.py +++ b/cookbook/migrations/0189_alter_automation_type_and_more.py @@ -1,4 +1,4 @@ -# Generated by Django 4.1.7 on 2023-04-24 15:00 +# Generated by Django 4.1.7 on 2023-04-24 16:22 from django.db import migrations, models @@ -13,7 +13,7 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='automation', name='type', - field=models.CharField(choices=[('FOOD_ALIAS', 'Food Alias'), ('UNIT_ALIAS', 'Unit Alias'), ('KEYWORD_ALIAS', 'Keyword Alias'), ('DESCRIPTION_REPLACE', 'Description Replace'), ('INSTRUCTION_REPLACE', 'Instruction Replace'), ('NEVER_UNIT', 'Never Unit')], max_length=128), + field=models.CharField(choices=[('FOOD_ALIAS', 'Food Alias'), ('UNIT_ALIAS', 'Unit Alias'), ('KEYWORD_ALIAS', 'Keyword Alias'), ('DESCRIPTION_REPLACE', 'Description Replace'), ('INSTRUCTION_REPLACE', 'Instruction Replace'), ('NEVER_UNIT', 'Never Unit'), ('TRANSPOSE_WORDS', 'Transpose Words')], max_length=128), ), migrations.AlterField( model_name='userpreference', diff --git a/cookbook/models.py b/cookbook/models.py index 93d1e7b391..87d5212f55 100644 --- a/cookbook/models.py +++ b/cookbook/models.py @@ -1316,11 +1316,12 @@ class Automation(ExportModelOperationsMixin('automations'), models.Model, Permis DESCRIPTION_REPLACE = 'DESCRIPTION_REPLACE' INSTRUCTION_REPLACE = 'INSTRUCTION_REPLACE' NEVER_UNIT = 'NEVER_UNIT' + TRANSPOSE_WORDS = 'TRANSPOSE_WORDS' type = models.CharField(max_length=128, choices=((FOOD_ALIAS, _('Food Alias')), (UNIT_ALIAS, _('Unit Alias')), (KEYWORD_ALIAS, _('Keyword Alias')), (DESCRIPTION_REPLACE, _('Description Replace')), (INSTRUCTION_REPLACE, _('Instruction Replace')), - (NEVER_UNIT, _('Never Unit')),)) + (NEVER_UNIT, _('Never Unit')), (TRANSPOSE_WORDS, _('Transpose Words')),)) name = models.CharField(max_length=128, default='') description = models.TextField(blank=True, null=True) diff --git a/docs/features/automation.md b/docs/features/automation.md index bd76dec99e..eaef3de019 100644 --- a/docs/features/automation.md +++ b/docs/features/automation.md @@ -68,6 +68,17 @@ You can also create them manually by setting the following These rules are processed whenever you are importing recipes from websites or other apps and when using the simple ingredient input (shopping, recipe editor, ...). +## Transpose Words + +Some recipes list the food before the units for some foods (garlic cloves). This automation will transpose 2 words in an +ingredient so "garlic cloves" will automatically become "cloves garlic" + +- **Parameter 1**: first word to detect +- **Parameter 2**: second word to detect + +These rules are processed whenever you are importing recipes from websites or other apps +and when using the simple ingredient input (shopping, recipe editor, ...). + # Order If the Automation type allows for more than one rule to be executed (for example description replace) diff --git a/vue/src/locales/en.json b/vue/src/locales/en.json index d9b683ede9..9634d600af 100644 --- a/vue/src/locales/en.json +++ b/vue/src/locales/en.json @@ -520,5 +520,6 @@ "plural_usage_info": "Use the plural form for units and food inside this space.", "Create Recipe": "Create Recipe", "Import Recipe": "Import Recipe", - "Never_Unit": "Never Unit" + "Never_Unit": "Never Unit", + "Transpose_Words": "Transpose Words" } diff --git a/vue/src/utils/models.js b/vue/src/utils/models.js index f36ac9951e..a4911ce914 100644 --- a/vue/src/utils/models.js +++ b/vue/src/utils/models.js @@ -558,6 +558,8 @@ export class Models { { value: "KEYWORD_ALIAS", text: "Keyword_Alias" }, { value: "DESCRIPTION_REPLACE", text: "Description_Replace" }, { value: "INSTRUCTION_REPLACE", text: "Instruction_Replace" }, + { value: "NEVER_UNIT", text: "Never_Unit" }, + { value: "TRANSPOSE_WORDS", text: "Transpose_Words" }, ], field: "type", label: "Type", From 4a93681870ee24029cae6ce6708de265720fbaf2 Mon Sep 17 00:00:00 2001 From: smilerz Date: Tue, 25 Apr 2023 12:38:14 -0500 Subject: [PATCH 24/71] filtered automations to tokens present --- cookbook/helper/ingredient_parser.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/cookbook/helper/ingredient_parser.py b/cookbook/helper/ingredient_parser.py index dc0d94f89c..6f6b7af5c1 100644 --- a/cookbook/helper/ingredient_parser.py +++ b/cookbook/helper/ingredient_parser.py @@ -3,6 +3,7 @@ import unicodedata from django.core.cache import caches +from django.db.models import Q from cookbook.models import Automation, Food, Ingredient, Unit @@ -59,7 +60,7 @@ def __init__(self, request, cache_mode, ignore_automations=False): else: i = 0 for a in Automation.objects.filter(space=self.request.space, disabled=False, type=Automation.TRANSPOSE_WORDS).only('param_1', 'param_2').order_by('order').all(): - self.never_unit[i] = [a.param_1, a.param_2] + self.transpose_words[i] = [a.param_1, a.param_2] i += 1 caches['default'].set(TRANSPOSE_WORDS_CACHE_KEY, self.transpose_words, 30) else: @@ -270,22 +271,21 @@ def apply_transpose_words_automations(self, ingredient): return: new ingredient string """ - #################################################### - #################################################### - #################################################### - #################################################### if self.ignore_rules: return ingredient else: + tokens = ingredient.replace(',',' ').split() if self.transpose_words: - for rule in self.transpose_words: - ingredient = re.sub(rf"\b({rule[0]}) ({rule[1]})\b", r"\2 \1", ingredient) - + filtered_rules = {} + for key, value in self.transpose_words.items(): + if value[0] in tokens and value[1] in tokens: + filtered_rules[key] = value + for k, v in filtered_rules.items(): + ingredient = re.sub(rf"\b({v[0]})\W*({v[1]})\b", r"\2 \1", ingredient) else: - for rule in Automation.objects.filter(space=self.request.space, type=Automation.TRANSPOSE_WORDS, disabled=False).order_by('order'): - ingredient = re.sub(rf"\b({rule.param_1}) ({rule.param_2})\b", r"\2 \1", ingredient) - + for rule in Automation.objects.filter(space=self.request.space, type=Automation.TRANSPOSE_WORDS, disabled=False).filter(Q(Q(param_1__in=tokens) | Q(param_2__in=tokens))).order_by('order'): + ingredient = re.sub(rf"\b({v[0]})\W*({v[1]})\b", r"\2 \1", ingredient) return ingredient def parse(self, ingredient): @@ -304,8 +304,6 @@ def parse(self, ingredient): if len(ingredient) == 0: raise ValueError('string to parse cannot be empty') - ingredient = self.apply_transpose_words_automations(ingredient) - # some people/languages put amount and unit at the end of the ingredient string # if something like this is detected move it to the beginning so the parser can handle it if len(ingredient) < 1000 and re.search(r'^([^\W\d_])+(.)*[1-9](\d)*\s*([^\W\d_])+', ingredient): @@ -330,6 +328,8 @@ def parse(self, ingredient): if re.match('([0-9])+([A-z])+\s', ingredient): ingredient = re.sub(r'(?<=([a-z])|\d)(?=(?(1)\d|[a-z]))', ' ', ingredient) + ingredient = self.apply_transpose_words_automations(ingredient) + tokens = ingredient.split() # split at each space into tokens if len(tokens) == 1: # there only is one argument, that must be the food From b3565451ff7615129f3b436828955ec4dd78256f Mon Sep 17 00:00:00 2001 From: smilerz Date: Mon, 1 May 2023 17:00:11 -0500 Subject: [PATCH 25/71] fixed defect in NEVER_UNIT automation --- cookbook/helper/ingredient_parser.py | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/cookbook/helper/ingredient_parser.py b/cookbook/helper/ingredient_parser.py index 6f6b7af5c1..21864c4315 100644 --- a/cookbook/helper/ingredient_parser.py +++ b/cookbook/helper/ingredient_parser.py @@ -158,10 +158,10 @@ def parse_amount(self, x): end = 0 while (end < len(x) and (x[end] in string.digits or ( - (x[end] == '.' or x[end] == ',' or x[end] == '/') - and end + 1 < len(x) - and x[end + 1] in string.digits - ))): + (x[end] == '.' or x[end] == ',' or x[end] == '/') + and end + 1 < len(x) + and x[end + 1] in string.digits + ))): end += 1 if end > 0: if "/" in x[:end]: @@ -185,7 +185,8 @@ def parse_amount(self, x): if unit is not None and unit.strip() == '': unit = None - if unit is not None and (unit.startswith('(') or unit.startswith('-')): # i dont know any unit that starts with ( or - so its likely an alternative like 1L (500ml) Water or 2-3 + if unit is not None and (unit.startswith('(') or unit.startswith( + '-')): # i dont know any unit that starts with ( or - so its likely an alternative like 1L (500ml) Water or 2-3 unit = None note = x return amount, unit, note @@ -254,7 +255,8 @@ def apply_never_unit_automations(self, tokens): return tokens else: - if automation := Automation.objects.filter(space=self.request.space, type=Automation.UNIT_ALIAS, param_1__in=[tokens[1], alt_unit], disabled=False).order_by('order').first(): + if automation := Automation.objects.filter(space=self.request.space, type=Automation.NEVER_UNIT, param_1__in=[ + tokens[1], alt_unit], disabled=False).order_by('order').first(): new_unit = automation.param_2 never_unit = True @@ -275,7 +277,7 @@ def apply_transpose_words_automations(self, ingredient): return ingredient else: - tokens = ingredient.replace(',',' ').split() + tokens = ingredient.replace(',', ' ').split() if self.transpose_words: filtered_rules = {} for key, value in self.transpose_words.items(): @@ -284,7 +286,8 @@ def apply_transpose_words_automations(self, ingredient): for k, v in filtered_rules.items(): ingredient = re.sub(rf"\b({v[0]})\W*({v[1]})\b", r"\2 \1", ingredient) else: - for rule in Automation.objects.filter(space=self.request.space, type=Automation.TRANSPOSE_WORDS, disabled=False).filter(Q(Q(param_1__in=tokens) | Q(param_2__in=tokens))).order_by('order'): + for rule in Automation.objects.filter(space=self.request.space, type=Automation.TRANSPOSE_WORDS, disabled=False).filter( + Q(Q(param_1__in=tokens) | Q(param_2__in=tokens))).order_by('order'): ingredient = re.sub(rf"\b({v[0]})\W*({v[1]})\b", r"\2 \1", ingredient) return ingredient @@ -313,8 +316,8 @@ def parse(self, ingredient): # if the string contains parenthesis early on remove it and place it at the end # because its likely some kind of note - if re.match('(.){1,6}\s\((.[^\(\)])+\)\s', ingredient): - match = re.search('\((.[^\(])+\)', ingredient) + if re.match('(.){1,6}\\s\\((.[^\\(\\)])+\\)\\s', ingredient): + match = re.search('\\((.[^\\(])+\\)', ingredient) ingredient = ingredient[:match.start()] + ingredient[match.end():] + ' ' + ingredient[match.start():match.end()] # leading spaces before commas result in extra tokens, clean them out @@ -322,10 +325,10 @@ def parse(self, ingredient): # handle "(from) - (to)" amounts by using the minimum amount and adding the range to the description # "10.5 - 200 g XYZ" => "100 g XYZ (10.5 - 200)" - ingredient = re.sub("^(\d+|\d+[\\.,]\d+) - (\d+|\d+[\\.,]\d+) (.*)", "\\1 \\3 (\\1 - \\2)", ingredient) + ingredient = re.sub("^(\\d+|\\d+[\\.,]\\d+) - (\\d+|\\d+[\\.,]\\d+) (.*)", "\\1 \\3 (\\1 - \\2)", ingredient) # if amount and unit are connected add space in between - if re.match('([0-9])+([A-z])+\s', ingredient): + if re.match('([0-9])+([A-z])+\\s', ingredient): ingredient = re.sub(r'(?<=([a-z])|\d)(?=(?(1)\d|[a-z]))', ' ', ingredient) ingredient = self.apply_transpose_words_automations(ingredient) From 743fae1ba74a5fa25504fb6f02ee266b6afcf5ab Mon Sep 17 00:00:00 2001 From: smilerz Date: Tue, 2 May 2023 16:50:13 -0500 Subject: [PATCH 26/71] make automation parameters case insensitive on search --- cookbook/helper/ingredient_parser.py | 34 ++++++++------- cookbook/helper/recipe_url_import.py | 64 +++++++++++++++++----------- 2 files changed, 57 insertions(+), 41 deletions(-) diff --git a/cookbook/helper/ingredient_parser.py b/cookbook/helper/ingredient_parser.py index 21864c4315..b5bdbf67ce 100644 --- a/cookbook/helper/ingredient_parser.py +++ b/cookbook/helper/ingredient_parser.py @@ -4,6 +4,7 @@ from django.core.cache import caches from django.db.models import Q +from django.db.models.functions import Lower from cookbook.models import Automation, Food, Ingredient, Unit @@ -32,7 +33,7 @@ def __init__(self, request, cache_mode, ignore_automations=False): caches['default'].touch(FOOD_CACHE_KEY, 30) else: for a in Automation.objects.filter(space=self.request.space, disabled=False, type=Automation.FOOD_ALIAS).only('param_1', 'param_2').order_by('order').all(): - self.food_aliases[a.param_1] = a.param_2 + self.food_aliases[a.param_1.lower()] = a.param_2 caches['default'].set(FOOD_CACHE_KEY, self.food_aliases, 30) UNIT_CACHE_KEY = f'automation_unit_alias_{self.request.space.pk}' @@ -41,7 +42,7 @@ def __init__(self, request, cache_mode, ignore_automations=False): caches['default'].touch(UNIT_CACHE_KEY, 30) else: for a in Automation.objects.filter(space=self.request.space, disabled=False, type=Automation.UNIT_ALIAS).only('param_1', 'param_2').order_by('order').all(): - self.unit_aliases[a.param_1] = a.param_2 + self.unit_aliases[a.param_1.lower()] = a.param_2 caches['default'].set(UNIT_CACHE_KEY, self.unit_aliases, 30) NEVER_UNIT_CACHE_KEY = f'automation_never_unit_{self.request.space.pk}' @@ -50,7 +51,7 @@ def __init__(self, request, cache_mode, ignore_automations=False): caches['default'].touch(NEVER_UNIT_CACHE_KEY, 30) else: for a in Automation.objects.filter(space=self.request.space, disabled=False, type=Automation.NEVER_UNIT).only('param_1', 'param_2').order_by('order').all(): - self.never_unit[a.param_1] = a.param_2 + self.never_unit[a.param_1.lower()] = a.param_2 caches['default'].set(NEVER_UNIT_CACHE_KEY, self.never_unit, 30) TRANSPOSE_WORDS_CACHE_KEY = f'automation_transpose_words_{self.request.space.pk}' @@ -60,7 +61,7 @@ def __init__(self, request, cache_mode, ignore_automations=False): else: i = 0 for a in Automation.objects.filter(space=self.request.space, disabled=False, type=Automation.TRANSPOSE_WORDS).only('param_1', 'param_2').order_by('order').all(): - self.transpose_words[i] = [a.param_1, a.param_2] + self.transpose_words[i] = [a.param_1.lower(), a.param_2.lower()] i += 1 caches['default'].set(TRANSPOSE_WORDS_CACHE_KEY, self.transpose_words, 30) else: @@ -80,11 +81,11 @@ def apply_food_automation(self, food): else: if self.food_aliases: try: - return self.food_aliases[food] + return self.food_aliases[food.lower()] except KeyError: return food else: - if automation := Automation.objects.filter(space=self.request.space, type=Automation.FOOD_ALIAS, param_1=food, disabled=False).order_by('order').first(): + if automation := Automation.objects.filter(space=self.request.space, type=Automation.FOOD_ALIAS, param_1__iexact=food, disabled=False).order_by('order').first(): return automation.param_2 return food @@ -99,11 +100,11 @@ def apply_unit_automation(self, unit): else: if self.transpose_words: try: - return self.unit_aliases[unit] + return self.unit_aliases[unit.lower()] except KeyError: return unit else: - if automation := Automation.objects.filter(space=self.request.space, type=Automation.UNIT_ALIAS, param_1=unit, disabled=False).order_by('order').first(): + if automation := Automation.objects.filter(space=self.request.space, type=Automation.UNIT_ALIAS, param_1__iexact=unit, disabled=False).order_by('order').first(): return automation.param_2 return unit @@ -249,14 +250,14 @@ def apply_never_unit_automations(self, tokens): never_unit = False if self.never_unit: try: - new_unit = self.never_unit[tokens[1]] + new_unit = self.never_unit[tokens[1].lower()] never_unit = True except KeyError: return tokens else: - if automation := Automation.objects.filter(space=self.request.space, type=Automation.NEVER_UNIT, param_1__in=[ - tokens[1], alt_unit], disabled=False).order_by('order').first(): + if automation := Automation.objects.annotate(param_1_lower=Lower('param_1')).filter(space=self.request.space, type=Automation.NEVER_UNIT, param_1_lower__in=[ + tokens[1].lower(), alt_unit.lower()], disabled=False).order_by('order').first(): new_unit = automation.param_2 never_unit = True @@ -277,18 +278,19 @@ def apply_transpose_words_automations(self, ingredient): return ingredient else: - tokens = ingredient.replace(',', ' ').split() + tokens = [x.lower() for x in ingredient.replace(',', ' ').split()] if self.transpose_words: filtered_rules = {} for key, value in self.transpose_words.items(): if value[0] in tokens and value[1] in tokens: filtered_rules[key] = value for k, v in filtered_rules.items(): - ingredient = re.sub(rf"\b({v[0]})\W*({v[1]})\b", r"\2 \1", ingredient) + ingredient = re.sub(rf"\b({v[0]})\W*({v[1]})\b", r"\2 \1", ingredient, flags=re.IGNORECASE) else: - for rule in Automation.objects.filter(space=self.request.space, type=Automation.TRANSPOSE_WORDS, disabled=False).filter( - Q(Q(param_1__in=tokens) | Q(param_2__in=tokens))).order_by('order'): - ingredient = re.sub(rf"\b({v[0]})\W*({v[1]})\b", r"\2 \1", ingredient) + for rule in Automation.objects.filter(space=self.request.space, type=Automation.TRANSPOSE_WORDS, disabled=False) \ + .annotate(param_1_lower=Lower('param_1'), param_2_lower=Lower('param_2')) \ + .filter(Q(Q(param_1_lower__in=tokens) | Q(param_2_lower__in=tokens))).order_by('order'): + ingredient = re.sub(rf"\b({v[0]})\W*({v[1]})\b", r"\2 \1", ingredient, flags=re.IGNORECASE) return ingredient def parse(self, ingredient): diff --git a/cookbook/helper/recipe_url_import.py b/cookbook/helper/recipe_url_import.py index a15df5e8e6..fea830c82c 100644 --- a/cookbook/helper/recipe_url_import.py +++ b/cookbook/helper/recipe_url_import.py @@ -15,7 +15,6 @@ from cookbook.helper.ingredient_parser import IngredientParser from cookbook.models import Automation, Keyword, PropertyType - # from unicodedata import decomposition @@ -51,7 +50,8 @@ def get_from_scraper(scrape, request): recipe_json['internal'] = True try: - servings = scrape.schema.data.get('recipeYield') or 1 # dont use scrape.yields() as this will always return "x servings" or "x items", should be improved in scrapers directly + # dont use scrape.yields() as this will always return "x servings" or "x items", should be improved in scrapers directly + servings = scrape.schema.data.get('recipeYield') or 1 except Exception: servings = 1 @@ -156,7 +156,14 @@ def get_from_scraper(scrape, request): parsed_description = parse_description(description) # TODO notify user about limit if reached # limits exist to limit the attack surface for dos style attacks - automations = Automation.objects.filter(type=Automation.DESCRIPTION_REPLACE, space=request.space, disabled=False).only('param_1', 'param_2', 'param_3').all().order_by('order')[:512] + automations = Automation.objects.filter( + type=Automation.DESCRIPTION_REPLACE, + space=request.space, + disabled=False).only( + 'param_1', + 'param_2', + 'param_3').all().order_by('order')[ + :512] for a in automations: if re.match(a.param_1, (recipe_json['source_url'])[:512]): parsed_description = re.sub(a.param_2, a.param_3, parsed_description, count=1) @@ -206,7 +213,14 @@ def get_from_scraper(scrape, request): pass if 'source_url' in recipe_json and recipe_json['source_url']: - automations = Automation.objects.filter(type=Automation.INSTRUCTION_REPLACE, space=request.space, disabled=False).only('param_1', 'param_2', 'param_3').order_by('order').all()[:512] + automations = Automation.objects.filter( + type=Automation.INSTRUCTION_REPLACE, + space=request.space, + disabled=False).only( + 'param_1', + 'param_2', + 'param_3').order_by('order').all()[ + :512] for a in automations: if re.match(a.param_1, (recipe_json['source_url'])[:512]): for s in recipe_json['steps']: @@ -272,7 +286,7 @@ def get_from_youtube_scraper(url, request): def parse_name(name): - if type(name) == list: + if isinstance(name, list): try: name = name[0] except Exception: @@ -316,16 +330,16 @@ def parse_instructions(instructions): """ instruction_list = [] - if type(instructions) == list: + if isinstance(instructions, list): for i in instructions: - if type(i) == str: + if isinstance(i, str): instruction_list.append(clean_instruction_string(i)) else: if 'text' in i: instruction_list.append(clean_instruction_string(i['text'])) elif 'itemListElement' in i: for ile in i['itemListElement']: - if type(ile) == str: + if isinstance(ile, str): instruction_list.append(clean_instruction_string(ile)) elif 'text' in ile: instruction_list.append(clean_instruction_string(ile['text'])) @@ -341,13 +355,13 @@ def parse_image(image): # check if list of images is returned, take first if so if not image: return None - if type(image) == list: + if isinstance(image, list): for pic in image: - if (type(pic) == str) and (pic[:4] == 'http'): + if (isinstance(pic, str)) and (pic[:4] == 'http'): image = pic elif 'url' in pic: image = pic['url'] - elif type(image) == dict: + elif isinstance(image, dict): if 'url' in image: image = image['url'] @@ -358,12 +372,12 @@ def parse_image(image): def parse_servings(servings): - if type(servings) == str: + if isinstance(servings, str): try: servings = int(re.search(r'\d+', servings).group()) except AttributeError: servings = 1 - elif type(servings) == list: + elif isinstance(servings, list): try: servings = int(re.findall(r'\b\d+\b', servings[0])[0]) except KeyError: @@ -372,12 +386,12 @@ def parse_servings(servings): def parse_servings_text(servings): - if type(servings) == str: + if isinstance(servings, str): try: - servings = re.sub("\d+", '', servings).strip() + servings = re.sub("\\d+", '', servings).strip() except Exception: servings = '' - if type(servings) == list: + if isinstance(servings, list): try: servings = parse_servings_text(servings[1]) except Exception: @@ -394,7 +408,7 @@ def parse_time(recipe_time): recipe_time = round(iso_parse_duration(recipe_time).seconds / 60) except ISO8601Error: try: - if (type(recipe_time) == list and len(recipe_time) > 0): + if (isinstance(recipe_time, list) and len(recipe_time) > 0): recipe_time = recipe_time[0] recipe_time = round(parse_duration(recipe_time).seconds / 60) except AttributeError: @@ -413,7 +427,7 @@ def parse_keywords(keyword_json, space): caches['default'].touch(KEYWORD_CACHE_KEY, 30) else: for a in Automation.objects.filter(space=space, disabled=False, type=Automation.KEYWORD_ALIAS).only('param_1', 'param_2').order_by('order').all(): - keyword_aliases[a.param_1] = a.param_2 + keyword_aliases[a.param_1.lower()] = a.param_2 caches['default'].set(KEYWORD_CACHE_KEY, keyword_aliases, 30) # keywords as list @@ -424,7 +438,7 @@ def parse_keywords(keyword_json, space): if len(kw) != 0: if keyword_aliases: try: - kw = keyword_aliases[kw] + kw = keyword_aliases[kw.lower()] except KeyError: pass if k := Keyword.objects.filter(name=kw, space=space).first(): @@ -438,15 +452,15 @@ def parse_keywords(keyword_json, space): def listify_keywords(keyword_list): # keywords as string try: - if type(keyword_list[0]) == dict: + if isinstance(keyword_list[0], dict): return keyword_list except (KeyError, IndexError): pass - if type(keyword_list) == str: + if isinstance(keyword_list, str): keyword_list = keyword_list.split(',') # keywords as string in list - if (type(keyword_list) == list and len(keyword_list) == 1 and ',' in keyword_list[0]): + if (isinstance(keyword_list, list) and len(keyword_list) == 1 and ',' in keyword_list[0]): keyword_list = keyword_list[0].split(',') return [x.strip() for x in keyword_list] @@ -500,13 +514,13 @@ def get_images_from_soup(soup, url): def clean_dict(input_dict, key): - if type(input_dict) == dict: + if isinstance(input_dict, dict): for x in list(input_dict): if x == key: del input_dict[x] - elif type(input_dict[x]) == dict: + elif isinstance(input_dict[x], dict): input_dict[x] = clean_dict(input_dict[x], key) - elif type(input_dict[x]) == list: + elif isinstance(input_dict[x], list): temp_list = [] for e in input_dict[x]: temp_list.append(clean_dict(e, key)) From 6eac48633bdd5b30add4ad04deba9a1499a27f67 Mon Sep 17 00:00:00 2001 From: smilerz Date: Thu, 10 Aug 2023 08:33:02 -0500 Subject: [PATCH 27/71] fix incorrect variable in apply_transpose_words_automations --- cookbook/helper/ingredient_parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cookbook/helper/ingredient_parser.py b/cookbook/helper/ingredient_parser.py index b5bdbf67ce..57b70f44c3 100644 --- a/cookbook/helper/ingredient_parser.py +++ b/cookbook/helper/ingredient_parser.py @@ -290,7 +290,7 @@ def apply_transpose_words_automations(self, ingredient): for rule in Automation.objects.filter(space=self.request.space, type=Automation.TRANSPOSE_WORDS, disabled=False) \ .annotate(param_1_lower=Lower('param_1'), param_2_lower=Lower('param_2')) \ .filter(Q(Q(param_1_lower__in=tokens) | Q(param_2_lower__in=tokens))).order_by('order'): - ingredient = re.sub(rf"\b({v[0]})\W*({v[1]})\b", r"\2 \1", ingredient, flags=re.IGNORECASE) + ingredient = re.sub(rf"\b({rule.param_1})\W*({rule.param_1})\b", r"\2 \1", ingredient, flags=re.IGNORECASE) return ingredient def parse(self, ingredient): From 8bf661c1ab6d8d4580ae12f997a4f2d885fe970f Mon Sep 17 00:00:00 2001 From: smilerz Date: Thu, 10 Aug 2023 09:06:41 -0500 Subject: [PATCH 28/71] update migrations --- .../0189_alter_automation_type_and_more.py | 23 ------------- .../0197_alter_automation_type_and_more.py | 34 +++++++++++++++++++ 2 files changed, 34 insertions(+), 23 deletions(-) delete mode 100644 cookbook/migrations/0189_alter_automation_type_and_more.py create mode 100644 cookbook/migrations/0197_alter_automation_type_and_more.py diff --git a/cookbook/migrations/0189_alter_automation_type_and_more.py b/cookbook/migrations/0189_alter_automation_type_and_more.py deleted file mode 100644 index bf997ab6d7..0000000000 --- a/cookbook/migrations/0189_alter_automation_type_and_more.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 4.1.7 on 2023-04-24 16:22 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('cookbook', '0188_space_no_sharing_limit'), - ] - - operations = [ - migrations.AlterField( - model_name='automation', - name='type', - field=models.CharField(choices=[('FOOD_ALIAS', 'Food Alias'), ('UNIT_ALIAS', 'Unit Alias'), ('KEYWORD_ALIAS', 'Keyword Alias'), ('DESCRIPTION_REPLACE', 'Description Replace'), ('INSTRUCTION_REPLACE', 'Instruction Replace'), ('NEVER_UNIT', 'Never Unit'), ('TRANSPOSE_WORDS', 'Transpose Words')], max_length=128), - ), - migrations.AlterField( - model_name='userpreference', - name='use_fractions', - field=models.BooleanField(default=True), - ), - ] diff --git a/cookbook/migrations/0197_alter_automation_type_and_more.py b/cookbook/migrations/0197_alter_automation_type_and_more.py new file mode 100644 index 0000000000..f16070058f --- /dev/null +++ b/cookbook/migrations/0197_alter_automation_type_and_more.py @@ -0,0 +1,34 @@ +# Generated by Django 4.1.10 on 2023-08-10 14:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('cookbook', '0196_food_url'), + ] + + operations = [ + migrations.AlterField( + model_name='automation', + name='type', + field=models.CharField( + choices=[ + ('FOOD_ALIAS', + 'Food Alias'), + ('UNIT_ALIAS', + 'Unit Alias'), + ('KEYWORD_ALIAS', + 'Keyword Alias'), + ('DESCRIPTION_REPLACE', + 'Description Replace'), + ('INSTRUCTION_REPLACE', + 'Instruction Replace'), + ('NEVER_UNIT', + 'Never Unit'), + ('TRANSPOSE_WORDS', + 'Transpose Words')], + max_length=128), + ), + ] From 3d7e2b1aa5fa4562fb5a1e7a3fe3a85fbb468e71 Mon Sep 17 00:00:00 2001 From: Jochum van der Heide Date: Mon, 14 Aug 2023 18:58:51 +0000 Subject: [PATCH 29/71] Translated using Weblate (Dutch) Currently translated at 100.0% (490 of 490 strings) Translation: Tandoor/Recipes Backend Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/nl/ --- cookbook/locale/nl/LC_MESSAGES/django.po | 28 ++++++++++-------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/cookbook/locale/nl/LC_MESSAGES/django.po b/cookbook/locale/nl/LC_MESSAGES/django.po index 15090b7949..0cb0f1227a 100644 --- a/cookbook/locale/nl/LC_MESSAGES/django.po +++ b/cookbook/locale/nl/LC_MESSAGES/django.po @@ -13,10 +13,10 @@ msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2023-05-18 14:28+0200\n" -"PO-Revision-Date: 2023-02-27 13:55+0000\n" -"Last-Translator: Jesse \n" -"Language-Team: Dutch \n" +"PO-Revision-Date: 2023-08-15 19:19+0000\n" +"Last-Translator: Jochum van der Heide \n" +"Language-Team: Dutch \n" "Language: nl\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -522,34 +522,32 @@ msgid "One of queryset or hash_key must be provided" msgstr "Er moet een queryset of hash_key opgegeven worden" #: .\cookbook\helper\recipe_url_import.py:266 -#, fuzzy -#| msgid "Use fractions" msgid "reverse rotation" -msgstr "Gebruik fracties" +msgstr "omgekeerde rotatie" #: .\cookbook\helper\recipe_url_import.py:267 msgid "careful rotation" -msgstr "" +msgstr "voorzichtige rotatie" #: .\cookbook\helper\recipe_url_import.py:268 msgid "knead" -msgstr "" +msgstr "kneden" #: .\cookbook\helper\recipe_url_import.py:269 msgid "thicken" -msgstr "" +msgstr "verdikken" #: .\cookbook\helper\recipe_url_import.py:270 msgid "warm up" -msgstr "" +msgstr "opwarmen" #: .\cookbook\helper\recipe_url_import.py:271 msgid "ferment" -msgstr "" +msgstr "gisten" #: .\cookbook\helper\recipe_url_import.py:272 msgid "sous-vide" -msgstr "" +msgstr "sous-vide" #: .\cookbook\helper\shopping_helper.py:157 msgid "You must supply a servings size" @@ -594,10 +592,8 @@ msgid "Imported %s recipes." msgstr "%s recepten geïmporteerd." #: .\cookbook\integration\openeats.py:26 -#, fuzzy -#| msgid "Recipe Home" msgid "Recipe source:" -msgstr "Recept thuis" +msgstr "Bron van het recept:" #: .\cookbook\integration\paprika.py:49 msgid "Notes" From c18a77bc9b3c0a13e3844784f0aeae957aec4042 Mon Sep 17 00:00:00 2001 From: Jochum van der Heide Date: Mon, 14 Aug 2023 19:11:02 +0000 Subject: [PATCH 30/71] Translated using Weblate (Dutch) Currently translated at 99.8% (519 of 520 strings) Translation: Tandoor/Recipes Frontend Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/nl/ --- vue/src/locales/nl.json | 45 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/vue/src/locales/nl.json b/vue/src/locales/nl.json index e0c0ab7771..d1a54a7fe4 100644 --- a/vue/src/locales/nl.json +++ b/vue/src/locales/nl.json @@ -465,7 +465,7 @@ "warning_space_delete": "Je kunt jouw space verwijderen inclusief alle recepten, boodschappenlijstjes, maaltijdplannen en alles wat je verder aangemaakt hebt. Dit kan niet ongedaan worden gemaakt! Weet je het zeker?", "food_inherit_info": "Voedselvelden die standaard geërfd worden.", "facet_count_info": "Geef receptenaantal bij zoekfilters weer.", - "Split_All_Steps": "Splits alle rijen in apparte stappen.", + "Split_All_Steps": "Splits alle rijen in aparte stappen.", "Combine_All_Steps": "Voeg alle stappen samen tot een veld.", "Plural": "Meervoud", "plural_short": "meervoud", @@ -480,5 +480,46 @@ "Description_Replace": "Vervang beschrijving", "Instruction_Replace": "Vervang instructie", "Auto_Sort_Help": "Verplaats alle ingrediënten naar de best passende stap.", - "Auto_Sort": "Automatisch sorteren" + "Auto_Sort": "Automatisch sorteren", + "Create Recipe": "Recept Maken", + "Import Recipe": "Recept Importeren", + "recipe_property_info": "Je kunt ook eigenschappen aan voedingsmiddelen toevoegen om ze automatisch te berekenen op basis van je recept!", + "per_serving": "per portie", + "Open_Data_Slug": "Open Data Slug", + "Open_Data_Import": "Open Data Import", + "Update_Existing_Data": "Bestaande gegevens bijwerken", + "Use_Metric": "Metrische eenheden gebruiken", + "Learn_More": "Meer informatie", + "converted_unit": "Aangepaste eenheid", + "converted_amount": "Aangepast Bedrag", + "Datatype": "Datatype", + "Number of Objects": "Aantal Objecten", + "open_data_help_text": "Het Tandoor Open Data-project biedt door de community bijgedragen gegevens voor Tandoor. Dit veld wordt automatisch gevuld bij het importeren en maakt updates in de toekomst mogelijk.", + "Data_Import_Info": "Verbeter je Space door een door de community samengestelde lijst van voedingsmiddelen, eenheden en meer te importeren om je receptenverzameling te verbeteren.", + "base_unit": "Basis Unit", + "base_amount": "Basisbedrag", + "Welcome": "Welkom", + "quart": "quart [qt] (VS, volume)", + "imperial_fluid_ounce": "imperial fluid ounce [imp fl oz] (Verenigd Koninkrijk, volume)", + "imperial_tbsp": "imperial theelepel [imp tbsp] (Verenigd Koningrijk, volume)", + "Property": "Eigenschap", + "Conversion": "Omrekening", + "Properties": "Eigenschappen", + "total": "totaal", + "g": "gram [g] (metrisch, gewicht)", + "kg": "kilogram [kg] (metrisch, gewicht)", + "ounce": "ons [oz] (gewicht)", + "pound": "pond (gewicht)", + "ml": "milliliter [ml] (metrisch, volume)", + "l": "liter [l] (metrisch, volume)", + "pint": "pint [pt] (VS, volume)", + "gallon": "gallon [gal] (VS, volume)", + "tbsp": "tablespoon [tbsp] (VS, volume)", + "tsp": "theelepel [tsp] (VS, volume)", + "imperial_pint": "imperial pint [imp pt] (Verenigd Koninkrijk, volume)", + "imperial_quart": "imperial quart [imp qt] (Verenigd Koninkrijk, volume)", + "imperial_gallon": "imperial gal [imp gal] (Verenigd Koninkrijk, volume)", + "imperial_tsp": "imperial thelepel [imp tsp] (UK, volume)", + "Choose_Category": "Kies Categorie", + "Back": "Terug" } From ffc96890acf48b02ebe0cd7295b557b341b72cad Mon Sep 17 00:00:00 2001 From: vabene1111 Date: Wed, 16 Aug 2023 06:18:02 +0200 Subject: [PATCH 31/71] Delete recipes.iml --- .idea/recipes.iml | 35 ----------------------------------- 1 file changed, 35 deletions(-) delete mode 100644 .idea/recipes.iml diff --git a/.idea/recipes.iml b/.idea/recipes.iml deleted file mode 100644 index 1b96c9d802..0000000000 --- a/.idea/recipes.iml +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file From 716976453a5a90884b3c617e1ea9965458530a3e Mon Sep 17 00:00:00 2001 From: vabene1111 Date: Wed, 16 Aug 2023 06:20:43 +0200 Subject: [PATCH 32/71] fixed pycharm file --- .idea/recipes.iml | 35 +++++++++++++++++++++++++++++++++++ .idea/vcs.xml | 6 ------ 2 files changed, 35 insertions(+), 6 deletions(-) create mode 100644 .idea/recipes.iml delete mode 100644 .idea/vcs.xml diff --git a/.idea/recipes.iml b/.idea/recipes.iml new file mode 100644 index 0000000000..8a0e59c8e7 --- /dev/null +++ b/.idea/recipes.iml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 94a25f7f4c..0000000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file From 8ff5142149879c4c12df436d75c3a3e3e9bbad9d Mon Sep 17 00:00:00 2001 From: vabene1111 Date: Wed, 16 Aug 2023 07:10:24 +0200 Subject: [PATCH 33/71] auto meal plan tweaks and improvements --- .idea/vcs.xml | 6 + cookbook/views/api.py | 13 +- vue/src/apps/MealPlanView/MealPlanView.vue | 70 ++---- vue/src/components/AutoMealPlanModal.vue | 274 ++++++++++++--------- 4 files changed, 184 insertions(+), 179 deletions(-) create mode 100644 .idea/vcs.xml diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000000..35eb1ddfbb --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/cookbook/views/api.py b/cookbook/views/api.py index 96e0ce2d6e..4840972145 100644 --- a/cookbook/views/api.py +++ b/cookbook/views/api.py @@ -678,7 +678,6 @@ def create(self, request): keywords = serializer.validated_data['keywords'] start_date = serializer.validated_data['start_date'] end_date = serializer.validated_data['end_date'] - meal_type = MealType.objects.get(pk=serializer.validated_data['meal_type_id']) servings = serializer.validated_data['servings'] shared = serializer.get_initial().get('shared', None) shared_pks = list() @@ -686,8 +685,9 @@ def create(self, request): for i in range(len(shared)): shared_pks.append(shared[i]['id']) - days = (end_date - start_date).days + 1 - recipes = Recipe.objects.all() + days = min((end_date - start_date).days + 1, 14) + + recipes = Recipe.objects.values('id', 'name') meal_plans = list() for keyword in keywords: @@ -695,15 +695,14 @@ def create(self, request): if len(recipes) == 0: return Response(serializer.data) - recipes = recipes.order_by('?')[:days] - recipes = list(recipes) + recipes = list(recipes.order_by('?')[:days]) for i in range(0, days): day = start_date + datetime.timedelta(i) recipe = recipes[i % len(recipes)] - args = {'recipe': recipe, 'servings': servings, 'title': recipe.name, + args = {'recipe_id': recipe['id'], 'servings': servings, 'created_by': request.user, - 'meal_type': meal_type, + 'meal_type_id': serializer.validated_data['meal_type_id'], 'note': '', 'date': day, 'space': request.space} m = MealPlan(**args) diff --git a/vue/src/apps/MealPlanView/MealPlanView.vue b/vue/src/apps/MealPlanView/MealPlanView.vue index 2763ac0d61..39342410ce 100644 --- a/vue/src/apps/MealPlanView/MealPlanView.vue +++ b/vue/src/apps/MealPlanView/MealPlanView.vue @@ -83,7 +83,7 @@
- +
- +
@@ -293,12 +292,6 @@ - - {{ $t("Export_To_ICal") }} @@ -307,7 +300,7 @@ @@ -351,7 +344,7 @@ let SETTINGS_COOKIE_NAME = "mealplan_settings" export default { name: "MealPlanView", components: { - AutoMealPlanModal, + AutoMealPlanModal, MealPlanEditModal, MealPlanCard, CalendarView, @@ -365,16 +358,16 @@ export default { mixins: [CalendarMathMixin, ApiMixin, ResolveUrlMixin], data: function () { return { - AutoPlan: { - meal_types: [], - keywords: [[]], - servings: 1, - date: Date.now(), - startDay: null, - endDay: null, - shared: [], - addshopping: false - }, + AutoPlan: { + meal_types: [], + keywords: [[]], + servings: 1, + date: Date.now(), + startDay: null, + endDay: null, + shared: [], + addshopping: false + }, showDate: new Date(), plan_entries: [], recipe_viewed: {}, @@ -688,36 +681,7 @@ export default { createAutoPlan() { this.$bvModal.show(`autoplan-modal`) }, - async autoPlanThread(autoPlan, mealTypeIndex) { - let apiClient = new ApiApiFactory() - let data = { - "start_date" : moment(autoPlan.startDay).format("YYYY-MM-DD"), - "end_date" : moment(autoPlan.endDay).format("YYYY-MM-DD"), - "meal_type_id" : autoPlan.meal_types[mealTypeIndex].id, - "keywords" : autoPlan.keywords[mealTypeIndex], - "servings" : autoPlan.servings, - "shared" : autoPlan.shared, - "addshopping": autoPlan.addshopping - } - await apiClient.createAutoPlanViewSet(data) - }, - async doAutoPlan(autoPlan) { - for (let i = 0; i < autoPlan.meal_types.length; i++) { - if (autoPlan.keywords[i].length === 0) continue - await this.autoPlanThread(autoPlan, i) - } - this.refreshEntries() - }, - refreshEntries(){//todo Remove method - let date = this.current_period - useMealPlanStore().refreshFromAPI(moment(date.periodStart).format("YYYY-MM-DD"), moment(date.periodEnd).format("YYYY-MM-DD")) - }, - deleteAll(){//todo Remove method, only used in debugging - for (let i = 0; i < useMealPlanStore().plan_list.length; i++) { - useMealPlanStore().deleteObject(useMealPlanStore().plan_list[i]) - } - } }, directives: { hover: { diff --git a/vue/src/components/AutoMealPlanModal.vue b/vue/src/components/AutoMealPlanModal.vue index a2997c5611..5428fdfe93 100644 --- a/vue/src/components/AutoMealPlanModal.vue +++ b/vue/src/components/AutoMealPlanModal.vue @@ -1,79 +1,82 @@ @@ -84,9 +87,11 @@ import GenericMultiselect from "@/components/GenericMultiselect" import {ApiMixin} from "@/utils/utils" import {useUserPreferenceStore} from "@/stores/UserPreferenceStore"; import VueCookies from "vue-cookies"; +import moment from "moment/moment"; +import {useMealPlanStore} from "@/stores/MealPlanStore"; -const { ApiApiFactory } = require("@/utils/openapi/api") -const { StandardToasts } = require("@/utils/utils") +const {ApiApiFactory} = require("@/utils/openapi/api") +const {StandardToasts} = require("@/utils/utils") Vue.use(BootstrapVue) Vue.use(VueCookies) @@ -94,8 +99,8 @@ let MEALPLAN_COOKIE_NAME = "mealplan_settings" export default { name: "AutoMealPlanModal", - components: { - GenericMultiselect + components: { + GenericMultiselect }, props: { modal_title: String, @@ -108,22 +113,23 @@ export default { mixins: [ApiMixin], data() { return { - AutoPlan: { - meal_types: [], - keywords: [[]], - servings: 1, - date: Date.now(), - startDay: null, - endDay: null, - shared: [], - addshopping: false - }, - mealplan_settings: { + AutoPlan: { + meal_types: [], + keywords: [[]], + servings: 1, + date: Date.now(), + startDay: null, + endDay: null, + shared: [], + addshopping: false + }, + mealplan_settings: { addshopping: false, - } + }, + loading: false, } }, - watch: { + watch: { mealplan_settings: { handler(newVal) { this.$cookies.set(MEALPLAN_COOKIE_NAME, this.mealplan_settings) @@ -141,22 +147,25 @@ export default { }, methods: { genericSelectChanged: function (obj) { - this.AutoPlan.keywords[obj.var] = obj.val + this.AutoPlan.keywords[obj.var] = obj.val }, showModal() { - if (this.$cookies.isKey(MEALPLAN_COOKIE_NAME)) { + if (this.$cookies.isKey(MEALPLAN_COOKIE_NAME)) { this.mealplan_settings = Object.assign({}, this.mealplan_settings, this.$cookies.get(MEALPLAN_COOKIE_NAME)) - } - this.refreshMealTypes() - - this.AutoPlan.servings = 1 - this.AutoPlan.startDay = new Date() - this.AutoPlan.endDay = this.current_period.periodEnd - useUserPreferenceStore().getData().then(userPreference => { - this.AutoPlan.shared = userPreference.plan_share + } + this.refreshMealTypes() + + this.AutoPlan.servings = 1 + this.AutoPlan.startDay = new Date() + this.AutoPlan.endDay = this.current_period.periodEnd + useUserPreferenceStore().getData().then(userPreference => { + this.AutoPlan.shared = userPreference.plan_share }) + this.AutoPlan.addshopping = this.mealplan_settings.addshopping + + this.loading = false }, - sortMealTypes() { + sortMealTypes() { this.meal_types.forEach(function (element, index) { element.order = index }) @@ -178,7 +187,7 @@ export default { }) }) }, - refreshMealTypes() { + refreshMealTypes() { let apiClient = new ApiApiFactory() Promise.resolve(apiClient.listMealTypes().then((result) => { @@ -186,30 +195,57 @@ export default { meal_type.editing = false }) this.AutoPlan.meal_types = result.data - })).then( () => { - let mealArray = this.AutoPlan.meal_types - for (let i = 0; i < mealArray.length; i++) { - this.AutoPlan.keywords[i] = []; - }} + })).then(() => { + let mealArray = this.AutoPlan.meal_types + for (let i = 0; i < mealArray.length; i++) { + this.AutoPlan.keywords[i] = []; + } + } ) }, - createPlan() { - this.$bvModal.hide(`autoplan-modal`) - this.AutoPlan.addshopping = this.mealplan_settings.addshopping - this.$emit("create-plan", this.AutoPlan) + createPlan() { + if (!this.loading) { + this.loading = true + + let requests = [] + for (let i = 0; i < this.AutoPlan.meal_types.length; i++) { + if (this.AutoPlan.keywords[i].length === 0) continue + requests.push(this.autoPlanThread(this.AutoPlan, i)) + } + + Promise.allSettled(requests).then(r => { + this.refreshEntries() + this.loading = false + this.$bvModal.hide(`autoplan-modal`) + }).catch(err => { + StandardToasts.makeStandardToast(this, StandardToasts.FAIL_CREATE, err) + this.loading = false + }) + } + + }, + + async autoPlanThread(autoPlan, mealTypeIndex) { + let apiClient = new ApiApiFactory() + let data = { + "start_date": moment(autoPlan.startDay).format("YYYY-MM-DD"), + "end_date": moment(autoPlan.endDay).format("YYYY-MM-DD"), + "meal_type_id": autoPlan.meal_types[mealTypeIndex].id, + "keywords": autoPlan.keywords[mealTypeIndex], + "servings": autoPlan.servings, + "shared": autoPlan.shared, + "addshopping": autoPlan.addshopping + } + return apiClient.createAutoPlanViewSet(data) + }, - updateStartDay(date){ - this.AutoPlan.startDay = date - }, - updateEndDay(date){ - this.AutoPlan.endDay = date - }, - updateServings(numberOfServings) { - this.AutoPlan.servings = numberOfServings - }, - exitPlan() { - this.$bvModal.hide(`autoplan-modal`) - } + refreshEntries() { //TODO move properly to MealPLanStore (save period for default refresh) + let date = this.current_period + useMealPlanStore().refreshFromAPI(moment(date.periodStart).format("YYYY-MM-DD"), moment(date.periodEnd).format("YYYY-MM-DD")) + }, + exitPlan() { + this.$bvModal.hide(`autoplan-modal`) + } }, From 1ecb57e795407ac2d7bb52006be99a00581d0ae8 Mon Sep 17 00:00:00 2001 From: vabene1111 Date: Wed, 16 Aug 2023 07:22:09 +0200 Subject: [PATCH 34/71] removed dependency and upgraded bleach clean --- cookbook/helper/template_helper.py | 21 ++++++++++++++++----- cookbook/templatetags/custom_tags.py | 20 ++++++++++++++++---- requirements.txt | 1 - 3 files changed, 32 insertions(+), 10 deletions(-) diff --git a/cookbook/helper/template_helper.py b/cookbook/helper/template_helper.py index 9bde2fc064..016779a77a 100644 --- a/cookbook/helper/template_helper.py +++ b/cookbook/helper/template_helper.py @@ -2,7 +2,6 @@ import bleach import markdown as md -from bleach_allowlist import markdown_attrs, markdown_tags from jinja2 import Template, TemplateSyntaxError, UndefinedError from markdown.extensions.tables import TableExtension @@ -53,9 +52,17 @@ def __str__(self): def render_instructions(step): # TODO deduplicate markdown cleanup code instructions = step.instruction - tags = markdown_tags + [ - 'pre', 'table', 'td', 'tr', 'th', 'tbody', 'style', 'thead', 'img' - ] + tags = { + "h1", "h2", "h3", "h4", "h5", "h6", + "b", "i", "strong", "em", "tt", + "p", "br", + "span", "div", "blockquote", "code", "pre", "hr", + "ul", "ol", "li", "dd", "dt", + "img", + "a", + "sub", "sup", + 'pre', 'table', 'td', 'tr', 'th', 'tbody', 'style', 'thead' + } parsed_md = md.markdown( instructions, extensions=[ @@ -63,7 +70,11 @@ def render_instructions(step): # TODO deduplicate markdown cleanup code UrlizeExtension(), MarkdownFormatExtension() ] ) - markdown_attrs['*'] = markdown_attrs['*'] + ['class', 'width', 'height'] + markdown_attrs = { + "*": ["id", "class", 'width', 'height'], + "img": ["src", "alt", "title"], + "a": ["href", "alt", "title"], + } instructions = bleach.clean(parsed_md, tags, markdown_attrs) diff --git a/cookbook/templatetags/custom_tags.py b/cookbook/templatetags/custom_tags.py index 7668e1234f..be8be581e1 100644 --- a/cookbook/templatetags/custom_tags.py +++ b/cookbook/templatetags/custom_tags.py @@ -5,7 +5,6 @@ import markdown as md from django_scopes import ScopeError from markdown.extensions.tables import TableExtension -from bleach_allowlist import markdown_attrs, markdown_tags from django import template from django.db.models import Avg from django.templatetags.static import static @@ -46,9 +45,17 @@ def delete_url(model, pk): @register.filter() def markdown(value): - tags = markdown_tags + [ + tags = { + "h1", "h2", "h3", "h4", "h5", "h6", + "b", "i", "strong", "em", "tt", + "p", "br", + "span", "div", "blockquote", "code", "pre", "hr", + "ul", "ol", "li", "dd", "dt", + "img", + "a", + "sub", "sup", 'pre', 'table', 'td', 'tr', 'th', 'tbody', 'style', 'thead' - ] + } parsed_md = md.markdown( value, extensions=[ @@ -56,7 +63,12 @@ def markdown(value): UrlizeExtension(), MarkdownFormatExtension() ] ) - markdown_attrs['*'] = markdown_attrs['*'] + ['class'] + markdown_attrs = { + "*": ["id", "class"], + "img": ["src", "alt", "title"], + "a": ["href", "alt", "title"], + } + parsed_md = parsed_md[3:] # remove outer paragraph parsed_md = parsed_md[:len(parsed_md)-4] return bleach.clean(parsed_md, tags, markdown_attrs) diff --git a/requirements.txt b/requirements.txt index b246e8d13b..e5ff47d1b0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,6 @@ drf-writable-nested==0.7.0 django-oauth-toolkit==2.2.0 django-debug-toolbar==3.8.1 bleach==6.0.0 -bleach-allowlist==1.0.3 gunicorn==20.1.0 lxml==4.9.3 Markdown==3.4.3 From f5fb4e563d7fd1759272e455a81bf932ef361c5b Mon Sep 17 00:00:00 2001 From: Henning Bopp Date: Wed, 16 Aug 2023 21:19:38 +0200 Subject: [PATCH 35/71] Changed var-name in env, info in docs and processing in settings Also added a deprecation warning and changed the structure of the authentication.md Signed-off-by: Henning Bopp --- .env.template | 8 +++++--- docs/features/authentication.md | 18 ++++++++++-------- recipes/settings.py | 8 ++++++-- 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/.env.template b/.env.template index a54e0fe0ae..5c4370fe1c 100644 --- a/.env.template +++ b/.env.template @@ -100,10 +100,12 @@ GUNICORN_MEDIA=0 # prefix used for account related emails (default "[Tandoor Recipes] ") # ACCOUNT_EMAIL_SUBJECT_PREFIX= -# allow authentication via reverse proxy (e.g. authelia), leave off if you dont know what you are doing -# see docs for more information https://docs.tandoor.dev/features/authentication/ +# allow authentication via the REMOTE-USER header (can be used for e.g. authelia). +# ATTENTION: Leave off if you don't know what you are doing! Enabling this without proper configuration will enable anybody +# to login with any username! +# See docs for additional information: https://docs.tandoor.dev/features/authentication/#reverse-proxy-authentication # when unset: 0 (false) -REVERSE_PROXY_AUTH=0 +REMOTE_USER_AUTH=0 # Default settings for spaces, apply per space and can be changed in the admin view # SPACE_DEFAULT_MAX_RECIPES=0 # 0=unlimited recipes diff --git a/docs/features/authentication.md b/docs/features/authentication.md index f218d66274..e0f8f2f019 100644 --- a/docs/features/authentication.md +++ b/docs/features/authentication.md @@ -100,15 +100,17 @@ AUTH_LDAP_START_TLS=1 AUTH_LDAP_TLS_CACERTFILE=/etc/ssl/certs/own-ca.pem ``` -## Reverse Proxy Authentication +## External Authentication + +!!! warning "Security Impact" + If you just set `REMOTE_USER_AUTH=1` without any additional configuration, _anybody_ can authenticate with _any_ username! !!! Info "Community Contributed Tutorial" - This tutorial was provided by a community member. Since I do not use reverse proxy authentication, I cannot provide any - assistance should you choose to use this authentication method. + This tutorial was provided by a community member. We are not able to provide any support! Please only use, if you know what you are doing! -In order use proxy authentication you will need to: +In order use external authentication (i.e. using a proxy auth like Authelia, Authentik, etc.) you will need to: -1. Set `REVERSE_PROXY_AUTH=1` in the `.env` file +1. Set `REMOTE_USER_AUTH=1` in the `.env` file 2. Update your nginx configuration file Using any of the examples above will automatically generate a configuration file inside a docker volume. @@ -116,10 +118,10 @@ Use `docker volume inspect recipes_nginx` to find out where your volume is store !!! warning "Configuration File Volume" The nginx config volume is generated when the container is first run. You can change the volume to a bind mount in the - warning `docker-compose.yml`, but then you will need to manually create it. See section `Volumes vs Bind Mounts` below + `docker-compose.yml`, but then you will need to manually create it. See section `Volumes vs Bind Mounts` below for more information. -The following example shows a configuration for Authelia: +### Configuration Example for Authelia ``` server { @@ -161,7 +163,7 @@ server { } ``` -Please refer to the appropriate documentation on how to setup the reverse proxy, authentication, and networks. +Please refer to the appropriate documentation on how to set up the reverse proxy, authentication, and networks. Ensure users have been configured for Authelia, and that the endpoint recipes is pointed to is protected but available. diff --git a/recipes/settings.py b/recipes/settings.py index f7a4418e03..9a36bfc19d 100644 --- a/recipes/settings.py +++ b/recipes/settings.py @@ -46,7 +46,11 @@ # allow djangos wsgi server to server mediafiles GUNICORN_MEDIA = bool(int(os.getenv('GUNICORN_MEDIA', True))) -REVERSE_PROXY_AUTH = bool(int(os.getenv('REVERSE_PROXY_AUTH', False))) +if os.getenv('REVERSE_PROXY_AUTH') is not None: + print('DEPRECATION WARNING: Environment var "REVERSE_PROXY_AUTH" is deprecated. Please use "REMOTE_USER_AUTH".') + REMOTE_USER_AUTH = bool(int(os.getenv('REVERSE_PROXY_AUTH', False))) +else: + REMOTE_USER_AUTH = bool(int(os.getenv('REMOTE_USER_AUTH', False))) # default value for user preference 'comment' COMMENT_PREF_DEFAULT = bool(int(os.getenv('COMMENT_PREF_DEFAULT', True))) @@ -273,7 +277,7 @@ ACCOUNT_ADAPTER = 'cookbook.helper.AllAuthCustomAdapter' -if REVERSE_PROXY_AUTH: +if REMOTE_USER_AUTH: MIDDLEWARE.insert(8, 'recipes.middleware.CustomRemoteUser') AUTHENTICATION_BACKENDS.append( 'django.contrib.auth.backends.RemoteUserBackend') From 75612781daa0820468c88b1cd8bc2cc5d9fbe07d Mon Sep 17 00:00:00 2001 From: Alexandre Braure Date: Tue, 15 Aug 2023 20:52:34 +0000 Subject: [PATCH 36/71] Translated using Weblate (French) Currently translated at 90.6% (444 of 490 strings) Translation: Tandoor/Recipes Backend Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/fr/ --- cookbook/locale/fr/LC_MESSAGES/django.po | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/cookbook/locale/fr/LC_MESSAGES/django.po b/cookbook/locale/fr/LC_MESSAGES/django.po index ba6f4a978b..395410671e 100644 --- a/cookbook/locale/fr/LC_MESSAGES/django.po +++ b/cookbook/locale/fr/LC_MESSAGES/django.po @@ -14,10 +14,10 @@ msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2023-05-18 14:28+0200\n" -"PO-Revision-Date: 2023-04-12 11:55+0000\n" -"Last-Translator: noxonad \n" -"Language-Team: French \n" +"PO-Revision-Date: 2023-08-16 21:19+0000\n" +"Last-Translator: Alexandre Braure \n" +"Language-Team: French \n" "Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" @@ -549,7 +549,7 @@ msgstr "Il est nécessaire de fournir soit le queryset, soit la clé de hachage" #, fuzzy #| msgid "Use fractions" msgid "reverse rotation" -msgstr "Utiliser les fractions" +msgstr "sens inverse" #: .\cookbook\helper\recipe_url_import.py:267 msgid "careful rotation" @@ -620,10 +620,8 @@ msgid "Imported %s recipes." msgstr "%s recettes importées." #: .\cookbook\integration\openeats.py:26 -#, fuzzy -#| msgid "Recipe Home" msgid "Recipe source:" -msgstr "Page d’accueil" +msgstr "Source de la recette :" #: .\cookbook\integration\paprika.py:49 msgid "Notes" From 7e9cef607504f3ec149a42543b3098a80d4b50d1 Mon Sep 17 00:00:00 2001 From: Bastian Date: Tue, 15 Aug 2023 20:33:15 +0000 Subject: [PATCH 37/71] Translated using Weblate (German) Currently translated at 98.0% (510 of 520 strings) Translation: Tandoor/Recipes Frontend Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/de/ --- vue/src/locales/de.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/vue/src/locales/de.json b/vue/src/locales/de.json index 338787dfeb..4e10562b7d 100644 --- a/vue/src/locales/de.json +++ b/vue/src/locales/de.json @@ -507,5 +507,9 @@ "g": "Gramm [g] (metrisch, Gewicht)", "kg": "Kilogramm [kg] (metrisch, Gewicht)", "ounce": "Unze [oz] (Gewicht)", - "pound": "Pfund (Gewicht)" + "pound": "Pfund (Gewicht)", + "ml": "Milliliter", + "l": "Liter", + "tbsp": "Esslöffel", + "tsp": "Teelöffel" } From db4f2db236e959df68fbb8133bd31d1d4bd7872f Mon Sep 17 00:00:00 2001 From: Alexandre Braure Date: Tue, 15 Aug 2023 20:41:09 +0000 Subject: [PATCH 38/71] Translated using Weblate (French) Currently translated at 88.6% (461 of 520 strings) Translation: Tandoor/Recipes Frontend Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/fr/ --- vue/src/locales/fr.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/vue/src/locales/fr.json b/vue/src/locales/fr.json index 1459378801..8d6cc6485b 100644 --- a/vue/src/locales/fr.json +++ b/vue/src/locales/fr.json @@ -460,5 +460,8 @@ "Unpin": "Détacher", "Split_All_Steps": "Diviser toutes les lignes en étapes séparées.", "Warning_Delete_Supermarket_Category": "Supprimer une catégorie de supermarché supprimera également toutes les relations avec les aliments. Êtes-vous sûr ?", - "Instruction_Replace": "Instruction Remplacer" + "Instruction_Replace": "Instruction Remplacer", + "recipe_property_info": "Vous pouvez également ajouter des propriétés aux aliments pour les calculer automatiquement en fonction de votre recette !", + "per_serving": "par portions", + "open_data_help_text": "Le projet «Tandoor Open Data» est une base de données fournit par la communauté. Ce champ est rempli automatiquement lors de l'importation des données et permet les mises à jour dans le futur." } From 8580aea43f2da905ba60ac04093e02309accf94c Mon Sep 17 00:00:00 2001 From: NeoID Date: Sat, 19 Aug 2023 08:42:45 +0000 Subject: [PATCH 39/71] =?UTF-8?q?Translated=20using=20Weblate=20(Norwegian?= =?UTF-8?q?=20Bokm=C3=A5l)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 71.4% (265 of 371 strings) Translation: Tandoor/Recipes Backend Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/nb_NO/ --- cookbook/locale/nb_NO/LC_MESSAGES/django.po | 143 ++++++++++++-------- 1 file changed, 85 insertions(+), 58 deletions(-) diff --git a/cookbook/locale/nb_NO/LC_MESSAGES/django.po b/cookbook/locale/nb_NO/LC_MESSAGES/django.po index af5ff98070..efd5c85a4c 100644 --- a/cookbook/locale/nb_NO/LC_MESSAGES/django.po +++ b/cookbook/locale/nb_NO/LC_MESSAGES/django.po @@ -8,8 +8,8 @@ msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2021-04-11 15:09+0200\n" -"PO-Revision-Date: 2023-04-17 20:55+0000\n" -"Last-Translator: Espen Sellevåg \n" +"PO-Revision-Date: 2023-08-19 21:36+0000\n" +"Last-Translator: NeoID \n" "Language-Team: Norwegian Bokmål \n" "Language: nb_NO\n" @@ -31,6 +31,8 @@ msgid "" "Color of the top navigation bar. Not all colors work with all themes, just " "try them out!" msgstr "" +"Farge på toppnavigasjonslinjen. Ikke alle farger fungerer med alle temaer, " +"så bare prøv dem ut!" #: .\cookbook\forms.py:46 msgid "Default Unit to be used when inserting a new ingredient into a recipe." @@ -79,13 +81,15 @@ msgstr "" #: .\cookbook\forms.py:56 msgid "Makes the navbar stick to the top of the page." -msgstr "" +msgstr "Fest navigasjonslinjen til toppen av siden." #: .\cookbook\forms.py:72 msgid "" "Both fields are optional. If none are given the username will be displayed " "instead" msgstr "" +"Begge feltene er valgfrie. Hvis ingen blir oppgitt, vil brukernavnet vises i " +"stedet" #: .\cookbook\forms.py:93 .\cookbook\forms.py:315 #: .\cookbook\templates\forms\edit_internal_recipe.html:45 @@ -97,15 +101,15 @@ msgstr "Navn" #: .\cookbook\templates\forms\edit_internal_recipe.html:81 #: .\cookbook\templates\stats.html:24 .\cookbook\templates\url_import.html:202 msgid "Keywords" -msgstr "" +msgstr "Nøkkelord" #: .\cookbook\forms.py:95 msgid "Preparation time in minutes" -msgstr "" +msgstr "Forberedelsestid i minutter" #: .\cookbook\forms.py:96 msgid "Waiting time (cooking/baking) in minutes" -msgstr "" +msgstr "Ventetid (til matlaging/baking) i minutter" #: .\cookbook\forms.py:97 .\cookbook\forms.py:317 msgid "Path" @@ -124,6 +128,8 @@ msgid "" "To prevent duplicates recipes with the same name as existing ones are " "ignored. Check this box to import everything." msgstr "" +"For å unngå duplikater, blir oppskrifter med samme navn som eksisterende " +"ignorert. Merk av denne boksen for å importere alt." #: .\cookbook\forms.py:149 msgid "New Unit" @@ -131,7 +137,7 @@ msgstr "Ny enhet" #: .\cookbook\forms.py:150 msgid "New unit that other gets replaced by." -msgstr "" +msgstr "Ny enhet som erstatter den gamle." #: .\cookbook\forms.py:155 msgid "Old Unit" @@ -143,19 +149,19 @@ msgstr "Enhet som skal erstattes." #: .\cookbook\forms.py:172 msgid "New Food" -msgstr "" +msgstr "Ny matvare" #: .\cookbook\forms.py:173 msgid "New food that other gets replaced by." -msgstr "" +msgstr "Ny matvare som erstatter den gamle." #: .\cookbook\forms.py:178 msgid "Old Food" -msgstr "" +msgstr "Gammel matvare" #: .\cookbook\forms.py:179 msgid "Food that should be replaced." -msgstr "" +msgstr "Matvare som bør erstattes." #: .\cookbook\forms.py:197 msgid "Add your comment: " @@ -163,17 +169,19 @@ msgstr "Legg til din kommentar: " #: .\cookbook\forms.py:238 msgid "Leave empty for dropbox and enter app password for nextcloud." -msgstr "" +msgstr "La det stå tomt for Dropbox og skriv inn app-passordet for Nextcloud." #: .\cookbook\forms.py:245 msgid "Leave empty for nextcloud and enter api token for dropbox." -msgstr "" +msgstr "La det stå tomt for Nextcloud og skriv inn API-tokenet for Dropbox." #: .\cookbook\forms.py:253 msgid "" "Leave empty for dropbox and enter only base url for nextcloud (/remote." "php/webdav/ is added automatically)" msgstr "" +"La det stå tomt for Dropbox, og skriv bare inn grunn-URLen for Nextcloud " +"(/remote.php/webdav/ blir lagt til automatisk)" #: .\cookbook\forms.py:291 msgid "Search String" @@ -185,11 +193,12 @@ msgstr "Fil-ID" #: .\cookbook\forms.py:354 msgid "You must provide at least a recipe or a title." -msgstr "" +msgstr "Du må oppgi minst en oppskrift eller en tittel." #: .\cookbook\forms.py:367 msgid "You can list default users to share recipes with in the settings." msgstr "" +"Du kan liste opp standardbrukere for å dele oppskrifter innen innstillingene." #: .\cookbook\forms.py:368 #: .\cookbook\templates\forms\edit_internal_recipe.html:377 @@ -197,10 +206,14 @@ msgid "" "You can use markdown to format this field. See the docs here" msgstr "" +"Du kan bruke Markdown for å formatere dette feltet. Se dokumentasjonen her" #: .\cookbook\forms.py:393 msgid "A username is not required, if left blank the new user can choose one." msgstr "" +"Et brukernavn er ikke påkrevd. Hvis det blir stående tomt, kan den nye " +"brukeren velge ett selv." #: .\cookbook\helper\permission_helper.py:123 #: .\cookbook\helper\permission_helper.py:129 @@ -222,26 +235,30 @@ msgstr "Du er ikke innlogget og kan derfor ikke vise siden!" #: .\cookbook\helper\permission_helper.py:167 #: .\cookbook\helper\permission_helper.py:182 msgid "You cannot interact with this object as it is not owned by you!" -msgstr "" +msgstr "Du kan ikke samhandle med dette objektet, da det ikke tilhører deg!" #: .\cookbook\helper\recipe_url_import.py:40 .\cookbook\views\api.py:549 msgid "The requested site provided malformed data and cannot be read." msgstr "" +"Nettstedet du har forespurt, har levert feilformatert data som ikke kan " +"leses." #: .\cookbook\helper\recipe_url_import.py:54 msgid "" "The requested site does not provide any recognized data format to import the " "recipe from." msgstr "" +"Det forespurte nettstedet gir ingen gjenkjennelig dataformat som kan " +"importeres oppskriften fra." #: .\cookbook\helper\recipe_url_import.py:160 msgid "Imported from" -msgstr "" +msgstr "Importert fra" #: .\cookbook\helper\template_helper.py:60 #: .\cookbook\helper\template_helper.py:62 msgid "Could not parse template code." -msgstr "" +msgstr "Kunne ikke analysere mal-koden." #: .\cookbook\integration\integration.py:102 #: .\cookbook\templates\import.html:14 .\cookbook\templates\import.html:20 @@ -250,50 +267,52 @@ msgstr "" #: .\cookbook\templates\url_import.html:233 .\cookbook\views\delete.py:60 #: .\cookbook\views\edit.py:190 msgid "Import" -msgstr "" +msgstr "Importér" #: .\cookbook\integration\integration.py:131 msgid "" "Importer expected a .zip file. Did you choose the correct importer type for " "your data ?" msgstr "" +"Importøren forventet en .zip-fil. Har du valgt riktig type importør for " +"dataene dine?" #: .\cookbook\integration\integration.py:134 msgid "The following recipes were ignored because they already existed:" -msgstr "" +msgstr "Følgende oppskrifter ble ignorert fordi de allerede eksisterte:" #: .\cookbook\integration\integration.py:137 #, python-format msgid "Imported %s recipes." -msgstr "" +msgstr "Importerte %s oppskrifter." #: .\cookbook\integration\paprika.py:44 msgid "Notes" -msgstr "" +msgstr "Notater" #: .\cookbook\integration\paprika.py:47 msgid "Nutritional Information" -msgstr "" +msgstr "Næringsinformasjon" #: .\cookbook\integration\paprika.py:50 msgid "Source" -msgstr "" +msgstr "Kilde" #: .\cookbook\integration\safron.py:23 #: .\cookbook\templates\forms\edit_internal_recipe.html:75 #: .\cookbook\templates\include\log_cooking.html:16 #: .\cookbook\templates\url_import.html:84 msgid "Servings" -msgstr "" +msgstr "Porsjoner" #: .\cookbook\integration\safron.py:25 msgid "Waiting time" -msgstr "" +msgstr "Ventetid" #: .\cookbook\integration\safron.py:27 #: .\cookbook\templates\forms\edit_internal_recipe.html:69 msgid "Preparation Time" -msgstr "" +msgstr "Forberedelsestid" #: .\cookbook\integration\safron.py:29 .\cookbook\templates\base.html:71 #: .\cookbook\templates\forms\ingredients.html:7 @@ -329,7 +348,7 @@ msgstr "Søk" #: .\cookbook\templates\meal_plan.html:5 .\cookbook\views\delete.py:152 #: .\cookbook\views\edit.py:224 .\cookbook\views\new.py:188 msgid "Meal-Plan" -msgstr "" +msgstr "Måltidsplan" #: .\cookbook\models.py:112 .\cookbook\templates\base.html:82 msgid "Books" @@ -337,11 +356,11 @@ msgstr "Bøker" #: .\cookbook\models.py:119 msgid "Small" -msgstr "" +msgstr "Liten" #: .\cookbook\models.py:119 msgid "Large" -msgstr "" +msgstr "Stor" #: .\cookbook\models.py:327 #: .\cookbook\templates\forms\edit_internal_recipe.html:198 @@ -1109,22 +1128,24 @@ msgstr "" #: .\cookbook\templates\markdown_info.html:125 msgid "Images & Links" -msgstr "" +msgstr "Bilder og lenker" #: .\cookbook\templates\markdown_info.html:126 msgid "" "Links can be formatted with Markdown. This application also allows to paste " "links directly into markdown fields without any formatting." msgstr "" +"Lenker kan formateres med Markdown. Denne applikasjonen lar deg også lime " +"inn lenker direkte i Markdown-felt uten noen formatering." #: .\cookbook\templates\markdown_info.html:132 #: .\cookbook\templates\markdown_info.html:145 msgid "This will become an image" -msgstr "" +msgstr "Dette vil bli til et bilde" #: .\cookbook\templates\markdown_info.html:152 msgid "Tables" -msgstr "" +msgstr "Tabeller" #: .\cookbook\templates\markdown_info.html:153 msgid "" @@ -1132,124 +1153,130 @@ msgid "" "editor like this one." msgstr "" +"Markdown-tabeller er vanskelige å lage for hånd. Det anbefales å bruke en " +"tabellredigerer som denne." #: .\cookbook\templates\markdown_info.html:155 #: .\cookbook\templates\markdown_info.html:157 #: .\cookbook\templates\markdown_info.html:171 #: .\cookbook\templates\markdown_info.html:177 msgid "Table" -msgstr "" +msgstr "Tabell" #: .\cookbook\templates\markdown_info.html:155 #: .\cookbook\templates\markdown_info.html:172 msgid "Header" -msgstr "" +msgstr "Overskrift" #: .\cookbook\templates\markdown_info.html:157 #: .\cookbook\templates\markdown_info.html:178 msgid "Cell" -msgstr "" +msgstr "Celle" #: .\cookbook\templates\meal_plan.html:101 msgid "New Entry" -msgstr "" +msgstr "Ny oppføring" #: .\cookbook\templates\meal_plan.html:113 #: .\cookbook\templates\shopping_list.html:52 msgid "Search Recipe" -msgstr "" +msgstr "Søk oppskrift" #: .\cookbook\templates\meal_plan.html:139 msgid "Title" -msgstr "" +msgstr "Tittel" #: .\cookbook\templates\meal_plan.html:141 msgid "Note (optional)" -msgstr "" +msgstr "Merknad (valgfritt)" #: .\cookbook\templates\meal_plan.html:143 msgid "" "You can use markdown to format this field. See the docs here" msgstr "" +"Du kan bruke Markdown for å formatere dette feltet. Se dokumentasjonen " +"her" #: .\cookbook\templates\meal_plan.html:147 #: .\cookbook\templates\meal_plan.html:251 msgid "Serving Count" -msgstr "" +msgstr "Antall porsjoner" #: .\cookbook\templates\meal_plan.html:153 msgid "Create only note" -msgstr "" +msgstr "Opprett kun en merknad" #: .\cookbook\templates\meal_plan.html:168 #: .\cookbook\templates\shopping_list.html:7 #: .\cookbook\templates\shopping_list.html:29 #: .\cookbook\templates\shopping_list.html:705 msgid "Shopping List" -msgstr "" +msgstr "Handleliste" #: .\cookbook\templates\meal_plan.html:172 msgid "Shopping list currently empty" -msgstr "" +msgstr "Handlelisten er for øyeblikket tom" #: .\cookbook\templates\meal_plan.html:175 msgid "Open Shopping List" -msgstr "" +msgstr "Åpne handlelisten" #: .\cookbook\templates\meal_plan.html:189 msgid "Plan" -msgstr "" +msgstr "Plan" #: .\cookbook\templates\meal_plan.html:196 msgid "Number of Days" -msgstr "" +msgstr "Antall dager" #: .\cookbook\templates\meal_plan.html:206 msgid "Weekday offset" -msgstr "" +msgstr "Ukedagsforskyvning" #: .\cookbook\templates\meal_plan.html:209 msgid "" "Number of days starting from the first day of the week to offset the default " "view." -msgstr "" +msgstr "Antall dager fra den første dagen i uken for å endre standardvisningen." #: .\cookbook\templates\meal_plan.html:217 #: .\cookbook\templates\meal_plan.html:294 msgid "Edit plan types" -msgstr "" +msgstr "Rediger plantyper" #: .\cookbook\templates\meal_plan.html:219 msgid "Show help" -msgstr "" +msgstr "Vis hjelp" #: .\cookbook\templates\meal_plan.html:220 msgid "Week iCal export" -msgstr "" +msgstr "Uke iCal-eksport" #: .\cookbook\templates\meal_plan.html:264 #: .\cookbook\templates\meal_plan_entry.html:18 msgid "Created by" -msgstr "" +msgstr "Opprettet av" #: .\cookbook\templates\meal_plan.html:270 #: .\cookbook\templates\meal_plan_entry.html:20 #: .\cookbook\templates\shopping_list.html:250 msgid "Shared with" -msgstr "" +msgstr "Delt med" #: .\cookbook\templates\meal_plan.html:280 msgid "Add to Shopping" -msgstr "" +msgstr "Legg til i handlelisten" #: .\cookbook\templates\meal_plan.html:323 msgid "New meal type" -msgstr "" +msgstr "Ny måltidstype" #: .\cookbook\templates\meal_plan.html:338 msgid "Meal Plan Help" -msgstr "" +msgstr "Hjelp for måltidsplanen" #: .\cookbook\templates\meal_plan.html:344 msgid "" @@ -1289,7 +1316,7 @@ msgstr "" #: .\cookbook\templates\meal_plan_entry.html:6 msgid "Meal Plan View" -msgstr "" +msgstr "Visning av måltidsplanen" #: .\cookbook\templates\meal_plan_entry.html:50 msgid "Never cooked before." @@ -1297,7 +1324,7 @@ msgstr "" #: .\cookbook\templates\meal_plan_entry.html:76 msgid "Other meals on this day" -msgstr "" +msgstr "Andre måltider denne dagen" #: .\cookbook\templates\no_groups_info.html:5 #: .\cookbook\templates\no_groups_info.html:12 From 835c5a1d3ab5afd72ffd3bc191b02920a56fc70b Mon Sep 17 00:00:00 2001 From: Theodoros Grammenos Date: Sat, 19 Aug 2023 21:00:03 +0000 Subject: [PATCH 40/71] Translated using Weblate (Greek) Currently translated at 13.4% (70 of 520 strings) Translation: Tandoor/Recipes Frontend Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/el/ --- vue/src/locales/el.json | 126 ++++++++++++++++++++-------------------- 1 file changed, 63 insertions(+), 63 deletions(-) diff --git a/vue/src/locales/el.json b/vue/src/locales/el.json index 1a09fd97f5..be2f371c39 100644 --- a/vue/src/locales/el.json +++ b/vue/src/locales/el.json @@ -1,70 +1,70 @@ { "warning_feature_beta": "Αυτή η λειτουργία βρίσκεται αυτήν τη στιγμή σε κατάσταση BETA (δοκιμαστική). Παρακαλούμε να αναμένετε σφάλματα και πιθανές αλλαγές που μπορεί να προκαλέσουν απώλεια δεδομένων που σχετίζονται με τις διάφορες λειτουργίες στο μέλλον.", - "err_fetching_resource": "", - "err_creating_resource": "", - "err_updating_resource": "", - "err_deleting_resource": "", - "err_deleting_protected_resource": "", - "err_moving_resource": "", - "err_merging_resource": "", - "success_fetching_resource": "", - "success_creating_resource": "", - "success_updating_resource": "", - "success_deleting_resource": "", - "success_moving_resource": "", - "success_merging_resource": "", - "file_upload_disabled": "", + "err_fetching_resource": "Παρουσιάστηκε ένα σφάλμα κατά τη λήψη ενός πόρου!", + "err_creating_resource": "Παρουσιάστηκε ένα σφάλμα κατά τη δημιουργία ενός πόρου!", + "err_updating_resource": "Παρουσιάστηκε ένα σφάλμα κατά την ενημέρωση ενός πόρου!", + "err_deleting_resource": "Παρουσιάστηκε ένα σφάλμα κατά τη διαγραφή ενός πόρου!", + "err_deleting_protected_resource": "Το αντικείμενο που προσπαθείτε να διαγράψετε είναι σε χρήση και δεν μπορεί να διαγραφεί.", + "err_moving_resource": "Παρουσιάστηκε ένα σφάλμα κατά τη μετακίνηση ενός πόρου!", + "err_merging_resource": "Παρουσιάστηκε ένα σφάλμα κατά τη συγχώνευση ενός πόρου!", + "success_fetching_resource": "Επιτυχής λήψη πόρου!", + "success_creating_resource": "Επιτυχής δημιουργία πόρου!", + "success_updating_resource": "Επιτυχής ενημέρωση πόρου!", + "success_deleting_resource": "Επιτυχής διαγραφή πόρου!", + "success_moving_resource": "Επιτυχής μετακίνηση πόρου!", + "success_merging_resource": "Επιτυχής συγχώνευση πόρου!", + "file_upload_disabled": "Το ανέβασμα αρχείων δεν είναι ενεργοποιημένο για τον χώρο σας.", "recipe_property_info": "", - "warning_space_delete": "", - "food_inherit_info": "", - "facet_count_info": "", - "step_time_minutes": "", - "confirm_delete": "", - "import_running": "", - "all_fields_optional": "", - "convert_internal": "", - "show_only_internal": "", - "show_split_screen": "", - "Log_Recipe_Cooking": "", - "External_Recipe_Image": "", - "Add_to_Shopping": "", - "Add_to_Plan": "", - "Step_start_time": "", - "Sort_by_new": "", - "Table_of_Contents": "", - "Recipes_per_page": "", - "Show_as_header": "", - "Hide_as_header": "", - "Add_nutrition_recipe": "", - "Remove_nutrition_recipe": "", + "warning_space_delete": "Μπορείτε να διαγράψετε τον χώρο σας μαζί με όλες τις συνταγές, τις λίστες αγορών, τα προγράμματα γευμάτων και οτιδήποτε άλλο έχετε δημιουργήσει. Η ενέργεια αυτή είναι μη αναστρέψιμη! Είστε σίγουροι ότι θέλετε να το κάνετε;", + "food_inherit_info": "Πεδία σε φαγητά τα οποία πρέπει να κληρονομούνται αυτόματα.", + "facet_count_info": "Εμφάνιση του αριθμού των συνταγών στα φίλτρα αναζήτησης.", + "step_time_minutes": "Χρόνος βήματος σε λεπτά", + "confirm_delete": "Είστε σίγουροι ότι θέλετε να διαγράψετε αυτό το {αντικείμενο};", + "import_running": "Εισαγωγή σε εξέλιξη, παρακαλώ περιμένετε!", + "all_fields_optional": "Όλα τα πεδία είναι προαιρετικά και μπορούν να μη συμπληρωθούν.", + "convert_internal": "Μετατροπή σε εσωτερική συνταγή", + "show_only_internal": "Εμφάνιση μόνο εσωτερικών συνταγών", + "show_split_screen": "Χωρισμένη οθόνη", + "Log_Recipe_Cooking": "Καταγραφή εκτέλεσης συνταγής", + "External_Recipe_Image": "Εξωτερική εικόνα συνταγής", + "Add_to_Shopping": "Προσθήκη στις αγορές", + "Add_to_Plan": "Προσθήκη στο πρόγραμμα", + "Step_start_time": "Χρόνος αρχής βήματος", + "Sort_by_new": "Ταξινόμηση κατά νέο", + "Table_of_Contents": "Πίνακας περιεχομένων", + "Recipes_per_page": "Συνταγές ανά σελίδα", + "Show_as_header": "Εμφάνιση ως κεφαλίδα", + "Hide_as_header": "Απόκρυψη ως κεφαλίδα", + "Add_nutrition_recipe": "Προσθήκη διατροφικής αξίας στη συνταγή", + "Remove_nutrition_recipe": "Αφαίρεση διατροφικής αξίας από τη συνταγή", "Copy_template_reference": "", - "per_serving": "", - "Save_and_View": "", - "Manage_Books": "", - "Meal_Plan": "", - "Select_Book": "", - "Select_File": "", - "Recipe_Image": "", - "Import_finished": "", - "View_Recipes": "", - "Log_Cooking": "", - "New_Recipe": "", - "Url_Import": "", - "Reset_Search": "", - "Recently_Viewed": "", - "Load_More": "", - "New_Keyword": "", - "Delete_Keyword": "", - "Edit_Keyword": "", - "Edit_Recipe": "", - "Move_Keyword": "", - "Merge_Keyword": "", - "Hide_Keywords": "", - "Hide_Recipes": "", - "Move_Up": "", - "Move_Down": "", - "Step_Name": "", - "Step_Type": "", + "per_serving": "ανά μερίδα", + "Save_and_View": "Αποθήκευση και προβολή", + "Manage_Books": "Διαχείριση βιβλίων", + "Meal_Plan": "Πρόγραμμα γευμάτων", + "Select_Book": "Επιλογή βιβλίου", + "Select_File": "Επιλογή αρχείου", + "Recipe_Image": "Εικόνα συνταγής", + "Import_finished": "Η εισαγωγή ολοκληρώθηκε", + "View_Recipes": "Προβολή συνταγών", + "Log_Cooking": "Καταγραφή μαγειρέματος", + "New_Recipe": "Νέα συνταγή", + "Url_Import": "Εισαγωγή Url", + "Reset_Search": "Επαναφορά αναζήτησης", + "Recently_Viewed": "Προβλήθηκαν πρόσφατα", + "Load_More": "Φόρτωση περισσότερων", + "New_Keyword": "Νέα λέξη-κλειδί", + "Delete_Keyword": "Διαγραφή λέξης-κλειδί", + "Edit_Keyword": "Τροποποίηση λέξης-κλειδί", + "Edit_Recipe": "Τροποποίηση συνταγής", + "Move_Keyword": "Μεταφορά λέξης-κλειδί", + "Merge_Keyword": "Συγχώνευση λέξης-κλειδί", + "Hide_Keywords": "Απόκρυψη λέξης-κλειδί", + "Hide_Recipes": "Απόκρυψη συνταγών", + "Move_Up": "Μετακίνηση πάνω", + "Move_Down": "Μετακίνηση κάτω", + "Step_Name": "Όνομα βήματος", + "Step_Type": "Είδος βήματος", "Make_Header": "", "Make_Ingredient": "", "Amount": "", From 2e606dc166faeac9ad7f17c343bb101f92edfc34 Mon Sep 17 00:00:00 2001 From: Theodoros Grammenos Date: Sun, 20 Aug 2023 09:05:54 +0000 Subject: [PATCH 41/71] Translated using Weblate (Greek) Currently translated at 54.9% (288 of 524 strings) Translation: Tandoor/Recipes Backend Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-backend/el/ --- cookbook/locale/el/LC_MESSAGES/django.po | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cookbook/locale/el/LC_MESSAGES/django.po b/cookbook/locale/el/LC_MESSAGES/django.po index 4b5698864e..03e0ec40a2 100644 --- a/cookbook/locale/el/LC_MESSAGES/django.po +++ b/cookbook/locale/el/LC_MESSAGES/django.po @@ -8,8 +8,8 @@ msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2022-07-12 19:20+0200\n" -"PO-Revision-Date: 2023-06-25 14:19+0000\n" -"Last-Translator: sweeney \n" +"PO-Revision-Date: 2023-08-21 09:19+0000\n" +"Last-Translator: Theodoros Grammenos \n" "Language-Team: Greek \n" "Language: el\n" @@ -22,7 +22,7 @@ msgstr "" #: .\cookbook\filters.py:23 .\cookbook\templates\forms\ingredients.html:34 #: .\cookbook\templates\stats.html:28 msgid "Ingredients" -msgstr "Συστατικά" +msgstr "Υλικά" #: .\cookbook\forms.py:53 msgid "Default unit" @@ -66,7 +66,7 @@ msgstr "Κοινοποίηση προγράμματος" #: .\cookbook\forms.py:63 msgid "Ingredient decimal places" -msgstr "" +msgstr "Δεκαδικά ψηφία υλικών" #: .\cookbook\forms.py:64 msgid "Shopping list auto sync period" From c01081255b646ebb1d4adaee81cb95a3ea14fdc0 Mon Sep 17 00:00:00 2001 From: Theodoros Grammenos Date: Sun, 20 Aug 2023 09:11:13 +0000 Subject: [PATCH 42/71] Translated using Weblate (Greek) Currently translated at 62.5% (325 of 520 strings) Translation: Tandoor/Recipes Frontend Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/el/ --- vue/src/locales/el.json | 515 ++++++++++++++++++++-------------------- 1 file changed, 258 insertions(+), 257 deletions(-) diff --git a/vue/src/locales/el.json b/vue/src/locales/el.json index be2f371c39..7ed4fb99a2 100644 --- a/vue/src/locales/el.json +++ b/vue/src/locales/el.json @@ -15,11 +15,11 @@ "success_merging_resource": "Επιτυχής συγχώνευση πόρου!", "file_upload_disabled": "Το ανέβασμα αρχείων δεν είναι ενεργοποιημένο για τον χώρο σας.", "recipe_property_info": "", - "warning_space_delete": "Μπορείτε να διαγράψετε τον χώρο σας μαζί με όλες τις συνταγές, τις λίστες αγορών, τα προγράμματα γευμάτων και οτιδήποτε άλλο έχετε δημιουργήσει. Η ενέργεια αυτή είναι μη αναστρέψιμη! Είστε σίγουροι ότι θέλετε να το κάνετε;", + "warning_space_delete": "Μπορείτε να διαγράψετε τον χώρο σας μαζί με όλες τις συνταγές, τις λίστες αγορών, τα προγράμματα γευμάτων και οτιδήποτε άλλο έχετε δημιουργήσει. Η ενέργεια αυτή είναι μη αναστρέψιμη! Θέλετε σίγουρα να το κάνετε;", "food_inherit_info": "Πεδία σε φαγητά τα οποία πρέπει να κληρονομούνται αυτόματα.", "facet_count_info": "Εμφάνιση του αριθμού των συνταγών στα φίλτρα αναζήτησης.", "step_time_minutes": "Χρόνος βήματος σε λεπτά", - "confirm_delete": "Είστε σίγουροι ότι θέλετε να διαγράψετε αυτό το {αντικείμενο};", + "confirm_delete": "Θέλετε σίγουρα να διαγράψετε αυτό το {object};", "import_running": "Εισαγωγή σε εξέλιξη, παρακαλώ περιμένετε!", "all_fields_optional": "Όλα τα πεδία είναι προαιρετικά και μπορούν να μη συμπληρωθούν.", "convert_internal": "Μετατροπή σε εσωτερική συνταγή", @@ -65,256 +65,256 @@ "Move_Down": "Μετακίνηση κάτω", "Step_Name": "Όνομα βήματος", "Step_Type": "Είδος βήματος", - "Make_Header": "", - "Make_Ingredient": "", - "Amount": "", - "Enable_Amount": "", - "Disable_Amount": "", - "Ingredient Editor": "", - "Description_Replace": "", - "Instruction_Replace": "", - "Auto_Sort": "", - "Auto_Sort_Help": "", - "Private_Recipe": "", - "Private_Recipe_Help": "", - "reusable_help_text": "", - "open_data_help_text": "", + "Make_Header": "Δημιουργία κεφαλίδας", + "Make_Ingredient": "Δημιουργία υλικού", + "Amount": "Ποσότητα", + "Enable_Amount": "Ενεργοποίηση ποσότητας", + "Disable_Amount": "Απενεργοποίηση ποσότητας", + "Ingredient Editor": "Επεξεργαστής συστατικών", + "Description_Replace": "Αλλαγή περιγραφής", + "Instruction_Replace": "Αλλαγή οδηγίας", + "Auto_Sort": "Αυτόματη ταξινόμηση", + "Auto_Sort_Help": "Μετακίνηση όλων των υλικών στο καταλληλότερο βήμα.", + "Private_Recipe": "Ιδιωτική συνταγή", + "Private_Recipe_Help": "Η συνταγή είναι ορατή μόνο σε εσάς και στα άτομα με τα οποία την μοιράζεστε.", + "reusable_help_text": "Ο σύνδεσμος πρόσκλησης μπορεί να χρησιμοποιηθεί από πολλαπλούς χρήστες.", + "open_data_help_text": "Μέσω του project Tandoor Open Data η κοινότητα παρέχει δεδομένα για το Tandoor. Αυτό το πεδίο συμπληρώνεται αυτόματα κατά την εισαγωγή του και επιτρέπει ενημερώσεις στο μέλλον.", "Open_Data_Slug": "", - "Open_Data_Import": "", - "Data_Import_Info": "", - "Update_Existing_Data": "", - "Use_Metric": "", - "Learn_More": "", - "converted_unit": "", - "converted_amount": "", - "base_unit": "", - "base_amount": "", - "Datatype": "", - "Number of Objects": "", - "Add_Step": "", + "Open_Data_Import": "Εισαγωγή ανοιχτών δεδομένων", + "Data_Import_Info": "Βελτιώστε τον χώρο και τη συλλογή συνταγών σας κάνοντας εισαγωγή μιας λίστας από φαγητά, μονάδες μέτρησης κ.α., επιμελημένη από την κοινότητα.", + "Update_Existing_Data": "Ενημέρωση υπαρχόντων δεδομένων", + "Use_Metric": "Χρήση μετρικών μονάδων μέτρησης", + "Learn_More": "Μάθετε περισσότερα", + "converted_unit": "Μετατρεπόμενη μονάδα μέτρησης", + "converted_amount": "Μετατρεπόμενη ποσότητα", + "base_unit": "Βασική μονάδα μέτρησης", + "base_amount": "Βασική ποσότητα", + "Datatype": "Τύπος δεδομένων", + "Number of Objects": "Αριθμός αντικειμένων", + "Add_Step": "Προσθήκη βήματος", "Keywords": "Λέξεις κλειδιά", "Books": "Βιβλία", - "Proteins": "", - "Fats": "", - "Carbohydrates": "", - "Calories": "", - "Energy": "", - "Nutrition": "", - "Date": "", - "Share": "", - "Automation": "", - "Parameter": "", - "Export": "", - "Copy": "", - "Rating": "", - "Close": "", - "Cancel": "", - "Link": "", - "Add": "", - "New": "", - "Note": "", - "Success": "", - "Failure": "", - "Protected": "", - "Ingredients": "", - "Supermarket": "", - "Categories": "", - "Category": "", - "Selected": "", - "min": "", + "Proteins": "Πρωτεΐνες", + "Fats": "Λιπαρά", + "Carbohydrates": "Υδατάνθρακες", + "Calories": "Θερμίδες", + "Energy": "Ενέργεια", + "Nutrition": "Διατροφική αξία", + "Date": "Ημερομηνία", + "Share": "Κοινοποίηση", + "Automation": "Αυτοματισμός", + "Parameter": "Παράμετρος", + "Export": "Εξαγωγή", + "Copy": "Αντιγραφή", + "Rating": "Βαθμολογία", + "Close": "Κλείσιμο", + "Cancel": "Ακύρωση", + "Link": "Σύνδεσμος", + "Add": "Προσθήκη", + "New": "Νέο", + "Note": "Σημείωση", + "Success": "Επιτυχία", + "Failure": "Αποτυχία", + "Protected": "Προστατευμένο", + "Ingredients": "Υλικά", + "Supermarket": "Supermarket", + "Categories": "Κατηγορίες", + "Category": "Κατηγορία", + "Selected": "Επιλεγμένο", + "min": "ελάχ", "Servings": "Μερίδες", - "Waiting": "", - "Preparation": "", + "Waiting": "Αναμονή", + "Preparation": "Προετοιμασία", "External": "", - "Size": "", - "Files": "", - "File": "", - "Edit": "", - "Image": "", - "Delete": "", - "Open": "", - "Ok": "", - "Save": "", - "Step": "", - "Search": "", - "Import": "", - "Print": "", + "Size": "Μέγεθος", + "Files": "Αρχεία", + "File": "Αρχείο", + "Edit": "Τροποποίηση", + "Image": "Εικόνα", + "Delete": "Διαγραφή", + "Open": "Άνοιγμα", + "Ok": "ΟΚ", + "Save": "Αποθήκευση", + "Step": "Βήμα", + "Search": "Αναζήτηση", + "Import": "Εισαγωγή", + "Print": "Εκτύπωση", "Settings": "Ρυθμίσεις", - "or": "", - "and": "", - "Information": "", - "Download": "", - "Create": "", - "Search Settings": "", - "View": "", - "Recipes": "", - "Move": "", - "Merge": "", - "Parent": "", - "Copy Link": "", - "Copy Token": "", - "delete_confirmation": "", - "move_confirmation": "", - "merge_confirmation": "", - "create_rule": "", - "move_selection": "", - "merge_selection": "", - "Root": "", - "Ignore_Shopping": "", - "Shopping_Category": "", - "Shopping_Categories": "", - "Edit_Food": "", - "Move_Food": "", - "New_Food": "", - "Hide_Food": "", - "Food_Alias": "", - "Unit_Alias": "", - "Keyword_Alias": "", - "Delete_Food": "", - "No_ID": "", - "Meal_Plan_Days": "", - "merge_title": "", - "move_title": "", + "or": "ή", + "and": "και", + "Information": "Πληροφορίες", + "Download": "Λήψη", + "Create": "Δημιουργία", + "Search Settings": "Επιλογές αναζήτησης", + "View": "Προβολή", + "Recipes": "Συνταγές", + "Move": "Μετακίνηση", + "Merge": "Συγχώνευση", + "Parent": "Γονέας", + "Copy Link": "Αντιγραφή συνδέσμου", + "Copy Token": "Αντιγραφή token", + "delete_confirmation": "Είστε σίγουροι ότι θέλετε να διαγράψετε το {source};", + "move_confirmation": "Μετακίνηση του {child} στο γονέα {parent}", + "merge_confirmation": "Αντικατάσταση του {source} με το {target}", + "create_rule": "και δημιουργία αυτοματισμού", + "move_selection": "Επιλέξτε έναν γονέα {type} για να μεταφέρετε το {source} σε αυτόν.", + "merge_selection": "Αντικατάσταση όλων των εμφανίσεων του {source} με το επιλεγμένο {type}.", + "Root": "Ρίζα", + "Ignore_Shopping": "Παράλειψη αγορών", + "Shopping_Category": "Κατηγορία αγορών", + "Shopping_Categories": "Κατηγορίες αγορών", + "Edit_Food": "Τροποποίηση φαγητού", + "Move_Food": "Μετακίνηση φαγητού", + "New_Food": "Νέο φαγητό", + "Hide_Food": "Απόκρυψη φαγητού", + "Food_Alias": "Ψευδώνυμο φαγητού", + "Unit_Alias": "Ψευδώνυμο μονάδας μέτρησης", + "Keyword_Alias": "Ψευδώνυμο λέξης-κλειδί", + "Delete_Food": "Διαγραφή φαγητού", + "No_ID": "Το ID δεν βρέθηκε, αδύνατη η διαγραφή.", + "Meal_Plan_Days": "Μελλοντικά προγράμματα γευμάτων", + "merge_title": "Συγχώνευση {type}", + "move_title": "Μετακίνηση {type}", "Food": "Φαγητό", - "Property": "", - "Conversion": "", - "Original_Text": "", - "Recipe_Book": "", - "del_confirmation_tree": "", - "delete_title": "", - "create_title": "", - "edit_title": "", - "Name": "", - "Properties": "", - "Type": "", - "Description": "", - "Recipe": "", - "tree_root": "", - "Icon": "", - "Unit": "", - "Decimals": "", - "Default_Unit": "", - "No_Results": "", - "New_Unit": "", - "Create_New_Shopping Category": "", - "Create_New_Food": "", - "Create_New_Keyword": "", - "Create_New_Unit": "", - "Create_New_Meal_Type": "", - "Create_New_Shopping_Category": "", - "and_up": "", - "and_down": "", - "Instructions": "", - "Unrated": "", - "Automate": "", - "Empty": "", - "Key_Ctrl": "", - "Key_Shift": "", - "Time": "", - "Text": "", - "Shopping_list": "", - "Added_by": "", - "Added_on": "", - "AddToShopping": "", - "IngredientInShopping": "", - "NotInShopping": "", - "OnHand": "", - "FoodOnHand": "", - "FoodNotOnHand": "", - "Undefined": "", - "Create_Meal_Plan_Entry": "", - "Edit_Meal_Plan_Entry": "", - "Title": "", - "Week": "", - "Month": "", - "Year": "", - "Planner": "", - "Planner_Settings": "", - "Period": "", - "Plan_Period_To_Show": "", - "Periods": "", - "Plan_Show_How_Many_Periods": "", - "Starting_Day": "", - "Meal_Types": "", - "Meal_Type": "", - "New_Entry": "", - "Clone": "", - "Drag_Here_To_Delete": "", - "Meal_Type_Required": "", - "Title_or_Recipe_Required": "", - "Color": "", - "New_Meal_Type": "", - "Use_Fractions": "", - "Use_Fractions_Help": "", - "AddFoodToShopping": "", - "RemoveFoodFromShopping": "", - "DeleteShoppingConfirm": "", + "Property": "Ιδιότητα", + "Conversion": "Μετατροπή", + "Original_Text": "Αρχικό κείμενο", + "Recipe_Book": "Βιβλίο συνταγών", + "del_confirmation_tree": "Θέλετε σίγουρα να διαγράψετε το {source} και όλα τα παιδιά του;", + "delete_title": "Διαγραφή {type}", + "create_title": "Νέο {type}", + "edit_title": "Τροποποίηση {type}", + "Name": "Όνομα", + "Properties": "Ιδιότητες", + "Type": "Είδος", + "Description": "Περιγραφή", + "Recipe": "Συνταγή", + "tree_root": "Ρίζα του δέντρου", + "Icon": "Εικονίδιο", + "Unit": "Μονάδα μέτρησης", + "Decimals": "Δεκαδικά", + "Default_Unit": "Προεπιλεγμένη μονάδα μέτρησης", + "No_Results": "Δεν υπάρχουν αποτελέσματα", + "New_Unit": "Νέα μονάδα μέτρησης", + "Create_New_Shopping Category": "Δημιουργία νέας κατηγορίας αγορών", + "Create_New_Food": "Προσθήκη νέου φαγητού", + "Create_New_Keyword": "Προσθήκη νέας λέξης-κλειδί", + "Create_New_Unit": "Προσθήκη νέας μονάδας μέτρησης", + "Create_New_Meal_Type": "Προσθήκη νέου είδους γεύματος", + "Create_New_Shopping_Category": "Προσθήκη νέας κατηγορίας αγορών", + "and_up": "και πάνω", + "and_down": "και κάτω", + "Instructions": "Οδηγίες", + "Unrated": "Χωρίς βαθμολογία", + "Automate": "Αυτοματοποίηση", + "Empty": "Κενό", + "Key_Ctrl": "Ctrl", + "Key_Shift": "Shift", + "Time": "Χρόνος", + "Text": "Κείμενο", + "Shopping_list": "Λίστα αγορών", + "Added_by": "Προστέθηκε από", + "Added_on": "Προστέθηκε στις", + "AddToShopping": "Προσθήκη στη λίστα αγορών", + "IngredientInShopping": "Αυτό το υλικό είναι στη λίστα αγορών.", + "NotInShopping": "Το { food} δεν είναι στη λίστα αγορών σας.", + "OnHand": "Τώρα διαθέσιμα", + "FoodOnHand": "Έχετε το {food} διαθέσιμο.", + "FoodNotOnHand": "Δεν έχετε το {food} διαθέσιμο.", + "Undefined": "Απροσδιόριστο", + "Create_Meal_Plan_Entry": "Δημιουργία εγγραφής στο πρόγραμμα γευμάτων", + "Edit_Meal_Plan_Entry": "Τροποποίηση εγγραφής στο πρόγραμμα γευμάτων", + "Title": "Τίτλος", + "Week": "Εβδομάδα", + "Month": "Μήνας", + "Year": "Έτος", + "Planner": "Σχεδιαστής", + "Planner_Settings": "Επιλογές σχεδιαστή", + "Period": "Περίοδος", + "Plan_Period_To_Show": "Εμφάνιση εβδομάδων, μηνών ή ετών", + "Periods": "Περίοδοι", + "Plan_Show_How_Many_Periods": "Πόσες περίοδοι να εμφανίζονται", + "Starting_Day": "Πρώτη μέρα της εβδομάδας", + "Meal_Types": "Είδη γευμάτων", + "Meal_Type": "Είδος γεύματος", + "New_Entry": "Νέα εγγραφή", + "Clone": "Αντιγραφή", + "Drag_Here_To_Delete": "Σύρετε εδώ για διαγραφή", + "Meal_Type_Required": "Το είδος του γεύματος είναι απαραίτητο", + "Title_or_Recipe_Required": "Η επιλογή τίτλου ή συνταγής είναι απαραίτητη", + "Color": "Χρώμα", + "New_Meal_Type": "Νέο είδος γεύματος", + "Use_Fractions": "Χρήση κλασμάτων", + "Use_Fractions_Help": "Αυτόματη μετατροπή δεκαδικών σε κλάσματα κατά την προβολή μιας συνταγής.", + "AddFoodToShopping": "Προσθήκη του {food} στη λίστα αγορών σας", + "RemoveFoodFromShopping": "Αφαίρεση του {food} από τη λίστα αγορών σας", + "DeleteShoppingConfirm": "Θέλετε σίγουρα να αφαιρέσετε όλα τα {food} από τη λίστα αγορών;", "IgnoredFood": "", - "Add_Servings_to_Shopping": "", - "Week_Numbers": "", - "Show_Week_Numbers": "", - "Export_As_ICal": "", - "Export_To_ICal": "", - "Cannot_Add_Notes_To_Shopping": "", - "Added_To_Shopping_List": "", - "Shopping_List_Empty": "", - "Next_Period": "", - "Previous_Period": "", - "Current_Period": "", - "Next_Day": "", - "Previous_Day": "", + "Add_Servings_to_Shopping": "Προσθήκη {servings} μερίδων στις αγορές", + "Week_Numbers": "Αριθμοί εδομάδων", + "Show_Week_Numbers": "Εμφάνιση αριθμών εβδομάδων;", + "Export_As_ICal": "Εξαγωγή της τρέχουσας περιόδου σε μορφή iCal", + "Export_To_ICal": "Εξαγωγή .ics", + "Cannot_Add_Notes_To_Shopping": "Δεν είναι δυνατή η προσθήκη σημειώσεων στη λίστα αγορών", + "Added_To_Shopping_List": "Προστέθηκε στη λίστα αγορών", + "Shopping_List_Empty": "Η λίστα αγορών σας είναι κενή, μπορείτε να προσθέσετε αντικείμενα από το μενού μιας εγγραφής στο πρόγραμμα γευμάτων (δεξί κλικ στην κάρτα ή αριστερό κλικ στο εικονίδιο του μενού)", + "Next_Period": "Επόμενη περίοδος", + "Previous_Period": "Προηγούμενη περίοδος", + "Current_Period": "Τρέχουσα περίοδος", + "Next_Day": "Επόμενη μέρα", + "Previous_Day": "Προηγούμενη μέρα", "Inherit": "", - "InheritFields": "", - "FoodInherit": "", - "ShowUncategorizedFood": "", - "GroupBy": "", - "Language": "", - "Theme": "", - "SupermarketCategoriesOnly": "", - "MoveCategory": "", - "CountMore": "", - "IgnoreThis": "", - "DelayFor": "", - "Warning": "", - "NoCategory": "", + "InheritFields": "Κληρονόμηση τιμών πεδίων", + "FoodInherit": "Πεδία φαγητών που κληρονομούνται", + "ShowUncategorizedFood": "Εμφάνιση απροσδιόριστων", + "GroupBy": "Ομαδοποίηση κατά", + "Language": "Γλώσσα", + "Theme": "Θέμα", + "SupermarketCategoriesOnly": "Μόνο κατηγορίες supermarket", + "MoveCategory": "Μετακίνηση σε: ", + "CountMore": "...+{count} περισσότερα", + "IgnoreThis": "Μην προσθέτεις αυτόματα το {food} στις αγορές", + "DelayFor": "Καθυστέρηση για {hours} ώρες", + "Warning": "Προειδοποίηση", + "NoCategory": "Δεν έχει επιλεγεί κατηγορία.", "InheritWarning": "", - "ShowDelayed": "", - "Completed": "", - "OfflineAlert": "", - "shopping_share": "", - "shopping_auto_sync": "", - "one_url_per_line": "", - "mealplan_autoadd_shopping": "", - "mealplan_autoexclude_onhand": "", - "mealplan_autoinclude_related": "", - "default_delay": "", - "plan_share_desc": "", - "shopping_share_desc": "", + "ShowDelayed": "Εμφάνιση αντικειμένων που έχουν καθυστερήσει", + "Completed": "Ολοκληρωμένο", + "OfflineAlert": "Είστε εκτός σύνδεσης, η λίστα αγορών μπορεί να μην συγχρονιστεί.", + "shopping_share": "Κοινοποίηση λίστας αγορών", + "shopping_auto_sync": "Αυτόματος συγχρονισμός", + "one_url_per_line": "Ένα URL ανά γραμμή", + "mealplan_autoadd_shopping": "Αυτόματη προσθήκη προγράμματος γευμάτων", + "mealplan_autoexclude_onhand": "Εξαίρεση διαθέσιμων φαγητών", + "mealplan_autoinclude_related": "Προσθήκη σχετικών συνταγών", + "default_delay": "Προεπιλεγμένες ώρες καθυστέρησης", + "plan_share_desc": "Οι νέες εγγραφές στο πρόγραμμα γευμάτων θα κοινοποιηθούν αυτόματα με τους επιλεγμένους χρήστες.", + "shopping_share_desc": "Οι χρήστες θα μπορούν να δουν όλα τα αντικείμενα που τοποθετείτε στη λίστα αγορών σας. Πρέπει να σας προσθέσουν για να δείτε τα αντικείμενα στη λίστα τους.", "shopping_auto_sync_desc": "", - "mealplan_autoadd_shopping_desc": "", - "mealplan_autoexclude_onhand_desc": "", - "mealplan_autoinclude_related_desc": "", - "default_delay_desc": "", + "mealplan_autoadd_shopping_desc": "Αυτόματη προθήκη συστατικών του προγράμματος γευμάτων στη λίστα αγορών.", + "mealplan_autoexclude_onhand_desc": "Κατά την προσθήκη ενός προγράμματος γευμάτων στη λίστα αγορών (χειροκίνητα ή αυτόματα), εξαίρεσε τυχόν υλικά που είναι διαθέσιμα.", + "mealplan_autoinclude_related_desc": "Κατά την προσθήκη ενός προγράμματος γευμάτων στη λίστα αγορών (χειροκίνητα ή αυτόματα), συμπερίλαβε όλες τις σχετικές συνταγές.", + "default_delay_desc": "Προεπιλεγμένος αριθμός ωρών καθυστέρησης μια εγγραφής στην λίστα αγορών.", "filter_to_supermarket": "", "Coming_Soon": "", - "Auto_Planner": "", - "New_Cookbook": "", - "Hide_Keyword": "", - "Hour": "", - "Hours": "", - "Day": "", - "Days": "", - "Second": "", - "Seconds": "", - "Clear": "", - "Users": "", - "Invites": "", - "err_move_self": "", + "Auto_Planner": "Αυτόματος προγραμματιστής", + "New_Cookbook": "Νέο βιβλίο μαγειρικής", + "Hide_Keyword": "Απόκρυψη λέξεων-κλειδί", + "Hour": "Ώρα", + "Hours": "Ώρες", + "Day": "Ημέρα", + "Days": "Ημέρες", + "Second": "Δευτερόλεπτο", + "Seconds": "Δευτερόλεπτα", + "Clear": "Εκκαθάριση", + "Users": "Χρήστες", + "Invites": "Προσκλήσεις", + "err_move_self": "Δεν είναι δυνατή η μετακίνηση ενός αντικειμένου στον εαυτό του", "nothing": "", - "err_merge_self": "", - "show_sql": "", - "filter_to_supermarket_desc": "", - "CategoryName": "", + "err_merge_self": "Δεν είναι δυνατή η συγχώνευση ενός αντικειμένου με τον εαυτό του", + "show_sql": "Εμφάνιση SQL", + "filter_to_supermarket_desc": "Αυτόματο φιλτράρισμα λίστας αγορών ώστε να περιέχει μόνο κατηγορίες για το επιλεγμένο supermarket.", + "CategoryName": "Όνομα κατηγορίας", "SupermarketName": "", "CategoryInstruction": "", "shopping_recent_days_desc": "", @@ -329,11 +329,11 @@ "csv_prefix_label": "", "copy_markdown_table": "", "in_shopping": "", - "DelayUntil": "", - "Pin": "", - "Unpin": "", + "DelayUntil": "Καθυστέρηση μέχρι", + "Pin": "Καρφίτσωμα", + "Unpin": "Αφαίρεση καρφιτσώματος", "PinnedConfirmation": "", - "UnpinnedConfirmation": "", + "UnpinnedConfirmation": "Η συνταγή {recipe} αφαιρέθηκε από τις καρφιτσωμένες.", "mark_complete": "", "QuickEntry": "", "shopping_add_onhand_desc": "", @@ -348,22 +348,22 @@ "ignore_shopping_help": "", "shopping_category_help": "", "food_recipe_help": "", - "Foods": "", - "Account": "", - "Cosmetic": "", - "API": "", + "Foods": "Φαγητά", + "Account": "Λογαριασμός", + "Cosmetic": "Κοσμητικό", + "API": "API", "enable_expert": "", "expert_mode": "", "simple_mode": "", - "advanced": "", - "fields": "", + "advanced": "Για προχωρημένους", + "fields": "Πεδία", "show_keywords": "", "show_foods": "", "show_books": "", "show_rating": "", "show_units": "", "show_filters": "", - "not": "", + "not": "όχι", "save_filter": "", "filter_name": "", "left_handed": "", @@ -371,8 +371,8 @@ "Custom Filter": "", "shared_with": "", "sort_by": "", - "asc": "", - "desc": "", + "asc": "Αύξουσα", + "desc": "Φθίνουσα", "date_viewed": "", "last_cooked": "", "times_cooked": "", @@ -390,16 +390,16 @@ "paste_ingredients": "", "ingredient_list": "", "explain": "", - "filter": "", - "Website": "", - "App": "", - "Message": "", + "filter": "Φίλτρο", + "Website": "Ιστοσελίδα", + "App": "Εφαρμογή", + "Message": "Μήνυμα", "Bookmarklet": "", "Sticky_Nav": "", "Sticky_Nav_Help": "", "Nav_Color": "", "Nav_Color_Help": "", - "Use_Kj": "", + "Use_Kj": "Χρήση kJ αντί για kcal", "Comments_setting": "", "click_image_import": "", "no_more_images_found": "", @@ -497,5 +497,6 @@ "Use_Plural_Food_Simple": "", "plural_usage_info": "", "Create Recipe": "", - "Import Recipe": "" + "Import Recipe": "", + "Welcome": "Καλώς ήρθατε" } From 2888b188199a8b8617ae9ab16e250d3e5d2ddbde Mon Sep 17 00:00:00 2001 From: Theodoros Grammenos Date: Mon, 21 Aug 2023 19:47:43 +0000 Subject: [PATCH 43/71] Translated using Weblate (Greek) Currently translated at 100.0% (520 of 520 strings) Translation: Tandoor/Recipes Frontend Translate-URL: http://translate.tandoor.dev/projects/tandoor/recipes-frontend/el/ --- vue/src/locales/el.json | 386 +++++++++++++++++++++------------------- 1 file changed, 203 insertions(+), 183 deletions(-) diff --git a/vue/src/locales/el.json b/vue/src/locales/el.json index 7ed4fb99a2..afecfad4f5 100644 --- a/vue/src/locales/el.json +++ b/vue/src/locales/el.json @@ -14,7 +14,7 @@ "success_moving_resource": "Επιτυχής μετακίνηση πόρου!", "success_merging_resource": "Επιτυχής συγχώνευση πόρου!", "file_upload_disabled": "Το ανέβασμα αρχείων δεν είναι ενεργοποιημένο για τον χώρο σας.", - "recipe_property_info": "", + "recipe_property_info": "Μπορείτε επίσης να προσθέσετε ιδιότητες σε φαγητά ώστε να υπολογίζονται αυτόματα βάσει της συνταγής σας!", "warning_space_delete": "Μπορείτε να διαγράψετε τον χώρο σας μαζί με όλες τις συνταγές, τις λίστες αγορών, τα προγράμματα γευμάτων και οτιδήποτε άλλο έχετε δημιουργήσει. Η ενέργεια αυτή είναι μη αναστρέψιμη! Θέλετε σίγουρα να το κάνετε;", "food_inherit_info": "Πεδία σε φαγητά τα οποία πρέπει να κληρονομούνται αυτόματα.", "facet_count_info": "Εμφάνιση του αριθμού των συνταγών στα φίλτρα αναζήτησης.", @@ -37,7 +37,7 @@ "Hide_as_header": "Απόκρυψη ως κεφαλίδα", "Add_nutrition_recipe": "Προσθήκη διατροφικής αξίας στη συνταγή", "Remove_nutrition_recipe": "Αφαίρεση διατροφικής αξίας από τη συνταγή", - "Copy_template_reference": "", + "Copy_template_reference": "Αντιγραφή αναφοράς σε πρότυπο", "per_serving": "ανά μερίδα", "Save_and_View": "Αποθήκευση και προβολή", "Manage_Books": "Διαχείριση βιβλίων", @@ -79,7 +79,7 @@ "Private_Recipe_Help": "Η συνταγή είναι ορατή μόνο σε εσάς και στα άτομα με τα οποία την μοιράζεστε.", "reusable_help_text": "Ο σύνδεσμος πρόσκλησης μπορεί να χρησιμοποιηθεί από πολλαπλούς χρήστες.", "open_data_help_text": "Μέσω του project Tandoor Open Data η κοινότητα παρέχει δεδομένα για το Tandoor. Αυτό το πεδίο συμπληρώνεται αυτόματα κατά την εισαγωγή του και επιτρέπει ενημερώσεις στο μέλλον.", - "Open_Data_Slug": "", + "Open_Data_Slug": "Αναγνωριστικό (Slug) Open Data", "Open_Data_Import": "Εισαγωγή ανοιχτών δεδομένων", "Data_Import_Info": "Βελτιώστε τον χώρο και τη συλλογή συνταγών σας κάνοντας εισαγωγή μιας λίστας από φαγητά, μονάδες μέτρησης κ.α., επιμελημένη από την κοινότητα.", "Update_Existing_Data": "Ενημέρωση υπαρχόντων δεδομένων", @@ -125,7 +125,7 @@ "Servings": "Μερίδες", "Waiting": "Αναμονή", "Preparation": "Προετοιμασία", - "External": "", + "External": "Εξωτερική", "Size": "Μέγεθος", "Files": "Αρχεία", "File": "Αρχείο", @@ -217,10 +217,10 @@ "Added_on": "Προστέθηκε στις", "AddToShopping": "Προσθήκη στη λίστα αγορών", "IngredientInShopping": "Αυτό το υλικό είναι στη λίστα αγορών.", - "NotInShopping": "Το { food} δεν είναι στη λίστα αγορών σας.", + "NotInShopping": "Το φαγητό { food} δεν είναι στη λίστα αγορών σας.", "OnHand": "Τώρα διαθέσιμα", - "FoodOnHand": "Έχετε το {food} διαθέσιμο.", - "FoodNotOnHand": "Δεν έχετε το {food} διαθέσιμο.", + "FoodOnHand": "Έχετε το φαγητό {food} διαθέσιμο.", + "FoodNotOnHand": "Δεν έχετε το φαγητό {food} διαθέσιμο.", "Undefined": "Απροσδιόριστο", "Create_Meal_Plan_Entry": "Δημιουργία εγγραφής στο πρόγραμμα γευμάτων", "Edit_Meal_Plan_Entry": "Τροποποίηση εγγραφής στο πρόγραμμα γευμάτων", @@ -246,10 +246,10 @@ "New_Meal_Type": "Νέο είδος γεύματος", "Use_Fractions": "Χρήση κλασμάτων", "Use_Fractions_Help": "Αυτόματη μετατροπή δεκαδικών σε κλάσματα κατά την προβολή μιας συνταγής.", - "AddFoodToShopping": "Προσθήκη του {food} στη λίστα αγορών σας", - "RemoveFoodFromShopping": "Αφαίρεση του {food} από τη λίστα αγορών σας", - "DeleteShoppingConfirm": "Θέλετε σίγουρα να αφαιρέσετε όλα τα {food} από τη λίστα αγορών;", - "IgnoredFood": "", + "AddFoodToShopping": "Προσθήκη του φαγητού {food} στη λίστα αγορών σας", + "RemoveFoodFromShopping": "Αφαίρεση του φαγητού {food} από τη λίστα αγορών σας", + "DeleteShoppingConfirm": "Θέλετε σίγουρα να αφαιρέσετε τα {food} από τη λίστα αγορών;", + "IgnoredFood": "Το φαγητό {food} έχει ρυθμιστεί να αγνοεί τις αγορές.", "Add_Servings_to_Shopping": "Προσθήκη {servings} μερίδων στις αγορές", "Week_Numbers": "Αριθμοί εδομάδων", "Show_Week_Numbers": "Εμφάνιση αριθμών εβδομάδων;", @@ -263,7 +263,7 @@ "Current_Period": "Τρέχουσα περίοδος", "Next_Day": "Επόμενη μέρα", "Previous_Day": "Προηγούμενη μέρα", - "Inherit": "", + "Inherit": "Κληρονόμηση", "InheritFields": "Κληρονόμηση τιμών πεδίων", "FoodInherit": "Πεδία φαγητών που κληρονομούνται", "ShowUncategorizedFood": "Εμφάνιση απροσδιόριστων", @@ -273,11 +273,11 @@ "SupermarketCategoriesOnly": "Μόνο κατηγορίες supermarket", "MoveCategory": "Μετακίνηση σε: ", "CountMore": "...+{count} περισσότερα", - "IgnoreThis": "Μην προσθέτεις αυτόματα το {food} στις αγορές", + "IgnoreThis": "Να μην προστίθεται αυτόματα το φαγητό {food} στις αγορές", "DelayFor": "Καθυστέρηση για {hours} ώρες", "Warning": "Προειδοποίηση", "NoCategory": "Δεν έχει επιλεγεί κατηγορία.", - "InheritWarning": "", + "InheritWarning": "To φαγητό {food} έχει ρυθμιστεί να κληρονομεί, οι αλλαγές μπορεί να μην διατηρηθούν.", "ShowDelayed": "Εμφάνιση αντικειμένων που έχουν καθυστερήσει", "Completed": "Ολοκληρωμένο", "OfflineAlert": "Είστε εκτός σύνδεσης, η λίστα αγορών μπορεί να μην συγχρονιστεί.", @@ -290,13 +290,13 @@ "default_delay": "Προεπιλεγμένες ώρες καθυστέρησης", "plan_share_desc": "Οι νέες εγγραφές στο πρόγραμμα γευμάτων θα κοινοποιηθούν αυτόματα με τους επιλεγμένους χρήστες.", "shopping_share_desc": "Οι χρήστες θα μπορούν να δουν όλα τα αντικείμενα που τοποθετείτε στη λίστα αγορών σας. Πρέπει να σας προσθέσουν για να δείτε τα αντικείμενα στη λίστα τους.", - "shopping_auto_sync_desc": "", + "shopping_auto_sync_desc": "Θέτοντας το στο 0 θα απενεργοποιηθεί ο αυτόματος συγχρονισμός. Κατά την προβολή μιας λίστας, η λίστα ενημερώνεται ανά τα ορισμένα δευτερόλεπτα, ώστε να συγχρονιστούν τυχόν αλλαγές που έχουν κάνει άλλοι χρήστες. Η λειτουργία είναι χρήσιμη αν πραγματοποιούν αγορές πολλαπλοί χρήστες αλλά χρησιμοποιεί επιπλέον δεδομένα.", "mealplan_autoadd_shopping_desc": "Αυτόματη προθήκη συστατικών του προγράμματος γευμάτων στη λίστα αγορών.", "mealplan_autoexclude_onhand_desc": "Κατά την προσθήκη ενός προγράμματος γευμάτων στη λίστα αγορών (χειροκίνητα ή αυτόματα), εξαίρεσε τυχόν υλικά που είναι διαθέσιμα.", "mealplan_autoinclude_related_desc": "Κατά την προσθήκη ενός προγράμματος γευμάτων στη λίστα αγορών (χειροκίνητα ή αυτόματα), συμπερίλαβε όλες τις σχετικές συνταγές.", "default_delay_desc": "Προεπιλεγμένος αριθμός ωρών καθυστέρησης μια εγγραφής στην λίστα αγορών.", - "filter_to_supermarket": "", - "Coming_Soon": "", + "filter_to_supermarket": "Ταξινόμηση ανά Supermarket", + "Coming_Soon": "Σύντομα διαθέσιμο", "Auto_Planner": "Αυτόματος προγραμματιστής", "New_Cookbook": "Νέο βιβλίο μαγειρικής", "Hide_Keyword": "Απόκρυψη λέξεων-κλειδί", @@ -310,193 +310,213 @@ "Users": "Χρήστες", "Invites": "Προσκλήσεις", "err_move_self": "Δεν είναι δυνατή η μετακίνηση ενός αντικειμένου στον εαυτό του", - "nothing": "", + "nothing": "Καμία δράση", "err_merge_self": "Δεν είναι δυνατή η συγχώνευση ενός αντικειμένου με τον εαυτό του", "show_sql": "Εμφάνιση SQL", "filter_to_supermarket_desc": "Αυτόματο φιλτράρισμα λίστας αγορών ώστε να περιέχει μόνο κατηγορίες για το επιλεγμένο supermarket.", "CategoryName": "Όνομα κατηγορίας", - "SupermarketName": "", - "CategoryInstruction": "", - "shopping_recent_days_desc": "", - "shopping_recent_days": "", - "download_pdf": "", - "download_csv": "", - "csv_delim_help": "", - "csv_delim_label": "", - "SuccessClipboard": "", - "copy_to_clipboard": "", - "csv_prefix_help": "", - "csv_prefix_label": "", - "copy_markdown_table": "", - "in_shopping": "", + "SupermarketName": "Όνομα supermarket", + "CategoryInstruction": "Σύρετε κατηγορίες για να αλλάξετε τη σειρά με την οποία εμφανίζονται στη λίστα αγορών.", + "shopping_recent_days_desc": "Ημέρες πρόσφατων εγγραφών στη λίστα αγορών που προβάλλονται.", + "shopping_recent_days": "Πρόσφατες ημέρες", + "download_pdf": "Λήψη PDF", + "download_csv": "Λήψη CSV", + "csv_delim_help": "Χαρακτήρας διαχωρισμού για εξαγωγή σε CSV.", + "csv_delim_label": "Χαρακτήρας διαχωρισμού CSV", + "SuccessClipboard": "Η λίστα αγορών αντιγράφηκε στο πρόχειρο", + "copy_to_clipboard": "Αντιγραφή στο πρόχειρο", + "csv_prefix_help": "Πρόθεμα που προστίθεται κατά την αντιγραφή της λίστας στο πρόχειρο (clipboard).", + "csv_prefix_label": "Πρόθεμα λίστας", + "copy_markdown_table": "Αντιγραφή ως πίνακας Markdown", + "in_shopping": "Στη λίστα αγορών", "DelayUntil": "Καθυστέρηση μέχρι", "Pin": "Καρφίτσωμα", "Unpin": "Αφαίρεση καρφιτσώματος", - "PinnedConfirmation": "", + "PinnedConfirmation": "Η συνταγή {recipe} έχει καρφιτσωθεί.", "UnpinnedConfirmation": "Η συνταγή {recipe} αφαιρέθηκε από τις καρφιτσωμένες.", - "mark_complete": "", - "QuickEntry": "", - "shopping_add_onhand_desc": "", - "shopping_add_onhand": "", - "related_recipes": "", - "today_recipes": "", - "sql_debug": "", - "remember_search": "", - "remember_hours": "", - "tree_select": "", - "OnHand_help": "", - "ignore_shopping_help": "", - "shopping_category_help": "", - "food_recipe_help": "", + "mark_complete": "Σήμανση ως ολοκληρωμένο", + "QuickEntry": "Γρήγορη καταχώρηση", + "shopping_add_onhand_desc": "Χαρακτηρισμός ενός τροφίμου ως 'Διαθέσιμο' όταν τσεκαριστεί στη λίστα αγορών.", + "shopping_add_onhand": "Αυτόματα διαθέσιμο", + "related_recipes": "Σχετικές συνταγές", + "today_recipes": "Συνταγές της ημέρας", + "sql_debug": "Αποσφαλμάτωση SQL", + "remember_search": "Αποθήκευση αναζήτησης", + "remember_hours": "Ώρες αποθήκευσης", + "tree_select": "Χρήση επιλογής δέντρου", + "OnHand_help": "Το φαγητό είναι διαθέσιμο και δε θα προστεθεί αυτόματα στη λίστα αγορών. Η διαθεσιμότητα ενός φαγητού είναι κοινή για όλους τους χρήστες των αγορών.", + "ignore_shopping_help": "Το φαγητό να μην προστίθεται στη λίστα αγορών (π.χ. νερό)", + "shopping_category_help": "Τα supermarket μπορούν να διαταχθούν και φιλτραριστούν ανάλογα με την κατηγορία αγορών σύμφωνα με την διάταξη των διαδρόμων τους.", + "food_recipe_help": "Η σύνδεση μιας συνταγής εδώ θα συμπεριλάβει τη συνταγή που συνδέεται σε κάθε άλλη συνταγή που χρησιμοποιεί αυτό το φαγητό", "Foods": "Φαγητά", "Account": "Λογαριασμός", "Cosmetic": "Κοσμητικό", "API": "API", - "enable_expert": "", - "expert_mode": "", - "simple_mode": "", + "enable_expert": "Ενεργοποίηση λειτουργίας για προχωρημένους", + "expert_mode": "Λειτουργία για προχωρημένους", + "simple_mode": "Απλή λειτουργία", "advanced": "Για προχωρημένους", "fields": "Πεδία", - "show_keywords": "", - "show_foods": "", - "show_books": "", - "show_rating": "", - "show_units": "", - "show_filters": "", + "show_keywords": "Εμφάνιση λέξεων-κλειδί", + "show_foods": "Εμφάνιση φαγητών", + "show_books": "Εμφάνιση βιβλίων", + "show_rating": "Εμφάνιση βαθμολογίας", + "show_units": "Εμφάνιση μονάδων μέτρησης", + "show_filters": "Εμφάνιση φίλτρων", "not": "όχι", - "save_filter": "", - "filter_name": "", - "left_handed": "", - "left_handed_help": "", - "Custom Filter": "", - "shared_with": "", - "sort_by": "", + "save_filter": "Αποθήκευση φίλτρου", + "filter_name": "Όνομα φίλτρου", + "left_handed": "Έκδοση για αριστερόχειρες", + "left_handed_help": "Θα βελτιστοποιήσει το περιβάλλον χρήστη για χρήση με το αριστερό χέρι.", + "Custom Filter": "Προσαρμοσμένο φίλτρο", + "shared_with": "Διαμοιράζεται με", + "sort_by": "Ταξινόμηση κατά", "asc": "Αύξουσα", "desc": "Φθίνουσα", - "date_viewed": "", - "last_cooked": "", - "times_cooked": "", - "date_created": "", - "show_sortby": "", - "search_rank": "", - "make_now": "", - "recipe_filter": "", - "book_filter_help": "", - "review_shopping": "", - "view_recipe": "", - "copy_to_new": "", - "recipe_name": "", - "paste_ingredients_placeholder": "", - "paste_ingredients": "", - "ingredient_list": "", - "explain": "", + "date_viewed": "Προβλήθηκαν τελευταία", + "last_cooked": "Μαγειρεύτηκαν τελευταία", + "times_cooked": "Φορές που έχει μαγειρευτεί", + "date_created": "Ημερομηνία δημιουργίας", + "show_sortby": "Εμφάνιση ταξινόμησης κατά", + "search_rank": "Σειρά αναζήτησης", + "make_now": "Άμεσα διαθέσιμη", + "recipe_filter": "Φίλτρο συνταγών", + "book_filter_help": "Συμπερίλαβε συνταγές από το φίλτρο μαζί με αυτές που έχουν ανατεθεί χειροκίνητα.", + "review_shopping": "Ανασκόπηση εγγραφών στις αγορές πριν την αποθήκευση", + "view_recipe": "Εμφάνιση συνταγής", + "copy_to_new": "Αντιγραφή σε νέα συνταγή", + "recipe_name": "Όνομα συνταγής", + "paste_ingredients_placeholder": "Κάντε επικόλληση της λίστας υλικών εδώ...", + "paste_ingredients": "Επικόλληση υλικών", + "ingredient_list": "Λίστα υλικών", + "explain": "Επεξήγηση", "filter": "Φίλτρο", "Website": "Ιστοσελίδα", "App": "Εφαρμογή", "Message": "Μήνυμα", - "Bookmarklet": "", - "Sticky_Nav": "", - "Sticky_Nav_Help": "", - "Nav_Color": "", - "Nav_Color_Help": "", + "Bookmarklet": "Bookmarklet", + "Sticky_Nav": "Κολλητική πλοήγηση", + "Sticky_Nav_Help": "Μόνιμη εμφάνιση του μενού πλοήγησης στο πάνω μέρος της οθόνης.", + "Nav_Color": "Χρώμα πλοήγησης", + "Nav_Color_Help": "Αλλαγή χρώματος πλοήγησης.", "Use_Kj": "Χρήση kJ αντί για kcal", - "Comments_setting": "", - "click_image_import": "", - "no_more_images_found": "", - "import_duplicates": "", - "paste_json": "", - "Click_To_Edit": "", - "search_no_recipes": "", - "search_import_help_text": "", - "search_create_help_text": "", - "warning_duplicate_filter": "", - "reset_children": "", - "reset_children_help": "", - "reset_food_inheritance": "", - "reset_food_inheritance_info": "", - "substitute_help": "", - "substitute_siblings_help": "", - "substitute_children_help": "", - "substitute_siblings": "", - "substitute_children": "", - "SubstituteOnHand": "", - "ChildInheritFields": "", - "ChildInheritFields_help": "", - "InheritFields_help": "", - "show_ingredient_overview": "", - "Ingredient Overview": "", - "last_viewed": "", - "created_on": "", - "updatedon": "", - "Imported_From": "", - "advanced_search_settings": "", - "nothing_planned_today": "", - "no_pinned_recipes": "", - "Planned": "", - "Pinned": "", - "Imported": "", - "Quick actions": "", - "Ratings": "", - "Internal": "", - "Units": "", - "Manage_Emails": "", - "Change_Password": "", - "Social_Authentication": "", - "Random Recipes": "", - "parameter_count": "", - "select_keyword": "", - "add_keyword": "", - "select_file": "", - "select_recipe": "", - "select_unit": "", - "select_food": "", - "remove_selection": "", - "empty_list": "", - "Select": "", - "Supermarkets": "", - "User": "", - "Username": "", - "First_name": "", - "Last_name": "", + "Comments_setting": "Εμφάνιση σχολίων", + "click_image_import": "Κάντε κλικ στην εικόνα που θέλετε να εισάγετε για αυτή τη συνταγή", + "no_more_images_found": "Δεν βρέθηκαν επιπλέον εικόνες στην ιστοσελίδα.", + "import_duplicates": "Για να αποφευχθεί η δημιουργία διπλών συνταγών αγνοούνται συνταγές που έχουν ίδιο όνομα με υπάρχουσες. Τσεκάρετε το κουτί για να τις εισάγετε όλες.", + "paste_json": "Κάντε επικόλληση κώδικα html ή json για να εισάγετε τη συνταγή.", + "Click_To_Edit": "Κάντε κλικ για τροποποίηση", + "search_no_recipes": "Δεν βρέθηκαν συνταγές!", + "search_import_help_text": "Εισαγωγή συνταγής από μια ιστοσελίδα ή εφαρμογή.", + "search_create_help_text": "Δημιουργία νέας συνταγής απευθείας στο Tandoor.", + "warning_duplicate_filter": "Προειδοποίηση: Λόγω τεχνικών περιορισμών η ύπαρξη πολλαπλών φίλτρων με τους ίδιους συνδυασμούς (και/ή/όχι) μπορεί να οδηγήσει σε απρόσμενα αποτελέσματα.", + "reset_children": "Επαναφορά κληρονομικότητας παιδιών", + "reset_children_help": "Αντικατάσταση όλων των παιδιών με τιμές από τα επιλεγμένα πεδία. Τα πεδία που κληρονομούνται από τα παιδιά θα οριστούν να \"Κληρονομούν πεδία\" εκτός και αν έχει ενεργοποιηθεί η επιλογή \"Τα παιδιά κληρονομούν τα πεδία\".", + "reset_food_inheritance": "Επαναφορά κληρονομικότητας", + "reset_food_inheritance_info": "Επαναφορά όλων των φαγητών στα προεπιλεγμένα κληρονομούμενα πεδία και τις τιμές των γονέων τους.", + "substitute_help": "Τα υποκατάστατα εξετάζονται όταν αναζητούνται συνταγής που μπορούν να γίνουν με τα διαθέσιμα υλικά.", + "substitute_siblings_help": "Όλα τα φαγητά που μοιράζονται έναν γονέα αυτού του φαγητού θεωρούνται υποκατάστατα.", + "substitute_children_help": "Όλα τα φαγητά που είναι παιδιά αυτού του φαγητού θεωρούνται υποκατάστατα.", + "substitute_siblings": "Αδέρφια υποκατάστατα", + "substitute_children": "Παιδιά υποκατάστατα", + "SubstituteOnHand": "Έχετε διαθέσιμο ένα υποκατάστατο.", + "ChildInheritFields": "Τα παιδιά κληρονομούν τα πεδία", + "ChildInheritFields_help": "Τα παιδιά θα κληρονομούν αυτά τα πεδία από προεπιλογή.", + "InheritFields_help": "Οι τιμές αυτών των πεδίων θα κληρονομηθούν από τον γονέα (Εξαίρεση: οι κενές κατηγορίες αγορών δεν κληρονομούνται)", + "show_ingredient_overview": "Εμφάνιση λίστας υλικών στην αρχή της συνταγής.", + "Ingredient Overview": "Σύνοψη υλικών", + "last_viewed": "Προβλήθηκαν τελευταία", + "created_on": "Δημιουργήθηκε στις", + "updatedon": "Ενημερώθηκε στις", + "Imported_From": "Πηγή", + "advanced_search_settings": "Προχωρημένες ρυθμίσεις αναζήτησης", + "nothing_planned_today": "Δεν έχετε τίποτα προγραμματισμένο για σήμερα!", + "no_pinned_recipes": "Δεν έχετε καρφιτσωμένες συνταγές!", + "Planned": "Προγραμματισμένα", + "Pinned": "Καρφιτσωμένα", + "Imported": "Εισαγμένα", + "Quick actions": "Γρήγηορες δράσεις", + "Ratings": "Βαθμολογίες", + "Internal": "Εσωτερική", + "Units": "Μονάδες μέτρησης", + "Manage_Emails": "Διαχείριση email", + "Change_Password": "Αλλαγή κωδικού πρόσβασης", + "Social_Authentication": "Ταυτοποίηση μέσω κοινωνικών δικτύων", + "Random Recipes": "Τυχαίες συνταγές", + "parameter_count": "Παράμετρος {count}", + "select_keyword": "Επιλογή λέξης-κλειδί", + "add_keyword": "Προσθήκη λέξης-κλειδί", + "select_file": "Επιλογή αρχείου", + "select_recipe": "Επιλογή συνταγής", + "select_unit": "Επιλογή μονάδας μέτρησης", + "select_food": "Επιλογή φαγητού", + "remove_selection": "Αφαίρεση επιλογής", + "empty_list": "Η λίστα είναι άδεια.", + "Select": "Επιλογή", + "Supermarkets": "Supermarket", + "User": "Χρήστης", + "Username": "Όνομα χρήστη", + "First_name": "Όνομα", + "Last_name": "Επίθετο", "Keyword": "Λέξη κλειδί", - "Advanced": "", - "Page": "", - "Single": "", - "Multiple": "", - "Reset": "", - "Disabled": "", - "Disable": "", - "Options": "", - "Create Food": "", - "create_food_desc": "", - "additional_options": "", - "Importer_Help": "", - "Documentation": "", - "Select_App_To_Import": "", - "Import_Supported": "", - "Export_Supported": "", - "Import_Not_Yet_Supported": "", - "Export_Not_Yet_Supported": "", - "Import_Result_Info": "", - "Recipes_In_Import": "", - "Toggle": "", - "total": "", - "Import_Error": "", - "Warning_Delete_Supermarket_Category": "", - "New_Supermarket": "", - "New_Supermarket_Category": "", - "Are_You_Sure": "", - "Valid Until": "", - "Split_All_Steps": "", - "Combine_All_Steps": "", - "Plural": "", - "plural_short": "", - "Use_Plural_Unit_Always": "", - "Use_Plural_Unit_Simple": "", - "Use_Plural_Food_Always": "", - "Use_Plural_Food_Simple": "", - "plural_usage_info": "", - "Create Recipe": "", - "Import Recipe": "", - "Welcome": "Καλώς ήρθατε" + "Advanced": "Για προχωρημένους", + "Page": "Σελίδα", + "Single": "Ενικός", + "Multiple": "Πολλαπλές", + "Reset": "Επαναφορά", + "Disabled": "Απενεροποιημένο", + "Disable": "Απενεργοποίηση", + "Options": "Επιλογές", + "Create Food": "Δημιουργία φαγητού", + "create_food_desc": "Δημιουργία φαγητού και δημιουργία συνδέσμου σε αυτή τη συνταγή.", + "additional_options": "Επιπλέον επιλογές", + "Importer_Help": "Περισσότερες πληροφορίες και βοήθεια για αυτό το πρόγραμμα εισαγωγής:", + "Documentation": "Τεκμηρίωση", + "Select_App_To_Import": "Επιλέξτε μια εφαρμογή από την οποία θα γίνει εισαγωγή", + "Import_Supported": "Υποστηρίζεται εισαγωγή", + "Export_Supported": "Υποστηρίζεται εξαγωγή", + "Import_Not_Yet_Supported": "Η εισαγωγή δεν υποστηρίζεται ακόμη", + "Export_Not_Yet_Supported": "Η εξαγωγή δεν υποστηρίζεται ακόμη", + "Import_Result_Info": "Έγινε εισαγωγή {imported} από τις {total} συνταγές", + "Recipes_In_Import": "Συνταγές στο αρχείο εισαγωγής", + "Toggle": "Εναλλαγή", + "total": "σύνολο", + "Import_Error": "Συνέβη ένα σφάλμα κατά την εισαγωγή. Για να το δείτε, εμφανίστε τις λεπτομέρειες στο κάτω μέρος της σελίδας.", + "Warning_Delete_Supermarket_Category": "Η διαγραφή μιας κατηγορίας supermarket θα διαγράψει και όλες τις σχέσεις της με φαγητά. Είστε σίγουροι;", + "New_Supermarket": "Δημιουργία νέου supermarket", + "New_Supermarket_Category": "Δημιουργία νέας κατηγορίας supermarket", + "Are_You_Sure": "Είστε σίγουροι;", + "Valid Until": "Ισχύει έως", + "Split_All_Steps": "Διαχωρισμός όλων των γραμμών σε χωριστά βήματα.", + "Combine_All_Steps": "Συγχώνευση όλων των βημάτων σε ένα πεδίο.", + "Plural": "Πληθυντικός", + "plural_short": "πληθυντικός", + "Use_Plural_Unit_Always": "Να χρησιμοποιείται πάντα ο πληθυντικός για τη μονάδα μέτρησης", + "Use_Plural_Unit_Simple": "Να επιλέγεται δυναμικά ο πληθυντικός για τη μονάδα μέτρησης", + "Use_Plural_Food_Always": "Να χρησιμοποιείται πάντα ο πληθυντικός για το φαγητό", + "Use_Plural_Food_Simple": "Να επιλέγεται δυναμικά ο πληθυντικός για το φαγητό", + "plural_usage_info": "Χρήση του πληθυντικού για τις μονάδες μέτρησης και τα φαγητά μέσα σε αυτόν τον χώρο.", + "Create Recipe": "Δημιουργία συνταγής", + "Import Recipe": "Εισαγωγή συνταγής", + "Welcome": "Καλώς ήρθατε", + "kg": "κιλό [kg] (μετρικό, βάρος)", + "l": "λίτρο [l] (μετρικό, όγκος)", + "gallon": "γαλόνι [gal] (ΗΠΑ, όγκος)", + "imperial_fluid_ounce": "αυτοκρατορική υγρή ουγγιά [imp fl oz] (Ηνωμένο Βασίλειο, όγκος)", + "imperial_quart": "αυτοκρατορικό τέταρτο γαλονιού [imp qt] (Ηνωμένο Βασίλειο, όγκος)", + "imperial_tsp": "αυτοκρατορικό κουτάλι του γλυκού [imp tsp] (Ηνωμένο Βασίλειο, όγκος)", + "g": "γραμμάριο [g] (μετρικό, βάρος)", + "ounce": "ουγγιά [oz] (βάρος)", + "pound": "λίβρα (βάρος)", + "ml": "μιλιλίτρο [ml] (μετρικό, όγκος)", + "fluid_ounce": "υγρή ουγγιά [fl oz] (ΗΠΑ, όγκος)", + "pint": "πίντα [pt] (ΗΠΑ, όγκος)", + "quart": "τέταρτο γαλονιού (ΗΠΑ, όγκος)", + "tbsp": "κουτάλι της σούπας [tbsp] (ΗΠΑ, όγκος)", + "tsp": "κουτάλι του γλυκού [tsp] (ΗΠΑ, όγκος)", + "imperial_pint": "αυτοκρατορική πίντα [imp pt] (Ηνωμένο Βασίλειο, όγκος)", + "imperial_gallon": "αυτοκρατορικό γαλόνι [imp gal] (Ηνωμένο Βασίλειο, όγκος)", + "imperial_tbsp": "αυτοκρατορικό κουτάλι της σούπας [imp tbsp] (Ηνωμένο Βασίλειο, όγκος)", + "Choose_Category": "Επιλογή κατηγορίας", + "Back": "Πίσω" } From 7163c33b2a2f35e85f48ebd91d8c9d6dc3af5406 Mon Sep 17 00:00:00 2001 From: vabene1111 Date: Wed, 23 Aug 2023 13:05:07 +0200 Subject: [PATCH 44/71] fixed food edit merge/move/automate not working --- vue/src/components/Modals/GenericModalForm.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vue/src/components/Modals/GenericModalForm.vue b/vue/src/components/Modals/GenericModalForm.vue index bdc59b50f3..22e3fcd44e 100644 --- a/vue/src/components/Modals/GenericModalForm.vue +++ b/vue/src/components/Modals/GenericModalForm.vue @@ -1,6 +1,6 @@