Merge branch 'develop' into feature/importer_to_vue

# Conflicts:
#	cookbook/helper/recipe_url_import.py
This commit is contained in:
vabene1111
2022-03-04 14:33:59 +01:00
60 changed files with 1172 additions and 1172 deletions

View File

@@ -35,8 +35,8 @@ jobs:
publish: true
imageName: vabene1111/recipes
tag: beta
dockerHubUser: ${{ secrets.DOCKER_USERNAME }}
dockerHubPassword: ${{ secrets.DOCKER_PASSWORD }}
dockerUser: ${{ secrets.DOCKER_USERNAME }}
dockerPassword: ${{ secrets.DOCKER_PASSWORD }}
# Send discord notification
- name: Discord notification
env:

View File

@@ -39,5 +39,5 @@ jobs:
publish: true
imageName: vabene1111/recipes
tag: latest
dockerHubUser: ${{ secrets.DOCKER_USERNAME }}
dockerHubPassword: ${{ secrets.DOCKER_PASSWORD }}
dockerUser: ${{ secrets.DOCKER_USERNAME }}
dockerPassword: ${{ secrets.DOCKER_PASSWORD }}

View File

@@ -41,8 +41,8 @@ jobs:
publish: true
imageName: vabene1111/recipes
tag: ${{ steps.get_version.outputs.VERSION }}
dockerHubUser: ${{ secrets.DOCKER_USERNAME }}
dockerHubPassword: ${{ secrets.DOCKER_PASSWORD }}
dockerUser: ${{ secrets.DOCKER_USERNAME }}
dockerPassword: ${{ secrets.DOCKER_PASSWORD }}
# Send discord notification
- name: Discord notification
env:

View File

@@ -6,11 +6,17 @@ Please have a look at the [list of pull requests](https://github.com/vabene1111/
a complete list of contributions.
Below are some of the larger contributions made yet.
- @tourn provided the serving feature and **several** other improvements!
- @l0c4lh057 provided a much improved ingredient text parser in [#277](https://github.com/vabene1111/recipes/pull/277)
- @sebimarkgraf added nutritional information [#199](https://github.com/vabene1111/recipes/pull/199)
- @cazier added reverse proxy authentication [#88](https://github.com/vabene1111/recipes/pull/88)
- [vabene1111]
- [Kaibu]
- [smilerz]
- [MaxJa4] Docker builds and other improvements
- [tourn] provided the serving feature and **several** other improvements!
- [l0c4lh057] provided a much improved ingredient text parser in [#277](https://github.com/vabene1111/recipes/pull/277)
- [sebimarkgraf] added nutritional information [#199](https://github.com/vabene1111/recipes/pull/199)
- [cazier] added reverse proxy authentication [#88](https://github.com/vabene1111/recipes/pull/88)
- [murphy83] added support for IPv6 #1490
- [TheHaf] added custom serving size component #1411
- [lostlont] added LDAP support #960
## Translations
@@ -30,6 +36,7 @@ Below are some of the larger contributions made yet.
### German
[eTaurus](https://www.transifex.com/user/profile/eTaurus/)
[l0c4lh057](https://www.transifex.com/user/profile/l0c4lh057/)
[hyperbit00]
### Hungarian
[igazka](https://www.transifex.com/user/profile/igazka/)
@@ -60,4 +67,4 @@ Below are some of the larger contributions made yet.
### Vietnamese
[vuongtrunghieu](https://www.transifex.com/user/profile/vuongtrunghieu/)
[vuongtrunghieu](https://www.transifex.com/user/profile/vuongtrunghieu/)

View File

@@ -1,7 +1,7 @@
FROM python:3.10-alpine3.15
#Install all dependencies.
RUN apk add --no-cache postgresql-libs postgresql-client gettext zlib libjpeg libwebp libxml2-dev libxslt-dev py-cryptography
RUN apk add --no-cache postgresql-libs postgresql-client gettext zlib libjpeg libwebp libxml2-dev libxslt-dev py-cryptography openldap
#Print all logs without buffering it.
ENV PYTHONUNBUFFERED 1
@@ -15,11 +15,12 @@ WORKDIR /opt/recipes
COPY requirements.txt ./
RUN apk add --no-cache --virtual .build-deps gcc musl-dev postgresql-dev zlib-dev jpeg-dev libwebp-dev libressl-dev libffi-dev cargo openssl-dev openldap-dev && \
RUN apk add --no-cache --virtual .build-deps gcc musl-dev postgresql-dev zlib-dev jpeg-dev libwebp-dev libressl-dev libffi-dev cargo openssl-dev openldap-dev python3-dev && \
echo -n "INPUT ( libldap.so )" > /usr/lib/libldap_r.so && \
python -m venv venv && \
/opt/recipes/venv/bin/python -m pip install --upgrade pip && \
venv/bin/pip install wheel==0.36.2 && \
venv/bin/pip install wheel==0.37.1 && \
venv/bin/pip install setuptools_rust==1.1.2 && \
venv/bin/pip install -r requirements.txt --no-cache-dir &&\
apk --purge del .build-deps

21
boot.sh
View File

@@ -21,18 +21,23 @@ if [ -z "${SECRET_KEY}" ]; then
display_warning "The environment variable 'SECRET_KEY' is not set but REQUIRED for running Tandoor!"
fi
# POSTGRES_PASSWORD must be set in .env file
if [ -z "${POSTGRES_PASSWORD}" ]; then
display_warning "The environment variable 'POSTGRES_PASSWORD' is not set but REQUIRED for running Tandoor!"
fi
echo "Waiting for database to be ready..."
attempt=0
max_attempts=20
while pg_isready --host=${POSTGRES_HOST} -q; status=$?; attempt=$((attempt+1)); [ $status -ne 0 ] && [ $attempt -le $max_attempts ]; do
sleep 5
done
if [ "${DB_ENGINE}" != 'django.db.backends.sqlite3' ]; then
# POSTGRES_PASSWORD must be set in .env file
if [ -z "${POSTGRES_PASSWORD}" ]; then
display_warning "The environment variable 'POSTGRES_PASSWORD' is not set but REQUIRED for running Tandoor!"
fi
while pg_isready --host=${POSTGRES_HOST} --port=${POSTGRES_PORT} -q; status=$?; attempt=$((attempt+1)); [ $status -ne 0 ] && [ $attempt -le $max_attempts ]; do
sleep 5
done
fi
if [ $attempt -gt $max_attempts ]; then
echo -e "\nDatabase not reachable. Maximum attempts exceeded."
@@ -58,4 +63,4 @@ echo "Done"
chmod -R 755 /opt/recipes/mediafiles
exec gunicorn -b :$TANDOOR_PORT --access-logfile - --error-logfile - --log-level INFO recipes.wsgi
exec gunicorn -b :$TANDOOR_PORT --access-logfile - --error-logfile - --log-level INFO recipes.wsgi

View File

@@ -179,6 +179,7 @@ class ImportForm(ImportExportBase):
class ExportForm(ImportExportBase):
recipes = forms.ModelMultipleChoiceField(widget=MultiSelectWidget, queryset=Recipe.objects.none(), required=False)
all = forms.BooleanField(required=False)
custom_filter = forms.IntegerField(required=False)
def __init__(self, *args, **kwargs):
space = kwargs.pop('space')

View File

@@ -13,8 +13,8 @@ from cookbook.filters import RecipeFilter
from cookbook.helper.HelperFunctions import Round, str2bool
from cookbook.helper.permission_helper import has_group_permission
from cookbook.managers import DICTIONARY
from cookbook.models import (CookLog, CustomFilter, Food, Keyword, Recipe, SearchFields,
SearchPreference, ViewLog, RecipeBook)
from cookbook.models import (CookLog, CustomFilter, Food, Keyword, Recipe, RecipeBook, SearchFields,
SearchPreference, ViewLog)
from recipes import settings
@@ -28,7 +28,7 @@ class RecipeSearch():
self._queryset = None
if f := params.get('filter', None):
custom_filter = CustomFilter.objects.filter(id=f, space=self._request.space).filter(Q(created_by=self._request.user) |
Q(shared=self._request.user) | Q(recipebook__shared=self._request.user)).first()
Q(shared=self._request.user) | Q(recipebook__shared=self._request.user)).first()
if custom_filter:
self._params = {**json.loads(custom_filter.search)}
self._original_params = {**(params or {})}
@@ -40,7 +40,7 @@ class RecipeSearch():
self._search_prefs = request.user.searchpreference
else:
self._search_prefs = SearchPreference()
self._string = params.get('query').strip() if params.get('query', None) else None
self._string = self._params.get('query').strip() if self._params.get('query', None) else None
self._rating = self._params.get('rating', None)
self._keywords = {
'or': self._params.get('keywords_or', None) or self._params.get('keywords', None),
@@ -89,7 +89,10 @@ class RecipeSearch():
self._search_type = self._search_prefs.search or 'plain'
if self._string:
self._unaccent_include = self._search_prefs.unaccent.values_list('field', flat=True)
if self._postgres:
self._unaccent_include = self._search_prefs.unaccent.values_list('field', flat=True)
else:
self._unaccent_include = []
self._icontains_include = [x + '__unaccent' if x in self._unaccent_include else x for x in self._search_prefs.icontains.values_list('field', flat=True)]
self._istartswith_include = [x + '__unaccent' if x in self._unaccent_include else x for x in self._search_prefs.istartswith.values_list('field', flat=True)]
self._trigram_include = None
@@ -205,7 +208,7 @@ class RecipeSearch():
else:
self._queryset = self._queryset.annotate(simularity=Coalesce(Subquery(simularity), 0.0))
if self._sort_includes('score') and self._fulltext_include and self._fuzzy_match is not None:
self._queryset = self._queryset.annotate(score=Sum(F('rank')+F('simularity')))
self._queryset = self._queryset.annotate(score=F('rank')+F('simularity'))
else:
query_filter = Q()
for f in [x + '__unaccent__iexact' if x in self._unaccent_include else x + '__iexact' for x in SearchFields.objects.all().values_list('field', flat=True)]:
@@ -726,9 +729,8 @@ class RecipeFacet():
return self.get_facets()
def _recipe_count_queryset(self, field, depth=1, steplen=4):
return Recipe.objects.filter(**{f'{field}__path__startswith': OuterRef('path')}, id__in=self._recipe_list, space=self._request.space
).values(child=Substr(f'{field}__path', 1, steplen*depth)
).annotate(count=Count('pk', distinct=True)).values('count')
return Recipe.objects.filter(**{f'{field}__path__startswith': OuterRef('path'), f'{field}__depth__gte': depth}, id__in=self._recipe_list, space=self._request.space
).annotate(count=Coalesce(Func('pk', function='Count'), 0)).values('count')
def _keyword_queryset(self, queryset, keyword=None):
depth = getattr(keyword, 'depth', 0) + 1

View File

@@ -4,8 +4,10 @@ from html import unescape
from unicodedata import decomposition
from django.utils.dateparse import parse_duration
from django.utils.translation import gettext as _
from isodate import parse_duration as iso_parse_duration
from isodate.isoerror import ISO8601Error
from recipe_scrapers._utils import get_minutes
from cookbook.helper import recipe_url_import as helper
from cookbook.helper.ingredient_parser import IngredientParser
@@ -29,9 +31,14 @@ def get_from_scraper(scrape, request):
recipe_json['name'] = ''
try:
description = scrape.schema.data.get("description") or ''
description = scrape.description() or None
except Exception:
description = ''
description = None
if not description:
try:
description = scrape.schema.data.get("description") or ''
except Exception:
description = ''
recipe_json['description'] = parse_description(description)[:512]
recipe_json['internal'] = True
@@ -53,13 +60,19 @@ def get_from_scraper(scrape, request):
recipe_json['servings'] = max(servings, 1)
try:
recipe_json['working_time'] = get_minutes(scrape.schema.data.get("prepTime")) or 0
recipe_json['working_time'] = get_minutes(scrape.prep_time()) or 0
except Exception:
recipe_json['working_time'] = 0
try:
recipe_json['working_time'] = get_minutes(scrape.schema.data.get("prepTime")) or 0
except Exception:
recipe_json['working_time'] = 0
try:
recipe_json['waiting_time'] = get_minutes(scrape.schema.data.get("cookTime")) or 0
recipe_json['waiting_time'] = get_minutes(scrape.cook_time()) or 0
except Exception:
recipe_json['waiting_time'] = 0
try:
recipe_json['waiting_time'] = get_minutes(scrape.schema.data.get("cookTime")) or 0
except Exception:
recipe_json['waiting_time'] = 0
if recipe_json['working_time'] + recipe_json['waiting_time'] == 0:
try:
@@ -87,15 +100,23 @@ def get_from_scraper(scrape, request):
except Exception:
pass
try:
if scrape.schema.data.get('recipeCategory'):
keywords += listify_keywords(scrape.schema.data.get("recipeCategory"))
if scrape.category():
keywords += listify_keywords(scrape.category())
except Exception:
pass
try:
if scrape.schema.data.get('recipeCategory'):
keywords += listify_keywords(scrape.schema.data.get("recipeCategory"))
except Exception:
pass
try:
if scrape.schema.data.get('recipeCuisine'):
keywords += listify_keywords(scrape.schema.data.get("recipeCuisine"))
if scrape.cuisine():
keywords += listify_keywords(scrape.cuisine())
except Exception:
pass
try:
if scrape.schema.data.get('recipeCuisine'):
keywords += listify_keywords(scrape.schema.data.get("recipeCuisine"))
except Exception:
pass
try:
recipe_json['keywords'] = parse_keywords(list(set(map(str.casefold, keywords))), request.space)
except AttributeError:
@@ -142,8 +163,8 @@ def get_from_scraper(scrape, request):
except Exception:
pass
if scrape.url:
recipe_json['source_url'] = scrape.url
if scrape.canonical_url():
recipe_json['source_url'] = scrape.canonical_url()
return recipe_json
@@ -307,56 +328,6 @@ def normalize_string(string):
return unescaped_string
# TODO deprecate when merged into recipe_scapers
def get_minutes(time_text):
if time_text is None:
return 0
TIME_REGEX = re.compile(
r"(\D*(?P<hours>\d*.?(\s\d)?\/?\d+)\s*(hours|hrs|hr|h|óra))?(\D*(?P<minutes>\d+)\s*(minutes|mins|min|m|perc))?",
re.IGNORECASE,
)
try:
return int(time_text)
except Exception:
pass
if time_text.startswith("P") and "T" in time_text:
time_text = time_text.split("T", 2)[1]
if "-" in time_text:
time_text = time_text.split("-", 2)[
1
] # sometimes formats are like this: '12-15 minutes'
if " to " in time_text:
time_text = time_text.split("to", 2)[
1
] # sometimes formats are like this: '12 to 15 minutes'
empty = ''
for x in time_text:
if 'fraction' in decomposition(x):
f = decomposition(x[-1:]).split()
empty += f" {f[1].replace('003', '')}/{f[3].replace('003', '')}"
else:
empty += x
time_text = empty
matched = TIME_REGEX.search(time_text)
minutes = int(matched.groupdict().get("minutes") or 0)
if "/" in (hours := matched.groupdict().get("hours") or ''):
number = hours.split(" ")
if len(number) == 2:
minutes += 60 * int(number[0])
fraction = number[-1:][0].split("/")
minutes += 60 * float(int(fraction[0]) / int(fraction[1]))
else:
minutes += 60 * float(hours)
return int(minutes)
def iso_duration_to_minutes(string):
match = re.match(
r'P((?P<years>\d+)Y)?((?P<months>\d+)M)?((?P<weeks>\d+)W)?((?P<days>\d+)D)?T((?P<hours>\d+)H)?((?P<minutes>\d+)M)?((?P<seconds>\d+)S)?',

View File

@@ -35,7 +35,7 @@ def shopping_helper(qs, request):
qs = qs.filter(Q(checked=False) | Q(completed_at__gte=week_ago))
supermarket_order = ['checked'] + supermarket_order
return qs.order_by(*supermarket_order).select_related('unit', 'food', 'ingredient', 'created_by', 'list_recipe', 'list_recipe__mealplan', 'list_recipe__recipe')
return qs.distinct().order_by(*supermarket_order).select_related('unit', 'food', 'ingredient', 'created_by', 'list_recipe', 'list_recipe__mealplan', 'list_recipe__recipe')
class RecipeShoppingEditor():

View File

@@ -2,14 +2,14 @@ import re
from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Ingredient
from cookbook.models import Ingredient, Recipe, Step
class ChefTap(Integration):
def import_file_name_filter(self, zip_info_object):
print("testing", zip_info_object.filename)
return re.match(r'^cheftap_export/([A-Za-z\d\w\s-])+.txt$', zip_info_object.filename) or re.match(r'^([A-Za-z\d\w\s-])+.txt$', zip_info_object.filename)
return re.match(r'^cheftap_export/([A-Za-z\d\s\-_()\[\]\u00C0-\u017F])+.txt$', zip_info_object.filename) or re.match(r'^([A-Za-z\d\s\-_()\[\]\u00C0-\u017F])+.txt$', zip_info_object.filename)
def get_recipe_from_file(self, file):
source_url = ''
@@ -45,11 +45,11 @@ class ChefTap(Integration):
ingredient_parser = IngredientParser(self.request, True)
for ingredient in ingredients:
if len(ingredient.strip()) > 0:
amount, unit, ingredient, note = ingredient_parser.parse(ingredient)
f = ingredient_parser.get_food(ingredient)
amount, unit, food, note = ingredient_parser.parse(ingredient)
f = ingredient_parser.get_food(food)
u = ingredient_parser.get_unit(unit)
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note, space=self.request.space,
food=f, unit=u, amount=amount, note=note, original_text=ingredient, space=self.request.space,
))
recipe.steps.add(step)

View File

@@ -5,14 +5,14 @@ from zipfile import ZipFile
from cookbook.helper.image_processing import get_filetype
from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Ingredient, Keyword
from cookbook.models import Ingredient, Keyword, Recipe, Step
class Chowdown(Integration):
def import_file_name_filter(self, zip_info_object):
print("testing", zip_info_object.filename)
return re.match(r'^(_)*recipes/([A-Za-z\d\s-])+.md$', zip_info_object.filename)
return re.match(r'^(_)*recipes/([A-Za-z\d\s\-_()\[\]\u00C0-\u017F])+.md$', zip_info_object.filename)
def get_recipe_from_file(self, file):
ingredient_mode = False
@@ -60,12 +60,13 @@ class Chowdown(Integration):
ingredient_parser = IngredientParser(self.request, True)
for ingredient in ingredients:
amount, unit, ingredient, note = ingredient_parser.parse(ingredient)
f = ingredient_parser.get_food(ingredient)
u = ingredient_parser.get_unit(unit)
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note, space=self.request.space,
))
if len(ingredient.strip()) > 0:
amount, unit, food, note = ingredient_parser.parse(ingredient)
f = ingredient_parser.get_food(food)
u = ingredient_parser.get_unit(unit)
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note, original_text=ingredient, space=self.request.space,
))
recipe.steps.add(step)
for f in self.files:

View File

@@ -2,6 +2,7 @@ import base64
import gzip
import json
import re
from gettext import gettext as _
from io import BytesIO
import requests
@@ -11,8 +12,7 @@ from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.helper.recipe_html_import import get_recipe_from_source
from cookbook.helper.recipe_url_import import iso_duration_to_minutes
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Ingredient, Keyword
from gettext import gettext as _
from cookbook.models import Ingredient, Keyword, Recipe, Step
class CookBookApp(Integration):
@@ -51,11 +51,11 @@ class CookBookApp(Integration):
ingredient_parser = IngredientParser(self.request, True)
for ingredient in recipe_json['recipeIngredient']:
f = ingredient_parser.get_food(ingredient['ingredient']['text'])
u = ingredient_parser.get_unit(ingredient['unit']['text'])
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=ingredient['amount'], note=ingredient['note'], space=self.request.space,
))
f = ingredient_parser.get_food(ingredient['ingredient']['text'])
u = ingredient_parser.get_unit(ingredient['unit']['text'])
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=ingredient['amount'], note=ingredient['note'], space=self.request.space,
))
if len(images) > 0:
try:

View File

@@ -4,11 +4,12 @@ from zipfile import ZipFile
from bs4 import BeautifulSoup
from django.utils.translation import gettext as _
from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.helper.recipe_html_import import get_recipe_from_source
from cookbook.helper.recipe_url_import import iso_duration_to_minutes, parse_servings
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Ingredient, Keyword
from cookbook.models import Ingredient, Keyword, Recipe, Step
from recipes.settings import DEBUG
@@ -41,11 +42,11 @@ class CopyMeThat(Integration):
for ingredient in file.find_all("li", {"class": "recipeIngredient"}):
if ingredient.text == "":
continue
amount, unit, ingredient, note = ingredient_parser.parse(ingredient.text.strip())
f = ingredient_parser.get_food(ingredient)
amount, unit, food, note = ingredient_parser.parse(ingredient.text.strip())
f = ingredient_parser.get_food(food)
u = ingredient_parser.get_unit(unit)
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note, space=self.request.space,
food=f, unit=u, amount=amount, note=note, original_text=ingredient.text.strip(), space=self.request.space,
))
for s in file.find_all("li", {"class": "instruction"}):
@@ -60,7 +61,7 @@ class CopyMeThat(Integration):
try:
if file.find("a", {"id": "original_link"}).text != '':
step.instruction += "\n\nImported from: " + file.find("a", {"id": "original_link"}).text
step.instruction += "\n\n" + _("Imported from") + ": " + file.find("a", {"id": "original_link"}).text
step.save()
except AttributeError:
pass

View File

@@ -4,7 +4,7 @@ from io import BytesIO
from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Ingredient
from cookbook.models import Ingredient, Recipe, Step
class Domestica(Integration):
@@ -37,11 +37,11 @@ class Domestica(Integration):
ingredient_parser = IngredientParser(self.request, True)
for ingredient in file['ingredients'].split('\n'):
if len(ingredient.strip()) > 0:
amount, unit, ingredient, note = ingredient_parser.parse(ingredient)
f = ingredient_parser.get_food(ingredient)
amount, unit, food, note = ingredient_parser.parse(ingredient)
f = ingredient_parser.get_food(food)
u = ingredient_parser.get_unit(unit)
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note, space=self.request.space,
food=f, unit=u, amount=amount, note=note, original_text=ingredient, space=self.request.space,
))
recipe.steps.add(step)

View File

@@ -172,7 +172,7 @@ class Integration:
traceback.print_exc()
self.handle_exception(e, log=il, message=f'-------------------- \nERROR \n{e}\n--------------------\n')
import_zip.close()
elif '.json' in f['name'] or '.txt' in f['name'] or '.mmf' in f['name']:
elif '.json' in f['name'] or '.txt' in f['name'] or '.mmf' in f['name'] or '.rk' in f['name']:
data_list = self.split_recipe_file(f['file'])
il.total_recipes += len(data_list)
for d in data_list:

View File

@@ -6,13 +6,13 @@ from zipfile import ZipFile
from cookbook.helper.image_processing import get_filetype
from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Ingredient
from cookbook.models import Ingredient, Recipe, Step
class Mealie(Integration):
def import_file_name_filter(self, zip_info_object):
return re.match(r'^recipes/([A-Za-z\d-])+/([A-Za-z\d-])+.json$', zip_info_object.filename)
return re.match(r'^recipes/([A-Za-z\d\s\-_()\[\]\u00C0-\u017F])+/([A-Za-z\d\s\-_()\[\]\u00C0-\u017F])+.json$', zip_info_object.filename)
def get_recipe_from_file(self, file):
recipe_json = json.loads(file.getvalue().decode("utf-8"))
@@ -45,12 +45,14 @@ class Mealie(Integration):
u = ingredient_parser.get_unit(ingredient['unit'])
amount = ingredient['quantity']
note = ingredient['note']
original_text = None
else:
amount, unit, ingredient, note = ingredient_parser.parse(ingredient['note'])
f = ingredient_parser.get_food(ingredient)
amount, unit, food, note = ingredient_parser.parse(ingredient['note'])
f = ingredient_parser.get_food(food)
u = ingredient_parser.get_unit(unit)
original_text = ingredient['note']
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note, space=self.request.space,
food=f, unit=u, amount=amount, note=note, original_text=original_text, space=self.request.space,
))
except Exception:
pass
@@ -60,7 +62,8 @@ class Mealie(Integration):
if '.zip' in f['name']:
import_zip = ZipFile(f['file'])
try:
self.import_recipe_image(recipe, BytesIO(import_zip.read(f'recipes/{recipe_json["slug"]}/images/min-original.webp')), filetype=get_filetype(f'recipes/{recipe_json["slug"]}/images/original'))
self.import_recipe_image(recipe, BytesIO(import_zip.read(f'recipes/{recipe_json["slug"]}/images/min-original.webp')),
filetype=get_filetype(f'recipes/{recipe_json["slug"]}/images/original'))
except Exception:
pass

View File

@@ -2,7 +2,7 @@ import re
from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Ingredient, Keyword
from cookbook.models import Ingredient, Keyword, Recipe, Step
class MealMaster(Integration):
@@ -45,11 +45,11 @@ class MealMaster(Integration):
ingredient_parser = IngredientParser(self.request, True)
for ingredient in ingredients:
if len(ingredient.strip()) > 0:
amount, unit, ingredient, note = ingredient_parser.parse(ingredient)
amount, unit, food, note = ingredient_parser.parse(ingredient)
f = ingredient_parser.get_food(ingredient)
u = ingredient_parser.get_unit(unit)
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note, space=self.request.space,
food=f, unit=u, amount=amount, note=note, original_text=ingredient, space=self.request.space,
))
recipe.steps.add(step)

View File

@@ -7,7 +7,7 @@ from cookbook.helper.image_processing import get_filetype
from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.helper.recipe_url_import import iso_duration_to_minutes
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Ingredient, Keyword
from cookbook.models import Ingredient, Keyword, Recipe, Step
class NextcloudCookbook(Integration):
@@ -57,11 +57,11 @@ class NextcloudCookbook(Integration):
ingredient_parser = IngredientParser(self.request, True)
for ingredient in recipe_json['recipeIngredient']:
amount, unit, ingredient, note = ingredient_parser.parse(ingredient)
f = ingredient_parser.get_food(ingredient)
amount, unit, food, note = ingredient_parser.parse(ingredient)
f = ingredient_parser.get_food(food)
u = ingredient_parser.get_unit(unit)
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note, space=self.request.space,
food=f, unit=u, amount=amount, note=note, original_text=ingredient, space=self.request.space,
))
recipe.steps.add(step)

View File

@@ -2,7 +2,7 @@ import json
from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Ingredient
from cookbook.models import Ingredient, Recipe, Step
class OpenEats(Integration):

View File

@@ -2,12 +2,12 @@ import base64
import gzip
import json
import re
from gettext import gettext as _
from io import BytesIO
from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Ingredient, Keyword
from gettext import gettext as _
from cookbook.models import Ingredient, Keyword, Recipe, Step
class Paprika(Integration):
@@ -70,11 +70,11 @@ class Paprika(Integration):
try:
for ingredient in recipe_json['ingredients'].split('\n'):
if len(ingredient.strip()) > 0:
amount, unit, ingredient, note = ingredient_parser.parse(ingredient)
f = ingredient_parser.get_food(ingredient)
amount, unit, food, note = ingredient_parser.parse(ingredient)
f = ingredient_parser.get_food(food)
u = ingredient_parser.get_unit(unit)
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note, space=self.request.space,
food=f, unit=u, amount=amount, note=note, original_text=ingredient, space=self.request.space,
))
except AttributeError:
pass

View File

@@ -1,6 +1,6 @@
from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Ingredient
from cookbook.models import Ingredient, Recipe, Step
class Pepperplate(Integration):
@@ -41,11 +41,11 @@ class Pepperplate(Integration):
ingredient_parser = IngredientParser(self.request, True)
for ingredient in ingredients:
if len(ingredient.strip()) > 0:
amount, unit, ingredient, note = ingredient_parser.parse(ingredient)
f = ingredient_parser.get_food(ingredient)
amount, unit, food, note = ingredient_parser.parse(ingredient)
f = ingredient_parser.get_food(food)
u = ingredient_parser.get_unit(unit)
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note, space=self.request.space,
food=f, unit=u, amount=amount, note=note, original_text=ingredient, space=self.request.space,
))
recipe.steps.add(step)

View File

@@ -4,7 +4,7 @@ import requests
from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Ingredient, Keyword
from cookbook.models import Ingredient, Keyword, Recipe, Step
class Plantoeat(Integration):
@@ -56,11 +56,11 @@ class Plantoeat(Integration):
ingredient_parser = IngredientParser(self.request, True)
for ingredient in ingredients:
if len(ingredient.strip()) > 0:
amount, unit, ingredient, note = ingredient_parser.parse(ingredient)
f = ingredient_parser.get_food(ingredient)
amount, unit, food, note = ingredient_parser.parse(ingredient)
f = ingredient_parser.get_food(food)
u = ingredient_parser.get_unit(unit)
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note, space=self.request.space,
food=f, unit=u, amount=amount, note=note, original_text=ingredient, space=self.request.space,
))
recipe.steps.add(step)

View File

@@ -1,14 +1,16 @@
import re
import imghdr
import json
import requests
import re
from io import BytesIO
from zipfile import ZipFile
import imghdr
import requests
from django.utils.translation import gettext as _
from cookbook.helper.image_processing import get_filetype
from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Ingredient, Keyword
from cookbook.models import Ingredient, Keyword, Recipe, Step
class RecetteTek(Integration):
@@ -48,7 +50,7 @@ class RecetteTek(Integration):
# Append the original import url to the step (if it exists)
try:
if file['url'] != '':
step.instruction += '\n\nImported from: ' + file['url']
step.instruction += '\n\n' + _('Imported from') + ': ' + file['url']
step.save()
except Exception as e:
print(recipe.name, ': failed to import source url ', str(e))
@@ -58,11 +60,11 @@ class RecetteTek(Integration):
ingredient_parser = IngredientParser(self.request, True)
for ingredient in file['ingredients'].split('\n'):
if len(ingredient.strip()) > 0:
amount, unit, ingredient, note = ingredient_parser.parse(ingredient)
amount, unit, food, note = ingredient_parser.parse(food)
f = ingredient_parser.get_food(ingredient)
u = ingredient_parser.get_unit(unit)
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note, space=self.request.space,
food=f, unit=u, amount=amount, note=note, original_text=ingredient, space=self.request.space,
))
except Exception as e:
print(recipe.name, ': failed to parse recipe ingredients ', str(e))

View File

@@ -1,12 +1,14 @@
import re
from bs4 import BeautifulSoup
from io import BytesIO
from zipfile import ZipFile
from bs4 import BeautifulSoup
from django.utils.translation import gettext as _
from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.helper.recipe_url_import import parse_servings, iso_duration_to_minutes
from cookbook.helper.recipe_url_import import iso_duration_to_minutes, parse_servings
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Ingredient, Keyword
from cookbook.models import Ingredient, Keyword, Recipe, Step
class RecipeKeeper(Integration):
@@ -45,11 +47,11 @@ class RecipeKeeper(Integration):
for ingredient in file.find("div", {"itemprop": "recipeIngredients"}).findChildren("p"):
if ingredient.text == "":
continue
amount, unit, ingredient, note = ingredient_parser.parse(ingredient.text.strip())
f = ingredient_parser.get_food(ingredient)
amount, unit, food, note = ingredient_parser.parse(ingredient.text.strip())
f = ingredient_parser.get_food(food)
u = ingredient_parser.get_unit(unit)
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note, space=self.request.space,
food=f, unit=u, amount=amount, note=note, original_text=ingredient, space=self.request.space,
))
for s in file.find("div", {"itemprop": "recipeDirections"}).find_all("p"):
@@ -58,7 +60,7 @@ class RecipeKeeper(Integration):
step.instruction += s.text + ' \n'
if file.find("span", {"itemprop": "recipeSource"}).text != '':
step.instruction += "\n\nImported from: " + file.find("span", {"itemprop": "recipeSource"}).text
step.instruction += "\n\n" + _("Imported from") + ": " + file.find("span", {"itemprop": "recipeSource"}).text
step.save()
recipe.steps.add(step)

View File

@@ -5,7 +5,7 @@ import requests
from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Ingredient
from cookbook.models import Ingredient, Recipe, Step
class RecipeSage(Integration):
@@ -31,7 +31,7 @@ class RecipeSage(Integration):
except Exception as e:
print('failed to parse yield or time ', str(e))
ingredient_parser = IngredientParser(self.request,True)
ingredient_parser = IngredientParser(self.request, True)
ingredients_added = False
for s in file['recipeInstructions']:
step = Step.objects.create(
@@ -41,11 +41,11 @@ class RecipeSage(Integration):
ingredients_added = True
for ingredient in file['recipeIngredient']:
amount, unit, ingredient, note = ingredient_parser.parse(ingredient)
f = ingredient_parser.get_food(ingredient)
amount, unit, food, note = ingredient_parser.parse(ingredient)
f = ingredient_parser.get_food(food)
u = ingredient_parser.get_unit(unit)
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note, space=self.request.space,
food=f, unit=u, amount=amount, note=note, original_text=ingredient, space=self.request.space,
))
recipe.steps.add(step)

View File

@@ -1,6 +1,6 @@
from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Ingredient, Keyword
from cookbook.models import Ingredient, Keyword, Recipe, Step
class RezKonv(Integration):
@@ -44,11 +44,11 @@ class RezKonv(Integration):
ingredient_parser = IngredientParser(self.request, True)
for ingredient in ingredients:
if len(ingredient.strip()) > 0:
amount, unit, ingredient, note = ingredient_parser.parse(ingredient)
f = ingredient_parser.get_food(ingredient)
amount, unit, food, note = ingredient_parser.parse(ingredient)
f = ingredient_parser.get_food(food)
u = ingredient_parser.get_unit(unit)
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note, space=self.request.space,
food=f, unit=u, amount=amount, note=note, original_text=ingredient, space=self.request.space,
))
recipe.steps.add(step)
@@ -60,9 +60,14 @@ class RezKonv(Integration):
def split_recipe_file(self, file):
recipe_list = []
current_recipe = ''
encoding_list = ['windows-1250', 'latin-1'] #TODO build algorithm to try trough encodings and fail if none work, use for all importers
encoding = 'windows-1250'
for fl in file.readlines():
line = fl.decode("windows-1250")
try:
line = fl.decode(encoding)
except UnicodeDecodeError:
encoding = 'latin-1'
line = fl.decode(encoding)
if line.startswith('=====') and 'rezkonv' in line.lower():
if current_recipe != '':
recipe_list.append(current_recipe)

View File

@@ -2,7 +2,7 @@ from django.utils.translation import gettext as _
from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Ingredient
from cookbook.models import Ingredient, Recipe, Step
class Saffron(Integration):
@@ -47,11 +47,11 @@ class Saffron(Integration):
ingredient_parser = IngredientParser(self.request, True)
for ingredient in ingredients:
amount, unit, ingredient, note = ingredient_parser.parse(ingredient)
f = ingredient_parser.get_food(ingredient)
amount, unit, food, note = ingredient_parser.parse(ingredient)
f = ingredient_parser.get_food(food)
u = ingredient_parser.get_unit(unit)
step.ingredients.add(Ingredient.objects.create(
food=f, unit=u, amount=amount, note=note, space=self.request.space,
food=f, unit=u, amount=amount, note=note, original_text=ingredient, space=self.request.space,
))
recipe.steps.add(step)
@@ -76,7 +76,7 @@ class Saffron(Integration):
for i in s.ingredients.all():
recipeIngredient.append(f'{float(i.amount)} {i.unit} {i.food}')
data += "Ingredients: \n"
for ingredient in recipeIngredient:
data += ingredient+"\n"
@@ -91,10 +91,10 @@ class Saffron(Integration):
files = []
for r in recipes:
filename, data = self.get_file_from_recipe(r)
files.append([ filename, data ])
files.append([filename, data])
el.exported_recipes += 1
el.msg += self.get_recipe_processed_msg(r)
el.save()
return files
return files

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.2.12 on 2022-02-25 15:19
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('cookbook', '0171_alter_searchpreference_trigram_threshold'),
]
operations = [
migrations.AddField(
model_name='ingredient',
name='original_text',
field=models.CharField(blank=True, default=None, max_length=512, null=True),
),
]

View File

@@ -62,9 +62,10 @@ class TreeManager(MP_NodeManager):
# model.Manager get_or_create() is not compatible with MP_Tree
def get_or_create(self, *args, **kwargs):
kwargs['name'] = kwargs['name'].strip()
try:
return self.get(name__iexact=kwargs['name'], space=kwargs['space']), False
except self.model.DoesNotExist:
if obj := self.filter(name__iexact=kwargs['name'], space=kwargs['space']).first():
return obj, False
else:
with scopes_disabled():
try:
defaults = kwargs.pop('defaults', None)
@@ -590,6 +591,7 @@ class Ingredient(ExportModelOperationsMixin('ingredient'), models.Model, Permiss
is_header = models.BooleanField(default=False)
no_amount = models.BooleanField(default=False)
order = models.IntegerField(default=0)
original_text = models.CharField(max_length=512, null=True, blank=True, default=None)
original_text = models.CharField(max_length=512, null=True, blank=True, default=None)

View File

@@ -9,6 +9,8 @@ from cookbook.models import Recipe, RecipeImport, SyncLog
from cookbook.provider.provider import Provider
from requests.auth import HTTPBasicAuth
from recipes.settings import DEBUG
class Nextcloud(Provider):
@@ -28,15 +30,18 @@ class Nextcloud(Provider):
def import_all(monitor):
client = Nextcloud.get_client(monitor.storage)
if DEBUG:
print(f'TANDOOR_PROVIDER_DEBUG checking path {monitor.path} with client {client}')
files = client.list(monitor.path)
try:
files.pop(0) # remove first element because its the folder itself
except IndexError:
pass # folder is empty, no recipes will be imported
if DEBUG:
print(f'TANDOOR_PROVIDER_DEBUG file list {files}')
import_count = 0
for file in files:
if DEBUG:
print(f'TANDOOR_PROVIDER_DEBUG importing file {file}')
path = monitor.path + '/' + file
if not Recipe.objects.filter(file_path__iexact=path, space=monitor.space).exists() and not RecipeImport.objects.filter(file_path=path, space=monitor.space).exists():
name = os.path.splitext(file)[0]

View File

@@ -337,7 +337,7 @@ class SupermarketCategorySerializer(UniqueFieldsMixin, WritableNestedModelSerial
def create(self, validated_data):
name = validated_data.pop('name').strip()
space = validated_data.pop('space', self.context['request'].space)
obj, created = SupermarketCategory.objects.get_or_create(name__iexact=name, space=space, defaults=validated_data)
obj, created = SupermarketCategory.objects.get_or_create(name=name, space=space)
return obj
def update(self, instance, validated_data):
@@ -421,9 +421,11 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR
space = validated_data.pop('space', self.context['request'].space)
# supermarket category needs to be handled manually as food.get or create does not create nested serializers unlike a super.create of serializer
if 'supermarket_category' in validated_data and validated_data['supermarket_category']:
sm_category = validated_data['supermarket_category']
sc_name = sm_category.pop('name', None)
validated_data['supermarket_category'], sc_created = SupermarketCategory.objects.get_or_create(
name__iexact=validated_data.pop('supermarket_category')['name'],
space=self.context['request'].space)
name=sc_name,
space=space, defaults=sm_category)
onhand = validated_data.pop('food_onhand', None)
# assuming if on hand for user also onhand for shopping_share users
@@ -479,6 +481,10 @@ class IngredientSerializer(WritableNestedModelSerializer):
validated_data['space'] = self.context['request'].space
return super().create(validated_data)
def update(self, instance, validated_data):
validated_data.pop('original_text', None)
return super().update(instance, validated_data)
class Meta:
model = Ingredient
fields = (
@@ -681,7 +687,7 @@ class RecipeBookEntrySerializer(serializers.ModelSerializer):
def create(self, validated_data):
book = validated_data['book']
recipe = validated_data['recipe']
if not book.get_owner() == self.context['request'].user and not self.context['request'].user in book.get_shared():
if not book.get_owner() == self.context['request'].user and not self.context['request'].user in book.get_shared():
raise NotFound(detail=None, code=None)
obj, created = RecipeBookEntry.objects.get_or_create(book=book, recipe=recipe)
return obj
@@ -736,11 +742,11 @@ class ShoppingListRecipeSerializer(serializers.ModelSerializer):
value = Decimal(value)
value = value.quantize(Decimal(1)) if value == value.to_integral() else value.normalize() # strips trailing zero
return (
obj.name
or getattr(obj.mealplan, 'title', None)
or (d := getattr(obj.mealplan, 'date', None)) and ': '.join([obj.mealplan.recipe.name, str(d)])
or obj.recipe.name
) + f' ({value:.2g})'
obj.name
or getattr(obj.mealplan, 'title', None)
or (d := getattr(obj.mealplan, 'date', None)) and ': '.join([obj.mealplan.recipe.name, str(d)])
or obj.recipe.name
) + f' ({value:.2g})'
def update(self, instance, validated_data):
# TODO remove once old shopping list

View File

@@ -12,6 +12,7 @@
<script type="application/javascript">
window.IMAGE_PLACEHOLDER = "{% static 'assets/recipe_no_image.svg' %}"
window.CUSTOM_LOCALE = '{{ request.LANGUAGE_CODE }}'
</script>
{% render_bundle 'shopping_list_view' %} {% endblock %}

View File

@@ -88,9 +88,8 @@
<h4>
{% trans 'Members' %}
<small class="text-muted"
>{{ space_users|length }}/ {% if request.space.max_users > 0 %} {{ request.space.max_users }}{% else
%}∞{% endif %}</small
>
>{{ space_users|length }}/{% if request.space.max_users > 0 %} {{ request.space.max_users }}{% else %}∞{% endif %}
</small>
<a class="btn btn-success float-right" href="{% url 'new_invite_link' %}"
><i class="fas fa-plus-circle"></i> {% trans 'Invite User' %}</a
>

View File

@@ -30,7 +30,7 @@
style="height:50%"
href="{% bookmarklet request %}"
title="{% trans 'Drag me to your bookmarks to import recipes from anywhere' %}">
<img src="{% static 'assets/favicon-16x16.png' %}">{% trans 'Bookmark Me!' %} </a>
<img src="{% static 'assets/favicon-16x16.png' %}" style="margin-right: 1em;">{% trans 'Bookmark Me!' %} </a>
</div>
<nav class="nav nav-pills flex-sm-row mb-2">
<a class="nav-link active" href="#nav-url" data-toggle="tab" role="tab" aria-controls="nav-url"
@@ -50,11 +50,11 @@
<div class="tab-pane fade show active" id="nav-url" role="tabpanel">
<div class="btn-group btn-group-toggle mt-2" data-toggle="buttons">
<label class="btn btn-outline-info btn-sm active" @click="automatic=true">
<input type="radio" autocomplete="off" checked> Automatic
<input type="radio" autocomplete="off" checked> {% trans 'Automatic' %}
</label>
<label class="btn btn-outline-info btn-sm" @click="automatic=false">
<input type="radio" autocomplete="off"> Manual
<input type="radio" autocomplete="off"> {% trans 'Manual' %}
</label>
</div>
<div role="group" class="input-group mt-4">
@@ -473,9 +473,9 @@
<div class="card" style="margin-top: 4px">
<div class="card-body">
<div class="row" v-if="i.original">
<div class="row" v-if="i.original_text">
<div class="col-md-12" style="margin-bottom: 4px">
<span class="text-muted"><i class="fas fa-globe"></i> [[i.original]]</span>
<span class="text-muted"><i class="fas fa-globe"></i> [[i.original_text]]</span>
</div>
</div>
<div class="row">
@@ -1024,7 +1024,7 @@
amount: String(response.body.amount),
ingredient: {id: Math.random() * 1000, text: response.body.food},
note: response.body.note,
original: v
original_text: v
}
this.recipe_json.recipeIngredient.push(new_ingredient)
}).catch((err) => {

File diff suppressed because it is too large Load Diff

View File

@@ -156,7 +156,7 @@ def import_url(request):
recipe.steps.add(step)
for kw in data['keywords']:
if data['all_keywords']: # do not remove this check :) https://github.com/vabene1111/recipes/issues/645
if data['all_keywords']: # do not remove this check :) https://github.com/vabene1111/recipes/issues/645
k, created = Keyword.objects.get_or_create(name=kw['text'], space=request.space)
recipe.keywords.add(k)
else:
@@ -168,7 +168,8 @@ def import_url(request):
ingredient_parser = IngredientParser(request, True)
for ing in data['recipeIngredient']:
ingredient = Ingredient(space=request.space, )
original = ing.pop('original', None) or ing.pop('original_text', None)
ingredient = Ingredient(original_text=original, space=request.space, )
if food_text := ing['ingredient']['text'].strip():
ingredient.food = ingredient_parser.get_food(food_text)

View File

@@ -11,6 +11,7 @@ from django.utils.translation import gettext as _
from cookbook.forms import ExportForm, ImportExportBase, ImportForm
from cookbook.helper.permission_helper import group_required
from cookbook.helper.recipe_search import RecipeSearch
from cookbook.integration.cheftap import ChefTap
from cookbook.integration.chowdown import Chowdown
from cookbook.integration.cookbookapp import CookBookApp
@@ -123,6 +124,9 @@ def export_recipe(request):
recipes = form.cleaned_data['recipes']
if form.cleaned_data['all']:
recipes = Recipe.objects.filter(space=request.space, internal=True).all()
elif custom_filter := form.cleaned_data['custom_filter']:
search = RecipeSearch(request, filter=custom_filter)
recipes = search.get_queryset(Recipe.objects.filter(space=request.space, internal=True))
integration = get_integration(request, form.cleaned_data['type'])

View File

@@ -48,11 +48,11 @@ def hook(request, token):
request.space = tb.space # TODO this is likely a bad idea. Verify and test
request.user = tb.created_by
ingredient_parser = IngredientParser(request, False)
amount, unit, ingredient, note = ingredient_parser.parse(data['message']['text'])
f = ingredient_parser.get_food(ingredient)
amount, unit, food, note = ingredient_parser.parse(data['message']['text'])
f = ingredient_parser.get_food(food)
u = ingredient_parser.get_unit(unit)
ShoppingListEntry.objects.create(food=f, unit=u, amount=amount, created_by=request.user, space=request.space)
ShoppingListEntry.objects.create(food=f, unit=u, amount=amount, original_text=ingredient, created_by=request.user, space=request.space)
return JsonResponse({'data': data['message']['text']})
except Exception:

View File

@@ -57,6 +57,7 @@ CORS_ORIGIN_ALLOW_ALL = True
LOGIN_REDIRECT_URL = "index"
LOGOUT_REDIRECT_URL = "index"
ACCOUNT_LOGOUT_REDIRECT_URL = "index"
ACCOUNT_EMAIL_CONFIRMATION_ANONYMOUS_REDIRECT_URL = "index"
SESSION_EXPIRE_AT_BROWSER_CLOSE = False
SESSION_COOKIE_AGE = 365 * 60 * 24 * 60

View File

@@ -17,7 +17,6 @@ Pillow==9.0.1
psycopg2-binary==2.9.3
python-dotenv==0.19.2
requests==2.27.1
simplejson==3.17.6
six==1.16.0
webdavclient3==3.14.6
whitenoise==5.3.0
@@ -30,7 +29,7 @@ Jinja2==3.0.3
django-webpack-loader==1.4.1
django-js-reverse==0.9.1
django-allauth==0.47.0
recipe-scrapers==13.16.0
recipe-scrapers==13.19.0
django-scopes==1.2.0
pytest==6.2.5
pytest-django==4.5.2

View File

@@ -19,7 +19,7 @@
"html2pdf.js": "^0.10.1",
"lodash": "^4.17.21",
"moment": "^2.29.1",
"prismjs": "^1.25.0",
"prismjs": "^1.27.0",
"vue": "^2.6.14",
"vue-class-component": "^7.2.3",
"vue-click-outside": "^1.1.0",

View File

@@ -1,145 +1,126 @@
<template>
<div id="app">
<br/>
<div id="app">
<br />
<template v-if="export_info !== undefined">
<template v-if="export_info !== undefined">
<template v-if="export_info.running">
<h5 style="text-align: center">{{ $t("Exporting") }}...</h5>
<template v-if="export_info.running">
<h5 style="text-align: center">{{ $t('Exporting') }}...</h5>
<b-progress :max="export_info.total_recipes">
<b-progress-bar :value="export_info.exported_recipes" :label="`${export_info.exported_recipes}/${export_info.total_recipes}`"></b-progress-bar>
</b-progress>
<b-progress :max="export_info.total_recipes">
<b-progress-bar :value="export_info.exported_recipes" :label="`${export_info.exported_recipes}/${export_info.total_recipes}`"></b-progress-bar>
</b-progress>
<loading-spinner :size="25"></loading-spinner>
</template>
<loading-spinner :size="25"></loading-spinner>
</template>
<div class="row">
<div class="col col-md-12" v-if="!export_info.running">
<span>{{ $t("Export_finished") }}! </span> <a :href="`${resolveDjangoUrl('viewExport')}`">{{ $t("Return to export") }} </a><br /><br />
<div class="row">
<div class="col col-md-12" v-if="!export_info.running">
<span>{{ $t('Export_finished') }}! </span> <a :href="`${resolveDjangoUrl('viewExport') }`">{{ $t('Return to export') }} </a><br><br>
{{ $t("If download did not start automatically: ") }}
{{ $t('If download did not start automatically: ') }}
<template v-if="export_info.expired">
<a disabled><del>{{ $t('Download') }}</del></a> ({{ $t('Expired') }})
</template>
<a v-else :href="`/export-file/${export_id}/`" ref="downloadAnchor" >{{ $t('Download') }}</a>
<template v-if="export_info.expired">
<a disabled
><del>{{ $t("Download") }}</del></a
>
({{ $t("Expired") }})
</template>
<a v-else :href="`${resolveDjangoUrl('view_export_file', export_id)}`" ref="downloadAnchor">{{ $t("Download") }}</a>
<br>
{{ $t('The link will remain active for') }}
<template v-if="export_info.cache_duration > 3600">
{{ export_info.cache_duration/3600 }}{{ $t('hr') }}
</template>
<template v-else-if="export_info.cache_duration > 60">
{{ export_info.cache_duration/60 }}{{ $t('min') }}
</template>
<template v-else>
{{ export_info.cache_duration }}{{ $t('sec') }}
</template>
<br />
{{ $t("The link will remain active for") }}
<template v-if="export_info.cache_duration > 3600"> {{ export_info.cache_duration / 3600 }}{{ $t("hr") }} </template>
<template v-else-if="export_info.cache_duration > 60"> {{ export_info.cache_duration / 60 }}{{ $t("min") }} </template>
<template v-else> {{ export_info.cache_duration }}{{ $t("sec") }} </template>
<br>
</div>
</div>
<br/>
<div class="row">
<div class="col col-md-12">
<label for="id_textarea">{{ $t('Information') }}</label>
<textarea id="id_textarea" ref="output_text" class="form-control" style="height: 50vh"
v-html="export_info.msg"
disabled></textarea>
</div>
</div>
<br/>
<br/>
</template>
</div>
<br />
</div>
</div>
<br />
<div class="row">
<div class="col col-md-12">
<label for="id_textarea">{{ $t("Information") }}</label>
<textarea id="id_textarea" ref="output_text" class="form-control" style="height: 50vh" v-html="export_info.msg" disabled></textarea>
</div>
</div>
<br />
<br />
</template>
</div>
</template>
<script>
import Vue from 'vue'
import {BootstrapVue} from 'bootstrap-vue'
import Vue from "vue"
import { BootstrapVue } from "bootstrap-vue"
import 'bootstrap-vue/dist/bootstrap-vue.css'
import "bootstrap-vue/dist/bootstrap-vue.css"
import {ResolveUrlMixin, makeToast, ToastMixin} from "@/utils/utils";
import { ResolveUrlMixin, makeToast, ToastMixin } from "@/utils/utils"
import LoadingSpinner from "@/components/LoadingSpinner";
import LoadingSpinner from "@/components/LoadingSpinner"
import {ApiApiFactory} from "@/utils/openapi/api.ts";
import { ApiApiFactory } from "@/utils/openapi/api.ts"
Vue.use(BootstrapVue)
export default {
name: 'ExportResponseView',
mixins: [
ResolveUrlMixin,
ToastMixin,
],
components: {
LoadingSpinner
},
data() {
return {
export_id: window.EXPORT_ID,
export_info: undefined,
}
},
mounted() {
this.refreshData()
this.$i18n.locale = window.CUSTOM_LOCALE
this.dynamicIntervalTimeout = 250 //initial refresh rate
this.run = setTimeout(this.dynamicInterval.bind(this), this.dynamicIntervalTimeout)
},
methods: {
dynamicInterval: function(){
//update frequently at start but slowdown as it takes longer
this.dynamicIntervalTimeout = Math.round(this.dynamicIntervalTimeout*((1+Math.sqrt(5))/2))
if(this.dynamicIntervalTimeout > 5000) this.dynamicIntervalTimeout = 5000
clearInterval(this.run);
this.run = setInterval(this.dynamicInterval.bind(this), this.dynamicIntervalTimeout);
if ((this.export_id !== null) && window.navigator.onLine && this.export_info.running) {
name: "ExportResponseView",
mixins: [ResolveUrlMixin, ToastMixin],
components: {
LoadingSpinner,
},
data() {
return {
export_id: window.EXPORT_ID,
export_info: undefined,
}
},
mounted() {
this.refreshData()
let el = this.$refs.output_text
el.scrollTop = el.scrollHeight;
this.$i18n.locale = window.CUSTOM_LOCALE
if(this.export_info.expired)
makeToast(this.$t("Error"), this.$t("The download link is expired!"), "danger")
}
this.dynamicIntervalTimeout = 250 //initial refresh rate
this.run = setTimeout(this.dynamicInterval.bind(this), this.dynamicIntervalTimeout)
},
methods: {
dynamicInterval: function () {
//update frequently at start but slowdown as it takes longer
this.dynamicIntervalTimeout = Math.round(this.dynamicIntervalTimeout * ((1 + Math.sqrt(5)) / 2))
if (this.dynamicIntervalTimeout > 5000) this.dynamicIntervalTimeout = 5000
clearInterval(this.run)
this.run = setInterval(this.dynamicInterval.bind(this), this.dynamicIntervalTimeout)
startDownload: function(){
this.$refs['downloadAnchor'].click()
if (this.export_id !== null && window.navigator.onLine && this.export_info.running) {
this.refreshData()
let el = this.$refs.output_text
el.scrollTop = el.scrollHeight
if (this.export_info.expired) makeToast(this.$t("Error"), this.$t("The download link is expired!"), "danger")
}
},
startDownload: function () {
this.$refs["downloadAnchor"].click()
},
refreshData: function () {
let apiClient = new ApiApiFactory()
apiClient.retrieveExportLog(this.export_id).then((result) => {
this.export_info = result.data
this.export_info.expired = !this.export_info.possibly_not_expired
if (!this.export_info.running)
this.$nextTick(() => {
this.startDownload()
})
})
},
},
refreshData: function () {
let apiClient = new ApiApiFactory()
apiClient.retrieveExportLog(this.export_id).then(result => {
this.export_info = result.data
this.export_info.expired = !this.export_info.possibly_not_expired
if(!this.export_info.running)
this.$nextTick(()=>{ this.startDownload(); } )
})
}
}
}
</script>
<style>
</style>
<style></style>

View File

@@ -1,174 +1,180 @@
<template>
<div id="app">
<div id="app">
<h2>{{ $t("Export") }}</h2>
<div class="row">
<div class="col col-md-12">
<br />
<!-- TODO get option dynamicaly -->
<select class="form-control" v-model="recipe_app">
<option value="DEFAULT">Default</option>
<option value="SAFFRON">Saffron</option>
<option value="RECIPESAGE">Recipe Sage</option>
<option value="PDF">PDF (experimental)</option>
</select>
<h2>{{ $t('Export') }}</h2>
<div class="row">
<div class="col col-md-12">
<br />
<b-form-checkbox v-model="export_all" @change="disabled_multiselect = $event" name="check-button" switch style="margin-top: 1vh">
{{ $t("All recipes") }}
</b-form-checkbox>
<br/>
<!-- TODO get option dynamicaly -->
<select class="form-control" v-model="recipe_app">
<option value="DEFAULT">Default</option>
<option value="SAFFRON">Saffron</option>
<option value="RECIPESAGE">Recipe Sage</option>
<option value="PDF">PDF (experimental)</option>
</select>
<!-- <multiselect
:searchable="true"
:disabled="disabled_multiselect"
v-model="recipe_list"
:options="recipes"
:close-on-select="false"
:clear-on-select="true"
:hide-selected="true"
:preserve-search="true"
placeholder="Select Recipes"
:taggable="false"
label="name"
track-by="id"
id="id_recipes"
:multiple="true"
:loading="recipes_loading"
@search-change="searchRecipes"
>
</multiselect> -->
<generic-multiselect
class="input-group-text m-0 p-0"
@change="recipe_list = $event.val"
label="name"
:model="Models.RECIPE"
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
v-bind:placeholder="$t('Recipe')"
:limit="20"
:multiple="true"
/>
<generic-multiselect
@change="filter = $event.val"
:model="Models.CUSTOM_FILTER"
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
:placeholder="$t('Custom Filter')"
:multiple="false"
:limit="50"
/>
<br/>
<b-form-checkbox v-model="export_all" @change="disabled_multiselect=$event" name="check-button" switch style="margin-top: 1vh">
{{ $t('All recipes') }}
</b-form-checkbox>
<multiselect
:searchable="true"
:disabled="disabled_multiselect"
v-model="recipe_list"
:options="recipes"
:close-on-select="false"
:clear-on-select="true"
:hide-selected="true"
:preserve-search="true"
placeholder="Select Recipes"
:taggable="false"
label="name"
track-by="id"
id="id_recipes"
:multiple="true"
:loading="recipes_loading"
@search-change="searchRecipes">
</multiselect>
<br/>
<button @click="exportRecipe()" class="btn btn-primary shadow-none"><i class="fas fa-file-export"></i> {{ $t('Export') }}
</button>
<br />
<button @click="exportRecipe()" class="btn btn-primary shadow-none"><i class="fas fa-file-export"></i> {{ $t("Export") }}</button>
</div>
</div>
</div>
</div>
</template>
<script>
import Vue from 'vue'
import {BootstrapVue} from 'bootstrap-vue'
import Vue from "vue"
import { BootstrapVue } from "bootstrap-vue"
import 'bootstrap-vue/dist/bootstrap-vue.css'
import "bootstrap-vue/dist/bootstrap-vue.css"
import LoadingSpinner from "@/components/LoadingSpinner"
import LoadingSpinner from "@/components/LoadingSpinner";
import {StandardToasts, makeToast, resolveDjangoUrl} from "@/utils/utils";
import Multiselect from "vue-multiselect";
import {ApiApiFactory} from "@/utils/openapi/api.ts";
import axios from "axios";
import { StandardToasts, makeToast, resolveDjangoUrl, ApiMixin } from "@/utils/utils"
// import Multiselect from "vue-multiselect"
import GenericMultiselect from "@/components/GenericMultiselect"
import { ApiApiFactory } from "@/utils/openapi/api.ts"
import axios from "axios"
Vue.use(BootstrapVue)
export default {
name: 'ExportView',
/*mixins: [
name: "ExportView",
/*mixins: [
ResolveUrlMixin,
ToastMixin,
],*/
components: {Multiselect},
data() {
return {
export_id: window.EXPORT_ID,
loading: false,
disabled_multiselect: false,
components: { GenericMultiselect },
mixins: [ApiMixin],
data() {
return {
export_id: window.EXPORT_ID,
loading: false,
disabled_multiselect: false,
recipe_app: 'DEFAULT',
recipe_list: [],
recipes_loading: false,
recipes: [],
export_all: false,
}
},
mounted() {
if(this.export_id)
this.insertRequested()
else
this.searchRecipes('')
},
methods: {
insertRequested: function(){
let apiFactory = new ApiApiFactory()
this.recipes_loading = true
apiFactory.retrieveRecipe(this.export_id).then((response) => {
this.recipes_loading = false
this.recipe_list.push(response.data)
}).catch((err) => {
console.log(err)
StandardToasts.makeStandardToast(StandardToasts.FAIL_FETCH)
}).then(e => this.searchRecipes(''))
recipe_app: "DEFAULT",
recipe_list: [],
recipes_loading: false,
recipes: [],
export_all: false,
filter: undefined,
}
},
searchRecipes: function (query) {
let apiFactory = new ApiApiFactory()
this.recipes_loading = true
let maxResultLenght = 1000
apiFactory.listRecipes(query, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, 1, maxResultLenght).then((response) => {
this.recipes = response.data.results;
this.recipes_loading = false
}).catch((err) => {
console.log(err)
StandardToasts.makeStandardToast(StandardToasts.FAIL_FETCH)
})
mounted() {
if (this.export_id) this.insertRequested()
// else this.searchRecipes("")
},
methods: {
insertRequested: function () {
let apiFactory = new ApiApiFactory()
exportRecipe: function () {
this.recipes_loading = true
if (this.recipe_list.length < 1 && this.export_all == false) {
makeToast(this.$t("Error"), this.$t("Select at least one recipe"), "danger")
return;
}
apiFactory
.retrieveRecipe(this.export_id)
.then((response) => {
this.recipes_loading = false
this.recipe_list.push(response.data)
})
.catch((err) => {
console.log(err)
StandardToasts.makeStandardToast(StandardToasts.FAIL_FETCH)
})
// .then((e) => this.searchRecipes(""))
},
this.error = undefined
this.loading = true
let formData = new FormData();
formData.append('type', this.recipe_app);
formData.append('all', this.export_all)
// searchRecipes: function (query) {
// this.recipes_loading = true
for (var i = 0; i < this.recipe_list.length; i++) {
formData.append('recipes', this.recipe_list[i].id);
}
// this.genericAPI(this.Models.RECIPE, this.Actions.LIST, { query: query })
// .then((response) => {
// this.recipes = response.data.results
// this.recipes_loading = false
// })
// .catch((err) => {
// console.log(err)
// StandardToasts.makeStandardToast(StandardToasts.FAIL_FETCH)
// })
// },
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded';
axios.post(resolveDjangoUrl('view_export',), formData).then((response) => {
if (response.data['error'] !== undefined){
makeToast(this.$t("Error"), response.data['error'],"warning")
}else{
window.location.href = resolveDjangoUrl('view_export_response', response.data['export_id'])
}
exportRecipe: function () {
if (this.recipe_list.length < 1 && this.export_all == false && this.filter === undefined) {
makeToast(this.$t("Error"), this.$t("Select at least one recipe"), "danger")
return
}
}).catch((err) => {
this.error = err.data
this.loading = false
console.log(err)
makeToast(this.$t("Error"), this.$t("There was an error loading a resource!"), "warning")
})
this.error = undefined
this.loading = true
let formData = new FormData()
formData.append("type", this.recipe_app)
formData.append("all", this.export_all)
formData.append("filter", this.filter?.id ?? null)
for (var i = 0; i < this.recipe_list.length; i++) {
formData.append("recipes", this.recipe_list[i].id)
}
axios.defaults.headers.post["Content-Type"] = "application/x-www-form-urlencoded"
axios
.post(resolveDjangoUrl("view_export"), formData)
.then((response) => {
if (response.data["error"] !== undefined) {
makeToast(this.$t("Error"), response.data["error"], "warning")
} else {
window.location.href = resolveDjangoUrl("view_export_response", response.data["export_id"])
}
})
.catch((err) => {
this.error = err.data
this.loading = false
console.log(err)
makeToast(this.$t("Error"), this.$t("There was an error loading a resource!"), "warning")
})
},
},
}
}
</script>
<style src="vue-multiselect/dist/vue-multiselect.min.css"></style>
<style>
</style>
<style></style>

View File

@@ -65,8 +65,11 @@
:preserve-search="true"
:internal-search="false"
:limit="options_limit"
placeholder="Select Keyword"
tag-placeholder="Add Keyword"
:placeholder="$t('select_keyword')"
:tag-placeholder="$t('add_keyword')"
:select-label="$t('Select')"
:selected-label="$t('Selected')"
:deselect-label="$t('remove_selection')"
:taggable="true"
@tag="addKeyword"
label="label"
@@ -76,6 +79,7 @@
:loading="keywords_loading"
@search-change="searchKeywords"
>
<template v-slot:noOptions>{{ $t("empty_list") }}</template>
</multiselect>
</div>
</div>
@@ -244,8 +248,10 @@
:clear-on-select="true"
:allow-empty="true"
:preserve-search="true"
placeholder="Select File"
select-label="Select"
:placeholder="$t('select_file')"
:select-label="$t('Select')"
:selected-label="$t('Selected')"
:deselect-label="$t('remove_selection')"
:id="'id_step_' + step.id + '_file'"
label="name"
track-by="name"
@@ -254,6 +260,7 @@
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
@search-change="searchFiles"
>
<template v-slot:noOptions>{{ $t("empty_list") }}</template>
</multiselect>
<b-input-group-append>
<b-button
@@ -283,14 +290,17 @@
:preserve-search="true"
:internal-search="false"
:limit="options_limit"
placeholder="Select Recipe"
select-label="Select"
:placeholder="$t('select_recipe')"
:select-label="$t('Select')"
:selected-label="$t('Selected')"
:deselect-label="$t('remove_selection')"
:id="'id_step_' + step.id + '_recipe'"
:custom-label="(opt) => recipes.find((x) => x.id === opt).name"
:multiple="false"
:loading="recipes_loading"
@search-change="searchRecipes"
>
<template v-slot:noOptions>{{ $t("empty_list") }}</template>
</multiselect>
</div>
</div>
@@ -340,9 +350,11 @@
:preserve-search="true"
:internal-search="false"
:limit="options_limit"
placeholder="Select Unit"
tag-placeholder="Create"
select-label="Select"
:placeholder="$t('select_unit')"
:tag-placeholder="$t('Create')"
:select-label="$t('Select')"
:selected-label="$t('Selected')"
:deselect-label="$t('remove_selection')"
:taggable="true"
@tag="addUnitType"
:id="`unit_${step_index}_${index}`"
@@ -352,6 +364,7 @@
:loading="units_loading"
@search-change="searchUnits"
>
<template v-slot:noOptions>{{ $t("empty_list") }}</template>
</multiselect>
</div>
<div class="col-lg-4 col-md-6 small-padding" v-if="!ingredient.is_header">
@@ -367,9 +380,11 @@
:preserve-search="true"
:internal-search="false"
:limit="options_limit"
placeholder="Select Food"
tag-placeholder="Create"
select-label="Select"
:placeholder="$t('select_food')"
:tag-placeholder="$t('Create')"
:select-label="$t('Select')"
:selected-label="$t('Selected')"
:deselect-label="$t('remove_selection')"
:taggable="true"
@tag="addFoodType"
:id="`ingredient_${step_index}_${index}`"
@@ -379,6 +394,7 @@
:loading="foods_loading"
@search-change="searchFoods"
>
<template v-slot:noOptions>{{ $t("empty_list") }}</template>
</multiselect>
</div>
<div class="small-padding" v-bind:class="{ 'col-lg-4 col-md-6': !ingredient.is_header, 'col-lg-12 col-md-12': ingredient.is_header }">
@@ -804,7 +820,7 @@ export default {
no_amount: false,
})
this.sortIngredients(step)
this.$nextTick(() => document.getElementById(`amount_${this.recipe.steps.indexOf(step)}_${step.ingredients.length - 1}`).focus())
this.$nextTick(() => document.getElementById(`amount_${this.recipe.steps.indexOf(step)}_${step.ingredients.length - 1}`).select())
},
removeIngredient: function (step, ingredient) {
if (confirm(this.$t("confirm_delete", { object: this.$t("Ingredient") }))) {
@@ -985,6 +1001,7 @@ export default {
unit: unit,
food: { name: result.data.food },
note: result.data.note,
original_text: ing,
})
})
order++

File diff suppressed because it is too large Load Diff

View File

@@ -934,6 +934,8 @@ export default {
this.ui = Object.assign({}, this.ui, this.$cookies.get(SETTINGS_COOKIE_NAME))
}
})
this.$i18n.locale = window.CUSTOM_LOCALE
console.log(window.CUSTOM_LOCALE)
},
methods: {
// this.genericAPI inherited from ApiMixin
@@ -1491,7 +1493,7 @@ export default {
flex-grow: 1;
overflow-y: scroll;
overflow-x: hidden;
height: 6vh;
height: 60vh; /* TODO use proper fill height here to not render list underneath bottom buttons */
padding-right: 8px !important;
}
}

View File

@@ -6,7 +6,7 @@
<b-sidebar id="related-recipes" backdrop right bottom no-header shadow="sm" style="z-index: 10000" @shown="updatePinnedRecipes()">
<template #default="{ hide }">
<div class="d-flex flex-column justify-content-end h-100 p-3 align-items-end">
<h5>Planned <i class="fas fa-calendar fa-fw"></i></h5>
<h5>{{$t("Planned")}} <i class="fas fa-calendar fa-fw"></i></h5>
<div class="text-right">
<template v-if="planned_recipes.length > 0">
@@ -24,11 +24,11 @@
</div>
</template>
<template v-else>
<span class="text-muted">You have nothing planned for today!</span>
<span class="text-muted">{{$t("nothing_planned_today")}}</span>
</template>
</div>
<h5>Pinned <i class="fas fa-thumbtack fa-fw"></i></h5>
<h5>{{$t("Pinned")}} <i class="fas fa-thumbtack fa-fw"></i></h5>
<template v-if="pinned_recipes.length > 0">
<div class="text-right">
@@ -53,7 +53,7 @@
</div>
</template>
<template v-else>
<span class="text-muted">You have no pinned recipes!</span>
<span class="text-muted">{{$t("no_pinned_recipes")}}</span>
</template>
<template v-if="related_recipes.length > 0">
@@ -77,8 +77,8 @@
</template>
<template #footer="{ hide }">
<div class="d-flex bg-dark text-light align-items-center px-3 py-2">
<strong class="mr-auto">Quick actions</strong>
<b-button size="sm" @click="hide">Close</b-button>
<strong class="mr-auto">{{$t("Quick actions")}}</strong>
<b-button size="sm" @click="hide">{{$t("Close")}}</b-button>
</div>
</template>
</b-sidebar>

View File

@@ -52,10 +52,12 @@ export default {
page_count: function () {
return Math.ceil(this.page_count_pagination / this.per_page_count)
},
display_recipes: function() {
return this.recipes.slice((this.current_page - 1 - 1) * 2, (this.current_page - 1) * 2)
}
},
data() {
return {
display_recipes: [],
current_page: 1,
per_page_count: 2,
bounce_left: false,
@@ -66,18 +68,23 @@ export default {
methods: {
pageChange: function (page) {
this.current_page = page
this.display_recipes = this.recipes.slice((this.current_page - 1 - 1) * 2, (this.current_page - 1) * 2)
this.loadRecipeDetails(page)
},
loadRecipeDetails: function (page) {
this.display_recipes.forEach((recipe, index) => {
if (recipe.recipe_content.steps === undefined) {
let apiClient = new ApiApiFactory()
apiClient.retrieveRecipe(recipe.recipe).then((result) => {
let new_entry = Object.assign({}, recipe)
new_entry.recipe_content = result.data
this.$set(this.display_recipes, index, new_entry)
let new_entry = Object.assign({}, recipe)
new_entry.recipe_content = result.data
this.recipes.forEach((rec, i) => {
if (rec.recipe === new_entry.recipe) {
this.$set(this.recipes, i, new_entry)
}
})
})
}
})
},
swipeLeft: function () {

View File

@@ -21,11 +21,11 @@
</div>
<div class="actionArea pt-1 pb-1 d-none d-lg-flex">
<span class="period-span-1 pt-1 pb-1 pl-1 pr-1 d-none d-xl-inline-flex text-body align-items-center">
<small>Period:</small>
<small>{{ $t('Period') }}:</small>
<b-form-select class="ml-1" id="UomInput" v-model="settings.displayPeriodUom" :options="options.displayPeriodUom"></b-form-select>
</span>
<span class="period-span-2 pt-1 pb-1 pl-1 pr-1 mr-1 ml-1 d-none d-xl-inline-flex text-body align-items-center">
<small>Periods:</small>
<small>{{ $t('Periods') }}:</small>
<b-form-select class="ml-1" id="UomInput" v-model="settings.displayPeriodCount" :options="options.displayPeriodCount"></b-form-select>
</span>
<span

View File

@@ -206,7 +206,7 @@ export default {
}
if (!cancel) {
this.$bvModal.hide(`edit-modal`)
this.$emit("save-entry", { ...this.mealplan_settings, ...this.entryEditing, ...{ addshopping: this.entryEditing.addshopping && !this.autoMealPlan } })
this.$emit("save-entry", { ...this.mealplan_settings, ...this.entryEditing, ...{ addshopping: this.mealplan_settings.addshopping && !this.autoMealPlan } })
}
},
deleteEntry() {

View File

@@ -1,39 +1,42 @@
<template>
<div>
<b-form-group
v-bind:label="label"
class="mb-3">
<b-form-select v-model="new_value" :placeholder="placeholder" :options="options"></b-form-select>
<b-form-group v-bind:label="label" class="mb-3">
<b-form-select v-model="new_value" :placeholder="placeholder" :options="translatedOptions"></b-form-select>
</b-form-group>
</div>
</template>
<script>
export default {
name: 'ChoiceInput',
props: {
field: {type: String, default: 'You Forgot To Set Field Name'},
label: {type: String, default: 'Text Field'},
value: {type: String, default: ''},
options: [],
placeholder: {type: String, default: 'You Should Add Placeholder Text'},
show_merge: {type: Boolean, default: false},
},
data() {
return {
new_value: undefined,
}
},
mounted() {
this.new_value = this.value
},
watch: {
'new_value': function () {
this.$root.$emit('change', this.field, this.new_value)
name: "ChoiceInput",
props: {
field: { type: String, default: "You Forgot To Set Field Name" },
label: { type: String, default: "Text Field" },
value: { type: String, default: "" },
options: [],
placeholder: { type: String, default: "You Should Add Placeholder Text" },
show_merge: { type: Boolean, default: false },
},
},
methods: {
}
data() {
return {
new_value: undefined,
}
},
mounted() {
this.new_value = this.value
},
watch: {
new_value: function () {
this.$root.$emit("change", this.field, this.new_value)
},
},
computed: {
translatedOptions() {
return this.options.map((x) => {
return { ...x, text: this.$t(x.text) }
})
},
},
methods: {},
}
</script>
</script>

View File

@@ -8,7 +8,7 @@
:initialContent="value"
:emojiData="emojiDataAll"
:emojiGroups="emojiGroups"
triggerType="hover"
triggerType="click"
:recentEmojisFeat="true"
recentEmojisStorage="local"
@contentChanged="setIcon"

View File

@@ -67,6 +67,9 @@ export default {
this.field = this.form?.field ?? "You Forgot To Set Field Name"
this.label = this.form?.label ?? ""
this.sticky_options = this.form?.sticky_options ?? []
this.sticky_options = this.sticky_options.map((x) => {
return { ...x, name: this.$t(x.name) }
})
this.list_label = this.form?.list_label ?? undefined
if (this.list_label?.includes("::")) {
this.list_label = this.list_label.split("::")[1]
@@ -74,7 +77,7 @@ export default {
},
computed: {
modelName() {
return this?.model?.name ?? this.$t("Search")
return this.$t(this?.model?.name) ?? this.$t("Search")
},
useMultiple() {
return this.form?.multiple || this.form?.ordered || false

View File

@@ -7,7 +7,7 @@
<div class="text-nowrap"><i class="fa fa-chevron-right rotate" :class="showDetails ? 'rotated' : ''"></i></div>
</b-button>
</b-col>
<b-col cols="1" class="align-items-center d-flex">
<b-col cols="2" md="1" class="align-items-center d-flex">
<div class="dropdown b-dropdown position-static inline-block" data-html2canvas-ignore="true" @click.stop="$emit('open-context-menu', $event, entries)">
<button
aria-haspopup="true"
@@ -23,7 +23,7 @@
<b-col cols="1" class="px-1 justify-content-center align-items-center d-none d-md-flex">
<input type="checkbox" class="form-control form-control-sm checkbox-control" :checked="formatChecked" @change="updateChecked" :key="entries[0].id" />
</b-col>
<b-col cols="8" md="9">
<b-col cols="7" md="9">
<b-row class="d-flex h-100">
<b-col cols="5" md="3" class="d-flex align-items-center" v-if="Object.entries(formatAmount).length == 1">
<strong class="mr-1">{{ Object.entries(formatAmount)[0][1] }}</strong> {{ Object.entries(formatAmount)[0][0] }}
@@ -86,7 +86,7 @@
<b-col cols="3" md="2" class="justify-content-start align-items-center d-flex d-md-none pr-0" v-if="settings.left_handed">
<input type="checkbox" class="form-control form-control-sm checkbox-control-mobile" :checked="formatChecked" @change="updateChecked" :key="entries[0].id" />
</b-col>
<b-col cols="1" class="align-items-center d-flex">
<b-col cols="2" md="1" class="align-items-center d-flex">
<div class="dropdown b-dropdown position-static inline-block" data-html2canvas-ignore="true" @click.stop="$emit('open-context-menu', $event, e)">
<button
aria-haspopup="true"
@@ -102,7 +102,7 @@
<b-col cols="1" class="justify-content-center align-items-center d-none d-md-flex">
<input type="checkbox" class="form-control form-control-sm checkbox-control" :checked="formatChecked" @change="updateChecked" :key="entries[0].id" />
</b-col>
<b-col cols="8" md="9">
<b-col cols="7" md="9">
<b-row class="d-flex align-items-center h-100">
<b-col cols="5" md="3" class="d-flex align-items-center">
<strong class="mr-1">{{ formatOneAmount(e) }}</strong> {{ formatOneUnit(e) }}

View File

@@ -137,7 +137,7 @@
"Move_Down": "Runter",
"Step_Name": "Schritt Name",
"Create": "Erstellen",
"Advanced Search Settings": "Erweiterte Sucheinstellungen",
"advanced_search_settings": "Erweiterte Sucheinstellungen",
"View": "Ansicht",
"Recipes": "Rezepte",
"Move": "Verschieben",
@@ -249,7 +249,7 @@
"shopping_auto_sync_desc": "Bei 0 wird Auto-Sync deaktiviert. Beim Betrachten einer Einkaufsliste wird die Liste alle gesetzten Sekunden aktualisiert, um mögliche Änderungen anderer zu zeigen. Nützlich, wenn mehrere Personen einkaufen und mobile Daten nutzen.",
"MoveCategory": "Verschieben nach: ",
"mealplan_autoadd_shopping_desc": "Essensplan-Zutaten automatisch zur Einkaufsliste hinzufügen.",
"Pin": "Pin",
"Pin": "Anheften",
"mark_complete": "Vollständig markieren",
"shopping_add_onhand_desc": "Markiere Lebensmittel als \"Vorrätig\", wenn von der Einkaufsliste abgehakt wurden.",
"left_handed": "Linkshänder-Modus",
@@ -298,5 +298,61 @@
"Foods": "Lebensmittel",
"food_recipe_help": "Wird ein Rezept hier verknüpft, wird diese Verknüpfung in allen anderen Rezepten übernommen, die dieses Lebensmittel beinhaltet",
"review_shopping": "Überprüfe die Einkaufsliste vor dem Speichern",
"view_recipe": "Rezept anschauen"
"view_recipe": "Rezept anschauen",
"Planned": "Geplant",
"Pinned": "Angeheftet",
"nothing_planned_today": "Sie haben für heute nichts geplant!",
"no_pinned_recipes": "Sie haben nichts angeheftet!",
"Quick actions": "Schnellbefehle",
"search_no_recipes": "Keine Rezepte gefunden!",
"search_import_help_text": "Importiere ein Rezept von einer externen Webseite oder Anwendung.",
"search_create_help_text": "Erstelle ein neues Rezept direkt in Tandoor.",
"Ratings": "Bewertungen",
"Custom Filter": "Benutzerdefinierter Filter",
"expert_mode": "Experten-Modus",
"simple_mode": "Einfacher Modus",
"explain": "Erklären",
"save_filter": "Filter speichern",
"Internal": "Intern",
"advanced": "Erweitert",
"fields": "Felder",
"show_keywords": "Schlüsselwörter anzeigen",
"show_foods": "Zutaten anzeigen",
"show_books": "Bücher anzeigen",
"show_rating": "Bewertungen anzeigen",
"show_units": "Einheiten anzeigen",
"show_filters": "Filter anzeigen",
"times_cooked": "Wie oft gekocht",
"show_sortby": "Zeige 'Sortiere nach'",
"make_now": "Jetzt machen",
"date_viewed": "Letztens besucht",
"last_cooked": "Letztens gekocht",
"created_on": "Erstellt am",
"updatedon": "Geändert am",
"date_created": "Erstellungsdatum",
"Units": "Einheiten",
"last_viewed": "Letztens besucht",
"sort_by": "Sortiere nach",
"Random Recipes": "Zufällige Rezepte",
"recipe_filter": "Rezept-Filter",
"parameter_count": "Parameter {count}",
"select_keyword": "Stichwort auswählen",
"add_keyword": "Stichwort hinzufügen",
"select_file": "Datei auswählen",
"select_recipe": "Rezept auswählen",
"select_unit": "Einheit wählen",
"select_food": "Zutat auswählen",
"remove_selection": "Abwählen",
"empty_list": "Liste ist leer.",
"Select": "Auswählen",
"Supermarkets": "Supermärkte",
"User": "Benutzer",
"Keyword": "Schlüsselwort",
"Advanced": "Erweitert",
"Substitutes": "Zusätze",
"copy_to_new": "Kopiere zu neuem Rezept",
"Page": "Seite",
"Reset": "Zurücksetzen",
"search_rank": "Such-Rang",
"paste_ingredients": "Zutaten einfügen"
}

View File

@@ -355,5 +355,31 @@
"InheritFields_help": "The values of these fields will be inheritted from parent (Exception: blank shopping categories are not inheritted)",
"last_viewed": "Last Viewed",
"created_on": "Created On",
"updatedon": "Updated On"
"updatedon": "Updated On",
"advanced_search_settings": "Advanced Search Settings",
"nothing_planned_today": "You have nothing planned for today!",
"no_pinned_recipes": "You have no pinned recipes!",
"Planned": "Planned",
"Pinned": "Pinned",
"Quick actions": "Quick actions",
"Ratings": "Ratings",
"Internal": "Internal",
"Units": "Units",
"Random Recipes": "Random Recipes",
"parameter_count": "Parameter {count}",
"select_keyword": "Select Keyword",
"add_keyword": "Add Keyword",
"select_file": "Select File",
"select_recipe": "Select Recipe",
"select_unit": "Select Unit",
"select_food": "Select Food",
"remove_selection": "Deselect",
"empty_list": "List is empty.",
"Select": "Select",
"Supermarkets": "Supermarkets",
"User": "User",
"Keyword": "Keyword",
"Advanced": "Advanced",
"Page": "Page",
"Reset": "Reset"
}

View File

@@ -1,7 +1,6 @@
/*
* Utility CLASS to define model configurations
* */
import i18n from "@/i18n"
// TODO this needs rethought and simplified
// maybe a function that returns a single dictionary based on action?
@@ -51,7 +50,7 @@ export class Models {
type: "lookup",
field: "target",
list: "self",
sticky_options: [{ id: 0, name: i18n.t("tree_root") }],
sticky_options: [{ id: 0, name: "tree_root" }],
},
},
},
@@ -59,7 +58,7 @@ export class Models {
// MODELS - inherits and takes precedence over MODEL_TYPES and ACTIONS
static FOOD = {
name: i18n.t("Food"), // *OPTIONAL* : parameters will be built model -> model_type -> default
name: "Food", // *OPTIONAL* : parameters will be built model -> model_type -> default
apiName: "Food", // *REQUIRED* : the name that is used in api.ts for this model
model_type: this.TREE, // *OPTIONAL* : model specific params for api, if not present will attempt modeltype_create then default_create
paginated: true,
@@ -100,15 +99,15 @@ export class Models {
form_field: true,
type: "text",
field: "name",
label: i18n.t("Name"),
placeholder: "",
label: "Name", // form.label always translated in utils.getForm()
placeholder: "", // form.placeholder always translated
subtitle_field: "full_name",
},
description: {
form_field: true,
type: "text",
field: "description",
label: i18n.t("Description"),
label: "Description", // form.label always translated in utils.getForm()
placeholder: "",
},
recipe: {
@@ -116,31 +115,31 @@ export class Models {
type: "lookup",
field: "recipe",
list: "RECIPE",
label: i18n.t("Recipe"),
help_text: i18n.t("food_recipe_help"),
label: "Recipe", // form.label always translated in utils.getForm()
help_text: "food_recipe_help", // form.help_text always translated
},
onhand: {
form_field: true,
type: "checkbox",
field: "food_onhand",
label: i18n.t("OnHand"),
help_text: i18n.t("OnHand_help"),
label: "OnHand",
help_text: "OnHand_help",
},
ignore_shopping: {
form_field: true,
type: "checkbox",
field: "ignore_shopping",
label: i18n.t("Ignore_Shopping"),
help_text: i18n.t("ignore_shopping_help"),
label: "Ignore_Shopping",
help_text: "ignore_shopping_help",
},
shopping_category: {
form_field: true,
type: "lookup",
field: "supermarket_category",
list: "SHOPPING_CATEGORY",
label: i18n.t("Shopping_Category"),
label: "Shopping_Category",
allow_create: true,
help_text: i18n.t("shopping_category_help"),
help_text: "shopping_category_help", // form.help_text always translated
},
substitute: {
form_field: true,
@@ -149,17 +148,17 @@ export class Models {
multiple: true,
field: "substitute",
list: "FOOD",
label: i18n.t("Substitutes"),
label: "Substitutes",
allow_create: false,
help_text: i18n.t("substitute_help"),
help_text: "substitute_help",
},
substitute_siblings: {
form_field: true,
advanced: true,
type: "checkbox",
field: "substitute_siblings",
label: i18n.t("substitute_siblings"),
help_text: i18n.t("substitute_siblings_help"),
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" },
},
substitute_children: {
@@ -167,8 +166,8 @@ export class Models {
advanced: true,
type: "checkbox",
field: "substitute_children",
label: i18n.t("substitute_children"),
help_text: i18n.t("substitute_children_help"),
label: "substitute_children",
help_text: "substitute_children_help",
condition: { field: "numchild", value: 0, condition: "gt" },
},
inherit_fields: {
@@ -178,9 +177,9 @@ export class Models {
multiple: true,
field: "inherit_fields",
list: "FOOD_INHERIT_FIELDS",
label: i18n.t("InheritFields"),
label: "InheritFields",
condition: { field: "food_children_exist", value: true, condition: "preference_equals" },
help_text: i18n.t("InheritFields_help"),
help_text: "InheritFields_help",
},
child_inherit_fields: {
form_field: true,
@@ -189,17 +188,17 @@ export class Models {
multiple: true,
field: "child_inherit_fields",
list: "FOOD_INHERIT_FIELDS",
label: i18n.t("ChildInheritFields"),
label: "ChildInheritFields", // form.label always translated in utils.getForm()
condition: { field: "numchild", value: 0, condition: "gt" },
help_text: i18n.t("ChildInheritFields_help"),
help_text: "ChildInheritFields_help", // form.help_text always translated
},
reset_inherit: {
form_field: true,
advanced: true,
type: "checkbox",
field: "reset_inherit",
label: i18n.t("reset_children"),
help_text: i18n.t("reset_children_help"),
label: "reset_children",
help_text: "reset_children_help",
condition: { field: "numchild", value: 0, condition: "gt" },
},
form_function: "FoodCreateDefault",
@@ -215,7 +214,7 @@ export class Models {
}
static KEYWORD = {
name: i18n.t("Keyword"), // *OPTIONAL: parameters will be built model -> model_type -> default
name: "Keyword", // *OPTIONAL: parameters will be built model -> model_type -> default
apiName: "Keyword",
model_type: this.TREE,
paginated: true,
@@ -232,21 +231,21 @@ export class Models {
form_field: true,
type: "text",
field: "name",
label: i18n.t("Name"),
label: "Name",
placeholder: "",
},
description: {
form_field: true,
type: "text",
field: "description",
label: i18n.t("Description"),
label: "Description",
placeholder: "",
},
icon: {
form_field: true,
type: "emoji",
field: "icon",
label: i18n.t("Icon"),
label: "Icon",
},
full_name: {
form_field: true,
@@ -258,7 +257,7 @@ export class Models {
}
static UNIT = {
name: i18n.t("Unit"),
name: "Unit",
apiName: "Unit",
paginated: true,
create: {
@@ -268,14 +267,14 @@ export class Models {
form_field: true,
type: "text",
field: "name",
label: i18n.t("Name"),
label: "Name",
placeholder: "",
},
description: {
form_field: true,
type: "text",
field: "description",
label: i18n.t("Description"),
label: "Description",
placeholder: "",
},
},
@@ -284,7 +283,7 @@ export class Models {
}
static SHOPPING_LIST = {
name: i18n.t("Shopping_list"),
name: "Shopping_list",
apiName: "ShoppingListEntry",
list: {
params: ["id", "checked", "supermarket", "options"],
@@ -297,7 +296,7 @@ export class Models {
type: "lookup",
field: "unit",
list: "UNIT",
label: i18n.t("Unit"),
label: "Unit",
allow_create: true,
},
food: {
@@ -305,7 +304,7 @@ export class Models {
type: "lookup",
field: "food",
list: "FOOD",
label: i18n.t("Food"),
label: "Food", // form.label always translated in utils.getForm()
allow_create: true,
},
},
@@ -313,7 +312,7 @@ export class Models {
}
static RECIPE_BOOK = {
name: i18n.t("Recipe_Book"),
name: "Recipe_Book",
apiName: "RecipeBook",
create: {
params: [["name", "description", "icon", "filter"]],
@@ -322,27 +321,27 @@ export class Models {
form_field: true,
type: "text",
field: "name",
label: i18n.t("Name"),
label: "Name",
placeholder: "",
},
description: {
form_field: true,
type: "text",
field: "description",
label: i18n.t("Description"),
label: "Description",
placeholder: "",
},
icon: {
form_field: true,
type: "emoji",
field: "icon",
label: i18n.t("Icon"),
label: "Icon",
},
filter: {
form_field: true,
type: "lookup",
field: "filter",
label: i18n.t("Custom Filter"),
label: "Custom Filter",
list: "CUSTOM_FILTER",
},
},
@@ -350,7 +349,7 @@ export class Models {
}
static SHOPPING_CATEGORY = {
name: i18n.t("Shopping_Category"),
name: "Shopping_Category",
apiName: "SupermarketCategory",
create: {
params: [["name", "description"]],
@@ -359,14 +358,14 @@ export class Models {
form_field: true,
type: "text",
field: "name",
label: i18n.t("Name"),
label: "Name", // form.label always translated in utils.getForm()
placeholder: "",
},
description: {
form_field: true,
type: "text",
field: "description",
label: i18n.t("Description"),
label: "Description",
placeholder: "",
},
},
@@ -374,7 +373,7 @@ export class Models {
}
static SHOPPING_CATEGORY_RELATION = {
name: i18n.t("Shopping_Category_Relation"),
name: "Shopping_Category_Relation",
apiName: "SupermarketCategoryRelation",
create: {
params: [["category", "supermarket", "order"]],
@@ -383,14 +382,14 @@ export class Models {
form_field: true,
type: "text",
field: "name",
label: i18n.t("Name"),
label: "Name",
placeholder: "",
},
description: {
form_field: true,
type: "text",
field: "description",
label: i18n.t("Description"),
label: "Description",
placeholder: "",
},
},
@@ -398,7 +397,7 @@ export class Models {
}
static SUPERMARKET = {
name: i18n.t("Supermarket"),
name: "Supermarket",
apiName: "Supermarket",
ordered_tags: [{ field: "category_to_supermarket", label: "category::name", color: "info" }],
create: {
@@ -408,14 +407,14 @@ export class Models {
form_field: true,
type: "text",
field: "name",
label: i18n.t("Name"),
label: "Name",
placeholder: "",
},
description: {
form_field: true,
type: "text",
field: "description",
label: i18n.t("Description"),
label: "Description",
placeholder: "",
},
categories: {
@@ -425,7 +424,7 @@ export class Models {
list_label: "category::name",
ordered: true, // ordered lookups assume working with relation field
field: "category_to_supermarket",
label: i18n.t("Categories"),
label: "Categories", // form.label always translated in utils.getForm()
placeholder: "",
},
},
@@ -441,7 +440,7 @@ export class Models {
}
static AUTOMATION = {
name: i18n.t("Automation"),
name: "Automation",
apiName: "Automation",
paginated: true,
list: {
@@ -456,47 +455,74 @@ export class Models {
form_field: true,
type: "text",
field: "name",
label: i18n.t("Name"),
label: "Name",
placeholder: "",
},
description: {
form_field: true,
type: "text",
field: "description",
label: i18n.t("Description"),
label: "Description",
placeholder: "",
},
type: {
form_field: true,
type: "choice",
options: [
{ value: "FOOD_ALIAS", text: i18n.t("Food_Alias") },
{ value: "UNIT_ALIAS", text: i18n.t("Unit_Alias") },
{ value: "KEYWORD_ALIAS", text: i18n.t("Keyword_Alias") },
{ value: "FOOD_ALIAS", text: "Food_Alias" },
{ value: "UNIT_ALIAS", text: "Unit_Alias" },
{ value: "KEYWORD_ALIAS", text: "Keyword_Alias" },
],
field: "type",
label: i18n.t("Type"),
label: "Type",
placeholder: "",
},
param_1: {
form_field: true,
type: "text",
field: "param_1",
label: i18n.t("Parameter") + " 1",
label: {
function: "translate",
phrase: "parameter_count",
params: [
{
token: "count",
attribute: "1",
},
],
},
placeholder: "",
},
param_2: {
form_field: true,
type: "text",
field: "param_2",
label: i18n.t("Parameter") + " 2",
label: {
function: "translate",
phrase: "parameter_count",
params: [
{
token: "count",
attribute: "2",
},
],
},
placeholder: "",
},
param_3: {
form_field: true,
type: "text",
field: "param_3",
label: i18n.t("Parameter") + " 3",
label: {
function: "translate",
phrase: "parameter_count",
params: [
{
token: "count",
attribute: "3",
},
],
},
placeholder: "",
},
},
@@ -504,7 +530,7 @@ export class Models {
}
static RECIPE = {
name: i18n.t("Recipe"),
name: "Recipe",
apiName: "Recipe",
list: {
params: [
@@ -546,7 +572,7 @@ export class Models {
}
static CUSTOM_FILTER = {
name: i18n.t("Custom Filter"),
name: "Custom Filter",
apiName: "CustomFilter",
create: {
@@ -556,7 +582,7 @@ export class Models {
form_field: true,
type: "text",
field: "name",
label: i18n.t("Name"),
label: "Name", // form.label always translated in utils.getForm()
placeholder: "",
},
@@ -566,14 +592,14 @@ export class Models {
field: "shared",
list: "USER",
list_label: "username",
label: i18n.t("shared_with"),
label: "shared_with",
multiple: true,
},
},
},
}
static USER_NAME = {
name: i18n.t("User"),
name: "User",
apiName: "User",
list: {
params: ["filter_list"],
@@ -581,7 +607,7 @@ export class Models {
}
static MEAL_TYPE = {
name: i18n.t("Meal_Type"),
name: "Meal_Type",
apiName: "MealType",
list: {
params: ["filter_list"],
@@ -589,7 +615,7 @@ export class Models {
}
static MEAL_PLAN = {
name: i18n.t("Meal_Plan"),
name: "Meal_Plan",
apiName: "MealPlan",
list: {
params: ["options"],
@@ -597,7 +623,7 @@ export class Models {
}
static USERFILE = {
name: i18n.t("File"),
name: "File",
apiName: "UserFile",
paginated: false,
list: {
@@ -612,27 +638,27 @@ export class Models {
form_field: true,
type: "text",
field: "name",
label: i18n.t("Name"),
label: "Name",
placeholder: "",
},
file: {
form_field: true,
type: "file",
field: "file",
label: i18n.t("File"),
label: "File", // form.label always translated in utils.getForm()
placeholder: "",
},
},
},
}
static USER = {
name: i18n.t("User"),
name: "User",
apiName: "User",
paginated: false,
}
static STEP = {
name: i18n.t("Step"),
name: "Step",
apiName: "Step",
list: {
params: ["recipe", "query", "page", "pageSize", "options"],
@@ -652,10 +678,11 @@ export class Actions {
token: "type",
from: "model",
attribute: "name",
translate: true,
},
],
},
ok_label: i18n.t("Save"),
ok_label: { function: "translate", phrase: "Save" },
},
}
static UPDATE = {
@@ -669,6 +696,7 @@ export class Actions {
token: "type",
from: "model",
attribute: "name",
translate: true,
},
],
},
@@ -685,10 +713,11 @@ export class Actions {
token: "type",
from: "model",
attribute: "name",
translate: true,
},
],
},
ok_label: i18n.t("Delete"),
ok_label: { function: "translate", phrase: "Delete" },
instruction: {
form_field: true,
type: "instruction",
@@ -736,10 +765,11 @@ export class Actions {
token: "type",
from: "model",
attribute: "name",
translate: true,
},
],
},
ok_label: i18n.t("Merge"),
ok_label: { function: "translate", phrase: "Merge" },
instruction: {
form_field: true,
type: "instruction",
@@ -756,6 +786,7 @@ export class Actions {
token: "type",
from: "model",
attribute: "name",
translate: true,
},
],
},
@@ -784,10 +815,11 @@ export class Actions {
token: "type",
from: "model",
attribute: "name",
translate: true,
},
],
},
ok_label: i18n.t("Move"),
ok_label: { function: "translate", phrase: "Move" },
instruction: {
form_field: true,
type: "instruction",
@@ -804,6 +836,7 @@ export class Actions {
token: "type",
from: "model",
attribute: "name",
translate: true,
},
],
},

View File

@@ -349,7 +349,7 @@ export function getConfig(model, action) {
}
let config = {
name: model.name,
name: i18n.t(model.name),
apiName: model.apiName,
}
// spread operator merges dictionaries - last item in list takes precedence
@@ -391,8 +391,11 @@ export function getForm(model, action, item1, item2) {
value = v
}
if (value?.form_field) {
for (const [i, h] of Object.entries(value)) {
// console.log("formfield", i)
}
value["value"] = item1?.[value?.field] ?? undefined
value["help"] = item1?.[value?.help_text_field] ?? value?.help_text ?? undefined
value["help"] = item1?.[value?.help_text_field] ?? formTranslate(value?.help_text) ?? undefined
value["subtitle"] = item1?.[value?.subtitle_field] ?? value?.subtitle ?? undefined
form.fields.push({
...value,
@@ -410,23 +413,31 @@ export function getForm(model, action, item1, item2) {
function formTranslate(translate, model, item1, item2) {
if (typeof translate !== "object") {
return translate
return i18n.t(translate)
}
let phrase = translate.phrase
let options = {}
let obj = undefined
translate?.params.forEach(function (x, index) {
switch (x.from) {
let value = undefined
translate?.params?.forEach(function (x, index) {
switch (x?.from) {
case "item1":
obj = item1
value = item1[x.attribute]
break
case "item2":
obj = item2
value = item2[x.attribute]
break
case "model":
obj = model
value = model[x.attribute]
break
default:
value = x.attribute
}
if (x.translate) {
options[x.token] = i18n.t(value)
} else {
options[x.token] = value
}
options[x.token] = obj[x.attribute]
})
return i18n.t(phrase, options)
}

View File

@@ -8548,10 +8548,10 @@ pretty-error@^2.0.2:
lodash "^4.17.20"
renderkid "^2.0.4"
prismjs@^1.13.0, prismjs@^1.23.0, prismjs@^1.25.0:
version "1.25.0"
resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.25.0.tgz#6f822df1bdad965734b310b315a23315cf999756"
integrity sha512-WCjJHl1KEWbnkQom1+SzftbtXMKQoezOCYs5rECqMN+jP+apI7ftoflyqigqzopSO3hMhTEb0mFClA8lkolgEg==
prismjs@^1.13.0, prismjs@^1.23.0, prismjs@^1.27.0:
version "1.27.0"
resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.27.0.tgz#bb6ee3138a0b438a3653dd4d6ce0cc6510a45057"
integrity sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==
process-nextick-args@~2.0.0:
version "2.0.1"
@@ -10410,9 +10410,9 @@ url-loader@^2.2.0:
schema-utils "^2.5.0"
url-parse@^1.4.3, url-parse@^1.5.3:
version "1.5.7"
resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.7.tgz#00780f60dbdae90181f51ed85fb24109422c932a"
integrity sha512-HxWkieX+STA38EDk7CE9MEryFeHCKzgagxlGvsdS7WBImq9Mk+PGwiT56w82WI3aicwJA8REp42Cxo98c8FZMA==
version "1.5.10"
resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.5.10.tgz#9d3c2f736c1d75dd3bd2be507dcc111f1e2ea9c1"
integrity sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==
dependencies:
querystringify "^2.1.1"
requires-port "^1.0.0"