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 publish: true
imageName: vabene1111/recipes imageName: vabene1111/recipes
tag: beta tag: beta
dockerHubUser: ${{ secrets.DOCKER_USERNAME }} dockerUser: ${{ secrets.DOCKER_USERNAME }}
dockerHubPassword: ${{ secrets.DOCKER_PASSWORD }} dockerPassword: ${{ secrets.DOCKER_PASSWORD }}
# Send discord notification # Send discord notification
- name: Discord notification - name: Discord notification
env: env:

View File

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

View File

@ -41,8 +41,8 @@ jobs:
publish: true publish: true
imageName: vabene1111/recipes imageName: vabene1111/recipes
tag: ${{ steps.get_version.outputs.VERSION }} tag: ${{ steps.get_version.outputs.VERSION }}
dockerHubUser: ${{ secrets.DOCKER_USERNAME }} dockerUser: ${{ secrets.DOCKER_USERNAME }}
dockerHubPassword: ${{ secrets.DOCKER_PASSWORD }} dockerPassword: ${{ secrets.DOCKER_PASSWORD }}
# Send discord notification # Send discord notification
- name: Discord notification - name: Discord notification
env: 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. a complete list of contributions.
Below are some of the larger contributions made yet. Below are some of the larger contributions made yet.
- [vabene1111]
- @tourn provided the serving feature and **several** other improvements! - [Kaibu]
- @l0c4lh057 provided a much improved ingredient text parser in [#277](https://github.com/vabene1111/recipes/pull/277) - [smilerz]
- @sebimarkgraf added nutritional information [#199](https://github.com/vabene1111/recipes/pull/199) - [MaxJa4] Docker builds and other improvements
- @cazier added reverse proxy authentication [#88](https://github.com/vabene1111/recipes/pull/88) - [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 ## Translations
@ -30,6 +36,7 @@ Below are some of the larger contributions made yet.
### German ### German
[eTaurus](https://www.transifex.com/user/profile/eTaurus/) [eTaurus](https://www.transifex.com/user/profile/eTaurus/)
[l0c4lh057](https://www.transifex.com/user/profile/l0c4lh057/) [l0c4lh057](https://www.transifex.com/user/profile/l0c4lh057/)
[hyperbit00]
### Hungarian ### Hungarian
[igazka](https://www.transifex.com/user/profile/igazka/) [igazka](https://www.transifex.com/user/profile/igazka/)
@ -60,4 +67,4 @@ Below are some of the larger contributions made yet.
### Vietnamese ### 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 FROM python:3.10-alpine3.15
#Install all dependencies. #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. #Print all logs without buffering it.
ENV PYTHONUNBUFFERED 1 ENV PYTHONUNBUFFERED 1
@ -15,11 +15,12 @@ WORKDIR /opt/recipes
COPY requirements.txt ./ 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 && \ echo -n "INPUT ( libldap.so )" > /usr/lib/libldap_r.so && \
python -m venv venv && \ python -m venv venv && \
/opt/recipes/venv/bin/python -m pip install --upgrade pip && \ /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 &&\ venv/bin/pip install -r requirements.txt --no-cache-dir &&\
apk --purge del .build-deps 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!" display_warning "The environment variable 'SECRET_KEY' is not set but REQUIRED for running Tandoor!"
fi 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..." echo "Waiting for database to be ready..."
attempt=0 attempt=0
max_attempts=20 max_attempts=20
while pg_isready --host=${POSTGRES_HOST} -q; status=$?; attempt=$((attempt+1)); [ $status -ne 0 ] && [ $attempt -le $max_attempts ]; do
sleep 5 if [ "${DB_ENGINE}" != 'django.db.backends.sqlite3' ]; then
done
# 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 if [ $attempt -gt $max_attempts ]; then
echo -e "\nDatabase not reachable. Maximum attempts exceeded." echo -e "\nDatabase not reachable. Maximum attempts exceeded."
@ -58,4 +63,4 @@ echo "Done"
chmod -R 755 /opt/recipes/mediafiles 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): class ExportForm(ImportExportBase):
recipes = forms.ModelMultipleChoiceField(widget=MultiSelectWidget, queryset=Recipe.objects.none(), required=False) recipes = forms.ModelMultipleChoiceField(widget=MultiSelectWidget, queryset=Recipe.objects.none(), required=False)
all = forms.BooleanField(required=False) all = forms.BooleanField(required=False)
custom_filter = forms.IntegerField(required=False)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
space = kwargs.pop('space') 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.HelperFunctions import Round, str2bool
from cookbook.helper.permission_helper import has_group_permission from cookbook.helper.permission_helper import has_group_permission
from cookbook.managers import DICTIONARY from cookbook.managers import DICTIONARY
from cookbook.models import (CookLog, CustomFilter, Food, Keyword, Recipe, SearchFields, from cookbook.models import (CookLog, CustomFilter, Food, Keyword, Recipe, RecipeBook, SearchFields,
SearchPreference, ViewLog, RecipeBook) SearchPreference, ViewLog)
from recipes import settings from recipes import settings
@ -28,7 +28,7 @@ class RecipeSearch():
self._queryset = None self._queryset = None
if f := params.get('filter', 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) | 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: if custom_filter:
self._params = {**json.loads(custom_filter.search)} self._params = {**json.loads(custom_filter.search)}
self._original_params = {**(params or {})} self._original_params = {**(params or {})}
@ -40,7 +40,7 @@ class RecipeSearch():
self._search_prefs = request.user.searchpreference self._search_prefs = request.user.searchpreference
else: else:
self._search_prefs = SearchPreference() 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._rating = self._params.get('rating', None)
self._keywords = { self._keywords = {
'or': self._params.get('keywords_or', None) or self._params.get('keywords', None), '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' self._search_type = self._search_prefs.search or 'plain'
if self._string: 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._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._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 self._trigram_include = None
@ -205,7 +208,7 @@ class RecipeSearch():
else: else:
self._queryset = self._queryset.annotate(simularity=Coalesce(Subquery(simularity), 0.0)) 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: 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: else:
query_filter = Q() 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)]: 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() return self.get_facets()
def _recipe_count_queryset(self, field, depth=1, steplen=4): 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 return Recipe.objects.filter(**{f'{field}__path__startswith': OuterRef('path'), f'{field}__depth__gte': depth}, id__in=self._recipe_list, space=self._request.space
).values(child=Substr(f'{field}__path', 1, steplen*depth) ).annotate(count=Coalesce(Func('pk', function='Count'), 0)).values('count')
).annotate(count=Count('pk', distinct=True)).values('count')
def _keyword_queryset(self, queryset, keyword=None): def _keyword_queryset(self, queryset, keyword=None):
depth = getattr(keyword, 'depth', 0) + 1 depth = getattr(keyword, 'depth', 0) + 1

View File

@ -4,8 +4,10 @@ from html import unescape
from unicodedata import decomposition from unicodedata import decomposition
from django.utils.dateparse import parse_duration 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 import parse_duration as iso_parse_duration
from isodate.isoerror import ISO8601Error from isodate.isoerror import ISO8601Error
from recipe_scrapers._utils import get_minutes
from cookbook.helper import recipe_url_import as helper from cookbook.helper import recipe_url_import as helper
from cookbook.helper.ingredient_parser import IngredientParser from cookbook.helper.ingredient_parser import IngredientParser
@ -29,9 +31,14 @@ def get_from_scraper(scrape, request):
recipe_json['name'] = '' recipe_json['name'] = ''
try: try:
description = scrape.schema.data.get("description") or '' description = scrape.description() or None
except Exception: 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['description'] = parse_description(description)[:512]
recipe_json['internal'] = True recipe_json['internal'] = True
@ -53,13 +60,19 @@ def get_from_scraper(scrape, request):
recipe_json['servings'] = max(servings, 1) recipe_json['servings'] = max(servings, 1)
try: 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: 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: 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: 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: if recipe_json['working_time'] + recipe_json['waiting_time'] == 0:
try: try:
@ -87,15 +100,23 @@ def get_from_scraper(scrape, request):
except Exception: except Exception:
pass pass
try: try:
if scrape.schema.data.get('recipeCategory'): if scrape.category():
keywords += listify_keywords(scrape.schema.data.get("recipeCategory")) keywords += listify_keywords(scrape.category())
except Exception: except Exception:
pass try:
if scrape.schema.data.get('recipeCategory'):
keywords += listify_keywords(scrape.schema.data.get("recipeCategory"))
except Exception:
pass
try: try:
if scrape.schema.data.get('recipeCuisine'): if scrape.cuisine():
keywords += listify_keywords(scrape.schema.data.get("recipeCuisine")) keywords += listify_keywords(scrape.cuisine())
except Exception: except Exception:
pass try:
if scrape.schema.data.get('recipeCuisine'):
keywords += listify_keywords(scrape.schema.data.get("recipeCuisine"))
except Exception:
pass
try: try:
recipe_json['keywords'] = parse_keywords(list(set(map(str.casefold, keywords))), request.space) recipe_json['keywords'] = parse_keywords(list(set(map(str.casefold, keywords))), request.space)
except AttributeError: except AttributeError:
@ -142,8 +163,8 @@ def get_from_scraper(scrape, request):
except Exception: except Exception:
pass pass
if scrape.url: if scrape.canonical_url():
recipe_json['source_url'] = scrape.url recipe_json['source_url'] = scrape.canonical_url()
return recipe_json return recipe_json
@ -307,56 +328,6 @@ def normalize_string(string):
return unescaped_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): def iso_duration_to_minutes(string):
match = re.match( 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)?', 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)) qs = qs.filter(Q(checked=False) | Q(completed_at__gte=week_ago))
supermarket_order = ['checked'] + supermarket_order 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(): class RecipeShoppingEditor():

View File

@ -2,14 +2,14 @@ import re
from cookbook.helper.ingredient_parser import IngredientParser from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.integration.integration import Integration from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Ingredient from cookbook.models import Ingredient, Recipe, Step
class ChefTap(Integration): class ChefTap(Integration):
def import_file_name_filter(self, zip_info_object): def import_file_name_filter(self, zip_info_object):
print("testing", zip_info_object.filename) 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): def get_recipe_from_file(self, file):
source_url = '' source_url = ''
@ -45,11 +45,11 @@ class ChefTap(Integration):
ingredient_parser = IngredientParser(self.request, True) ingredient_parser = IngredientParser(self.request, True)
for ingredient in ingredients: for ingredient in ingredients:
if len(ingredient.strip()) > 0: 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) f = ingredient_parser.get_food(food)
u = ingredient_parser.get_unit(unit) u = ingredient_parser.get_unit(unit)
step.ingredients.add(Ingredient.objects.create( 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) 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.image_processing import get_filetype
from cookbook.helper.ingredient_parser import IngredientParser from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.integration.integration import Integration 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): class Chowdown(Integration):
def import_file_name_filter(self, zip_info_object): def import_file_name_filter(self, zip_info_object):
print("testing", zip_info_object.filename) 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): def get_recipe_from_file(self, file):
ingredient_mode = False ingredient_mode = False
@ -60,12 +60,13 @@ class Chowdown(Integration):
ingredient_parser = IngredientParser(self.request, True) ingredient_parser = IngredientParser(self.request, True)
for ingredient in ingredients: for ingredient in ingredients:
amount, unit, ingredient, note = ingredient_parser.parse(ingredient) if len(ingredient.strip()) > 0:
f = ingredient_parser.get_food(ingredient) amount, unit, food, note = ingredient_parser.parse(ingredient)
u = ingredient_parser.get_unit(unit) f = ingredient_parser.get_food(food)
step.ingredients.add(Ingredient.objects.create( u = ingredient_parser.get_unit(unit)
food=f, unit=u, amount=amount, note=note, space=self.request.space, 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) recipe.steps.add(step)
for f in self.files: for f in self.files:

View File

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

View File

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

View File

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

View File

@ -172,7 +172,7 @@ class Integration:
traceback.print_exc() traceback.print_exc()
self.handle_exception(e, log=il, message=f'-------------------- \nERROR \n{e}\n--------------------\n') self.handle_exception(e, log=il, message=f'-------------------- \nERROR \n{e}\n--------------------\n')
import_zip.close() 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']) data_list = self.split_recipe_file(f['file'])
il.total_recipes += len(data_list) il.total_recipes += len(data_list)
for d in 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.image_processing import get_filetype
from cookbook.helper.ingredient_parser import IngredientParser from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.integration.integration import Integration from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Ingredient from cookbook.models import Ingredient, Recipe, Step
class Mealie(Integration): class Mealie(Integration):
def import_file_name_filter(self, zip_info_object): 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): def get_recipe_from_file(self, file):
recipe_json = json.loads(file.getvalue().decode("utf-8")) recipe_json = json.loads(file.getvalue().decode("utf-8"))
@ -45,12 +45,14 @@ class Mealie(Integration):
u = ingredient_parser.get_unit(ingredient['unit']) u = ingredient_parser.get_unit(ingredient['unit'])
amount = ingredient['quantity'] amount = ingredient['quantity']
note = ingredient['note'] note = ingredient['note']
original_text = None
else: else:
amount, unit, ingredient, note = ingredient_parser.parse(ingredient['note']) amount, unit, food, note = ingredient_parser.parse(ingredient['note'])
f = ingredient_parser.get_food(ingredient) f = ingredient_parser.get_food(food)
u = ingredient_parser.get_unit(unit) u = ingredient_parser.get_unit(unit)
original_text = ingredient['note']
step.ingredients.add(Ingredient.objects.create( 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: except Exception:
pass pass
@ -60,7 +62,8 @@ class Mealie(Integration):
if '.zip' in f['name']: if '.zip' in f['name']:
import_zip = ZipFile(f['file']) import_zip = ZipFile(f['file'])
try: 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: except Exception:
pass pass

View File

@ -2,7 +2,7 @@ import re
from cookbook.helper.ingredient_parser import IngredientParser from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.integration.integration import Integration 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): class MealMaster(Integration):
@ -45,11 +45,11 @@ class MealMaster(Integration):
ingredient_parser = IngredientParser(self.request, True) ingredient_parser = IngredientParser(self.request, True)
for ingredient in ingredients: for ingredient in ingredients:
if len(ingredient.strip()) > 0: 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) f = ingredient_parser.get_food(ingredient)
u = ingredient_parser.get_unit(unit) u = ingredient_parser.get_unit(unit)
step.ingredients.add(Ingredient.objects.create( 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) 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.ingredient_parser import IngredientParser
from cookbook.helper.recipe_url_import import iso_duration_to_minutes from cookbook.helper.recipe_url_import import iso_duration_to_minutes
from cookbook.integration.integration import Integration 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): class NextcloudCookbook(Integration):
@ -57,11 +57,11 @@ class NextcloudCookbook(Integration):
ingredient_parser = IngredientParser(self.request, True) ingredient_parser = IngredientParser(self.request, True)
for ingredient in recipe_json['recipeIngredient']: for ingredient in recipe_json['recipeIngredient']:
amount, unit, ingredient, note = ingredient_parser.parse(ingredient) amount, unit, food, note = ingredient_parser.parse(ingredient)
f = ingredient_parser.get_food(ingredient) f = ingredient_parser.get_food(food)
u = ingredient_parser.get_unit(unit) u = ingredient_parser.get_unit(unit)
step.ingredients.add(Ingredient.objects.create( 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) recipe.steps.add(step)

View File

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

View File

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

View File

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

View File

@ -4,7 +4,7 @@ import requests
from cookbook.helper.ingredient_parser import IngredientParser from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.integration.integration import Integration 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): class Plantoeat(Integration):
@ -56,11 +56,11 @@ class Plantoeat(Integration):
ingredient_parser = IngredientParser(self.request, True) ingredient_parser = IngredientParser(self.request, True)
for ingredient in ingredients: for ingredient in ingredients:
if len(ingredient.strip()) > 0: 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) f = ingredient_parser.get_food(food)
u = ingredient_parser.get_unit(unit) u = ingredient_parser.get_unit(unit)
step.ingredients.add(Ingredient.objects.create( 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) recipe.steps.add(step)

View File

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

View File

@ -1,12 +1,14 @@
import re import re
from bs4 import BeautifulSoup
from io import BytesIO from io import BytesIO
from zipfile import ZipFile 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.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.integration.integration import Integration
from cookbook.models import Recipe, Step, Ingredient, Keyword from cookbook.models import Ingredient, Keyword, Recipe, Step
class RecipeKeeper(Integration): class RecipeKeeper(Integration):
@ -45,11 +47,11 @@ class RecipeKeeper(Integration):
for ingredient in file.find("div", {"itemprop": "recipeIngredients"}).findChildren("p"): for ingredient in file.find("div", {"itemprop": "recipeIngredients"}).findChildren("p"):
if ingredient.text == "": if ingredient.text == "":
continue continue
amount, unit, ingredient, note = ingredient_parser.parse(ingredient.text.strip()) amount, unit, food, note = ingredient_parser.parse(ingredient.text.strip())
f = ingredient_parser.get_food(ingredient) f = ingredient_parser.get_food(food)
u = ingredient_parser.get_unit(unit) u = ingredient_parser.get_unit(unit)
step.ingredients.add(Ingredient.objects.create( 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"): for s in file.find("div", {"itemprop": "recipeDirections"}).find_all("p"):
@ -58,7 +60,7 @@ class RecipeKeeper(Integration):
step.instruction += s.text + ' \n' step.instruction += s.text + ' \n'
if file.find("span", {"itemprop": "recipeSource"}).text != '': 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() step.save()
recipe.steps.add(step) recipe.steps.add(step)

View File

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

View File

@ -1,6 +1,6 @@
from cookbook.helper.ingredient_parser import IngredientParser from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.integration.integration import Integration 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): class RezKonv(Integration):
@ -44,11 +44,11 @@ class RezKonv(Integration):
ingredient_parser = IngredientParser(self.request, True) ingredient_parser = IngredientParser(self.request, True)
for ingredient in ingredients: for ingredient in ingredients:
if len(ingredient.strip()) > 0: 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) f = ingredient_parser.get_food(food)
u = ingredient_parser.get_unit(unit) u = ingredient_parser.get_unit(unit)
step.ingredients.add(Ingredient.objects.create( 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) recipe.steps.add(step)
@ -60,9 +60,14 @@ class RezKonv(Integration):
def split_recipe_file(self, file): def split_recipe_file(self, file):
recipe_list = [] recipe_list = []
current_recipe = '' 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(): 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 line.startswith('=====') and 'rezkonv' in line.lower():
if current_recipe != '': if current_recipe != '':
recipe_list.append(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.helper.ingredient_parser import IngredientParser
from cookbook.integration.integration import Integration from cookbook.integration.integration import Integration
from cookbook.models import Recipe, Step, Ingredient from cookbook.models import Ingredient, Recipe, Step
class Saffron(Integration): class Saffron(Integration):
@ -47,11 +47,11 @@ class Saffron(Integration):
ingredient_parser = IngredientParser(self.request, True) ingredient_parser = IngredientParser(self.request, True)
for ingredient in ingredients: for ingredient in ingredients:
amount, unit, ingredient, note = ingredient_parser.parse(ingredient) amount, unit, food, note = ingredient_parser.parse(ingredient)
f = ingredient_parser.get_food(ingredient) f = ingredient_parser.get_food(food)
u = ingredient_parser.get_unit(unit) u = ingredient_parser.get_unit(unit)
step.ingredients.add(Ingredient.objects.create( 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) recipe.steps.add(step)
@ -76,7 +76,7 @@ class Saffron(Integration):
for i in s.ingredients.all(): for i in s.ingredients.all():
recipeIngredient.append(f'{float(i.amount)} {i.unit} {i.food}') recipeIngredient.append(f'{float(i.amount)} {i.unit} {i.food}')
data += "Ingredients: \n" data += "Ingredients: \n"
for ingredient in recipeIngredient: for ingredient in recipeIngredient:
data += ingredient+"\n" data += ingredient+"\n"
@ -91,10 +91,10 @@ class Saffron(Integration):
files = [] files = []
for r in recipes: for r in recipes:
filename, data = self.get_file_from_recipe(r) filename, data = self.get_file_from_recipe(r)
files.append([ filename, data ]) files.append([filename, data])
el.exported_recipes += 1 el.exported_recipes += 1
el.msg += self.get_recipe_processed_msg(r) el.msg += self.get_recipe_processed_msg(r)
el.save() 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 # model.Manager get_or_create() is not compatible with MP_Tree
def get_or_create(self, *args, **kwargs): def get_or_create(self, *args, **kwargs):
kwargs['name'] = kwargs['name'].strip() kwargs['name'] = kwargs['name'].strip()
try:
return self.get(name__iexact=kwargs['name'], space=kwargs['space']), False if obj := self.filter(name__iexact=kwargs['name'], space=kwargs['space']).first():
except self.model.DoesNotExist: return obj, False
else:
with scopes_disabled(): with scopes_disabled():
try: try:
defaults = kwargs.pop('defaults', None) defaults = kwargs.pop('defaults', None)
@ -590,6 +591,7 @@ class Ingredient(ExportModelOperationsMixin('ingredient'), models.Model, Permiss
is_header = models.BooleanField(default=False) is_header = models.BooleanField(default=False)
no_amount = models.BooleanField(default=False) no_amount = models.BooleanField(default=False)
order = models.IntegerField(default=0) 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) 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 cookbook.provider.provider import Provider
from requests.auth import HTTPBasicAuth from requests.auth import HTTPBasicAuth
from recipes.settings import DEBUG
class Nextcloud(Provider): class Nextcloud(Provider):
@ -28,15 +30,18 @@ class Nextcloud(Provider):
def import_all(monitor): def import_all(monitor):
client = Nextcloud.get_client(monitor.storage) 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) files = client.list(monitor.path)
try: if DEBUG:
files.pop(0) # remove first element because its the folder itself print(f'TANDOOR_PROVIDER_DEBUG file list {files}')
except IndexError:
pass # folder is empty, no recipes will be imported
import_count = 0 import_count = 0
for file in files: for file in files:
if DEBUG:
print(f'TANDOOR_PROVIDER_DEBUG importing file {file}')
path = monitor.path + '/' + 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(): 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] name = os.path.splitext(file)[0]

View File

@ -337,7 +337,7 @@ class SupermarketCategorySerializer(UniqueFieldsMixin, WritableNestedModelSerial
def create(self, validated_data): def create(self, validated_data):
name = validated_data.pop('name').strip() name = validated_data.pop('name').strip()
space = validated_data.pop('space', self.context['request'].space) 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 return obj
def update(self, instance, validated_data): def update(self, instance, validated_data):
@ -421,9 +421,11 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR
space = validated_data.pop('space', self.context['request'].space) 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 # 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']: 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( validated_data['supermarket_category'], sc_created = SupermarketCategory.objects.get_or_create(
name__iexact=validated_data.pop('supermarket_category')['name'], name=sc_name,
space=self.context['request'].space) space=space, defaults=sm_category)
onhand = validated_data.pop('food_onhand', None) onhand = validated_data.pop('food_onhand', None)
# assuming if on hand for user also onhand for shopping_share users # 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 validated_data['space'] = self.context['request'].space
return super().create(validated_data) 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: class Meta:
model = Ingredient model = Ingredient
fields = ( fields = (
@ -681,7 +687,7 @@ class RecipeBookEntrySerializer(serializers.ModelSerializer):
def create(self, validated_data): def create(self, validated_data):
book = validated_data['book'] book = validated_data['book']
recipe = validated_data['recipe'] 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) raise NotFound(detail=None, code=None)
obj, created = RecipeBookEntry.objects.get_or_create(book=book, recipe=recipe) obj, created = RecipeBookEntry.objects.get_or_create(book=book, recipe=recipe)
return obj return obj
@ -736,11 +742,11 @@ class ShoppingListRecipeSerializer(serializers.ModelSerializer):
value = Decimal(value) value = Decimal(value)
value = value.quantize(Decimal(1)) if value == value.to_integral() else value.normalize() # strips trailing zero value = value.quantize(Decimal(1)) if value == value.to_integral() else value.normalize() # strips trailing zero
return ( return (
obj.name obj.name
or getattr(obj.mealplan, 'title', None) or getattr(obj.mealplan, 'title', None)
or (d := getattr(obj.mealplan, 'date', None)) and ': '.join([obj.mealplan.recipe.name, str(d)]) or (d := getattr(obj.mealplan, 'date', None)) and ': '.join([obj.mealplan.recipe.name, str(d)])
or obj.recipe.name or obj.recipe.name
) + f' ({value:.2g})' ) + f' ({value:.2g})'
def update(self, instance, validated_data): def update(self, instance, validated_data):
# TODO remove once old shopping list # TODO remove once old shopping list

View File

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

View File

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

View File

@ -30,7 +30,7 @@
style="height:50%" style="height:50%"
href="{% bookmarklet request %}" href="{% bookmarklet request %}"
title="{% trans 'Drag me to your bookmarks to import recipes from anywhere' %}"> 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> </div>
<nav class="nav nav-pills flex-sm-row mb-2"> <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" <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="tab-pane fade show active" id="nav-url" role="tabpanel">
<div class="btn-group btn-group-toggle mt-2" data-toggle="buttons"> <div class="btn-group btn-group-toggle mt-2" data-toggle="buttons">
<label class="btn btn-outline-info btn-sm active" @click="automatic=true"> <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>
<label class="btn btn-outline-info btn-sm" @click="automatic=false"> <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> </label>
</div> </div>
<div role="group" class="input-group mt-4"> <div role="group" class="input-group mt-4">
@ -473,9 +473,9 @@
<div class="card" style="margin-top: 4px"> <div class="card" style="margin-top: 4px">
<div class="card-body"> <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"> <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> </div>
<div class="row"> <div class="row">
@ -1024,7 +1024,7 @@
amount: String(response.body.amount), amount: String(response.body.amount),
ingredient: {id: Math.random() * 1000, text: response.body.food}, ingredient: {id: Math.random() * 1000, text: response.body.food},
note: response.body.note, note: response.body.note,
original: v original_text: v
} }
this.recipe_json.recipeIngredient.push(new_ingredient) this.recipe_json.recipeIngredient.push(new_ingredient)
}).catch((err) => { }).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) recipe.steps.add(step)
for kw in data['keywords']: 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) k, created = Keyword.objects.get_or_create(name=kw['text'], space=request.space)
recipe.keywords.add(k) recipe.keywords.add(k)
else: else:
@ -168,7 +168,8 @@ def import_url(request):
ingredient_parser = IngredientParser(request, True) ingredient_parser = IngredientParser(request, True)
for ing in data['recipeIngredient']: 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(): if food_text := ing['ingredient']['text'].strip():
ingredient.food = ingredient_parser.get_food(food_text) 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.forms import ExportForm, ImportExportBase, ImportForm
from cookbook.helper.permission_helper import group_required from cookbook.helper.permission_helper import group_required
from cookbook.helper.recipe_search import RecipeSearch
from cookbook.integration.cheftap import ChefTap from cookbook.integration.cheftap import ChefTap
from cookbook.integration.chowdown import Chowdown from cookbook.integration.chowdown import Chowdown
from cookbook.integration.cookbookapp import CookBookApp from cookbook.integration.cookbookapp import CookBookApp
@ -123,6 +124,9 @@ def export_recipe(request):
recipes = form.cleaned_data['recipes'] recipes = form.cleaned_data['recipes']
if form.cleaned_data['all']: if form.cleaned_data['all']:
recipes = Recipe.objects.filter(space=request.space, internal=True).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']) 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.space = tb.space # TODO this is likely a bad idea. Verify and test
request.user = tb.created_by request.user = tb.created_by
ingredient_parser = IngredientParser(request, False) ingredient_parser = IngredientParser(request, False)
amount, unit, ingredient, note = ingredient_parser.parse(data['message']['text']) amount, unit, food, note = ingredient_parser.parse(data['message']['text'])
f = ingredient_parser.get_food(ingredient) f = ingredient_parser.get_food(food)
u = ingredient_parser.get_unit(unit) 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']}) return JsonResponse({'data': data['message']['text']})
except Exception: except Exception:

View File

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

View File

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

View File

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

View File

@ -1,145 +1,126 @@
<template> <template>
<div id="app"> <div id="app">
<br/> <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"> <b-progress :max="export_info.total_recipes">
<h5 style="text-align: center">{{ $t('Exporting') }}...</h5> <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"> <loading-spinner :size="25"></loading-spinner>
<b-progress-bar :value="export_info.exported_recipes" :label="`${export_info.exported_recipes}/${export_info.total_recipes}`"></b-progress-bar> </template>
</b-progress>
<loading-spinner :size="25"></loading-spinner> <div class="row">
</template> <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"> {{ $t("If download did not start automatically: ") }}
<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: ') }} <template v-if="export_info.expired">
<a disabled
<template v-if="export_info.expired"> ><del>{{ $t("Download") }}</del></a
<a disabled><del>{{ $t('Download') }}</del></a> ({{ $t('Expired') }}) >
</template> ({{ $t("Expired") }})
<a v-else :href="`/export-file/${export_id}/`" ref="downloadAnchor" >{{ $t('Download') }}</a> </template>
<a v-else :href="`${resolveDjangoUrl('view_export_file', export_id)}`" ref="downloadAnchor">{{ $t("Download") }}</a>
<br> <br />
{{ $t('The link will remain active for') }} {{ $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>
<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> <br />
</div>
</div> </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 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> </template>
<script> <script>
import Vue from 'vue' import Vue from "vue"
import {BootstrapVue} from 'bootstrap-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) Vue.use(BootstrapVue)
export default { export default {
name: 'ExportResponseView', name: "ExportResponseView",
mixins: [ mixins: [ResolveUrlMixin, ToastMixin],
ResolveUrlMixin, components: {
ToastMixin, LoadingSpinner,
], },
components: { data() {
LoadingSpinner return {
}, export_id: window.EXPORT_ID,
data() { export_info: undefined,
return { }
export_id: window.EXPORT_ID, },
export_info: undefined, mounted() {
}
},
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) {
this.refreshData() this.refreshData()
let el = this.$refs.output_text this.$i18n.locale = window.CUSTOM_LOCALE
el.scrollTop = el.scrollHeight;
if(this.export_info.expired) this.dynamicIntervalTimeout = 250 //initial refresh rate
makeToast(this.$t("Error"), this.$t("The download link is expired!"), "danger") 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(){ if (this.export_id !== null && window.navigator.onLine && this.export_info.running) {
this.$refs['downloadAnchor'].click() 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> </script>
<style> <style></style>
</style>

View File

@ -1,174 +1,180 @@
<template> <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> <br />
<div class="row"> <b-form-checkbox v-model="export_all" @change="disabled_multiselect = $event" name="check-button" switch style="margin-top: 1vh">
<div class="col col-md-12"> {{ $t("All recipes") }}
</b-form-checkbox>
<br/> <!-- <multiselect
<!-- TODO get option dynamicaly --> :searchable="true"
<select class="form-control" v-model="recipe_app"> :disabled="disabled_multiselect"
<option value="DEFAULT">Default</option> v-model="recipe_list"
<option value="SAFFRON">Saffron</option> :options="recipes"
<option value="RECIPESAGE">Recipe Sage</option> :close-on-select="false"
<option value="PDF">PDF (experimental)</option> :clear-on-select="true"
</select> :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/> <br />
<b-form-checkbox v-model="export_all" @change="disabled_multiselect=$event" name="check-button" switch style="margin-top: 1vh"> <button @click="exportRecipe()" class="btn btn-primary shadow-none"><i class="fas fa-file-export"></i> {{ $t("Export") }}</button>
{{ $t('All recipes') }} </div>
</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>
</div> </div>
</div> </div>
</div>
</template> </template>
<script> <script>
import Vue from 'vue' import Vue from "vue"
import {BootstrapVue} from 'bootstrap-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, ApiMixin } from "@/utils/utils"
// import Multiselect from "vue-multiselect"
import {StandardToasts, makeToast, resolveDjangoUrl} from "@/utils/utils"; import GenericMultiselect from "@/components/GenericMultiselect"
import Multiselect from "vue-multiselect"; import { ApiApiFactory } from "@/utils/openapi/api.ts"
import {ApiApiFactory} from "@/utils/openapi/api.ts"; import axios from "axios"
import axios from "axios";
Vue.use(BootstrapVue) Vue.use(BootstrapVue)
export default { export default {
name: 'ExportView', name: "ExportView",
/*mixins: [ /*mixins: [
ResolveUrlMixin, ResolveUrlMixin,
ToastMixin, ToastMixin,
],*/ ],*/
components: {Multiselect}, components: { GenericMultiselect },
data() { mixins: [ApiMixin],
return { data() {
export_id: window.EXPORT_ID, return {
loading: false, export_id: window.EXPORT_ID,
disabled_multiselect: false, loading: false,
disabled_multiselect: false,
recipe_app: 'DEFAULT', recipe_app: "DEFAULT",
recipe_list: [], recipe_list: [],
recipes_loading: false, recipes_loading: false,
recipes: [], recipes: [],
export_all: false, export_all: false,
} filter: undefined,
}, }
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(''))
}, },
mounted() {
searchRecipes: function (query) { if (this.export_id) this.insertRequested()
// else this.searchRecipes("")
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)
})
}, },
methods: {
insertRequested: function () {
let apiFactory = new ApiApiFactory()
exportRecipe: function () { this.recipes_loading = true
if (this.recipe_list.length < 1 && this.export_all == false) { apiFactory
makeToast(this.$t("Error"), this.$t("Select at least one recipe"), "danger") .retrieveRecipe(this.export_id)
return; .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 // searchRecipes: function (query) {
this.loading = true // this.recipes_loading = true
let formData = new FormData();
formData.append('type', this.recipe_app);
formData.append('all', this.export_all)
for (var i = 0; i < this.recipe_list.length; i++) { // this.genericAPI(this.Models.RECIPE, this.Actions.LIST, { query: query })
formData.append('recipes', this.recipe_list[i].id); // .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'; exportRecipe: function () {
axios.post(resolveDjangoUrl('view_export',), formData).then((response) => { if (this.recipe_list.length < 1 && this.export_all == false && this.filter === undefined) {
if (response.data['error'] !== undefined){ makeToast(this.$t("Error"), this.$t("Select at least one recipe"), "danger")
makeToast(this.$t("Error"), response.data['error'],"warning") return
}else{ }
window.location.href = resolveDjangoUrl('view_export_response', response.data['export_id'])
}
}).catch((err) => { this.error = undefined
this.error = err.data this.loading = true
this.loading = false let formData = new FormData()
console.log(err) formData.append("type", this.recipe_app)
makeToast(this.$t("Error"), this.$t("There was an error loading a resource!"), "warning") 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> </script>
<style src="vue-multiselect/dist/vue-multiselect.min.css"></style> <style src="vue-multiselect/dist/vue-multiselect.min.css"></style>
<style> <style></style>
</style>

View File

@ -65,8 +65,11 @@
:preserve-search="true" :preserve-search="true"
:internal-search="false" :internal-search="false"
:limit="options_limit" :limit="options_limit"
placeholder="Select Keyword" :placeholder="$t('select_keyword')"
tag-placeholder="Add Keyword" :tag-placeholder="$t('add_keyword')"
:select-label="$t('Select')"
:selected-label="$t('Selected')"
:deselect-label="$t('remove_selection')"
:taggable="true" :taggable="true"
@tag="addKeyword" @tag="addKeyword"
label="label" label="label"
@ -76,6 +79,7 @@
:loading="keywords_loading" :loading="keywords_loading"
@search-change="searchKeywords" @search-change="searchKeywords"
> >
<template v-slot:noOptions>{{ $t("empty_list") }}</template>
</multiselect> </multiselect>
</div> </div>
</div> </div>
@ -244,8 +248,10 @@
:clear-on-select="true" :clear-on-select="true"
:allow-empty="true" :allow-empty="true"
:preserve-search="true" :preserve-search="true"
placeholder="Select File" :placeholder="$t('select_file')"
select-label="Select" :select-label="$t('Select')"
:selected-label="$t('Selected')"
:deselect-label="$t('remove_selection')"
:id="'id_step_' + step.id + '_file'" :id="'id_step_' + step.id + '_file'"
label="name" label="name"
track-by="name" track-by="name"
@ -254,6 +260,7 @@
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0" style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
@search-change="searchFiles" @search-change="searchFiles"
> >
<template v-slot:noOptions>{{ $t("empty_list") }}</template>
</multiselect> </multiselect>
<b-input-group-append> <b-input-group-append>
<b-button <b-button
@ -283,14 +290,17 @@
:preserve-search="true" :preserve-search="true"
:internal-search="false" :internal-search="false"
:limit="options_limit" :limit="options_limit"
placeholder="Select Recipe" :placeholder="$t('select_recipe')"
select-label="Select" :select-label="$t('Select')"
:selected-label="$t('Selected')"
:deselect-label="$t('remove_selection')"
:id="'id_step_' + step.id + '_recipe'" :id="'id_step_' + step.id + '_recipe'"
:custom-label="(opt) => recipes.find((x) => x.id === opt).name" :custom-label="(opt) => recipes.find((x) => x.id === opt).name"
:multiple="false" :multiple="false"
:loading="recipes_loading" :loading="recipes_loading"
@search-change="searchRecipes" @search-change="searchRecipes"
> >
<template v-slot:noOptions>{{ $t("empty_list") }}</template>
</multiselect> </multiselect>
</div> </div>
</div> </div>
@ -340,9 +350,11 @@
:preserve-search="true" :preserve-search="true"
:internal-search="false" :internal-search="false"
:limit="options_limit" :limit="options_limit"
placeholder="Select Unit" :placeholder="$t('select_unit')"
tag-placeholder="Create" :tag-placeholder="$t('Create')"
select-label="Select" :select-label="$t('Select')"
:selected-label="$t('Selected')"
:deselect-label="$t('remove_selection')"
:taggable="true" :taggable="true"
@tag="addUnitType" @tag="addUnitType"
:id="`unit_${step_index}_${index}`" :id="`unit_${step_index}_${index}`"
@ -352,6 +364,7 @@
:loading="units_loading" :loading="units_loading"
@search-change="searchUnits" @search-change="searchUnits"
> >
<template v-slot:noOptions>{{ $t("empty_list") }}</template>
</multiselect> </multiselect>
</div> </div>
<div class="col-lg-4 col-md-6 small-padding" v-if="!ingredient.is_header"> <div class="col-lg-4 col-md-6 small-padding" v-if="!ingredient.is_header">
@ -367,9 +380,11 @@
:preserve-search="true" :preserve-search="true"
:internal-search="false" :internal-search="false"
:limit="options_limit" :limit="options_limit"
placeholder="Select Food" :placeholder="$t('select_food')"
tag-placeholder="Create" :tag-placeholder="$t('Create')"
select-label="Select" :select-label="$t('Select')"
:selected-label="$t('Selected')"
:deselect-label="$t('remove_selection')"
:taggable="true" :taggable="true"
@tag="addFoodType" @tag="addFoodType"
:id="`ingredient_${step_index}_${index}`" :id="`ingredient_${step_index}_${index}`"
@ -379,6 +394,7 @@
:loading="foods_loading" :loading="foods_loading"
@search-change="searchFoods" @search-change="searchFoods"
> >
<template v-slot:noOptions>{{ $t("empty_list") }}</template>
</multiselect> </multiselect>
</div> </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 }"> <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, no_amount: false,
}) })
this.sortIngredients(step) 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) { removeIngredient: function (step, ingredient) {
if (confirm(this.$t("confirm_delete", { object: this.$t("Ingredient") }))) { if (confirm(this.$t("confirm_delete", { object: this.$t("Ingredient") }))) {
@ -985,6 +1001,7 @@ export default {
unit: unit, unit: unit,
food: { name: result.data.food }, food: { name: result.data.food },
note: result.data.note, note: result.data.note,
original_text: ing,
}) })
}) })
order++ 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.ui = Object.assign({}, this.ui, this.$cookies.get(SETTINGS_COOKIE_NAME))
} }
}) })
this.$i18n.locale = window.CUSTOM_LOCALE
console.log(window.CUSTOM_LOCALE)
}, },
methods: { methods: {
// this.genericAPI inherited from ApiMixin // this.genericAPI inherited from ApiMixin
@ -1491,7 +1493,7 @@ export default {
flex-grow: 1; flex-grow: 1;
overflow-y: scroll; overflow-y: scroll;
overflow-x: hidden; overflow-x: hidden;
height: 6vh; height: 60vh; /* TODO use proper fill height here to not render list underneath bottom buttons */
padding-right: 8px !important; 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()"> <b-sidebar id="related-recipes" backdrop right bottom no-header shadow="sm" style="z-index: 10000" @shown="updatePinnedRecipes()">
<template #default="{ hide }"> <template #default="{ hide }">
<div class="d-flex flex-column justify-content-end h-100 p-3 align-items-end"> <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"> <div class="text-right">
<template v-if="planned_recipes.length > 0"> <template v-if="planned_recipes.length > 0">
@ -24,11 +24,11 @@
</div> </div>
</template> </template>
<template v-else> <template v-else>
<span class="text-muted">You have nothing planned for today!</span> <span class="text-muted">{{$t("nothing_planned_today")}}</span>
</template> </template>
</div> </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"> <template v-if="pinned_recipes.length > 0">
<div class="text-right"> <div class="text-right">
@ -53,7 +53,7 @@
</div> </div>
</template> </template>
<template v-else> <template v-else>
<span class="text-muted">You have no pinned recipes!</span> <span class="text-muted">{{$t("no_pinned_recipes")}}</span>
</template> </template>
<template v-if="related_recipes.length > 0"> <template v-if="related_recipes.length > 0">
@ -77,8 +77,8 @@
</template> </template>
<template #footer="{ hide }"> <template #footer="{ hide }">
<div class="d-flex bg-dark text-light align-items-center px-3 py-2"> <div class="d-flex bg-dark text-light align-items-center px-3 py-2">
<strong class="mr-auto">Quick actions</strong> <strong class="mr-auto">{{$t("Quick actions")}}</strong>
<b-button size="sm" @click="hide">Close</b-button> <b-button size="sm" @click="hide">{{$t("Close")}}</b-button>
</div> </div>
</template> </template>
</b-sidebar> </b-sidebar>

View File

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

View File

@ -21,11 +21,11 @@
</div> </div>
<div class="actionArea pt-1 pb-1 d-none d-lg-flex"> <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"> <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> <b-form-select class="ml-1" id="UomInput" v-model="settings.displayPeriodUom" :options="options.displayPeriodUom"></b-form-select>
</span> </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"> <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> <b-form-select class="ml-1" id="UomInput" v-model="settings.displayPeriodCount" :options="options.displayPeriodCount"></b-form-select>
</span> </span>
<span <span

View File

@ -206,7 +206,7 @@ export default {
} }
if (!cancel) { if (!cancel) {
this.$bvModal.hide(`edit-modal`) 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() { deleteEntry() {

View File

@ -1,39 +1,42 @@
<template> <template>
<div> <div>
<b-form-group <b-form-group v-bind:label="label" class="mb-3">
v-bind:label="label" <b-form-select v-model="new_value" :placeholder="placeholder" :options="translatedOptions"></b-form-select>
class="mb-3">
<b-form-select v-model="new_value" :placeholder="placeholder" :options="options"></b-form-select>
</b-form-group> </b-form-group>
</div> </div>
</template> </template>
<script> <script>
export default { export default {
name: 'ChoiceInput', name: "ChoiceInput",
props: { props: {
field: {type: String, default: 'You Forgot To Set Field Name'}, field: { type: String, default: "You Forgot To Set Field Name" },
label: {type: String, default: 'Text Field'}, label: { type: String, default: "Text Field" },
value: {type: String, default: ''}, value: { type: String, default: "" },
options: [], options: [],
placeholder: {type: String, default: 'You Should Add Placeholder Text'}, placeholder: { type: String, default: "You Should Add Placeholder Text" },
show_merge: {type: Boolean, default: false}, 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)
}, },
}, data() {
methods: { 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" :initialContent="value"
:emojiData="emojiDataAll" :emojiData="emojiDataAll"
:emojiGroups="emojiGroups" :emojiGroups="emojiGroups"
triggerType="hover" triggerType="click"
:recentEmojisFeat="true" :recentEmojisFeat="true"
recentEmojisStorage="local" recentEmojisStorage="local"
@contentChanged="setIcon" @contentChanged="setIcon"

View File

@ -67,6 +67,9 @@ export default {
this.field = this.form?.field ?? "You Forgot To Set Field Name" this.field = this.form?.field ?? "You Forgot To Set Field Name"
this.label = this.form?.label ?? "" this.label = this.form?.label ?? ""
this.sticky_options = this.form?.sticky_options ?? [] 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 this.list_label = this.form?.list_label ?? undefined
if (this.list_label?.includes("::")) { if (this.list_label?.includes("::")) {
this.list_label = this.list_label.split("::")[1] this.list_label = this.list_label.split("::")[1]
@ -74,7 +77,7 @@ export default {
}, },
computed: { computed: {
modelName() { modelName() {
return this?.model?.name ?? this.$t("Search") return this.$t(this?.model?.name) ?? this.$t("Search")
}, },
useMultiple() { useMultiple() {
return this.form?.multiple || this.form?.ordered || false 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> <div class="text-nowrap"><i class="fa fa-chevron-right rotate" :class="showDetails ? 'rotated' : ''"></i></div>
</b-button> </b-button>
</b-col> </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)"> <div class="dropdown b-dropdown position-static inline-block" data-html2canvas-ignore="true" @click.stop="$emit('open-context-menu', $event, entries)">
<button <button
aria-haspopup="true" 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"> <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" /> <input type="checkbox" class="form-control form-control-sm checkbox-control" :checked="formatChecked" @change="updateChecked" :key="entries[0].id" />
</b-col> </b-col>
<b-col cols="8" md="9"> <b-col cols="7" md="9">
<b-row class="d-flex h-100"> <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"> <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] }} <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"> <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" /> <input type="checkbox" class="form-control form-control-sm checkbox-control-mobile" :checked="formatChecked" @change="updateChecked" :key="entries[0].id" />
</b-col> </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)"> <div class="dropdown b-dropdown position-static inline-block" data-html2canvas-ignore="true" @click.stop="$emit('open-context-menu', $event, e)">
<button <button
aria-haspopup="true" aria-haspopup="true"
@ -102,7 +102,7 @@
<b-col cols="1" class="justify-content-center align-items-center d-none d-md-flex"> <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" /> <input type="checkbox" class="form-control form-control-sm checkbox-control" :checked="formatChecked" @change="updateChecked" :key="entries[0].id" />
</b-col> </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-row class="d-flex align-items-center h-100">
<b-col cols="5" md="3" class="d-flex align-items-center"> <b-col cols="5" md="3" class="d-flex align-items-center">
<strong class="mr-1">{{ formatOneAmount(e) }}</strong> {{ formatOneUnit(e) }} <strong class="mr-1">{{ formatOneAmount(e) }}</strong> {{ formatOneUnit(e) }}

View File

@ -137,7 +137,7 @@
"Move_Down": "Runter", "Move_Down": "Runter",
"Step_Name": "Schritt Name", "Step_Name": "Schritt Name",
"Create": "Erstellen", "Create": "Erstellen",
"Advanced Search Settings": "Erweiterte Sucheinstellungen", "advanced_search_settings": "Erweiterte Sucheinstellungen",
"View": "Ansicht", "View": "Ansicht",
"Recipes": "Rezepte", "Recipes": "Rezepte",
"Move": "Verschieben", "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.", "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: ", "MoveCategory": "Verschieben nach: ",
"mealplan_autoadd_shopping_desc": "Essensplan-Zutaten automatisch zur Einkaufsliste hinzufügen.", "mealplan_autoadd_shopping_desc": "Essensplan-Zutaten automatisch zur Einkaufsliste hinzufügen.",
"Pin": "Pin", "Pin": "Anheften",
"mark_complete": "Vollständig markieren", "mark_complete": "Vollständig markieren",
"shopping_add_onhand_desc": "Markiere Lebensmittel als \"Vorrätig\", wenn von der Einkaufsliste abgehakt wurden.", "shopping_add_onhand_desc": "Markiere Lebensmittel als \"Vorrätig\", wenn von der Einkaufsliste abgehakt wurden.",
"left_handed": "Linkshänder-Modus", "left_handed": "Linkshänder-Modus",
@ -298,5 +298,61 @@
"Foods": "Lebensmittel", "Foods": "Lebensmittel",
"food_recipe_help": "Wird ein Rezept hier verknüpft, wird diese Verknüpfung in allen anderen Rezepten übernommen, die dieses Lebensmittel beinhaltet", "food_recipe_help": "Wird ein Rezept hier verknüpft, wird diese Verknüpfung in allen anderen Rezepten übernommen, die dieses Lebensmittel beinhaltet",
"review_shopping": "Überprüfe die Einkaufsliste vor dem Speichern", "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)", "InheritFields_help": "The values of these fields will be inheritted from parent (Exception: blank shopping categories are not inheritted)",
"last_viewed": "Last Viewed", "last_viewed": "Last Viewed",
"created_on": "Created On", "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 * Utility CLASS to define model configurations
* */ * */
import i18n from "@/i18n"
// TODO this needs rethought and simplified // TODO this needs rethought and simplified
// maybe a function that returns a single dictionary based on action? // maybe a function that returns a single dictionary based on action?
@ -51,7 +50,7 @@ export class Models {
type: "lookup", type: "lookup",
field: "target", field: "target",
list: "self", 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 // MODELS - inherits and takes precedence over MODEL_TYPES and ACTIONS
static FOOD = { 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 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 model_type: this.TREE, // *OPTIONAL* : model specific params for api, if not present will attempt modeltype_create then default_create
paginated: true, paginated: true,
@ -100,15 +99,15 @@ export class Models {
form_field: true, form_field: true,
type: "text", type: "text",
field: "name", field: "name",
label: i18n.t("Name"), label: "Name", // form.label always translated in utils.getForm()
placeholder: "", placeholder: "", // form.placeholder always translated
subtitle_field: "full_name", subtitle_field: "full_name",
}, },
description: { description: {
form_field: true, form_field: true,
type: "text", type: "text",
field: "description", field: "description",
label: i18n.t("Description"), label: "Description", // form.label always translated in utils.getForm()
placeholder: "", placeholder: "",
}, },
recipe: { recipe: {
@ -116,31 +115,31 @@ export class Models {
type: "lookup", type: "lookup",
field: "recipe", field: "recipe",
list: "RECIPE", list: "RECIPE",
label: i18n.t("Recipe"), label: "Recipe", // form.label always translated in utils.getForm()
help_text: i18n.t("food_recipe_help"), help_text: "food_recipe_help", // form.help_text always translated
}, },
onhand: { onhand: {
form_field: true, form_field: true,
type: "checkbox", type: "checkbox",
field: "food_onhand", field: "food_onhand",
label: i18n.t("OnHand"), label: "OnHand",
help_text: i18n.t("OnHand_help"), help_text: "OnHand_help",
}, },
ignore_shopping: { ignore_shopping: {
form_field: true, form_field: true,
type: "checkbox", type: "checkbox",
field: "ignore_shopping", field: "ignore_shopping",
label: i18n.t("Ignore_Shopping"), label: "Ignore_Shopping",
help_text: i18n.t("ignore_shopping_help"), help_text: "ignore_shopping_help",
}, },
shopping_category: { shopping_category: {
form_field: true, form_field: true,
type: "lookup", type: "lookup",
field: "supermarket_category", field: "supermarket_category",
list: "SHOPPING_CATEGORY", list: "SHOPPING_CATEGORY",
label: i18n.t("Shopping_Category"), label: "Shopping_Category",
allow_create: true, allow_create: true,
help_text: i18n.t("shopping_category_help"), help_text: "shopping_category_help", // form.help_text always translated
}, },
substitute: { substitute: {
form_field: true, form_field: true,
@ -149,17 +148,17 @@ export class Models {
multiple: true, multiple: true,
field: "substitute", field: "substitute",
list: "FOOD", list: "FOOD",
label: i18n.t("Substitutes"), label: "Substitutes",
allow_create: false, allow_create: false,
help_text: i18n.t("substitute_help"), help_text: "substitute_help",
}, },
substitute_siblings: { substitute_siblings: {
form_field: true, form_field: true,
advanced: true, advanced: true,
type: "checkbox", type: "checkbox",
field: "substitute_siblings", field: "substitute_siblings",
label: i18n.t("substitute_siblings"), label: "substitute_siblings", // form.label always translated in utils.getForm()
help_text: i18n.t("substitute_siblings_help"), help_text: "substitute_siblings_help", // form.help_text always translated
condition: { field: "parent", value: true, condition: "field_exists" }, condition: { field: "parent", value: true, condition: "field_exists" },
}, },
substitute_children: { substitute_children: {
@ -167,8 +166,8 @@ export class Models {
advanced: true, advanced: true,
type: "checkbox", type: "checkbox",
field: "substitute_children", field: "substitute_children",
label: i18n.t("substitute_children"), label: "substitute_children",
help_text: i18n.t("substitute_children_help"), help_text: "substitute_children_help",
condition: { field: "numchild", value: 0, condition: "gt" }, condition: { field: "numchild", value: 0, condition: "gt" },
}, },
inherit_fields: { inherit_fields: {
@ -178,9 +177,9 @@ export class Models {
multiple: true, multiple: true,
field: "inherit_fields", field: "inherit_fields",
list: "FOOD_INHERIT_FIELDS", list: "FOOD_INHERIT_FIELDS",
label: i18n.t("InheritFields"), label: "InheritFields",
condition: { field: "food_children_exist", value: true, condition: "preference_equals" }, condition: { field: "food_children_exist", value: true, condition: "preference_equals" },
help_text: i18n.t("InheritFields_help"), help_text: "InheritFields_help",
}, },
child_inherit_fields: { child_inherit_fields: {
form_field: true, form_field: true,
@ -189,17 +188,17 @@ export class Models {
multiple: true, multiple: true,
field: "child_inherit_fields", field: "child_inherit_fields",
list: "FOOD_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" }, condition: { field: "numchild", value: 0, condition: "gt" },
help_text: i18n.t("ChildInheritFields_help"), help_text: "ChildInheritFields_help", // form.help_text always translated
}, },
reset_inherit: { reset_inherit: {
form_field: true, form_field: true,
advanced: true, advanced: true,
type: "checkbox", type: "checkbox",
field: "reset_inherit", field: "reset_inherit",
label: i18n.t("reset_children"), label: "reset_children",
help_text: i18n.t("reset_children_help"), help_text: "reset_children_help",
condition: { field: "numchild", value: 0, condition: "gt" }, condition: { field: "numchild", value: 0, condition: "gt" },
}, },
form_function: "FoodCreateDefault", form_function: "FoodCreateDefault",
@ -215,7 +214,7 @@ export class Models {
} }
static KEYWORD = { 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", apiName: "Keyword",
model_type: this.TREE, model_type: this.TREE,
paginated: true, paginated: true,
@ -232,21 +231,21 @@ export class Models {
form_field: true, form_field: true,
type: "text", type: "text",
field: "name", field: "name",
label: i18n.t("Name"), label: "Name",
placeholder: "", placeholder: "",
}, },
description: { description: {
form_field: true, form_field: true,
type: "text", type: "text",
field: "description", field: "description",
label: i18n.t("Description"), label: "Description",
placeholder: "", placeholder: "",
}, },
icon: { icon: {
form_field: true, form_field: true,
type: "emoji", type: "emoji",
field: "icon", field: "icon",
label: i18n.t("Icon"), label: "Icon",
}, },
full_name: { full_name: {
form_field: true, form_field: true,
@ -258,7 +257,7 @@ export class Models {
} }
static UNIT = { static UNIT = {
name: i18n.t("Unit"), name: "Unit",
apiName: "Unit", apiName: "Unit",
paginated: true, paginated: true,
create: { create: {
@ -268,14 +267,14 @@ export class Models {
form_field: true, form_field: true,
type: "text", type: "text",
field: "name", field: "name",
label: i18n.t("Name"), label: "Name",
placeholder: "", placeholder: "",
}, },
description: { description: {
form_field: true, form_field: true,
type: "text", type: "text",
field: "description", field: "description",
label: i18n.t("Description"), label: "Description",
placeholder: "", placeholder: "",
}, },
}, },
@ -284,7 +283,7 @@ export class Models {
} }
static SHOPPING_LIST = { static SHOPPING_LIST = {
name: i18n.t("Shopping_list"), name: "Shopping_list",
apiName: "ShoppingListEntry", apiName: "ShoppingListEntry",
list: { list: {
params: ["id", "checked", "supermarket", "options"], params: ["id", "checked", "supermarket", "options"],
@ -297,7 +296,7 @@ export class Models {
type: "lookup", type: "lookup",
field: "unit", field: "unit",
list: "UNIT", list: "UNIT",
label: i18n.t("Unit"), label: "Unit",
allow_create: true, allow_create: true,
}, },
food: { food: {
@ -305,7 +304,7 @@ export class Models {
type: "lookup", type: "lookup",
field: "food", field: "food",
list: "FOOD", list: "FOOD",
label: i18n.t("Food"), label: "Food", // form.label always translated in utils.getForm()
allow_create: true, allow_create: true,
}, },
}, },
@ -313,7 +312,7 @@ export class Models {
} }
static RECIPE_BOOK = { static RECIPE_BOOK = {
name: i18n.t("Recipe_Book"), name: "Recipe_Book",
apiName: "RecipeBook", apiName: "RecipeBook",
create: { create: {
params: [["name", "description", "icon", "filter"]], params: [["name", "description", "icon", "filter"]],
@ -322,27 +321,27 @@ export class Models {
form_field: true, form_field: true,
type: "text", type: "text",
field: "name", field: "name",
label: i18n.t("Name"), label: "Name",
placeholder: "", placeholder: "",
}, },
description: { description: {
form_field: true, form_field: true,
type: "text", type: "text",
field: "description", field: "description",
label: i18n.t("Description"), label: "Description",
placeholder: "", placeholder: "",
}, },
icon: { icon: {
form_field: true, form_field: true,
type: "emoji", type: "emoji",
field: "icon", field: "icon",
label: i18n.t("Icon"), label: "Icon",
}, },
filter: { filter: {
form_field: true, form_field: true,
type: "lookup", type: "lookup",
field: "filter", field: "filter",
label: i18n.t("Custom Filter"), label: "Custom Filter",
list: "CUSTOM_FILTER", list: "CUSTOM_FILTER",
}, },
}, },
@ -350,7 +349,7 @@ export class Models {
} }
static SHOPPING_CATEGORY = { static SHOPPING_CATEGORY = {
name: i18n.t("Shopping_Category"), name: "Shopping_Category",
apiName: "SupermarketCategory", apiName: "SupermarketCategory",
create: { create: {
params: [["name", "description"]], params: [["name", "description"]],
@ -359,14 +358,14 @@ export class Models {
form_field: true, form_field: true,
type: "text", type: "text",
field: "name", field: "name",
label: i18n.t("Name"), label: "Name", // form.label always translated in utils.getForm()
placeholder: "", placeholder: "",
}, },
description: { description: {
form_field: true, form_field: true,
type: "text", type: "text",
field: "description", field: "description",
label: i18n.t("Description"), label: "Description",
placeholder: "", placeholder: "",
}, },
}, },
@ -374,7 +373,7 @@ export class Models {
} }
static SHOPPING_CATEGORY_RELATION = { static SHOPPING_CATEGORY_RELATION = {
name: i18n.t("Shopping_Category_Relation"), name: "Shopping_Category_Relation",
apiName: "SupermarketCategoryRelation", apiName: "SupermarketCategoryRelation",
create: { create: {
params: [["category", "supermarket", "order"]], params: [["category", "supermarket", "order"]],
@ -383,14 +382,14 @@ export class Models {
form_field: true, form_field: true,
type: "text", type: "text",
field: "name", field: "name",
label: i18n.t("Name"), label: "Name",
placeholder: "", placeholder: "",
}, },
description: { description: {
form_field: true, form_field: true,
type: "text", type: "text",
field: "description", field: "description",
label: i18n.t("Description"), label: "Description",
placeholder: "", placeholder: "",
}, },
}, },
@ -398,7 +397,7 @@ export class Models {
} }
static SUPERMARKET = { static SUPERMARKET = {
name: i18n.t("Supermarket"), name: "Supermarket",
apiName: "Supermarket", apiName: "Supermarket",
ordered_tags: [{ field: "category_to_supermarket", label: "category::name", color: "info" }], ordered_tags: [{ field: "category_to_supermarket", label: "category::name", color: "info" }],
create: { create: {
@ -408,14 +407,14 @@ export class Models {
form_field: true, form_field: true,
type: "text", type: "text",
field: "name", field: "name",
label: i18n.t("Name"), label: "Name",
placeholder: "", placeholder: "",
}, },
description: { description: {
form_field: true, form_field: true,
type: "text", type: "text",
field: "description", field: "description",
label: i18n.t("Description"), label: "Description",
placeholder: "", placeholder: "",
}, },
categories: { categories: {
@ -425,7 +424,7 @@ export class Models {
list_label: "category::name", list_label: "category::name",
ordered: true, // ordered lookups assume working with relation field ordered: true, // ordered lookups assume working with relation field
field: "category_to_supermarket", field: "category_to_supermarket",
label: i18n.t("Categories"), label: "Categories", // form.label always translated in utils.getForm()
placeholder: "", placeholder: "",
}, },
}, },
@ -441,7 +440,7 @@ export class Models {
} }
static AUTOMATION = { static AUTOMATION = {
name: i18n.t("Automation"), name: "Automation",
apiName: "Automation", apiName: "Automation",
paginated: true, paginated: true,
list: { list: {
@ -456,47 +455,74 @@ export class Models {
form_field: true, form_field: true,
type: "text", type: "text",
field: "name", field: "name",
label: i18n.t("Name"), label: "Name",
placeholder: "", placeholder: "",
}, },
description: { description: {
form_field: true, form_field: true,
type: "text", type: "text",
field: "description", field: "description",
label: i18n.t("Description"), label: "Description",
placeholder: "", placeholder: "",
}, },
type: { type: {
form_field: true, form_field: true,
type: "choice", type: "choice",
options: [ options: [
{ value: "FOOD_ALIAS", text: i18n.t("Food_Alias") }, { value: "FOOD_ALIAS", text: "Food_Alias" },
{ value: "UNIT_ALIAS", text: i18n.t("Unit_Alias") }, { value: "UNIT_ALIAS", text: "Unit_Alias" },
{ value: "KEYWORD_ALIAS", text: i18n.t("Keyword_Alias") }, { value: "KEYWORD_ALIAS", text: "Keyword_Alias" },
], ],
field: "type", field: "type",
label: i18n.t("Type"), label: "Type",
placeholder: "", placeholder: "",
}, },
param_1: { param_1: {
form_field: true, form_field: true,
type: "text", type: "text",
field: "param_1", field: "param_1",
label: i18n.t("Parameter") + " 1", label: {
function: "translate",
phrase: "parameter_count",
params: [
{
token: "count",
attribute: "1",
},
],
},
placeholder: "", placeholder: "",
}, },
param_2: { param_2: {
form_field: true, form_field: true,
type: "text", type: "text",
field: "param_2", field: "param_2",
label: i18n.t("Parameter") + " 2", label: {
function: "translate",
phrase: "parameter_count",
params: [
{
token: "count",
attribute: "2",
},
],
},
placeholder: "", placeholder: "",
}, },
param_3: { param_3: {
form_field: true, form_field: true,
type: "text", type: "text",
field: "param_3", field: "param_3",
label: i18n.t("Parameter") + " 3", label: {
function: "translate",
phrase: "parameter_count",
params: [
{
token: "count",
attribute: "3",
},
],
},
placeholder: "", placeholder: "",
}, },
}, },
@ -504,7 +530,7 @@ export class Models {
} }
static RECIPE = { static RECIPE = {
name: i18n.t("Recipe"), name: "Recipe",
apiName: "Recipe", apiName: "Recipe",
list: { list: {
params: [ params: [
@ -546,7 +572,7 @@ export class Models {
} }
static CUSTOM_FILTER = { static CUSTOM_FILTER = {
name: i18n.t("Custom Filter"), name: "Custom Filter",
apiName: "CustomFilter", apiName: "CustomFilter",
create: { create: {
@ -556,7 +582,7 @@ export class Models {
form_field: true, form_field: true,
type: "text", type: "text",
field: "name", field: "name",
label: i18n.t("Name"), label: "Name", // form.label always translated in utils.getForm()
placeholder: "", placeholder: "",
}, },
@ -566,14 +592,14 @@ export class Models {
field: "shared", field: "shared",
list: "USER", list: "USER",
list_label: "username", list_label: "username",
label: i18n.t("shared_with"), label: "shared_with",
multiple: true, multiple: true,
}, },
}, },
}, },
} }
static USER_NAME = { static USER_NAME = {
name: i18n.t("User"), name: "User",
apiName: "User", apiName: "User",
list: { list: {
params: ["filter_list"], params: ["filter_list"],
@ -581,7 +607,7 @@ export class Models {
} }
static MEAL_TYPE = { static MEAL_TYPE = {
name: i18n.t("Meal_Type"), name: "Meal_Type",
apiName: "MealType", apiName: "MealType",
list: { list: {
params: ["filter_list"], params: ["filter_list"],
@ -589,7 +615,7 @@ export class Models {
} }
static MEAL_PLAN = { static MEAL_PLAN = {
name: i18n.t("Meal_Plan"), name: "Meal_Plan",
apiName: "MealPlan", apiName: "MealPlan",
list: { list: {
params: ["options"], params: ["options"],
@ -597,7 +623,7 @@ export class Models {
} }
static USERFILE = { static USERFILE = {
name: i18n.t("File"), name: "File",
apiName: "UserFile", apiName: "UserFile",
paginated: false, paginated: false,
list: { list: {
@ -612,27 +638,27 @@ export class Models {
form_field: true, form_field: true,
type: "text", type: "text",
field: "name", field: "name",
label: i18n.t("Name"), label: "Name",
placeholder: "", placeholder: "",
}, },
file: { file: {
form_field: true, form_field: true,
type: "file", type: "file",
field: "file", field: "file",
label: i18n.t("File"), label: "File", // form.label always translated in utils.getForm()
placeholder: "", placeholder: "",
}, },
}, },
}, },
} }
static USER = { static USER = {
name: i18n.t("User"), name: "User",
apiName: "User", apiName: "User",
paginated: false, paginated: false,
} }
static STEP = { static STEP = {
name: i18n.t("Step"), name: "Step",
apiName: "Step", apiName: "Step",
list: { list: {
params: ["recipe", "query", "page", "pageSize", "options"], params: ["recipe", "query", "page", "pageSize", "options"],
@ -652,10 +678,11 @@ export class Actions {
token: "type", token: "type",
from: "model", from: "model",
attribute: "name", attribute: "name",
translate: true,
}, },
], ],
}, },
ok_label: i18n.t("Save"), ok_label: { function: "translate", phrase: "Save" },
}, },
} }
static UPDATE = { static UPDATE = {
@ -669,6 +696,7 @@ export class Actions {
token: "type", token: "type",
from: "model", from: "model",
attribute: "name", attribute: "name",
translate: true,
}, },
], ],
}, },
@ -685,10 +713,11 @@ export class Actions {
token: "type", token: "type",
from: "model", from: "model",
attribute: "name", attribute: "name",
translate: true,
}, },
], ],
}, },
ok_label: i18n.t("Delete"), ok_label: { function: "translate", phrase: "Delete" },
instruction: { instruction: {
form_field: true, form_field: true,
type: "instruction", type: "instruction",
@ -736,10 +765,11 @@ export class Actions {
token: "type", token: "type",
from: "model", from: "model",
attribute: "name", attribute: "name",
translate: true,
}, },
], ],
}, },
ok_label: i18n.t("Merge"), ok_label: { function: "translate", phrase: "Merge" },
instruction: { instruction: {
form_field: true, form_field: true,
type: "instruction", type: "instruction",
@ -756,6 +786,7 @@ export class Actions {
token: "type", token: "type",
from: "model", from: "model",
attribute: "name", attribute: "name",
translate: true,
}, },
], ],
}, },
@ -784,10 +815,11 @@ export class Actions {
token: "type", token: "type",
from: "model", from: "model",
attribute: "name", attribute: "name",
translate: true,
}, },
], ],
}, },
ok_label: i18n.t("Move"), ok_label: { function: "translate", phrase: "Move" },
instruction: { instruction: {
form_field: true, form_field: true,
type: "instruction", type: "instruction",
@ -804,6 +836,7 @@ export class Actions {
token: "type", token: "type",
from: "model", from: "model",
attribute: "name", attribute: "name",
translate: true,
}, },
], ],
}, },

View File

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

View File

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