mirror of
https://github.com/TandoorRecipes/recipes.git
synced 2025-12-27 20:18:58 -05:00
Compare commits
142 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
af9a2a89ec | ||
|
|
c50a89c651 | ||
|
|
f21587605a | ||
|
|
2e69a00fce | ||
|
|
bddaa77f71 | ||
|
|
3743a08996 | ||
|
|
f16e457d14 | ||
|
|
64f2787943 | ||
|
|
3ff15b6766 | ||
|
|
b8e0a7cf69 | ||
|
|
e2915dde55 | ||
|
|
05f2fdecb3 | ||
|
|
5d33d82d70 | ||
|
|
e3a3220f00 | ||
|
|
f15f34887a | ||
|
|
e470a70321 | ||
|
|
1a99a2d6f1 | ||
|
|
cf3ddfc610 | ||
|
|
ecbd3edb97 | ||
|
|
7837467c30 | ||
|
|
84759383fa | ||
|
|
aaaae5b1ba | ||
|
|
ea62c10d9a | ||
|
|
3516505dd1 | ||
|
|
4a747f5cd4 | ||
|
|
0623a8ebc7 | ||
|
|
5941022b5e | ||
|
|
2559905a78 | ||
|
|
edde015b71 | ||
|
|
9b7b8beea4 | ||
|
|
2eae8e5eeb | ||
|
|
6d8bc396f8 | ||
|
|
4118c8d9e3 | ||
|
|
78c2eacbd8 | ||
|
|
01510f39e5 | ||
|
|
09cc5aafe9 | ||
|
|
e8b2f57812 | ||
|
|
664e83143f | ||
|
|
f1309cc624 | ||
|
|
6fb7f6bd1f | ||
|
|
158bb1bf03 | ||
|
|
086e802873 | ||
|
|
c94c8d3559 | ||
|
|
f99010aa1d | ||
|
|
32e00999f3 | ||
|
|
e3196a79a8 | ||
|
|
e926b34bec | ||
|
|
a460123184 | ||
|
|
c89c88b981 | ||
|
|
cf6ea04f30 | ||
|
|
15c4609db3 | ||
|
|
053804f8cb | ||
|
|
da748995e7 | ||
|
|
1d80ba3a3b | ||
|
|
29fe6c7363 | ||
|
|
42d4a32ffc | ||
|
|
e8ae844fb0 | ||
|
|
c93f68804a | ||
|
|
b4ea236241 | ||
|
|
2bef5c3b51 | ||
|
|
52f2086616 | ||
|
|
03e1474113 | ||
|
|
9829ab68a6 | ||
|
|
7e07508a31 | ||
|
|
94b0438516 | ||
|
|
b97c90e22f | ||
|
|
f78264620f | ||
|
|
571a618818 | ||
|
|
6c97594591 | ||
|
|
5d353a0839 | ||
|
|
0be1f6a170 | ||
|
|
5cd042fa7c | ||
|
|
e02d2530aa | ||
|
|
b35f5047ab | ||
|
|
f10bec8ab4 | ||
|
|
3bc1daa72e | ||
|
|
5d6574b8cc | ||
|
|
adc65baf9c | ||
|
|
4d2e7eadb6 | ||
|
|
7c985cec23 | ||
|
|
2cd33ee40a | ||
|
|
f61146123e | ||
|
|
4806bd63b6 | ||
|
|
41242c8d09 | ||
|
|
57a967b91d | ||
|
|
fb931f4715 | ||
|
|
e86b476b3a | ||
|
|
7f22e0a275 | ||
|
|
1907223a8a | ||
|
|
9b5fe8f4e7 | ||
|
|
d76fdd090a | ||
|
|
55a0304700 | ||
|
|
5b6dd62f8e | ||
|
|
19f5684d26 | ||
|
|
d6ad1354db | ||
|
|
6faabe3759 | ||
|
|
69acca7de1 | ||
|
|
9d8c08341f | ||
|
|
d488559e42 | ||
|
|
85f7740e9b | ||
|
|
72e831afcf | ||
|
|
cb59f046c0 | ||
|
|
25d505161f | ||
|
|
62aa62b90f | ||
|
|
3fe5340592 | ||
|
|
9233cb9cf9 | ||
|
|
fd9f6f6dca | ||
|
|
ecd4ce603c | ||
|
|
695cab29a1 | ||
|
|
7b6ca94d49 | ||
|
|
35e04f94c6 | ||
|
|
7c4cd02dfa | ||
|
|
4626af3505 | ||
|
|
5ae440d5c9 | ||
|
|
e88010310c | ||
|
|
b6eba9c5e7 | ||
|
|
9d827ac174 | ||
|
|
27679ae8a5 | ||
|
|
6cb9a7068e | ||
|
|
f41c2ee7bb | ||
|
|
af581bb27c | ||
|
|
885c8982c1 | ||
|
|
64a9f67802 | ||
|
|
df45e1d523 | ||
|
|
03d7aa37da | ||
|
|
dd3d28ec75 | ||
|
|
ea377c2f3b | ||
|
|
6f0bf886f6 | ||
|
|
16fbd9fe48 | ||
|
|
79cdb56f9a | ||
|
|
1e6ba924ab | ||
|
|
9ae076e426 | ||
|
|
f346022d8b | ||
|
|
c9cd5325c4 | ||
|
|
ba6c80e04a | ||
|
|
be3f860ba1 | ||
|
|
1ac4020b3d | ||
|
|
5da535b8ac | ||
|
|
b8ed99a59a | ||
|
|
c199536fca | ||
|
|
d60a9f0379 | ||
|
|
c5da006f4a |
@@ -7,7 +7,9 @@ SQL_DEBUG=0
|
||||
ALLOWED_HOSTS=*
|
||||
|
||||
# random secret key, use for example `base64 /dev/urandom | head -c50` to generate one
|
||||
# ---------------------------- REQUIRED -------------------------
|
||||
SECRET_KEY=
|
||||
# ---------------------------------------------------------------
|
||||
|
||||
# your default timezone See https://timezonedb.com/time-zones for a list of timezones
|
||||
TIMEZONE=Europe/Berlin
|
||||
@@ -18,7 +20,9 @@ DB_ENGINE=django.db.backends.postgresql
|
||||
POSTGRES_HOST=db_recipes
|
||||
POSTGRES_PORT=5432
|
||||
POSTGRES_USER=djangouser
|
||||
# ---------------------------- REQUIRED -------------------------
|
||||
POSTGRES_PASSWORD=
|
||||
# ---------------------------------------------------------------
|
||||
POSTGRES_DB=djangodb
|
||||
|
||||
# database connection string, when used overrides other database settings.
|
||||
|
||||
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@@ -9,14 +9,14 @@ jobs:
|
||||
strategy:
|
||||
max-parallel: 4
|
||||
matrix:
|
||||
python-version: [3.9]
|
||||
python-version: ['3.10']
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Set up Python 3.9
|
||||
- name: Set up Python 3.10
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: 3.9
|
||||
python-version: '3.10'
|
||||
# Build Vue frontend
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
|
||||
3
.github/workflows/docker-publish-dev.yml
vendored
3
.github/workflows/docker-publish-dev.yml
vendored
@@ -24,6 +24,9 @@ jobs:
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '14'
|
||||
- name: Clear Cache
|
||||
working-directory: ./vue
|
||||
run: yarn cache clean --all
|
||||
- name: Install dependencies
|
||||
working-directory: ./vue
|
||||
run: yarn install
|
||||
|
||||
2
.github/workflows/docker-publish-release.yml
vendored
2
.github/workflows/docker-publish-release.yml
vendored
@@ -49,4 +49,4 @@ jobs:
|
||||
DISCORD_WEBHOOK: ${{ secrets.DISCORD_RELEASE_WEBHOOK }}
|
||||
uses: Ilshidur/action-discord@0.3.2
|
||||
with:
|
||||
args: '🚀 Version {{ EVENT_PAYLOAD.release.tag_name }} of tandoor has been released 🥳 \nCheck it out https://github.com/vabene1111/recipes/releases/tag/{{ EVENT_PAYLOAD.release.tag_name }}'
|
||||
args: '🚀 Version {{ EVENT_PAYLOAD.release.tag_name }} of tandoor has been released 🥳 Check it out https://github.com/vabene1111/recipes/releases/tag/{{ EVENT_PAYLOAD.release.tag_name }}'
|
||||
|
||||
2
.github/workflows/docs.yml
vendored
2
.github/workflows/docs.yml
vendored
@@ -14,5 +14,5 @@ jobs:
|
||||
- uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.x
|
||||
- run: pip install mkdocs-material
|
||||
- run: pip install mkdocs-material mkdocs-include-markdown-plugin
|
||||
- run: mkdocs gh-deploy --force
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -79,8 +79,8 @@ postgresql/
|
||||
/docker-compose.override.yml
|
||||
vue/node_modules
|
||||
.vscode/
|
||||
vue/yarn.lock
|
||||
vetur.config.js
|
||||
cookbook/static/vue
|
||||
vue/webpack-stats.json
|
||||
cookbook/templates/sw.js
|
||||
.prettierignore
|
||||
|
||||
@@ -257,7 +257,7 @@ admin.site.register(ViewLog, ViewLogAdmin)
|
||||
|
||||
class InviteLinkAdmin(admin.ModelAdmin):
|
||||
list_display = (
|
||||
'group', 'valid_until',
|
||||
'group', 'valid_until', 'space',
|
||||
'created_by', 'created_at', 'used_by'
|
||||
)
|
||||
|
||||
|
||||
@@ -1,26 +1,40 @@
|
||||
import traceback
|
||||
|
||||
from django.apps import AppConfig
|
||||
from django.conf import settings
|
||||
from django.db import OperationalError, ProgrammingError
|
||||
from django_scopes import scopes_disabled
|
||||
|
||||
from recipes.settings import DEBUG
|
||||
|
||||
|
||||
class CookbookConfig(AppConfig):
|
||||
name = 'cookbook'
|
||||
|
||||
def ready(self):
|
||||
# post_save signal is only necessary if using full-text search on postgres
|
||||
if settings.DATABASES['default']['ENGINE'] in ['django.db.backends.postgresql_psycopg2', 'django.db.backends.postgresql']:
|
||||
if settings.DATABASES['default']['ENGINE'] in ['django.db.backends.postgresql_psycopg2',
|
||||
'django.db.backends.postgresql']:
|
||||
import cookbook.signals # noqa
|
||||
|
||||
# when starting up run fix_tree to:
|
||||
# a) make sure that nodes are sorted when switching between sort modes
|
||||
# b) fix problems, if any, with tree consistency
|
||||
with scopes_disabled():
|
||||
try:
|
||||
from cookbook.models import Keyword, Food
|
||||
Keyword.fix_tree(fix_paths=True)
|
||||
Food.fix_tree(fix_paths=True)
|
||||
except OperationalError:
|
||||
pass # if model does not exist there is no need to fix it
|
||||
except ProgrammingError:
|
||||
pass # if migration has not been run database cannot be fixed yet
|
||||
if not settings.DISABLE_TREE_FIX_STARTUP:
|
||||
# when starting up run fix_tree to:
|
||||
# a) make sure that nodes are sorted when switching between sort modes
|
||||
# b) fix problems, if any, with tree consistency
|
||||
with scopes_disabled():
|
||||
try:
|
||||
from cookbook.models import Keyword, Food
|
||||
#Keyword.fix_tree(fix_paths=True) # disabled for now, causes to many unknown issues
|
||||
#Food.fix_tree(fix_paths=True)
|
||||
except OperationalError:
|
||||
if DEBUG:
|
||||
traceback.print_exc()
|
||||
pass # if model does not exist there is no need to fix it
|
||||
except ProgrammingError:
|
||||
if DEBUG:
|
||||
traceback.print_exc()
|
||||
pass # if migration has not been run database cannot be fixed yet
|
||||
except Exception:
|
||||
if DEBUG:
|
||||
traceback.print_exc()
|
||||
pass # dont break startup just because fix could not run, need to investigate cases when this happens
|
||||
|
||||
@@ -152,13 +152,14 @@ class ImportExportBase(forms.Form):
|
||||
OPENEATS = 'OPENEATS'
|
||||
PLANTOEAT = 'PLANTOEAT'
|
||||
COOKBOOKAPP = 'COOKBOOKAPP'
|
||||
COPYMETHAT = 'COPYMETHAT'
|
||||
|
||||
type = forms.ChoiceField(choices=(
|
||||
(DEFAULT, _('Default')), (PAPRIKA, 'Paprika'), (NEXTCLOUD, 'Nextcloud Cookbook'),
|
||||
(MEALIE, 'Mealie'), (CHOWDOWN, 'Chowdown'), (SAFRON, 'Safron'), (CHEFTAP, 'ChefTap'),
|
||||
(PEPPERPLATE, 'Pepperplate'), (RECETTETEK, 'RecetteTek'), (RECIPESAGE, 'Recipe Sage'), (DOMESTICA, 'Domestica'),
|
||||
(MEALMASTER, 'MealMaster'), (REZKONV, 'RezKonv'), (OPENEATS, 'Openeats'), (RECIPEKEEPER, 'Recipe Keeper'),
|
||||
(PLANTOEAT, 'Plantoeat'), (COOKBOOKAPP, 'CookBookApp'),
|
||||
(PLANTOEAT, 'Plantoeat'), (COOKBOOKAPP, 'CookBookApp'), (COPYMETHAT, 'CopyMeThat'),
|
||||
))
|
||||
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ from PIL import Image
|
||||
from io import BytesIO
|
||||
|
||||
|
||||
def rescale_image_jpeg(image_object, base_width=720):
|
||||
def rescale_image_jpeg(image_object, base_width=1020):
|
||||
img = Image.open(image_object)
|
||||
icc_profile = img.info.get('icc_profile') # remember color profile to not mess up colors
|
||||
width_percent = (base_width / float(img.size[0]))
|
||||
@@ -13,20 +13,20 @@ def rescale_image_jpeg(image_object, base_width=720):
|
||||
|
||||
img = img.resize((base_width, height), Image.ANTIALIAS)
|
||||
img_bytes = BytesIO()
|
||||
img.save(img_bytes, 'JPEG', quality=75, optimize=True, icc_profile=icc_profile)
|
||||
img.save(img_bytes, 'JPEG', quality=90, optimize=True, icc_profile=icc_profile)
|
||||
|
||||
return img_bytes
|
||||
|
||||
|
||||
def rescale_image_png(image_object, base_width=720):
|
||||
basewidth = 720
|
||||
wpercent = (basewidth / float(image_object.size[0]))
|
||||
def rescale_image_png(image_object, base_width=1020):
|
||||
image_object = Image.open(image_object)
|
||||
wpercent = (base_width / float(image_object.size[0]))
|
||||
hsize = int((float(image_object.size[1]) * float(wpercent)))
|
||||
img = image_object.resize((basewidth, hsize), Image.ANTIALIAS)
|
||||
img = image_object.resize((base_width, hsize), Image.ANTIALIAS)
|
||||
|
||||
im_io = BytesIO()
|
||||
img.save(im_io, 'PNG', quality=70)
|
||||
return img
|
||||
img.save(im_io, 'PNG', quality=90)
|
||||
return im_io
|
||||
|
||||
|
||||
def get_filetype(name):
|
||||
@@ -36,9 +36,11 @@ def get_filetype(name):
|
||||
return '.jpeg'
|
||||
|
||||
|
||||
# TODO this whole file needs proper documentation, refactoring, and testing
|
||||
# TODO also add env variable to define which images sizes should be compressed
|
||||
def handle_image(request, image_object, filetype='.jpeg'):
|
||||
if sys.getsizeof(image_object) / 8 > 500:
|
||||
if filetype == '.jpeg':
|
||||
if (image_object.size / 1000) > 500: # if larger than 500 kb compress
|
||||
if filetype == '.jpeg' or filetype == '.jpg':
|
||||
return rescale_image_jpeg(image_object), filetype
|
||||
if filetype == '.png':
|
||||
return rescale_image_png(image_object), filetype
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import json
|
||||
import re
|
||||
from json import JSONDecodeError
|
||||
from urllib.parse import unquote
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
from bs4.element import Tag
|
||||
from recipe_scrapers._utils import get_host_name, normalize_string
|
||||
|
||||
from cookbook.helper import recipe_url_import as helper
|
||||
from cookbook.helper.scrapers.scrapers import text_scraper
|
||||
from json import JSONDecodeError
|
||||
from recipe_scrapers._utils import get_host_name, normalize_string
|
||||
from urllib.parse import unquote
|
||||
|
||||
|
||||
def get_recipe_from_source(text, url, request):
|
||||
@@ -58,7 +59,7 @@ def get_recipe_from_source(text, url, request):
|
||||
return kid_list
|
||||
|
||||
recipe_json = {
|
||||
'name': '',
|
||||
'name': '',
|
||||
'url': '',
|
||||
'description': '',
|
||||
'image': '',
|
||||
@@ -188,6 +189,6 @@ def remove_graph(el):
|
||||
for x in el['@graph']:
|
||||
if '@type' in x and x['@type'] == 'Recipe':
|
||||
el = x
|
||||
except TypeError:
|
||||
except (TypeError, JSONDecodeError):
|
||||
pass
|
||||
return el
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
from collections import Counter
|
||||
from datetime import timedelta
|
||||
|
||||
from recipes import settings
|
||||
from django.contrib.postgres.search import (
|
||||
SearchQuery, SearchRank, TrigramSimilarity
|
||||
)
|
||||
from django.contrib.postgres.search import SearchQuery, SearchRank, TrigramSimilarity
|
||||
from django.core.cache import caches
|
||||
from django.db.models import Avg, Case, Count, Func, Max, Q, Subquery, Value, When
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.utils import timezone, translation
|
||||
|
||||
from cookbook.filters import RecipeFilter
|
||||
from cookbook.helper.permission_helper import has_group_permission
|
||||
from cookbook.managers import DICTIONARY
|
||||
from cookbook.models import Food, Keyword, ViewLog, SearchPreference
|
||||
from cookbook.models import Food, Keyword, Recipe, SearchPreference, ViewLog
|
||||
from recipes import settings
|
||||
|
||||
|
||||
class Round(Func):
|
||||
@@ -38,6 +38,7 @@ def search_recipes(request, queryset, params):
|
||||
search_keywords = params.getlist('keywords', [])
|
||||
search_foods = params.getlist('foods', [])
|
||||
search_books = params.getlist('books', [])
|
||||
search_steps = params.getlist('steps', [])
|
||||
search_units = params.get('units', None)
|
||||
|
||||
# TODO I think default behavior should be 'AND' which is how most sites operate with facet/filters based on results
|
||||
@@ -61,7 +62,7 @@ def search_recipes(request, queryset, params):
|
||||
|
||||
# return queryset.annotate(last_view=Max('viewlog__pk')).annotate(new=Case(When(pk__in=last_viewed_recipes, then=('last_view')), default=Value(0))).filter(new__gt=0).order_by('-new')
|
||||
# queryset that only annotates most recent view (higher pk = lastest view)
|
||||
queryset = queryset.annotate(recent=Coalesce(Max('viewlog__pk'), Value(0)))
|
||||
queryset = queryset.annotate(recent=Coalesce(Max(Case(When(viewlog__created_by=request.user, then='viewlog__pk'))), Value(0)))
|
||||
orderby += ['-recent']
|
||||
|
||||
# TODO create setting for default ordering - most cooked, rating,
|
||||
@@ -142,9 +143,9 @@ def search_recipes(request, queryset, params):
|
||||
|
||||
# TODO add order by user settings - only do search rank and annotation if rank order is configured
|
||||
search_rank = (
|
||||
SearchRank('name_search_vector', search_query, cover_density=True)
|
||||
+ SearchRank('desc_search_vector', search_query, cover_density=True)
|
||||
+ SearchRank('steps__search_vector', search_query, cover_density=True)
|
||||
SearchRank('name_search_vector', search_query, cover_density=True)
|
||||
+ SearchRank('desc_search_vector', search_query, cover_density=True)
|
||||
+ SearchRank('steps__search_vector', search_query, cover_density=True)
|
||||
)
|
||||
queryset = queryset.filter(query_filter).annotate(rank=search_rank)
|
||||
orderby += ['-rank']
|
||||
@@ -191,6 +192,10 @@ def search_recipes(request, queryset, params):
|
||||
if search_units:
|
||||
queryset = queryset.filter(steps__ingredients__unit__id=search_units)
|
||||
|
||||
# probably only useful in Unit list view, so keeping it simple
|
||||
if search_steps:
|
||||
queryset = queryset.filter(steps__id__in=search_steps)
|
||||
|
||||
if search_internal:
|
||||
queryset = queryset.filter(internal=True)
|
||||
|
||||
@@ -395,3 +400,13 @@ def annotated_qs(qs, root=False, fill=False):
|
||||
if start_depth and start_depth > 0:
|
||||
info['close'] = list(range(0, prev_depth - start_depth + 1))
|
||||
return result
|
||||
|
||||
|
||||
def old_search(request):
|
||||
if has_group_permission(request.user, ('guest',)):
|
||||
params = dict(request.GET)
|
||||
params['internal'] = None
|
||||
f = RecipeFilter(params,
|
||||
queryset=Recipe.objects.filter(space=request.user.userpreference.space).all().order_by('name'),
|
||||
space=request.space)
|
||||
return f.qs
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
from django.urls import reverse
|
||||
from django_scopes import scope, scopes_disabled
|
||||
from rest_framework.authentication import TokenAuthentication
|
||||
from rest_framework.authtoken.models import Token
|
||||
from rest_framework.exceptions import AuthenticationFailed
|
||||
|
||||
from cookbook.views import views
|
||||
|
||||
@@ -33,6 +36,15 @@ class ScopeMiddleware:
|
||||
with scope(space=request.space):
|
||||
return self.get_response(request)
|
||||
else:
|
||||
if request.path.startswith('/api/'):
|
||||
try:
|
||||
if auth := TokenAuthentication().authenticate(request):
|
||||
request.space = auth[0].userpreference.space
|
||||
with scope(space=request.space):
|
||||
return self.get_response(request)
|
||||
except AuthenticationFailed:
|
||||
pass
|
||||
|
||||
with scopes_disabled():
|
||||
request.space = None
|
||||
return self.get_response(request)
|
||||
|
||||
@@ -5,7 +5,7 @@ from cookbook.helper.mdx_attributes import MarkdownFormatExtension
|
||||
from cookbook.helper.mdx_urlize import UrlizeExtension
|
||||
from jinja2 import Template, TemplateSyntaxError, UndefinedError
|
||||
from gettext import gettext as _
|
||||
|
||||
from markdown.extensions.tables import TableExtension
|
||||
|
||||
class IngredientObject(object):
|
||||
amount = ""
|
||||
@@ -41,7 +41,7 @@ def render_instructions(step): # TODO deduplicate markdown cleanup code
|
||||
parsed_md = md.markdown(
|
||||
instructions,
|
||||
extensions=[
|
||||
'markdown.extensions.fenced_code', 'tables',
|
||||
'markdown.extensions.fenced_code', TableExtension(),
|
||||
UrlizeExtension(), MarkdownFormatExtension()
|
||||
]
|
||||
)
|
||||
|
||||
84
cookbook/integration/copymethat.py
Normal file
84
cookbook/integration/copymethat.py
Normal file
@@ -0,0 +1,84 @@
|
||||
import re
|
||||
from io import BytesIO
|
||||
from zipfile import ZipFile
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
from cookbook.helper.ingredient_parser import IngredientParser
|
||||
from cookbook.helper.recipe_html_import import get_recipe_from_source
|
||||
from cookbook.helper.recipe_url_import import iso_duration_to_minutes, parse_servings
|
||||
from cookbook.integration.integration import Integration
|
||||
from cookbook.models import Recipe, Step, Ingredient, Keyword
|
||||
from recipes.settings import DEBUG
|
||||
|
||||
|
||||
class CopyMeThat(Integration):
|
||||
|
||||
def import_file_name_filter(self, zip_info_object):
|
||||
if DEBUG:
|
||||
print("testing", zip_info_object.filename, zip_info_object.filename == 'recipes.html')
|
||||
return zip_info_object.filename == 'recipes.html'
|
||||
|
||||
def get_recipe_from_file(self, file):
|
||||
# 'file' comes is as a beautifulsoup object
|
||||
recipe = Recipe.objects.create(name=file.find("div", {"id": "name"}).text.strip(), created_by=self.request.user, internal=True, space=self.request.space, )
|
||||
|
||||
for category in file.find_all("span", {"class": "recipeCategory"}):
|
||||
keyword, created = Keyword.objects.get_or_create(name=category.text, space=self.request.space)
|
||||
recipe.keywords.add(keyword)
|
||||
|
||||
try:
|
||||
recipe.servings = parse_servings(file.find("a", {"id": "recipeYield"}).text.strip())
|
||||
recipe.working_time = iso_duration_to_minutes(file.find("span", {"meta": "prepTime"}).text.strip())
|
||||
recipe.waiting_time = iso_duration_to_minutes(file.find("span", {"meta": "cookTime"}).text.strip())
|
||||
recipe.save()
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
step = Step.objects.create(instruction='', space=self.request.space, )
|
||||
|
||||
ingredient_parser = IngredientParser(self.request, True)
|
||||
for ingredient in file.find_all("li", {"class": "recipeIngredient"}):
|
||||
if ingredient.text == "":
|
||||
continue
|
||||
amount, unit, ingredient, note = ingredient_parser.parse(ingredient.text.strip())
|
||||
f = ingredient_parser.get_food(ingredient)
|
||||
u = ingredient_parser.get_unit(unit)
|
||||
step.ingredients.add(Ingredient.objects.create(
|
||||
food=f, unit=u, amount=amount, note=note, space=self.request.space,
|
||||
))
|
||||
|
||||
for s in file.find_all("li", {"class": "instruction"}):
|
||||
if s.text == "":
|
||||
continue
|
||||
step.instruction += s.text.strip() + ' \n\n'
|
||||
|
||||
for s in file.find_all("li", {"class": "recipeNote"}):
|
||||
if s.text == "":
|
||||
continue
|
||||
step.instruction += s.text.strip() + ' \n\n'
|
||||
|
||||
try:
|
||||
if file.find("a", {"id": "original_link"}).text != '':
|
||||
step.instruction += "\n\nImported from: " + file.find("a", {"id": "original_link"}).text
|
||||
step.save()
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
recipe.steps.add(step)
|
||||
|
||||
# import the Primary recipe image that is stored in the Zip
|
||||
try:
|
||||
for f in self.files:
|
||||
if '.zip' in f['name']:
|
||||
import_zip = ZipFile(f['file'])
|
||||
self.import_recipe_image(recipe, BytesIO(import_zip.read(file.find("img", class_="recipeImage").get("src"))), filetype='.jpeg')
|
||||
except Exception as e:
|
||||
print(recipe.name, ': failed to import image ', str(e))
|
||||
|
||||
recipe.save()
|
||||
return recipe
|
||||
|
||||
def split_recipe_file(self, file):
|
||||
soup = BeautifulSoup(file, "html.parser")
|
||||
return soup.find_all("div", {"class": "recipe"})
|
||||
@@ -5,6 +5,7 @@ import uuid
|
||||
from io import BytesIO, StringIO
|
||||
from zipfile import ZipFile, BadZipFile
|
||||
|
||||
from bs4 import Tag
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.core.files import File
|
||||
from django.db import IntegrityError
|
||||
@@ -14,9 +15,9 @@ from django.utils.translation import gettext as _
|
||||
from django_scopes import scope
|
||||
|
||||
from cookbook.forms import ImportExportBase
|
||||
from cookbook.helper.image_processing import get_filetype
|
||||
from cookbook.helper.image_processing import get_filetype, handle_image
|
||||
from cookbook.models import Keyword, Recipe
|
||||
from recipes.settings import DATABASES, DEBUG
|
||||
from recipes.settings import DEBUG
|
||||
|
||||
|
||||
class Integration:
|
||||
@@ -52,7 +53,7 @@ class Integration:
|
||||
icon=icon,
|
||||
space=request.space
|
||||
)
|
||||
except IntegrityError: # in case, for whatever reason, the name does exist append UUID to it. Not nice but works for now.
|
||||
except IntegrityError: # in case, for whatever reason, the name does exist append UUID to it. Not nice but works for now.
|
||||
self.keyword = parent.add_child(
|
||||
name=f'{name} {str(uuid.uuid4())[0:8]}',
|
||||
description=description,
|
||||
@@ -153,9 +154,17 @@ class Integration:
|
||||
file_list.append(z)
|
||||
il.total_recipes += len(file_list)
|
||||
|
||||
import cookbook
|
||||
if isinstance(self, cookbook.integration.copymethat.CopyMeThat):
|
||||
file_list = self.split_recipe_file(BytesIO(import_zip.read('recipes.html')))
|
||||
il.total_recipes += len(file_list)
|
||||
|
||||
for z in file_list:
|
||||
try:
|
||||
recipe = self.get_recipe_from_file(BytesIO(import_zip.read(z.filename)))
|
||||
if isinstance(z, Tag):
|
||||
recipe = self.get_recipe_from_file(z)
|
||||
else:
|
||||
recipe = self.get_recipe_from_file(BytesIO(import_zip.read(z.filename)))
|
||||
recipe.keywords.add(self.keyword)
|
||||
il.msg += f'{recipe.pk} - {recipe.name} \n'
|
||||
self.handle_duplicates(recipe, import_duplicates)
|
||||
@@ -229,15 +238,14 @@ class Integration:
|
||||
self.ignored_recipes.append(recipe.name)
|
||||
recipe.delete()
|
||||
|
||||
@staticmethod
|
||||
def import_recipe_image(recipe, image_file, filetype='.jpeg'):
|
||||
def import_recipe_image(self, recipe, image_file, filetype='.jpeg'):
|
||||
"""
|
||||
Adds an image to a recipe naming it correctly
|
||||
:param recipe: Recipe object
|
||||
:param image_file: ByteIO stream containing the image
|
||||
:param filetype: type of file to write bytes to, default to .jpeg if unknown
|
||||
"""
|
||||
recipe.image = File(image_file, name=f'{uuid.uuid4()}_{recipe.pk}{filetype}')
|
||||
recipe.image = File(handle_image(self.request, File(image_file, name='image'), filetype=filetype)[0], name=f'{uuid.uuid4()}_{recipe.pk}{filetype}')
|
||||
recipe.save()
|
||||
|
||||
def get_recipe_from_file(self, file):
|
||||
|
||||
@@ -63,7 +63,7 @@ class MealMaster(Integration):
|
||||
current_recipe = ''
|
||||
|
||||
for fl in file.readlines():
|
||||
line = fl.decode("ANSI")
|
||||
line = fl.decode("windows-1250")
|
||||
if (line.startswith('MMMMM') or line.startswith('-----')) and 'meal-master' in line.lower():
|
||||
if current_recipe != '':
|
||||
recipe_list.append(current_recipe)
|
||||
|
||||
@@ -5,8 +5,9 @@ from zipfile import ZipFile
|
||||
|
||||
from cookbook.helper.image_processing import get_filetype
|
||||
from cookbook.helper.ingredient_parser import IngredientParser
|
||||
from cookbook.helper.recipe_url_import import iso_duration_to_minutes
|
||||
from cookbook.integration.integration import Integration
|
||||
from cookbook.models import Recipe, Step, Ingredient
|
||||
from cookbook.models import Recipe, Step, Ingredient, Keyword
|
||||
|
||||
|
||||
class NextcloudCookbook(Integration):
|
||||
@@ -24,9 +25,24 @@ class NextcloudCookbook(Integration):
|
||||
created_by=self.request.user, internal=True,
|
||||
servings=recipe_json['recipeYield'], space=self.request.space)
|
||||
|
||||
# TODO parse times (given in PT2H3M )
|
||||
# @vabene check recipe_url_import.iso_duration_to_minutes I think it does what you are looking for
|
||||
# TODO parse keywords
|
||||
try:
|
||||
recipe.working_time = iso_duration_to_minutes(recipe_json['prepTime'])
|
||||
recipe.waiting_time = iso_duration_to_minutes(recipe_json['cookTime'])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if 'recipeCategory' in recipe_json:
|
||||
try:
|
||||
recipe.keywords.add(Keyword.objects.get_or_create(space=self.request.space, name=recipe_json['recipeCategory'])[0])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if 'keywords' in recipe_json:
|
||||
try:
|
||||
for x in recipe_json['keywords'].split(','):
|
||||
recipe.keywords.add(Keyword.objects.get_or_create(space=self.request.space, name=x)[0])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
ingredients_added = False
|
||||
for s in recipe_json['recipeInstructions']:
|
||||
@@ -49,11 +65,20 @@ class NextcloudCookbook(Integration):
|
||||
))
|
||||
recipe.steps.add(step)
|
||||
|
||||
if 'nutrition' in recipe_json:
|
||||
try:
|
||||
recipe.nutrition.calories = recipe_json['nutrition']['calories'].replace(' kcal', '').replace(' ', '')
|
||||
recipe.nutrition.proteins = recipe_json['nutrition']['calories'].replace(' g', '').replace(',', '.').replace(' ', '')
|
||||
recipe.nutrition.fats = recipe_json['nutrition']['calories'].replace(' g', '').replace(',', '.').replace(' ', '')
|
||||
recipe.nutrition.carbohydrates = recipe_json['nutrition']['calories'].replace(' g', '').replace(',', '.').replace(' ', '')
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
for f in self.files:
|
||||
if '.zip' in f['name']:
|
||||
import_zip = ZipFile(f['file'])
|
||||
for z in import_zip.filelist:
|
||||
if re.match(f'^{recipe.name}/full.jpg$', z.filename):
|
||||
if re.match(f'^(.)+{recipe.name}/full.jpg$', z.filename):
|
||||
self.import_recipe_image(recipe, BytesIO(import_zip.read(z.filename)), filetype=get_filetype(z.filename))
|
||||
|
||||
return recipe
|
||||
|
||||
@@ -78,7 +78,7 @@ class Plantoeat(Integration):
|
||||
current_recipe = ''
|
||||
|
||||
for fl in file.readlines():
|
||||
line = fl.decode("ANSI")
|
||||
line = fl.decode("windows-1250")
|
||||
if line.startswith('--------------'):
|
||||
if current_recipe != '':
|
||||
recipe_list.append(current_recipe)
|
||||
|
||||
Binary file not shown.
@@ -15,10 +15,10 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-11-08 16:27+0100\n"
|
||||
"PO-Revision-Date: 2021-11-07 17:14+0000\n"
|
||||
"Last-Translator: Kaibu <notkaibu@gmail.com>\n"
|
||||
"Language-Team: German <http://translate.tandoor.dev/projects/tandoor/recipes-"
|
||||
"backend/de/>\n"
|
||||
"PO-Revision-Date: 2021-11-12 20:06+0000\n"
|
||||
"Last-Translator: A. L. <richard@anska.de>\n"
|
||||
"Language-Team: German <http://translate.tandoor.dev/projects/tandoor/"
|
||||
"recipes-backend/de/>\n"
|
||||
"Language: de\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
@@ -261,10 +261,6 @@ msgid "Email address already taken!"
|
||||
msgstr "Email-Adresse ist bereits vergeben!"
|
||||
|
||||
#: .\cookbook\forms.py:367
|
||||
#, fuzzy
|
||||
#| msgid ""
|
||||
#| "An email address is not required but if present the invite link will be "
|
||||
#| "send to the user."
|
||||
msgid ""
|
||||
"An email address is not required but if present the invite link will be sent "
|
||||
"to the user."
|
||||
|
||||
Binary file not shown.
@@ -11,8 +11,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-11-08 16:27+0100\n"
|
||||
"PO-Revision-Date: 2021-09-18 23:06+0000\n"
|
||||
"POT-Creation-Date: 2021-11-07 17:31+0100\n"
|
||||
"PO-Revision-Date: 2021-11-12 20:06+0000\n"
|
||||
"Last-Translator: Oliver Cervera <olivercervera@yahoo.it>\n"
|
||||
"Language-Team: Italian <http://translate.tandoor.dev/projects/tandoor/"
|
||||
"recipes-backend/it/>\n"
|
||||
@@ -31,66 +31,52 @@ msgid "Ingredients"
|
||||
msgstr "Ingredienti"
|
||||
|
||||
#: .\cookbook\forms.py:54
|
||||
#, fuzzy
|
||||
#| msgid "Default"
|
||||
msgid "Default unit"
|
||||
msgstr "Predefinito"
|
||||
msgstr "Unità predefinita"
|
||||
|
||||
#: .\cookbook\forms.py:55
|
||||
#, fuzzy
|
||||
#| msgid "System Information"
|
||||
msgid "Use fractions"
|
||||
msgstr "Informazioni di sistema"
|
||||
msgstr "Usa frazioni"
|
||||
|
||||
#: .\cookbook\forms.py:56
|
||||
msgid "Use KJ"
|
||||
msgstr ""
|
||||
msgstr "Usa KJ"
|
||||
|
||||
#: .\cookbook\forms.py:57
|
||||
msgid "Theme"
|
||||
msgstr ""
|
||||
msgstr "Tema"
|
||||
|
||||
#: .\cookbook\forms.py:58
|
||||
msgid "Navbar color"
|
||||
msgstr ""
|
||||
msgstr "Colore barra di navigazione"
|
||||
|
||||
#: .\cookbook\forms.py:59
|
||||
msgid "Sticky navbar"
|
||||
msgstr ""
|
||||
msgstr "Barra di navigazione persistente"
|
||||
|
||||
#: .\cookbook\forms.py:60
|
||||
#, fuzzy
|
||||
#| msgid "Default"
|
||||
msgid "Default page"
|
||||
msgstr "Predefinito"
|
||||
msgstr "Pagina predefinita"
|
||||
|
||||
#: .\cookbook\forms.py:61
|
||||
#, fuzzy
|
||||
#| msgid "Shopping Recipes"
|
||||
msgid "Show recent recipes"
|
||||
msgstr "Ricette per la spesa"
|
||||
msgstr "Mostra ricette recenti"
|
||||
|
||||
#: .\cookbook\forms.py:62
|
||||
#, fuzzy
|
||||
#| msgid "Search"
|
||||
msgid "Search style"
|
||||
msgstr "Cerca"
|
||||
msgstr "Cerca stile"
|
||||
|
||||
#: .\cookbook\forms.py:63
|
||||
msgid "Plan sharing"
|
||||
msgstr ""
|
||||
msgstr "Condivisione piano"
|
||||
|
||||
#: .\cookbook\forms.py:64
|
||||
#, fuzzy
|
||||
#| msgid "Ingredients"
|
||||
msgid "Ingredient decimal places"
|
||||
msgstr "Ingredienti"
|
||||
msgstr "Posizioni decimali degli ingredienti"
|
||||
|
||||
#: .\cookbook\forms.py:65
|
||||
#, fuzzy
|
||||
#| msgid "Shopping list currently empty"
|
||||
msgid "Shopping list auto sync period"
|
||||
msgstr "La lista della spesa è vuota"
|
||||
msgstr "Frequenza di sincronizzazione automatica della lista della spesa"
|
||||
|
||||
#: .\cookbook\forms.py:66 .\cookbook\templates\recipe_view.html:21
|
||||
#: .\cookbook\templates\space.html:62 .\cookbook\templates\stats.html:47
|
||||
@@ -121,7 +107,7 @@ msgstr ""
|
||||
|
||||
#: .\cookbook\forms.py:76
|
||||
msgid "Display nutritional energy amounts in joules instead of calories"
|
||||
msgstr ""
|
||||
msgstr "Mostra le informazioni nutrizionali in Joule invece che in calorie"
|
||||
|
||||
#: .\cookbook\forms.py:78
|
||||
msgid ""
|
||||
@@ -237,7 +223,7 @@ msgstr "Archiviazione"
|
||||
|
||||
#: .\cookbook\forms.py:260
|
||||
msgid "Active"
|
||||
msgstr ""
|
||||
msgstr "Attivo"
|
||||
|
||||
#: .\cookbook\forms.py:265
|
||||
msgid "Search String"
|
||||
@@ -304,8 +290,8 @@ msgid ""
|
||||
"Select type method of search. Click <a href=\"/docs/search/\">here</a> for "
|
||||
"full desciption of choices."
|
||||
msgstr ""
|
||||
"Seleziona il metodo di ricerca. Clicca<a href=\"/docs/search/\">qui</a> per "
|
||||
"avere maggiori informazioni."
|
||||
"Seleziona il metodo di ricerca. Clicca <a href=\"/docs/search/\">qui</a> "
|
||||
"per avere maggiori informazioni."
|
||||
|
||||
#: .\cookbook\forms.py:436
|
||||
msgid ""
|
||||
@@ -364,10 +350,8 @@ msgid "Starts Wtih"
|
||||
msgstr "Inizia con"
|
||||
|
||||
#: .\cookbook\forms.py:455
|
||||
#, fuzzy
|
||||
#| msgid "Search"
|
||||
msgid "Fuzzy Search"
|
||||
msgstr "Cerca"
|
||||
msgstr "Ricerca Fuzzy"
|
||||
|
||||
#: .\cookbook\forms.py:456
|
||||
msgid "Full Text"
|
||||
@@ -384,7 +368,7 @@ msgstr ""
|
||||
#: .\cookbook\helper\permission_helper.py:136
|
||||
#: .\cookbook\helper\permission_helper.py:159 .\cookbook\views\views.py:149
|
||||
msgid "You are not logged in and therefore cannot view this page!"
|
||||
msgstr "Non hai fatto l'accesso e quindi non puoi visualizzare questa pagina!"
|
||||
msgstr "Non sei loggato e quindi non puoi visualizzare questa pagina!"
|
||||
|
||||
#: .\cookbook\helper\permission_helper.py:140
|
||||
#: .\cookbook\helper\permission_helper.py:146
|
||||
@@ -478,7 +462,7 @@ msgstr "Tempo di preparazione"
|
||||
#: .\cookbook\templates\forms\ingredients.html:7
|
||||
#: .\cookbook\templates\index.html:7
|
||||
msgid "Cookbook"
|
||||
msgstr "Ricettario"
|
||||
msgstr "Ricette"
|
||||
|
||||
#: .\cookbook\integration\safron.py:31
|
||||
msgid "Section"
|
||||
@@ -595,22 +579,16 @@ msgid "Raw"
|
||||
msgstr "Raw"
|
||||
|
||||
#: .\cookbook\models.py:912
|
||||
#, fuzzy
|
||||
#| msgid "Food"
|
||||
msgid "Food Alias"
|
||||
msgstr "Alimento"
|
||||
msgstr "Alias Alimento"
|
||||
|
||||
#: .\cookbook\models.py:912
|
||||
#, fuzzy
|
||||
#| msgid "Units"
|
||||
msgid "Unit Alias"
|
||||
msgstr "Unità di misura"
|
||||
msgstr "Alias Unità"
|
||||
|
||||
#: .\cookbook\models.py:912
|
||||
#, fuzzy
|
||||
#| msgid "Keywords"
|
||||
msgid "Keyword Alias"
|
||||
msgstr "Parole chiave"
|
||||
msgstr "Alias Parola Chiave"
|
||||
|
||||
#: .\cookbook\serializer.py:157
|
||||
msgid "File uploads are not enabled for this Space."
|
||||
@@ -851,10 +829,8 @@ msgstr ""
|
||||
"minuto."
|
||||
|
||||
#: .\cookbook\templates\account\password_reset_from_key.html:13
|
||||
#, fuzzy
|
||||
#| msgid "API Token"
|
||||
msgid "Bad Token"
|
||||
msgstr "Token API"
|
||||
msgstr "Token non valido"
|
||||
|
||||
#: .\cookbook\templates\account\password_reset_from_key.html:25
|
||||
#, python-format
|
||||
@@ -866,15 +842,13 @@ msgid ""
|
||||
msgstr ""
|
||||
|
||||
#: .\cookbook\templates\account\password_reset_from_key.html:33
|
||||
#, fuzzy
|
||||
#| msgid "Change Password"
|
||||
msgid "change password"
|
||||
msgstr "Cambia Password"
|
||||
msgstr "cambia password"
|
||||
|
||||
#: .\cookbook\templates\account\password_reset_from_key.html:36
|
||||
#: .\cookbook\templates\account\password_reset_from_key_done.html:19
|
||||
msgid "Your password is now changed."
|
||||
msgstr ""
|
||||
msgstr "La tua password è stata aggiornata."
|
||||
|
||||
#: .\cookbook\templates\account\password_set.html:6
|
||||
#: .\cookbook\templates\account\password_set.html:16
|
||||
@@ -954,10 +928,8 @@ msgid "Supermarket"
|
||||
msgstr "Supermercato"
|
||||
|
||||
#: .\cookbook\templates\base.html:163
|
||||
#, fuzzy
|
||||
#| msgid "Supermarket"
|
||||
msgid "Supermarket Category"
|
||||
msgstr "Supermercato"
|
||||
msgstr "Categoria Supermercato"
|
||||
|
||||
#: .\cookbook\templates\base.html:175 .\cookbook\views\lists.py:195
|
||||
#, fuzzy
|
||||
@@ -1004,7 +976,7 @@ msgstr "Ricette esterne"
|
||||
#: .\cookbook\templates\base.html:262 .\cookbook\templates\space.html:7
|
||||
#: .\cookbook\templates\space.html:19
|
||||
msgid "Space Settings"
|
||||
msgstr "Impostazioni Istanza"
|
||||
msgstr "Impostazioni istanza"
|
||||
|
||||
#: .\cookbook\templates\base.html:267 .\cookbook\templates\system.html:13
|
||||
msgid "System"
|
||||
@@ -1024,7 +996,7 @@ msgstr "GitHub"
|
||||
|
||||
#: .\cookbook\templates\base.html:277
|
||||
msgid "Translate Tandoor"
|
||||
msgstr ""
|
||||
msgstr "Traduci Tandoor"
|
||||
|
||||
#: .\cookbook\templates\base.html:281
|
||||
msgid "API Browser"
|
||||
@@ -1089,16 +1061,12 @@ msgid "Sync Now!"
|
||||
msgstr "Sincronizza Ora!"
|
||||
|
||||
#: .\cookbook\templates\batch\monitor.html:29
|
||||
#, fuzzy
|
||||
#| msgid "Shopping Recipes"
|
||||
msgid "Show Recipes"
|
||||
msgstr "Ricette per la spesa"
|
||||
msgstr "Mostra ricette"
|
||||
|
||||
#: .\cookbook\templates\batch\monitor.html:30
|
||||
#, fuzzy
|
||||
#| msgid "Show Links"
|
||||
msgid "Show Log"
|
||||
msgstr "Mostra link"
|
||||
msgstr "Mostra registro"
|
||||
|
||||
#: .\cookbook\templates\batch\waiting.html:4
|
||||
#: .\cookbook\templates\batch\waiting.html:10
|
||||
@@ -1171,11 +1139,11 @@ msgstr "Sei sicuro di volere eliminare %(title)s: <b>%(object)s</b>"
|
||||
|
||||
#: .\cookbook\templates\generic\delete_template.html:26
|
||||
msgid "Protected"
|
||||
msgstr ""
|
||||
msgstr "Protetto"
|
||||
|
||||
#: .\cookbook\templates\generic\delete_template.html:41
|
||||
msgid "Cascade"
|
||||
msgstr ""
|
||||
msgstr "Cascata"
|
||||
|
||||
#: .\cookbook\templates\generic\delete_template.html:72
|
||||
msgid "Cancel"
|
||||
@@ -1613,10 +1581,8 @@ msgstr "Pagina iniziale ricette"
|
||||
#: .\cookbook\templates\search_info.html:5
|
||||
#: .\cookbook\templates\search_info.html:9
|
||||
#: .\cookbook\templates\settings.html:165
|
||||
#, fuzzy
|
||||
#| msgid "Search String"
|
||||
msgid "Search Settings"
|
||||
msgstr "Stringa di Ricerca"
|
||||
msgstr "Impostazioni di ricerca"
|
||||
|
||||
#: .\cookbook\templates\search_info.html:10
|
||||
msgid ""
|
||||
@@ -1639,10 +1605,8 @@ msgstr ""
|
||||
" "
|
||||
|
||||
#: .\cookbook\templates\search_info.html:19
|
||||
#, fuzzy
|
||||
#| msgid "Search"
|
||||
msgid "Search Methods"
|
||||
msgstr "Cerca"
|
||||
msgstr "Metodi di ricerca"
|
||||
|
||||
#: .\cookbook\templates\search_info.html:23
|
||||
msgid ""
|
||||
@@ -1724,10 +1688,8 @@ msgid ""
|
||||
msgstr ""
|
||||
|
||||
#: .\cookbook\templates\search_info.html:69
|
||||
#, fuzzy
|
||||
#| msgid "Search Recipe"
|
||||
msgid "Search Fields"
|
||||
msgstr "Cerca Ricetta"
|
||||
msgstr "Campi di ricerca"
|
||||
|
||||
#: .\cookbook\templates\search_info.html:73
|
||||
msgid ""
|
||||
@@ -1765,10 +1727,8 @@ msgid ""
|
||||
msgstr ""
|
||||
|
||||
#: .\cookbook\templates\search_info.html:95
|
||||
#, fuzzy
|
||||
#| msgid "Search"
|
||||
msgid "Search Index"
|
||||
msgstr "Cerca"
|
||||
msgstr "Indice di ricerca"
|
||||
|
||||
#: .\cookbook\templates\search_info.html:99
|
||||
msgid ""
|
||||
@@ -1796,10 +1756,8 @@ msgid "API-Settings"
|
||||
msgstr "Impostazioni API"
|
||||
|
||||
#: .\cookbook\templates\settings.html:49
|
||||
#, fuzzy
|
||||
#| msgid "Search String"
|
||||
msgid "Search-Settings"
|
||||
msgstr "Stringa di Ricerca"
|
||||
msgstr "Cerca-Impostazioni"
|
||||
|
||||
#: .\cookbook\templates\settings.html:58
|
||||
msgid "Name Settings"
|
||||
@@ -1855,22 +1813,28 @@ msgid ""
|
||||
"There are many options to configure the search depending on your personal "
|
||||
"preferences."
|
||||
msgstr ""
|
||||
"Ci sono molte opzioni per configurare la ricerca in base alle tue preferenze."
|
||||
|
||||
#: .\cookbook\templates\settings.html:167
|
||||
msgid ""
|
||||
"Usually you do <b>not need</b> to configure any of them and can just stick "
|
||||
"with either the default or one of the following presets."
|
||||
msgstr ""
|
||||
"Normalmente <b>non c'è bisogno</b> di configurare queste voci e puoi "
|
||||
"continuare a usare le impostazioni predefinite oppure scegliere una delle "
|
||||
"seguenti modalità."
|
||||
|
||||
#: .\cookbook\templates\settings.html:168
|
||||
msgid ""
|
||||
"If you do want to configure the search you can read about the different "
|
||||
"options <a href=\"/docs/search/\">here</a>."
|
||||
msgstr ""
|
||||
"Se vuoi comunque configurare la ricerca, puoi informarti riguardo le opzioni "
|
||||
"disponibili <a href=\"/docs/search/\">qui</a>."
|
||||
|
||||
#: .\cookbook\templates\settings.html:173
|
||||
msgid "Fuzzy"
|
||||
msgstr ""
|
||||
msgstr "Fuzzy"
|
||||
|
||||
#: .\cookbook\templates\settings.html:174
|
||||
msgid ""
|
||||
@@ -1881,16 +1845,16 @@ msgstr ""
|
||||
|
||||
#: .\cookbook\templates\settings.html:175
|
||||
msgid "This is the default behavior"
|
||||
msgstr ""
|
||||
msgstr "È il comportamento predefinito"
|
||||
|
||||
#: .\cookbook\templates\settings.html:176
|
||||
#: .\cookbook\templates\settings.html:184
|
||||
msgid "Apply"
|
||||
msgstr ""
|
||||
msgstr "Applica"
|
||||
|
||||
#: .\cookbook\templates\settings.html:181
|
||||
msgid "Precise"
|
||||
msgstr ""
|
||||
msgstr "Preciso"
|
||||
|
||||
#: .\cookbook\templates\settings.html:182
|
||||
msgid ""
|
||||
@@ -1904,7 +1868,7 @@ msgstr ""
|
||||
|
||||
#: .\cookbook\templates\setup.html:6 .\cookbook\templates\system.html:5
|
||||
msgid "Cookbook Setup"
|
||||
msgstr "Configurazione del ricettario"
|
||||
msgstr "Configurazione Recipes"
|
||||
|
||||
#: .\cookbook\templates\setup.html:14
|
||||
msgid "Setup"
|
||||
@@ -1927,10 +1891,8 @@ msgid "Shopping List"
|
||||
msgstr "Lista della spesa"
|
||||
|
||||
#: .\cookbook\templates\shopping_list.html:34
|
||||
#, fuzzy
|
||||
#| msgid "Open Shopping List"
|
||||
msgid "Try the new shopping list"
|
||||
msgstr "Apri lista della spesa"
|
||||
msgstr "Prova la nuova lista della spesa"
|
||||
|
||||
#: .\cookbook\templates\shopping_list.html:63
|
||||
msgid "Search Recipe"
|
||||
@@ -2030,7 +1992,7 @@ msgid ""
|
||||
" %(site_name)s. As a final step, please complete the following form:"
|
||||
msgstr ""
|
||||
"Stai per usare il tuo:\n"
|
||||
" Account %(provider_name)s per fare l'accesso a\n"
|
||||
" account %(provider_name)s per fare l'accesso a\n"
|
||||
" %(site_name)s. Per finire, completa il modulo qui sotto:"
|
||||
|
||||
#: .\cookbook\templates\socialaccount\snippets\provider_list.html:23
|
||||
@@ -2270,11 +2232,11 @@ msgstr "Salvami nei preferiti!"
|
||||
|
||||
#: .\cookbook\templates\url_import.html:36
|
||||
msgid "URL"
|
||||
msgstr ""
|
||||
msgstr "URL"
|
||||
|
||||
#: .\cookbook\templates\url_import.html:38
|
||||
msgid "App"
|
||||
msgstr ""
|
||||
msgstr "App"
|
||||
|
||||
#: .\cookbook\templates\url_import.html:62
|
||||
msgid "Enter website URL"
|
||||
@@ -2468,10 +2430,8 @@ msgid "No {self.basename} with id {target} exists"
|
||||
msgstr "Non esiste nessun {self.basename} con id {target}"
|
||||
|
||||
#: .\cookbook\views\api.py:168
|
||||
#, fuzzy
|
||||
#| msgid "Cannot merge with the same object!"
|
||||
msgid "Cannot merge with child object!"
|
||||
msgstr "Non è possibile unirlo con lo stesso oggetto!"
|
||||
msgstr "Non è possibile unirlo con un oggetto secondario!"
|
||||
|
||||
#: .\cookbook\views\api.py:201
|
||||
#, python-brace-format
|
||||
@@ -2514,10 +2474,9 @@ msgstr "{child.name} è stato spostato con successo al primario {parent.name}"
|
||||
|
||||
#: .\cookbook\views\api.py:723 .\cookbook\views\data.py:42
|
||||
#: .\cookbook\views\edit.py:129 .\cookbook\views\new.py:95
|
||||
#, fuzzy
|
||||
#| msgid "This feature is not available in the demo version!"
|
||||
msgid "This feature is not yet available in the hosted version of tandoor!"
|
||||
msgstr "Questa funzione non è disponibile nella versione demo!"
|
||||
msgstr ""
|
||||
"Questa funzione non è ancora disponibile nella versione hostata di Tandor!"
|
||||
|
||||
#: .\cookbook\views\api.py:745
|
||||
msgid "Sync successful!"
|
||||
@@ -2648,28 +2607,20 @@ msgid "Shopping Lists"
|
||||
msgstr "Liste della spesa"
|
||||
|
||||
#: .\cookbook\views\lists.py:129
|
||||
#, fuzzy
|
||||
#| msgid "Food"
|
||||
msgid "Foods"
|
||||
msgstr "Alimento"
|
||||
msgstr "Alimenti"
|
||||
|
||||
#: .\cookbook\views\lists.py:163
|
||||
#, fuzzy
|
||||
#| msgid "Supermarket"
|
||||
msgid "Supermarkets"
|
||||
msgstr "Supermercato"
|
||||
msgstr "Supermercati"
|
||||
|
||||
#: .\cookbook\views\lists.py:179
|
||||
#, fuzzy
|
||||
#| msgid "Shopping Recipes"
|
||||
msgid "Shopping Categories"
|
||||
msgstr "Ricette per la spesa"
|
||||
msgstr "Categorie della spesa"
|
||||
|
||||
#: .\cookbook\views\lists.py:232
|
||||
#, fuzzy
|
||||
#| msgid "Shopping List"
|
||||
msgid "New Shopping List"
|
||||
msgstr "Lista della spesa"
|
||||
msgstr "Nuova lista della spesa"
|
||||
|
||||
#: .\cookbook\views\new.py:126
|
||||
msgid "Imported new recipe!"
|
||||
@@ -2769,7 +2720,7 @@ msgstr ""
|
||||
|
||||
#: .\cookbook\views\views.py:349
|
||||
msgid "Fuzzy search is not compatible with this search method!"
|
||||
msgstr ""
|
||||
msgstr "La ricerca Fuzzy non è compatibile con questo metodo di ricerca!"
|
||||
|
||||
#: .\cookbook\views\views.py:452
|
||||
msgid ""
|
||||
|
||||
Binary file not shown.
@@ -12,11 +12,11 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-11-08 16:27+0100\n"
|
||||
"PO-Revision-Date: 2021-10-26 10:06+0000\n"
|
||||
"Last-Translator: Jesse <jesse.kamps@pm.me>\n"
|
||||
"Language-Team: Dutch <http://translate.tandoor.dev/projects/tandoor/recipes-"
|
||||
"backend/nl/>\n"
|
||||
"POT-Creation-Date: 2021-11-07 17:31+0100\n"
|
||||
"PO-Revision-Date: 2021-11-14 14:06+0000\n"
|
||||
"Last-Translator: Job Putters <me@ixbitz.com>\n"
|
||||
"Language-Team: Dutch <http://translate.tandoor.dev/projects/tandoor/"
|
||||
"recipes-backend/nl/>\n"
|
||||
"Language: nl\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
@@ -32,66 +32,52 @@ msgid "Ingredients"
|
||||
msgstr "Ingrediënten"
|
||||
|
||||
#: .\cookbook\forms.py:54
|
||||
#, fuzzy
|
||||
#| msgid "Default"
|
||||
msgid "Default unit"
|
||||
msgstr "Standaard waarde"
|
||||
msgstr "Standaard eenheid"
|
||||
|
||||
#: .\cookbook\forms.py:55
|
||||
#, fuzzy
|
||||
#| msgid "System Information"
|
||||
msgid "Use fractions"
|
||||
msgstr "Systeeminformatie"
|
||||
msgstr "Gebruik fracties"
|
||||
|
||||
#: .\cookbook\forms.py:56
|
||||
msgid "Use KJ"
|
||||
msgstr ""
|
||||
msgstr "Gebruik KJ"
|
||||
|
||||
#: .\cookbook\forms.py:57
|
||||
msgid "Theme"
|
||||
msgstr ""
|
||||
msgstr "Thema"
|
||||
|
||||
#: .\cookbook\forms.py:58
|
||||
msgid "Navbar color"
|
||||
msgstr ""
|
||||
msgstr "Navbar kleur"
|
||||
|
||||
#: .\cookbook\forms.py:59
|
||||
msgid "Sticky navbar"
|
||||
msgstr ""
|
||||
msgstr "Plak navbar"
|
||||
|
||||
#: .\cookbook\forms.py:60
|
||||
#, fuzzy
|
||||
#| msgid "Default"
|
||||
msgid "Default page"
|
||||
msgstr "Standaard waarde"
|
||||
msgstr "Standaard pagina"
|
||||
|
||||
#: .\cookbook\forms.py:61
|
||||
#, fuzzy
|
||||
#| msgid "Show Recipes"
|
||||
msgid "Show recent recipes"
|
||||
msgstr "Toon Recepten"
|
||||
msgstr "Toon recente recepten"
|
||||
|
||||
#: .\cookbook\forms.py:62
|
||||
#, fuzzy
|
||||
#| msgid "Search Index"
|
||||
msgid "Search style"
|
||||
msgstr "Zoekindex"
|
||||
msgstr "Zoekstijl"
|
||||
|
||||
#: .\cookbook\forms.py:63
|
||||
msgid "Plan sharing"
|
||||
msgstr ""
|
||||
msgstr "Plan delen"
|
||||
|
||||
#: .\cookbook\forms.py:64
|
||||
#, fuzzy
|
||||
#| msgid "Ingredients"
|
||||
msgid "Ingredient decimal places"
|
||||
msgstr "Ingrediënten"
|
||||
msgstr "Ingrediënt decimalen"
|
||||
|
||||
#: .\cookbook\forms.py:65
|
||||
#, fuzzy
|
||||
#| msgid "Shopping list currently empty"
|
||||
msgid "Shopping list auto sync period"
|
||||
msgstr "Boodschappenlijst is momenteel leeg"
|
||||
msgstr "Boodschappenlijst auto sync periode"
|
||||
|
||||
#: .\cookbook\forms.py:66 .\cookbook\templates\recipe_view.html:21
|
||||
#: .\cookbook\templates\space.html:62 .\cookbook\templates\stats.html:47
|
||||
@@ -122,7 +108,7 @@ msgstr ""
|
||||
|
||||
#: .\cookbook\forms.py:76
|
||||
msgid "Display nutritional energy amounts in joules instead of calories"
|
||||
msgstr ""
|
||||
msgstr "Geef energiewaardes weer in joules in plaats van calorieën"
|
||||
|
||||
#: .\cookbook\forms.py:78
|
||||
msgid ""
|
||||
@@ -235,7 +221,7 @@ msgstr "Opslag"
|
||||
|
||||
#: .\cookbook\forms.py:260
|
||||
msgid "Active"
|
||||
msgstr ""
|
||||
msgstr "Actief"
|
||||
|
||||
#: .\cookbook\forms.py:265
|
||||
msgid "Search String"
|
||||
@@ -272,10 +258,6 @@ msgid "Email address already taken!"
|
||||
msgstr "E-mailadres reeds in gebruik!"
|
||||
|
||||
#: .\cookbook\forms.py:367
|
||||
#, fuzzy
|
||||
#| msgid ""
|
||||
#| "An email address is not required but if present the invite link will be "
|
||||
#| "send to the user."
|
||||
msgid ""
|
||||
"An email address is not required but if present the invite link will be sent "
|
||||
"to the user."
|
||||
@@ -296,6 +278,8 @@ msgid ""
|
||||
"Determines how fuzzy a search is if it uses trigram similarity matching (e."
|
||||
"g. low values mean more typos are ignored)."
|
||||
msgstr ""
|
||||
"Bepaalt hoe 'fuzzy' een zoekopdracht is als het trigram vergelijken gebruikt "
|
||||
"(lage waarden betekenen bijvoorbeeld dat meer typefouten genegeerd worden)."
|
||||
|
||||
#: .\cookbook\forms.py:435
|
||||
msgid ""
|
||||
@@ -601,25 +585,19 @@ msgstr "Web"
|
||||
|
||||
#: .\cookbook\models.py:874 .\cookbook\templates\search_info.html:47
|
||||
msgid "Raw"
|
||||
msgstr "Raw"
|
||||
msgstr "Rauw"
|
||||
|
||||
#: .\cookbook\models.py:912
|
||||
#, fuzzy
|
||||
#| msgid "Foods"
|
||||
msgid "Food Alias"
|
||||
msgstr "Ingrediënten"
|
||||
msgstr "Ingrediënt alias"
|
||||
|
||||
#: .\cookbook\models.py:912
|
||||
#, fuzzy
|
||||
#| msgid "Units"
|
||||
msgid "Unit Alias"
|
||||
msgstr "Eenheden"
|
||||
msgstr "Eenheid alias"
|
||||
|
||||
#: .\cookbook\models.py:912
|
||||
#, fuzzy
|
||||
#| msgid "Keywords"
|
||||
msgid "Keyword Alias"
|
||||
msgstr "Etiketten"
|
||||
msgstr "Etiket alias"
|
||||
|
||||
#: .\cookbook\serializer.py:157
|
||||
msgid "File uploads are not enabled for this Space."
|
||||
@@ -859,10 +837,8 @@ msgstr ""
|
||||
"minuten ontvangen hebt."
|
||||
|
||||
#: .\cookbook\templates\account\password_reset_from_key.html:13
|
||||
#, fuzzy
|
||||
#| msgid "API Token"
|
||||
msgid "Bad Token"
|
||||
msgstr "API Token"
|
||||
msgstr "Bad token"
|
||||
|
||||
#: .\cookbook\templates\account\password_reset_from_key.html:25
|
||||
#, python-format
|
||||
@@ -872,17 +848,19 @@ msgid ""
|
||||
" Please request a <a href=\"%(passwd_reset_url)s\">new "
|
||||
"password reset</a>."
|
||||
msgstr ""
|
||||
"De link voor het opnieuw instellen van het wachtwoord was ongeldig, mogelijk "
|
||||
"omdat hij al gebruikt is.\n"
|
||||
" Vraag een <a href=\"%(passwd_reset_url)s\">nieuwe link "
|
||||
"voor een wachtwoord reset aan</a>."
|
||||
|
||||
#: .\cookbook\templates\account\password_reset_from_key.html:33
|
||||
#, fuzzy
|
||||
#| msgid "Change Password"
|
||||
msgid "change password"
|
||||
msgstr "Wijzig wachtwoord"
|
||||
|
||||
#: .\cookbook\templates\account\password_reset_from_key.html:36
|
||||
#: .\cookbook\templates\account\password_reset_from_key_done.html:19
|
||||
msgid "Your password is now changed."
|
||||
msgstr ""
|
||||
msgstr "Je wachtwoord is nu gewijzigd."
|
||||
|
||||
#: .\cookbook\templates\account\password_set.html:6
|
||||
#: .\cookbook\templates\account\password_set.html:16
|
||||
@@ -962,16 +940,12 @@ msgid "Supermarket"
|
||||
msgstr "Supermarkt"
|
||||
|
||||
#: .\cookbook\templates\base.html:163
|
||||
#, fuzzy
|
||||
#| msgid "Supermarket"
|
||||
msgid "Supermarket Category"
|
||||
msgstr "Supermarkt"
|
||||
msgstr "Supermarktcategorie"
|
||||
|
||||
#: .\cookbook\templates\base.html:175 .\cookbook\views\lists.py:195
|
||||
#, fuzzy
|
||||
#| msgid "Information"
|
||||
msgid "Automations"
|
||||
msgstr "Informatie"
|
||||
msgstr "Automatiseringen"
|
||||
|
||||
#: .\cookbook\templates\base.html:189 .\cookbook\views\lists.py:215
|
||||
msgid "Files"
|
||||
@@ -1001,7 +975,7 @@ msgstr "Recept importeren"
|
||||
#: .\cookbook\templates\shopping_list.html:195
|
||||
#: .\cookbook\templates\shopping_list.html:217
|
||||
msgid "Create"
|
||||
msgstr "Nieuw recept"
|
||||
msgstr "Aanmaken"
|
||||
|
||||
#: .\cookbook\templates\base.html:259
|
||||
#: .\cookbook\templates\generic\list_template.html:14
|
||||
@@ -1032,7 +1006,7 @@ msgstr "GitHub"
|
||||
|
||||
#: .\cookbook\templates\base.html:277
|
||||
msgid "Translate Tandoor"
|
||||
msgstr ""
|
||||
msgstr "Vertaal Tandoor"
|
||||
|
||||
#: .\cookbook\templates\base.html:281
|
||||
msgid "API Browser"
|
||||
@@ -1176,11 +1150,11 @@ msgstr "Weet je zeker dat je %(title)s: <b>%(object)s</b> wil verwijderen "
|
||||
|
||||
#: .\cookbook\templates\generic\delete_template.html:26
|
||||
msgid "Protected"
|
||||
msgstr ""
|
||||
msgstr "Beschermd"
|
||||
|
||||
#: .\cookbook\templates\generic\delete_template.html:41
|
||||
msgid "Cascade"
|
||||
msgstr ""
|
||||
msgstr "Cascade"
|
||||
|
||||
#: .\cookbook\templates\generic\delete_template.html:72
|
||||
msgid "Cancel"
|
||||
@@ -1267,11 +1241,11 @@ msgid ""
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
" Het <b>wachtwoord en token</b> veld worden als <b>plain text</b> in "
|
||||
"de database opgeslagen.\n"
|
||||
" Dit is nodig omdat deze benodigd zijn voor de API requests, Echter verhoogt "
|
||||
"dit ook het risico van diefstal.<br/>\n"
|
||||
" Om mogelijke schade te beperken kun je gebruik maken van account met "
|
||||
" Het <b>wachtwoord en token</b> veld worden als <b>plain text</b> "
|
||||
"opgeslagen in de database.\n"
|
||||
" Dit is nodig omdat deze benodigd zijn voor de API requests, Dit verhoogt "
|
||||
"echter ook het risico van diefstal.<br/>\n"
|
||||
" Om mogelijke schade te beperken kun je gebruik maken van accounts met "
|
||||
"gelimiteerde toegang.\n"
|
||||
" "
|
||||
|
||||
@@ -1942,22 +1916,29 @@ msgid ""
|
||||
"There are many options to configure the search depending on your personal "
|
||||
"preferences."
|
||||
msgstr ""
|
||||
"Er zijn vele mogelijkheden om het zoeken te configureren die afhangen van je "
|
||||
"persoonlijke voorkeur."
|
||||
|
||||
#: .\cookbook\templates\settings.html:167
|
||||
msgid ""
|
||||
"Usually you do <b>not need</b> to configure any of them and can just stick "
|
||||
"with either the default or one of the following presets."
|
||||
msgstr ""
|
||||
"Normaal gesproken is het <b>niet nodig</b> ze te configureren en kan je "
|
||||
"gebruikmaken van het standaard profiel of de volgende vooraf ingestelde "
|
||||
"profielen."
|
||||
|
||||
#: .\cookbook\templates\settings.html:168
|
||||
msgid ""
|
||||
"If you do want to configure the search you can read about the different "
|
||||
"options <a href=\"/docs/search/\">here</a>."
|
||||
msgstr ""
|
||||
"Als je het zoeken wil configureren kan je <a href=\"/docs/search/\">hier</a> "
|
||||
"over de verschillende opties lezen."
|
||||
|
||||
#: .\cookbook\templates\settings.html:173
|
||||
msgid "Fuzzy"
|
||||
msgstr ""
|
||||
msgstr "Fuzzy"
|
||||
|
||||
#: .\cookbook\templates\settings.html:174
|
||||
msgid ""
|
||||
@@ -1965,29 +1946,34 @@ msgid ""
|
||||
"return more results than needed to make sure you find what you are looking "
|
||||
"for."
|
||||
msgstr ""
|
||||
"Vind wat je nodig hebt, zelfs als je zoekopdracht of het recept typefouten "
|
||||
"bevat. Mogelijk krijg je meer resultaten dan je nodig hebt, om zeker te "
|
||||
"weten dat je vindt wat je nodig hebt."
|
||||
|
||||
#: .\cookbook\templates\settings.html:175
|
||||
msgid "This is the default behavior"
|
||||
msgstr ""
|
||||
msgstr "Dit is het standaard gedrag"
|
||||
|
||||
#: .\cookbook\templates\settings.html:176
|
||||
#: .\cookbook\templates\settings.html:184
|
||||
msgid "Apply"
|
||||
msgstr ""
|
||||
msgstr "Pas toe"
|
||||
|
||||
#: .\cookbook\templates\settings.html:181
|
||||
msgid "Precise"
|
||||
msgstr ""
|
||||
msgstr "Nauwkeurig"
|
||||
|
||||
#: .\cookbook\templates\settings.html:182
|
||||
msgid ""
|
||||
"Allows fine control over search results but might not return results if too "
|
||||
"many spelling mistakes are made."
|
||||
msgstr ""
|
||||
"Staat fijnmazige controle over zoekresultaten toe, maar toont mogelijk geen "
|
||||
"resultaten als er te veel spelfouten gemaakt zijn."
|
||||
|
||||
#: .\cookbook\templates\settings.html:183
|
||||
msgid "Perfect for large Databases"
|
||||
msgstr ""
|
||||
msgstr "Perfect voor grote databases"
|
||||
|
||||
#: .\cookbook\templates\setup.html:6 .\cookbook\templates\system.html:5
|
||||
msgid "Cookbook Setup"
|
||||
@@ -2014,10 +2000,8 @@ msgid "Shopping List"
|
||||
msgstr "Boodschappenlijst"
|
||||
|
||||
#: .\cookbook\templates\shopping_list.html:34
|
||||
#, fuzzy
|
||||
#| msgid "Open Shopping List"
|
||||
msgid "Try the new shopping list"
|
||||
msgstr "Open boodschappenlijst"
|
||||
msgstr "Probeer de nieuwe boodschappenlijst"
|
||||
|
||||
#: .\cookbook\templates\shopping_list.html:63
|
||||
msgid "Search Recipe"
|
||||
@@ -2367,11 +2351,11 @@ msgstr "Sla mij op als bladwijzer!"
|
||||
|
||||
#: .\cookbook\templates\url_import.html:36
|
||||
msgid "URL"
|
||||
msgstr ""
|
||||
msgstr "URL"
|
||||
|
||||
#: .\cookbook\templates\url_import.html:38
|
||||
msgid "App"
|
||||
msgstr ""
|
||||
msgstr "App"
|
||||
|
||||
#: .\cookbook\templates\url_import.html:62
|
||||
msgid "Enter website URL"
|
||||
@@ -2612,10 +2596,8 @@ msgstr "{child.name} is succesvol verplaatst naar {parent.name}"
|
||||
|
||||
#: .\cookbook\views\api.py:723 .\cookbook\views\data.py:42
|
||||
#: .\cookbook\views\edit.py:129 .\cookbook\views\new.py:95
|
||||
#, fuzzy
|
||||
#| msgid "This feature is not available in the demo version!"
|
||||
msgid "This feature is not yet available in the hosted version of tandoor!"
|
||||
msgstr "Deze optie is niet beschikbaar in de demo versie!"
|
||||
msgstr "Deze optie is nog niet beschikbaar in de gehoste versie van Tandoor!"
|
||||
|
||||
#: .\cookbook\views\api.py:745
|
||||
msgid "Sync successful!"
|
||||
@@ -2755,10 +2737,8 @@ msgid "Shopping Categories"
|
||||
msgstr "Boodschappencategorieën"
|
||||
|
||||
#: .\cookbook\views\lists.py:232
|
||||
#, fuzzy
|
||||
#| msgid "Shopping List"
|
||||
msgid "New Shopping List"
|
||||
msgstr "Boodschappenlijst"
|
||||
msgstr "Nieuwe boodschappenlijst"
|
||||
|
||||
#: .\cookbook\views\new.py:126
|
||||
msgid "Imported new recipe!"
|
||||
|
||||
Binary file not shown.
@@ -7,21 +7,21 @@
|
||||
# Henrique Diogo Silva <hdiogosilva@gmail.com>, 2020
|
||||
# João Cunha <st0rmss95@gmail.com>, 2020
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-11-08 16:27+0100\n"
|
||||
"PO-Revision-Date: 2020-06-02 19:28+0000\n"
|
||||
"Last-Translator: João Cunha <st0rmss95@gmail.com>, 2020\n"
|
||||
"Language-Team: Portuguese (https://www.transifex.com/django-recipes/"
|
||||
"teams/110507/pt/)\n"
|
||||
"PO-Revision-Date: 2021-11-12 20:06+0000\n"
|
||||
"Last-Translator: Henrique Silva <hds@mailbox.org>\n"
|
||||
"Language-Team: Portuguese <http://translate.tandoor.dev/projects/tandoor/"
|
||||
"recipes-backend/pt/>\n"
|
||||
"Language: pt\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"X-Generator: Weblate 4.8\n"
|
||||
|
||||
#: .\cookbook\filters.py:23 .\cookbook\templates\base.html:125
|
||||
#: .\cookbook\templates\forms\ingredients.html:34
|
||||
@@ -32,64 +32,56 @@ msgstr "Ingredientes"
|
||||
|
||||
#: .\cookbook\forms.py:54
|
||||
msgid "Default unit"
|
||||
msgstr ""
|
||||
msgstr "Unidade predefinida"
|
||||
|
||||
#: .\cookbook\forms.py:55
|
||||
#, fuzzy
|
||||
#| msgid "Instructions"
|
||||
msgid "Use fractions"
|
||||
msgstr "Instruções"
|
||||
msgstr "Usar frações"
|
||||
|
||||
#: .\cookbook\forms.py:56
|
||||
msgid "Use KJ"
|
||||
msgstr ""
|
||||
msgstr "Usar KJ"
|
||||
|
||||
#: .\cookbook\forms.py:57
|
||||
msgid "Theme"
|
||||
msgstr ""
|
||||
msgstr "Tema"
|
||||
|
||||
#: .\cookbook\forms.py:58
|
||||
msgid "Navbar color"
|
||||
msgstr ""
|
||||
msgstr "Cor de barra de navegação"
|
||||
|
||||
#: .\cookbook\forms.py:59
|
||||
msgid "Sticky navbar"
|
||||
msgstr ""
|
||||
msgstr "Prender barra de navegação"
|
||||
|
||||
#: .\cookbook\forms.py:60
|
||||
msgid "Default page"
|
||||
msgstr ""
|
||||
msgstr "Página predefinida"
|
||||
|
||||
#: .\cookbook\forms.py:61
|
||||
#, fuzzy
|
||||
#| msgid "Recipes"
|
||||
msgid "Show recent recipes"
|
||||
msgstr "Receitas"
|
||||
msgstr "Mostrar receitas recentes"
|
||||
|
||||
#: .\cookbook\forms.py:62
|
||||
#, fuzzy
|
||||
#| msgid "Search"
|
||||
msgid "Search style"
|
||||
msgstr "Procurar"
|
||||
msgstr "Estilo de pesquisa"
|
||||
|
||||
#: .\cookbook\forms.py:63
|
||||
msgid "Plan sharing"
|
||||
msgstr ""
|
||||
msgstr "Partilha de planos"
|
||||
|
||||
#: .\cookbook\forms.py:64
|
||||
#, fuzzy
|
||||
#| msgid "Ingredients"
|
||||
msgid "Ingredient decimal places"
|
||||
msgstr "Ingredientes"
|
||||
msgstr "Casas decimais de ingredientes"
|
||||
|
||||
#: .\cookbook\forms.py:65
|
||||
msgid "Shopping list auto sync period"
|
||||
msgstr ""
|
||||
msgstr "Período de sincronização automática"
|
||||
|
||||
#: .\cookbook\forms.py:66 .\cookbook\templates\recipe_view.html:21
|
||||
#: .\cookbook\templates\space.html:62 .\cookbook\templates\stats.html:47
|
||||
msgid "Comments"
|
||||
msgstr ""
|
||||
msgstr "Comentários"
|
||||
|
||||
#: .\cookbook\forms.py:70
|
||||
msgid ""
|
||||
@@ -106,16 +98,20 @@ msgid ""
|
||||
"Enables support for fractions in ingredient amounts (e.g. convert decimals "
|
||||
"to fractions automatically)"
|
||||
msgstr ""
|
||||
"Utilizar frações para apresentar quantidades de ingredientes decimais ("
|
||||
"converter quantidades decimais para frações automáticamente)"
|
||||
|
||||
#: .\cookbook\forms.py:76
|
||||
msgid "Display nutritional energy amounts in joules instead of calories"
|
||||
msgstr ""
|
||||
msgstr "Mostrar quantidades de energia nutricional em joules em vez de calorias"
|
||||
|
||||
#: .\cookbook\forms.py:78
|
||||
msgid ""
|
||||
"Users with whom newly created meal plan/shopping list entries should be "
|
||||
"shared by default."
|
||||
msgstr ""
|
||||
"Utilizadores com os quais novos planos de refeições/listas de compras devem "
|
||||
"ser partilhados por defeito."
|
||||
|
||||
#: .\cookbook\forms.py:80
|
||||
msgid "Show recently viewed recipes on search page."
|
||||
@@ -127,7 +123,7 @@ msgstr "Número de casas decimais para arredondamentos."
|
||||
|
||||
#: .\cookbook\forms.py:82
|
||||
msgid "If you want to be able to create and see comments underneath recipes."
|
||||
msgstr ""
|
||||
msgstr "Ativar a funcionalidade comentar receitas."
|
||||
|
||||
#: .\cookbook\forms.py:84
|
||||
msgid ""
|
||||
@@ -136,10 +132,15 @@ msgid ""
|
||||
"Useful when shopping with multiple people but might use a little bit of "
|
||||
"mobile data. If lower than instance limit it is reset when saving."
|
||||
msgstr ""
|
||||
"Definir esta opção como 0 desativará a sincronização automática. Ao "
|
||||
"visualizar uma lista de compras, a lista é atualizada a cada período aqui "
|
||||
"definido para sincronizar as alterações que outro utilizador possa ter "
|
||||
"feito. Útil ao fazer compras com vários utilizadores, mas pode aumentar o "
|
||||
"uso de dados móveis."
|
||||
|
||||
#: .\cookbook\forms.py:87
|
||||
msgid "Makes the navbar stick to the top of the page."
|
||||
msgstr ""
|
||||
msgstr "Mantém a barra de navegação no topo da página."
|
||||
|
||||
#: .\cookbook\forms.py:103
|
||||
msgid ""
|
||||
@@ -147,7 +148,7 @@ msgid ""
|
||||
"instead"
|
||||
msgstr ""
|
||||
"Ambos os campos são opcionais. Se nenhum for preenchido o nome de utilizador "
|
||||
"será apresentado."
|
||||
"será apresentado"
|
||||
|
||||
#: .\cookbook\forms.py:124 .\cookbook\forms.py:289
|
||||
#: .\cookbook\templates\url_import.html:158
|
||||
@@ -179,13 +180,15 @@ msgstr "UID de armazenamento"
|
||||
|
||||
#: .\cookbook\forms.py:157
|
||||
msgid "Default"
|
||||
msgstr ""
|
||||
msgstr "Predefinição"
|
||||
|
||||
#: .\cookbook\forms.py:168 .\cookbook\templates\url_import.html:94
|
||||
msgid ""
|
||||
"To prevent duplicates recipes with the same name as existing ones are "
|
||||
"ignored. Check this box to import everything."
|
||||
msgstr ""
|
||||
"Para evitar repetições, receitas com o mesmo nome de receitas já existentes "
|
||||
"são ignoradas. Marque esta caixa para importar tudo."
|
||||
|
||||
#: .\cookbook\forms.py:190
|
||||
msgid "Add your comment: "
|
||||
@@ -211,11 +214,11 @@ msgstr ""
|
||||
|
||||
#: .\cookbook\forms.py:258 .\cookbook\views\edit.py:166
|
||||
msgid "Storage"
|
||||
msgstr ""
|
||||
msgstr "Armazenamento"
|
||||
|
||||
#: .\cookbook\forms.py:260
|
||||
msgid "Active"
|
||||
msgstr ""
|
||||
msgstr "Ativo"
|
||||
|
||||
#: .\cookbook\forms.py:265
|
||||
msgid "Search String"
|
||||
@@ -245,49 +248,60 @@ msgstr ""
|
||||
|
||||
#: .\cookbook\forms.py:353
|
||||
msgid "Maximum number of users for this space reached."
|
||||
msgstr ""
|
||||
msgstr "Número máximo de utilizadores alcançado."
|
||||
|
||||
#: .\cookbook\forms.py:359
|
||||
msgid "Email address already taken!"
|
||||
msgstr ""
|
||||
msgstr "Endereço email já utilizado!"
|
||||
|
||||
#: .\cookbook\forms.py:367
|
||||
msgid ""
|
||||
"An email address is not required but if present the invite link will be sent "
|
||||
"to the user."
|
||||
msgstr ""
|
||||
"Um endereço de email não é obrigatório mas se fornecido será enviada uma "
|
||||
"mensagem ao utilizador."
|
||||
|
||||
#: .\cookbook\forms.py:382
|
||||
msgid "Name already taken."
|
||||
msgstr ""
|
||||
msgstr "Nome já existente."
|
||||
|
||||
#: .\cookbook\forms.py:393
|
||||
msgid "Accept Terms and Privacy"
|
||||
msgstr ""
|
||||
msgstr "Aceitar Termos e Condições"
|
||||
|
||||
#: .\cookbook\forms.py:425
|
||||
msgid ""
|
||||
"Determines how fuzzy a search is if it uses trigram similarity matching (e."
|
||||
"g. low values mean more typos are ignored)."
|
||||
msgstr ""
|
||||
"Determina o quão difusa uma pesquisa é se esta utilizar uma correspondência "
|
||||
"de semelhança de trigrama (valores mais baixos significam que mais erros são "
|
||||
"ignorados)."
|
||||
|
||||
#: .\cookbook\forms.py:435
|
||||
msgid ""
|
||||
"Select type method of search. Click <a href=\"/docs/search/\">here</a> for "
|
||||
"full desciption of choices."
|
||||
msgstr ""
|
||||
"Selecionar o método de pesquisa. Uma descrição completa das opções pode ser "
|
||||
"encontrada <a href=\"/docs/search/\">aqui</a>."
|
||||
|
||||
#: .\cookbook\forms.py:436
|
||||
msgid ""
|
||||
"Use fuzzy matching on units, keywords and ingredients when editing and "
|
||||
"importing recipes."
|
||||
msgstr ""
|
||||
"Utilizar correspondência difusa em unidades, palavras-chave e ingredientes "
|
||||
"ao editar e importar receitas."
|
||||
|
||||
#: .\cookbook\forms.py:438
|
||||
msgid ""
|
||||
"Fields to search ignoring accents. Selecting this option can improve or "
|
||||
"degrade search quality depending on language"
|
||||
msgstr ""
|
||||
"Campos de pesquisa que ignoram pontuação. Esta opção pode aumentar ou "
|
||||
"diminuir a qualidade de pesquisa dependendo da língua em uso"
|
||||
|
||||
#: .\cookbook\forms.py:440
|
||||
msgid ""
|
||||
|
||||
BIN
cookbook/locale/ro/LC_MESSAGES/django.mo
Normal file
BIN
cookbook/locale/ro/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
2572
cookbook/locale/ro/LC_MESSAGES/django.po
Normal file
2572
cookbook/locale/ro/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
BIN
cookbook/locale/sl/LC_MESSAGES/django.mo
Normal file
BIN
cookbook/locale/sl/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
2575
cookbook/locale/sl/LC_MESSAGES/django.po
Normal file
2575
cookbook/locale/sl/LC_MESSAGES/django.po
Normal file
File diff suppressed because it is too large
Load Diff
@@ -2,25 +2,30 @@ import operator
|
||||
import pathlib
|
||||
import re
|
||||
import uuid
|
||||
from collections import OrderedDict
|
||||
from datetime import date, timedelta
|
||||
from decimal import Decimal
|
||||
|
||||
from annoying.fields import AutoOneToOneField
|
||||
from django.contrib import auth
|
||||
from django.contrib.auth.models import Group, User
|
||||
from django.contrib.postgres.indexes import GinIndex
|
||||
from django.contrib.postgres.search import SearchVectorField
|
||||
from django.core.files.uploadedfile import UploadedFile, InMemoryUploadedFile
|
||||
from django.core.files.uploadedfile import InMemoryUploadedFile, UploadedFile
|
||||
from django.core.validators import MinLengthValidator
|
||||
from django.db import models, IntegrityError
|
||||
from django.db.models import Index, ProtectedError
|
||||
from django.db import IntegrityError, models
|
||||
from django.db.models import Index, ProtectedError, Q, Subquery
|
||||
from django.db.models.fields.related import ManyToManyField
|
||||
from django.db.models.functions import Substr
|
||||
from django.db.transaction import atomic
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext as _
|
||||
from treebeard.mp_tree import MP_Node, MP_NodeManager
|
||||
from django_scopes import ScopedManager, scopes_disabled
|
||||
from django_prometheus.models import ExportModelOperationsMixin
|
||||
from recipes.settings import (COMMENT_PREF_DEFAULT, FRACTION_PREF_DEFAULT,
|
||||
KJ_PREF_DEFAULT, STICKY_NAV_PREF_DEFAULT,
|
||||
SORT_TREE_BY_NAME)
|
||||
from django_scopes import ScopedManager, scopes_disabled
|
||||
from treebeard.mp_tree import MP_Node, MP_NodeManager
|
||||
|
||||
from recipes.settings import (COMMENT_PREF_DEFAULT, FRACTION_PREF_DEFAULT, KJ_PREF_DEFAULT,
|
||||
SORT_TREE_BY_NAME, STICKY_NAV_PREF_DEFAULT)
|
||||
|
||||
|
||||
def get_user_name(self):
|
||||
@@ -38,15 +43,26 @@ def get_model_name(model):
|
||||
|
||||
|
||||
class TreeManager(MP_NodeManager):
|
||||
def create(self, *args, **kwargs):
|
||||
return self.get_or_create(*args, **kwargs)[0]
|
||||
|
||||
# model.Manager get_or_create() is not compatible with MP_Tree
|
||||
def get_or_create(self, **kwargs):
|
||||
def get_or_create(self, *args, **kwargs):
|
||||
kwargs['name'] = kwargs['name'].strip()
|
||||
try:
|
||||
return self.get(name__exact=kwargs['name'], space=kwargs['space']), False
|
||||
except self.model.DoesNotExist:
|
||||
with scopes_disabled():
|
||||
try:
|
||||
return self.model.add_root(**kwargs), True
|
||||
# ManyToMany fields can't be set this way, so pop them out to save for later
|
||||
fields = [field.name for field in self.model._meta.get_fields() if issubclass(type(field), ManyToManyField)]
|
||||
many_to_many = {field: kwargs.pop(field) for field in list(kwargs) if field in fields}
|
||||
obj = self.model.add_root(**kwargs)
|
||||
for field in many_to_many:
|
||||
field_model = getattr(obj, field).model
|
||||
for related_obj in many_to_many[field]:
|
||||
getattr(obj, field).add(field_model.objects.get(**dict(related_obj)))
|
||||
return obj, True
|
||||
except IntegrityError as e:
|
||||
if 'Key (path)' in e.args[0]:
|
||||
self.model.fix_tree(fix_paths=True)
|
||||
|
||||
@@ -2,73 +2,29 @@ from rest_framework.schemas.openapi import AutoSchema
|
||||
from rest_framework.schemas.utils import is_list_view
|
||||
|
||||
|
||||
# TODO move to separate class to cleanup
|
||||
class RecipeSchema(AutoSchema):
|
||||
class QueryParam(object):
|
||||
def __init__(self, name, description=None, qtype='string', required=False):
|
||||
self.name = name
|
||||
self.description = description
|
||||
self.qtype = qtype
|
||||
self.required = required
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.name}, {self.qtype}, {self.description}'
|
||||
|
||||
|
||||
class QueryParamAutoSchema(AutoSchema):
|
||||
def get_path_parameters(self, path, method):
|
||||
if not is_list_view(path, method, self.view):
|
||||
return super(RecipeSchema, self).get_path_parameters(path, method)
|
||||
|
||||
return super().get_path_parameters(path, method)
|
||||
parameters = super().get_path_parameters(path, method)
|
||||
parameters.append({
|
||||
"name": 'query', "in": "query", "required": False,
|
||||
"description": 'Query string matched (fuzzy) against recipe name. In the future also fulltext search.',
|
||||
'schema': {'type': 'string', },
|
||||
})
|
||||
parameters.append({
|
||||
"name": 'keywords', "in": "query", "required": False,
|
||||
"description": 'Id of keyword a recipe should have. For multiple repeat parameter.',
|
||||
'schema': {'type': 'string', },
|
||||
})
|
||||
parameters.append({
|
||||
"name": 'foods', "in": "query", "required": False,
|
||||
"description": 'Id of food a recipe should have. For multiple repeat parameter.',
|
||||
'schema': {'type': 'string', },
|
||||
})
|
||||
parameters.append({
|
||||
"name": 'units', "in": "query", "required": False,
|
||||
"description": 'Id of unit a recipe should have.',
|
||||
'schema': {'type': 'int', },
|
||||
})
|
||||
parameters.append({
|
||||
"name": 'rating', "in": "query", "required": False,
|
||||
"description": 'Id of unit a recipe should have.',
|
||||
'schema': {'type': 'int', },
|
||||
})
|
||||
parameters.append({
|
||||
"name": 'books', "in": "query", "required": False,
|
||||
"description": 'Id of book a recipe should have. For multiple repeat parameter.',
|
||||
'schema': {'type': 'string', },
|
||||
})
|
||||
parameters.append({
|
||||
"name": 'keywords_or', "in": "query", "required": False,
|
||||
"description": 'If recipe should have all (AND) or any (OR) of the provided keywords.',
|
||||
'schema': {'type': 'string', },
|
||||
})
|
||||
parameters.append({
|
||||
"name": 'foods_or', "in": "query", "required": False,
|
||||
"description": 'If recipe should have all (AND) or any (OR) any of the provided foods.',
|
||||
'schema': {'type': 'string', },
|
||||
})
|
||||
parameters.append({
|
||||
"name": 'books_or', "in": "query", "required": False,
|
||||
"description": 'If recipe should be in all (AND) or any (OR) any of the provided books.',
|
||||
'schema': {'type': 'string', },
|
||||
})
|
||||
parameters.append({
|
||||
"name": 'internal', "in": "query", "required": False,
|
||||
"description": 'true or false. If only internal recipes should be returned or not.',
|
||||
'schema': {'type': 'string', },
|
||||
})
|
||||
parameters.append({
|
||||
"name": 'random', "in": "query", "required": False,
|
||||
"description": 'true or false. returns the results in randomized order.',
|
||||
'schema': {'type': 'string', },
|
||||
})
|
||||
parameters.append({
|
||||
"name": 'new', "in": "query", "required": False,
|
||||
"description": 'true or false. returns new results first in search results',
|
||||
'schema': {'type': 'string', },
|
||||
})
|
||||
for q in self.view.query_params:
|
||||
parameters.append({
|
||||
"name": q.name, "in": "query", "required": q.required,
|
||||
"description": q.description,
|
||||
'schema': {'type': q.qtype, },
|
||||
})
|
||||
|
||||
return parameters
|
||||
|
||||
|
||||
@@ -86,7 +42,8 @@ class TreeSchema(AutoSchema):
|
||||
})
|
||||
parameters.append({
|
||||
"name": 'root', "in": "query", "required": False,
|
||||
"description": 'Return first level children of {obj} with ID [int]. Integer 0 will return root {obj}s.'.format(obj=api_name),
|
||||
"description": 'Return first level children of {obj} with ID [int]. Integer 0 will return root {obj}s.'.format(
|
||||
obj=api_name),
|
||||
'schema': {'type': 'int', },
|
||||
})
|
||||
parameters.append({
|
||||
@@ -110,3 +67,17 @@ class FilterSchema(AutoSchema):
|
||||
'schema': {'type': 'string', },
|
||||
})
|
||||
return parameters
|
||||
|
||||
|
||||
# class QueryOnlySchema(AutoSchema):
|
||||
# def get_path_parameters(self, path, method):
|
||||
# if not is_list_view(path, method, self.view):
|
||||
# return super(QueryOnlySchema, self).get_path_parameters(path, method)
|
||||
|
||||
# parameters = super().get_path_parameters(path, method)
|
||||
# parameters.append({
|
||||
# "name": 'query', "in": "query", "required": False,
|
||||
# "description": 'Query string matched (fuzzy) against object name.',
|
||||
# 'schema': {'type': 'string', },
|
||||
# })
|
||||
# return parameters
|
||||
|
||||
@@ -34,7 +34,8 @@ class ExtendedRecipeMixin(serializers.ModelSerializer):
|
||||
api_serializer = None
|
||||
# extended values are computationally expensive and not needed in normal circumstances
|
||||
try:
|
||||
if bool(int(self.context['request'].query_params.get('extended', False))) and self.__class__ == api_serializer:
|
||||
if bool(int(
|
||||
self.context['request'].query_params.get('extended', False))) and self.__class__ == api_serializer:
|
||||
return fields
|
||||
except AttributeError:
|
||||
pass
|
||||
@@ -49,11 +50,13 @@ class ExtendedRecipeMixin(serializers.ModelSerializer):
|
||||
|
||||
def get_image(self, obj):
|
||||
# TODO add caching
|
||||
recipes = Recipe.objects.filter(**{self.recipe_filter: obj}, space=obj.space).exclude(image__isnull=True).exclude(image__exact='')
|
||||
recipes = Recipe.objects.filter(**{self.recipe_filter: obj}, space=obj.space).exclude(
|
||||
image__isnull=True).exclude(image__exact='')
|
||||
try:
|
||||
if recipes.count() == 0 and obj.has_children():
|
||||
obj__in = self.recipe_filter + '__in'
|
||||
recipes = Recipe.objects.filter(**{obj__in: obj.get_descendants()}, space=obj.space).exclude(image__isnull=True).exclude(image__exact='') # if no recipes found - check whole tree
|
||||
recipes = Recipe.objects.filter(**{obj__in: obj.get_descendants()}, space=obj.space).exclude(
|
||||
image__isnull=True).exclude(image__exact='') # if no recipes found - check whole tree
|
||||
except AttributeError:
|
||||
# probably not a tree
|
||||
pass
|
||||
@@ -134,6 +137,7 @@ class UserNameSerializer(WritableNestedModelSerializer):
|
||||
|
||||
|
||||
class UserPreferenceSerializer(serializers.ModelSerializer):
|
||||
plan_share = UserNameSerializer(many=True, read_only=True)
|
||||
|
||||
def create(self, validated_data):
|
||||
if validated_data['user'] != self.context['request'].user:
|
||||
@@ -404,7 +408,10 @@ class FoodSerializer(UniqueFieldsMixin, WritableNestedModelSerializer, ExtendedR
|
||||
|
||||
class Meta:
|
||||
model = Food
|
||||
fields = ('id', 'name', 'description', 'recipe', 'ignore_shopping', 'supermarket_category', 'image', 'parent', 'numchild', 'numrecipe')
|
||||
fields = (
|
||||
'id', 'name', 'description', 'recipe', 'ignore_shopping', 'supermarket_category', 'image', 'parent',
|
||||
'numchild',
|
||||
'numrecipe')
|
||||
read_only_fields = ('id', 'numchild', 'parent', 'image')
|
||||
|
||||
|
||||
@@ -425,12 +432,13 @@ class IngredientSerializer(WritableNestedModelSerializer):
|
||||
)
|
||||
|
||||
|
||||
class StepSerializer(WritableNestedModelSerializer):
|
||||
class StepSerializer(WritableNestedModelSerializer, ExtendedRecipeMixin):
|
||||
ingredients = IngredientSerializer(many=True)
|
||||
ingredients_markdown = serializers.SerializerMethodField('get_ingredients_markdown')
|
||||
ingredients_vue = serializers.SerializerMethodField('get_ingredients_vue')
|
||||
file = UserFileViewSerializer(allow_null=True, required=False)
|
||||
step_recipe_data = serializers.SerializerMethodField('get_step_recipe_data')
|
||||
recipe_filter = 'steps'
|
||||
|
||||
def create(self, validated_data):
|
||||
validated_data['space'] = self.context['request'].space
|
||||
@@ -442,6 +450,9 @@ class StepSerializer(WritableNestedModelSerializer):
|
||||
def get_ingredients_markdown(self, obj):
|
||||
return obj.get_instruction_render()
|
||||
|
||||
def get_step_recipes(self, obj):
|
||||
return list(obj.recipe_set.values_list('id', flat=True).all())
|
||||
|
||||
def get_step_recipe_data(self, obj):
|
||||
# check if root type is recipe to prevent infinite recursion
|
||||
# can be improved later to allow multi level embedding
|
||||
@@ -452,7 +463,7 @@ class StepSerializer(WritableNestedModelSerializer):
|
||||
model = Step
|
||||
fields = (
|
||||
'id', 'name', 'type', 'instruction', 'ingredients', 'ingredients_markdown',
|
||||
'ingredients_vue', 'time', 'order', 'show_as_header', 'file', 'step_recipe', 'step_recipe_data'
|
||||
'ingredients_vue', 'time', 'order', 'show_as_header', 'file', 'step_recipe', 'step_recipe_data', 'numrecipe'
|
||||
)
|
||||
|
||||
|
||||
@@ -610,6 +621,7 @@ class MealPlanSerializer(SpacedModelSerializer, WritableNestedModelSerializer):
|
||||
meal_type_name = serializers.ReadOnlyField(source='meal_type.name') # TODO deprecate once old meal plan was removed
|
||||
note_markdown = serializers.SerializerMethodField('get_note_markdown')
|
||||
servings = CustomDecimalField()
|
||||
shared = UserNameSerializer(many=True)
|
||||
|
||||
def get_note_markdown(self, obj):
|
||||
return markdown(obj.note)
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -67,7 +67,7 @@
|
||||
</button>
|
||||
|
||||
{% if not request.user.is_authenticated or request.user.userpreference.theme == request.user.userpreference.TANDOOR %}
|
||||
<a class="navbar-brand p-0 me-2 justify-content-center" href="/" aria-label="Tandoor">
|
||||
<a class="navbar-brand p-0 me-2 justify-content-center" href="{% base_path request 'base' %}" aria-label="Tandoor">
|
||||
<img class="brand-icon" src="{% static 'assets/brand_logo.png' %}" alt="" style="height: 5vh;">
|
||||
</a>
|
||||
{% endif %}
|
||||
@@ -336,6 +336,10 @@
|
||||
{% block content_fluid %}
|
||||
{% endblock %}
|
||||
|
||||
{% user_prefs request as prefs%}
|
||||
{{ prefs|json_script:'user_preference' }}
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
{% block script %}
|
||||
@@ -345,6 +349,7 @@
|
||||
localStorage.setItem('SCRIPT_NAME', "{% base_path request 'script' %}")
|
||||
localStorage.setItem('BASE_PATH', "{% base_path request 'base' %}")
|
||||
localStorage.setItem('STATIC_URL', "{% base_path request 'static_base' %}")
|
||||
localStorage.setItem('DEBUG', "{% is_debug %}")
|
||||
window.addEventListener("load", () => {
|
||||
if ("serviceWorker" in navigator) {
|
||||
navigator.serviceWorker.register("{% url 'service_worker' %}", {scope: "{% base_path request 'base' %}" + '/'}).then(function (reg) {
|
||||
|
||||
@@ -28,10 +28,10 @@
|
||||
{% trans 'Account' %}</a>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<a class="nav-link {% if active_tab == 'prefernces' %} active {% endif %}" id="preferences-tab"
|
||||
<a class="nav-link {% if active_tab == 'preferences' %} active {% endif %}" id="preferences-tab"
|
||||
data-toggle="tab" href="#preferences" role="tab"
|
||||
aria-controls="preferences"
|
||||
aria-selected="{% if active_tab == 'prefernces' %} 'true' {% else %} 'false' {% endif %}">
|
||||
aria-selected="{% if active_tab == 'preferences' %} 'true' {% else %} 'false' {% endif %}">
|
||||
{% trans 'Preferences' %}</a>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
@@ -225,4 +225,4 @@
|
||||
window.location.hash = e.target.hash;
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -28,13 +28,6 @@
|
||||
<span class="col col-md-9">
|
||||
<h2>{% trans 'Shopping List' %}</h2>
|
||||
</span>
|
||||
<span class="col-md-3">
|
||||
<a href="{% url 'view_shopping_new' %}" class="float-right">
|
||||
<button class="btn btn-outline-secondary shadow-none">
|
||||
<i class="fas fa-star"></i> {% trans 'Try the new shopping list' %}
|
||||
</button>
|
||||
</a>
|
||||
</span>
|
||||
<div class="col col-mdd-3 text-right">
|
||||
<b-form-checkbox switch size="lg" v-model="edit_mode"
|
||||
@change="$forceUpdate()">{% trans 'Edit' %}</b-form-checkbox>
|
||||
@@ -977,4 +970,4 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{% extends "base.html" %}
|
||||
{% comment %} TODO: refactor to be Vue app {% endcomment %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% load custom_tags %}
|
||||
@@ -75,6 +76,7 @@
|
||||
<option value="CHEFTAP">Cheftap</option>
|
||||
<option value="CHOWDOWN">Chowdown</option>
|
||||
<option value="COOKBOOKAPP">CookBookApp</option>
|
||||
<option value="COPYMETHAT">CopyMeThat</option>
|
||||
<option value="DOMESTICA">Domestica</option>
|
||||
<option value="MEALIE">Mealie</option>
|
||||
<option value="MEALMASTER">Mealmaster</option>
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
import re
|
||||
from gettext import gettext as _
|
||||
|
||||
import bleach
|
||||
import markdown as md
|
||||
import re
|
||||
from bleach_allowlist import markdown_attrs, markdown_tags
|
||||
from cookbook.helper.mdx_attributes import MarkdownFormatExtension
|
||||
from cookbook.helper.mdx_urlize import UrlizeExtension
|
||||
from cookbook.models import Space, get_model_name
|
||||
from django import template
|
||||
from django.db.models import Avg
|
||||
from django.templatetags.static import static
|
||||
from django.urls import NoReverseMatch, reverse
|
||||
from recipes import settings
|
||||
from rest_framework.authtoken.models import Token
|
||||
from gettext import gettext as _
|
||||
|
||||
from cookbook.helper.mdx_attributes import MarkdownFormatExtension
|
||||
from cookbook.helper.mdx_urlize import UrlizeExtension
|
||||
from cookbook.models import Space, get_model_name
|
||||
from recipes import settings
|
||||
|
||||
register = template.Library()
|
||||
|
||||
@@ -92,10 +94,10 @@ def recipe_last(recipe, user):
|
||||
@register.simple_tag
|
||||
def page_help(page_name):
|
||||
help_pages = {
|
||||
'edit_storage': 'https://vabene1111.github.io/recipes/features/external_recipes/',
|
||||
'view_shopping': 'https://vabene1111.github.io/recipes/features/shopping/',
|
||||
'view_import': 'https://vabene1111.github.io/recipes/features/import_export/',
|
||||
'view_export': 'https://vabene1111.github.io/recipes/features/import_export/',
|
||||
'edit_storage': 'https://docs.tandoor.dev/features/external_recipes/',
|
||||
'view_shopping': 'https://docs.tandoor.dev/features/shopping/',
|
||||
'view_import': 'https://docs.tandoor.dev/features/import_export/',
|
||||
'view_export': 'https://docs.tandoor.dev/features/import_export/',
|
||||
}
|
||||
|
||||
link = help_pages.get(page_name, '')
|
||||
@@ -124,10 +126,10 @@ def markdown_link():
|
||||
@register.simple_tag
|
||||
def bookmarklet(request):
|
||||
if request.is_secure():
|
||||
prefix = "https://"
|
||||
protocol = "https://"
|
||||
else:
|
||||
prefix = "http://"
|
||||
server = prefix + request.get_host()
|
||||
protocol = "http://"
|
||||
server = protocol + request.get_host()
|
||||
prefix = settings.JS_REVERSE_SCRIPT_PREFIX
|
||||
# TODO is it safe to store the token in clear text in a bookmark?
|
||||
if (api_token := Token.objects.filter(user=request.user).first()) is None:
|
||||
@@ -155,3 +157,13 @@ def base_path(request, path_type):
|
||||
return request.META.get('HTTP_X_SCRIPT_NAME', '')
|
||||
elif path_type == 'static_base':
|
||||
return static('vue/manifest.json').replace('vue/manifest.json', '')
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def user_prefs(request):
|
||||
from cookbook.serializer import \
|
||||
UserPreferenceSerializer # putting it with imports caused circular execution
|
||||
try:
|
||||
return UserPreferenceSerializer(request.user.userpreference, context={'request': request}).data
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
@@ -106,7 +106,7 @@ def test_add(arg, request, u1_s2, recipe_1_s1, meal_type):
|
||||
r = c.post(
|
||||
reverse(LIST_URL),
|
||||
{'recipe': {'id': recipe_1_s1.id, 'name': recipe_1_s1.name, 'keywords': []}, 'meal_type': {'id': meal_type.id, 'name': meal_type.name},
|
||||
'date': (datetime.now()).strftime("%Y-%m-%d"), 'servings': 1, 'title': 'test'},
|
||||
'date': (datetime.now()).strftime("%Y-%m-%d"), 'servings': 1, 'title': 'test','shared':[]},
|
||||
content_type='application/json'
|
||||
)
|
||||
response = json.loads(r.content)
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import json
|
||||
|
||||
import pytest
|
||||
from django.db.models import Subquery, OuterRef
|
||||
from django.db.models import OuterRef, Subquery
|
||||
from django.urls import reverse
|
||||
from django_scopes import scopes_disabled
|
||||
|
||||
from cookbook.models import Step, Ingredient
|
||||
from cookbook.models import Ingredient, Step
|
||||
|
||||
LIST_URL = 'api:step-list'
|
||||
DETAIL_URL = 'api:step-detail'
|
||||
@@ -23,8 +23,8 @@ def test_list_permission(arg, request):
|
||||
|
||||
|
||||
def test_list_space(recipe_1_s1, u1_s1, u1_s2, space_2):
|
||||
assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)) == 2
|
||||
assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)) == 0
|
||||
assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)['results']) == 2
|
||||
assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)['results']) == 0
|
||||
|
||||
with scopes_disabled():
|
||||
recipe_1_s1.space = space_2
|
||||
@@ -32,9 +32,9 @@ def test_list_space(recipe_1_s1, u1_s1, u1_s2, space_2):
|
||||
Step.objects.update(space=Subquery(Step.objects.filter(pk=OuterRef('pk')).values('recipe__space')[:1]))
|
||||
Ingredient.objects.update(space=Subquery(Ingredient.objects.filter(pk=OuterRef('pk')).values('step__recipe__space')[:1]))
|
||||
|
||||
assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)) == 0
|
||||
assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)) == 2
|
||||
|
||||
assert len(json.loads(u1_s1.get(reverse(LIST_URL)).content)['results']) == 0
|
||||
assert len(json.loads(u1_s2.get(reverse(LIST_URL)).content)['results']) == 2
|
||||
|
||||
|
||||
@pytest.mark.parametrize("arg", [
|
||||
['a_u', 403],
|
||||
|
||||
@@ -18,10 +18,10 @@ def test_add(u1_s1, u2_s1):
|
||||
with scopes_disabled():
|
||||
UserPreference.objects.filter(user=auth.get_user(u1_s1)).delete()
|
||||
|
||||
r = u2_s1.post(reverse(LIST_URL), {'user': auth.get_user(u1_s1).id}, content_type='application/json')
|
||||
r = u2_s1.post(reverse(LIST_URL), {'user': auth.get_user(u1_s1).id, 'plan_share': []}, content_type='application/json')
|
||||
assert r.status_code == 404
|
||||
|
||||
r = u1_s1.post(reverse(LIST_URL), {'user': auth.get_user(u1_s1).id}, content_type='application/json')
|
||||
r = u1_s1.post(reverse(LIST_URL), {'user': auth.get_user(u1_s1).id, 'plan_share': []}, content_type='application/json')
|
||||
assert r.status_code == 200
|
||||
|
||||
|
||||
|
||||
@@ -2,16 +2,17 @@ from pydoc import locate
|
||||
|
||||
from django.urls import include, path
|
||||
from django.views.generic import TemplateView
|
||||
from recipes.version import VERSION_NUMBER
|
||||
from rest_framework import routers, permissions
|
||||
from rest_framework import permissions, routers
|
||||
from rest_framework.schemas import get_schema_view
|
||||
|
||||
from cookbook.helper import dal
|
||||
from recipes.settings import DEBUG
|
||||
from recipes.version import VERSION_NUMBER
|
||||
|
||||
from .models import (Comment, Food, InviteLink, Keyword, MealPlan, Recipe,
|
||||
RecipeBook, RecipeBookEntry, RecipeImport, ShoppingList,
|
||||
Storage, Supermarket, SupermarketCategory, Sync, SyncLog, Unit, get_model_name, Automation, UserFile)
|
||||
from .views import api, data, delete, edit, import_export, lists, new, views, telegram
|
||||
from .models import (Automation, Comment, Food, InviteLink, Keyword, MealPlan, Recipe, RecipeBook,
|
||||
RecipeBookEntry, RecipeImport, ShoppingList, Step, Storage, Supermarket,
|
||||
SupermarketCategory, Sync, SyncLog, Unit, UserFile, get_model_name)
|
||||
from .views import api, data, delete, edit, import_export, lists, new, telegram, views
|
||||
|
||||
router = routers.DefaultRouter()
|
||||
router.register(r'user-name', api.UserNameViewSet, basename='username')
|
||||
@@ -67,8 +68,6 @@ urlpatterns = [
|
||||
path('history/', views.history, name='view_history'),
|
||||
path('supermarket/', views.supermarket, name='view_supermarket'),
|
||||
path('abuse/<slug:token>', views.report_share_abuse, name='view_report_share_abuse'),
|
||||
path('test/', views.test, name='view_test'),
|
||||
path('test2/', views.test2, name='view_test2'),
|
||||
|
||||
path('import/', import_export.import_recipe, name='view_import'),
|
||||
path('import-response/<int:pk>/', import_export.import_response, name='view_import_response'),
|
||||
@@ -177,7 +176,7 @@ for m in generic_models:
|
||||
)
|
||||
)
|
||||
|
||||
vue_models = [Food, Keyword, Unit, Supermarket, SupermarketCategory, Automation, UserFile]
|
||||
vue_models = [Food, Keyword, Unit, Supermarket, SupermarketCategory, Automation, UserFile, Step]
|
||||
for m in vue_models:
|
||||
py_name = get_model_name(m)
|
||||
url_name = py_name.replace('_', '-')
|
||||
@@ -188,3 +187,7 @@ for m in vue_models:
|
||||
f'list/{url_name}/', c, name=f'list_{py_name}'
|
||||
)
|
||||
)
|
||||
|
||||
if DEBUG:
|
||||
urlpatterns.append(path('test/', views.test, name='view_test'))
|
||||
urlpatterns.append(path('test2/', views.test2, name='view_test2'))
|
||||
|
||||
@@ -2,11 +2,11 @@ import io
|
||||
import json
|
||||
import re
|
||||
import uuid
|
||||
from collections import OrderedDict
|
||||
|
||||
import requests
|
||||
from annoying.decorators import ajax_request
|
||||
from annoying.functions import get_object_or_None
|
||||
from collections import OrderedDict
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.postgres.search import TrigramSimilarity
|
||||
@@ -15,12 +15,12 @@ from django.core.files import File
|
||||
from django.db.models import Case, ProtectedError, Q, Value, When
|
||||
from django.db.models.fields.related import ForeignObjectRel
|
||||
from django.http import FileResponse, HttpResponse, JsonResponse
|
||||
from django_scopes import scopes_disabled
|
||||
from django.shortcuts import redirect, get_object_or_404
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext as _
|
||||
from django_scopes import scopes_disabled
|
||||
from icalendar import Calendar, Event
|
||||
from recipe_scrapers import scrape_me, WebsiteNotImplementedError, NoSchemaFoundInWildMode
|
||||
from recipe_scrapers import NoSchemaFoundInWildMode, WebsiteNotImplementedError, scrape_me
|
||||
from rest_framework import decorators, status, viewsets
|
||||
from rest_framework.exceptions import APIException, PermissionDenied
|
||||
from rest_framework.pagination import PageNumberPagination
|
||||
@@ -28,41 +28,39 @@ from rest_framework.parsers import MultiPartParser
|
||||
from rest_framework.renderers import JSONRenderer, TemplateHTMLRenderer
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.viewsets import ViewSetMixin
|
||||
from treebeard.exceptions import PathOverflow, InvalidMoveToDescendant, InvalidPosition
|
||||
from treebeard.exceptions import InvalidMoveToDescendant, InvalidPosition, PathOverflow
|
||||
|
||||
from cookbook.helper.image_processing import handle_image
|
||||
from cookbook.helper.ingredient_parser import IngredientParser
|
||||
from cookbook.helper.permission_helper import (CustomIsAdmin, CustomIsGuest,
|
||||
CustomIsOwner, CustomIsShare,
|
||||
CustomIsShared, CustomIsUser,
|
||||
from cookbook.helper.permission_helper import (CustomIsAdmin, CustomIsGuest, CustomIsOwner,
|
||||
CustomIsShare, CustomIsShared, CustomIsUser,
|
||||
group_required)
|
||||
from cookbook.helper.recipe_html_import import get_recipe_from_source
|
||||
|
||||
from cookbook.helper.recipe_search import search_recipes, get_facet
|
||||
from cookbook.helper.recipe_search import get_facet, old_search, search_recipes
|
||||
from cookbook.helper.recipe_url_import import get_from_scraper
|
||||
from cookbook.models import (CookLog, Food, Ingredient, Keyword, MealPlan,
|
||||
MealType, Recipe, RecipeBook, ShoppingList,
|
||||
ShoppingListEntry, ShoppingListRecipe, Step,
|
||||
Storage, Sync, SyncLog, Unit, UserPreference,
|
||||
ViewLog, RecipeBookEntry, Supermarket, ImportLog, BookmarkletImport, SupermarketCategory, UserFile, ShareLink, SupermarketCategoryRelation, Automation)
|
||||
from cookbook.models import (Automation, BookmarkletImport, CookLog, Food, ImportLog, Ingredient,
|
||||
Keyword, MealPlan, MealType, Recipe, RecipeBook, RecipeBookEntry,
|
||||
ShareLink, ShoppingList, ShoppingListEntry, ShoppingListRecipe, Step,
|
||||
Storage, Supermarket, SupermarketCategory, SupermarketCategoryRelation,
|
||||
Sync, SyncLog, Unit, UserFile, UserPreference, ViewLog)
|
||||
from cookbook.provider.dropbox import Dropbox
|
||||
from cookbook.provider.local import Local
|
||||
from cookbook.provider.nextcloud import Nextcloud
|
||||
from cookbook.schemas import FilterSchema, RecipeSchema, TreeSchema
|
||||
from cookbook.serializer import (FoodSerializer, IngredientSerializer,
|
||||
KeywordSerializer, MealPlanSerializer,
|
||||
MealTypeSerializer, RecipeBookSerializer,
|
||||
RecipeImageSerializer, RecipeSerializer,
|
||||
ShoppingListAutoSyncSerializer,
|
||||
ShoppingListEntrySerializer,
|
||||
ShoppingListRecipeSerializer,
|
||||
ShoppingListSerializer, StepSerializer,
|
||||
StorageSerializer, SyncLogSerializer,
|
||||
SyncSerializer, UnitSerializer,
|
||||
UserNameSerializer, UserPreferenceSerializer,
|
||||
ViewLogSerializer, CookLogSerializer, RecipeBookEntrySerializer,
|
||||
RecipeOverviewSerializer, SupermarketSerializer, ImportLogSerializer,
|
||||
BookmarkletImportSerializer, SupermarketCategorySerializer, UserFileSerializer, SupermarketCategoryRelationSerializer, AutomationSerializer)
|
||||
from cookbook.schemas import FilterSchema, QueryParam, QueryParamAutoSchema, TreeSchema
|
||||
from cookbook.serializer import (AutomationSerializer, BookmarkletImportSerializer,
|
||||
CookLogSerializer, FoodSerializer, ImportLogSerializer,
|
||||
IngredientSerializer, KeywordSerializer, MealPlanSerializer,
|
||||
MealTypeSerializer, RecipeBookEntrySerializer,
|
||||
RecipeBookSerializer, RecipeImageSerializer,
|
||||
RecipeOverviewSerializer, RecipeSerializer,
|
||||
ShoppingListAutoSyncSerializer, ShoppingListEntrySerializer,
|
||||
ShoppingListRecipeSerializer, ShoppingListSerializer,
|
||||
StepSerializer, StorageSerializer,
|
||||
SupermarketCategoryRelationSerializer,
|
||||
SupermarketCategorySerializer, SupermarketSerializer,
|
||||
SyncLogSerializer, SyncSerializer, UnitSerializer,
|
||||
UserFileSerializer, UserNameSerializer, UserPreferenceSerializer,
|
||||
ViewLogSerializer)
|
||||
from recipes import settings
|
||||
|
||||
|
||||
@@ -110,7 +108,8 @@ class FuzzyFilterMixin(ViewSetMixin):
|
||||
if fuzzy:
|
||||
self.queryset = (
|
||||
self.queryset
|
||||
.annotate(exact=Case(When(name__iexact=query, then=(Value(100))), default=Value(0))) # put exact matches at the top of the result set
|
||||
.annotate(exact=Case(When(name__iexact=query, then=(Value(100))),
|
||||
default=Value(0))) # put exact matches at the top of the result set
|
||||
.annotate(trigram=TrigramSimilarity('name', query)).filter(trigram__gt=0.2)
|
||||
.order_by('-exact', '-trigram')
|
||||
)
|
||||
@@ -118,7 +117,8 @@ class FuzzyFilterMixin(ViewSetMixin):
|
||||
# TODO have this check unaccent search settings or other search preferences?
|
||||
self.queryset = (
|
||||
self.queryset
|
||||
.annotate(exact=Case(When(name__iexact=query, then=(Value(100))), default=Value(0))) # put exact matches at the top of the result set
|
||||
.annotate(exact=Case(When(name__iexact=query, then=(Value(100))),
|
||||
default=Value(0))) # put exact matches at the top of the result set
|
||||
.filter(name__icontains=query).order_by('-exact', 'name')
|
||||
)
|
||||
|
||||
@@ -202,7 +202,8 @@ class MergeMixin(ViewSetMixin):
|
||||
source.delete()
|
||||
return Response(content, status=status.HTTP_200_OK)
|
||||
except Exception:
|
||||
content = {'error': True, 'msg': _(f'An error occurred attempting to merge {source.name} with {target.name}')}
|
||||
content = {'error': True,
|
||||
'msg': _(f'An error occurred attempting to merge {source.name} with {target.name}')}
|
||||
return Response(content, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
@@ -218,7 +219,7 @@ class TreeMixin(MergeMixin, FuzzyFilterMixin):
|
||||
if root.isnumeric():
|
||||
try:
|
||||
root = int(root)
|
||||
except self.model.DoesNotExist:
|
||||
except ValueError:
|
||||
self.queryset = self.model.objects.none()
|
||||
if root == 0:
|
||||
self.queryset = self.model.get_root_nodes()
|
||||
@@ -246,7 +247,7 @@ class TreeMixin(MergeMixin, FuzzyFilterMixin):
|
||||
try:
|
||||
child = self.model.objects.get(pk=pk, space=self.request.space)
|
||||
except (self.model.DoesNotExist):
|
||||
content = {'error': True, 'msg': _(f'No {self.basename} with id {child} exists')}
|
||||
content = {'error': True, 'msg': _(f'No {self.basename} with id {pk} exists')}
|
||||
return Response(content, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
parent = int(parent)
|
||||
@@ -275,7 +276,7 @@ class TreeMixin(MergeMixin, FuzzyFilterMixin):
|
||||
child.move(parent, f'{node_location}-child')
|
||||
content = {'msg': _(f'{child.name} was moved successfully to parent {parent.name}')}
|
||||
return Response(content, status=status.HTTP_200_OK)
|
||||
except (PathOverflow, InvalidMoveToDescendant, InvalidPosition):
|
||||
except (PathOverflow, InvalidMoveToDescendant, InvalidPosition) as e:
|
||||
content = {'error': True, 'msg': _('An error occurred attempting to move ') + child.name}
|
||||
return Response(content, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@@ -410,7 +411,8 @@ class RecipeBookViewSet(viewsets.ModelViewSet, StandardFilterMixin):
|
||||
permission_classes = [CustomIsOwner]
|
||||
|
||||
def get_queryset(self):
|
||||
self.queryset = self.queryset.filter(created_by=self.request.user).filter(space=self.request.space)
|
||||
self.queryset = self.queryset.filter(Q(created_by=self.request.user) | Q(shared=self.request.user)).filter(
|
||||
space=self.request.space).distinct()
|
||||
return super().get_queryset()
|
||||
|
||||
|
||||
@@ -428,7 +430,9 @@ class RecipeBookEntryViewSet(viewsets.ModelViewSet, viewsets.GenericViewSet):
|
||||
permission_classes = [CustomIsOwner]
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = self.queryset.filter(Q(book__created_by=self.request.user) | Q(book__shared=self.request.user)).filter(book__space=self.request.space).distinct()
|
||||
queryset = self.queryset.filter(
|
||||
Q(book__created_by=self.request.user) | Q(book__shared=self.request.user)).filter(
|
||||
book__space=self.request.space).distinct()
|
||||
|
||||
recipe_id = self.request.query_params.get('recipe', None)
|
||||
if recipe_id is not None:
|
||||
@@ -498,8 +502,21 @@ class StepViewSet(viewsets.ModelViewSet):
|
||||
queryset = Step.objects
|
||||
serializer_class = StepSerializer
|
||||
permission_classes = [CustomIsUser]
|
||||
pagination_class = DefaultPagination
|
||||
query_params = [
|
||||
QueryParam(name='recipe', description=_('ID of recipe a step is part of. For multiple repeat parameter.'),
|
||||
qtype='int'),
|
||||
QueryParam(name='query', description=_('Query string matched (fuzzy) against object name.'), qtype='string'),
|
||||
]
|
||||
schema = QueryParamAutoSchema()
|
||||
|
||||
def get_queryset(self):
|
||||
recipes = self.request.query_params.getlist('recipe', [])
|
||||
query = self.request.query_params.get('query', None)
|
||||
if len(recipes) > 0:
|
||||
self.queryset = self.queryset.filter(recipe__in=recipes)
|
||||
if query is not None:
|
||||
self.queryset = self.queryset.filter(Q(name__icontains=query) | Q(recipe__name__icontains=query))
|
||||
return self.queryset.filter(recipe__space=self.request.space)
|
||||
|
||||
|
||||
@@ -528,8 +545,31 @@ class RecipeViewSet(viewsets.ModelViewSet):
|
||||
# TODO split read and write permission for meal plan guest
|
||||
permission_classes = [CustomIsShare | CustomIsGuest]
|
||||
pagination_class = RecipePagination
|
||||
|
||||
schema = RecipeSchema()
|
||||
# TODO the boolean params below (keywords_or through new) should be updated to boolean types with front end refactored accordingly
|
||||
query_params = [
|
||||
QueryParam(name='query', description=_(
|
||||
'Query string matched (fuzzy) against recipe name. In the future also fulltext search.')),
|
||||
QueryParam(name='keywords', description=_('ID of keyword a recipe should have. For multiple repeat parameter.'),
|
||||
qtype='int'),
|
||||
QueryParam(name='foods', description=_('ID of food a recipe should have. For multiple repeat parameter.'),
|
||||
qtype='int'),
|
||||
QueryParam(name='units', description=_('ID of unit a recipe should have.'), qtype='int'),
|
||||
QueryParam(name='rating', description=_('Rating a recipe should have. [0 - 5]'), qtype='int'),
|
||||
QueryParam(name='books', description=_('ID of book a recipe should be in. For multiple repeat parameter.')),
|
||||
QueryParam(name='keywords_or', description=_(
|
||||
'If recipe should have all (AND=''false'') or any (OR=''<b>true</b>'') of the provided keywords.')),
|
||||
QueryParam(name='foods_or', description=_(
|
||||
'If recipe should have all (AND=''false'') or any (OR=''<b>true</b>'') of the provided foods.')),
|
||||
QueryParam(name='books_or', description=_(
|
||||
'If recipe should be in all (AND=''false'') or any (OR=''<b>true</b>'') of the provided books.')),
|
||||
QueryParam(name='internal',
|
||||
description=_('If only internal recipes should be returned. [''true''/''<b>false</b>'']')),
|
||||
QueryParam(name='random',
|
||||
description=_('Returns the results in randomized order. [''true''/''<b>false</b>'']')),
|
||||
QueryParam(name='new',
|
||||
description=_('Returns new results first in search results. [''true''/''<b>false</b>'']')),
|
||||
]
|
||||
schema = QueryParamAutoSchema()
|
||||
|
||||
def get_queryset(self):
|
||||
share = self.request.query_params.get('share', None)
|
||||
@@ -540,7 +580,16 @@ class RecipeViewSet(viewsets.ModelViewSet):
|
||||
|
||||
return super().get_queryset()
|
||||
|
||||
def list(self, request, *args, **kwargs):
|
||||
if self.request.GET.get('debug', False):
|
||||
return JsonResponse({
|
||||
'new': str(self.get_queryset().query),
|
||||
'old': str(old_search(request).query)
|
||||
})
|
||||
return super().list(request, *args, **kwargs)
|
||||
|
||||
# TODO write extensive tests for permissions
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.action == 'list':
|
||||
return RecipeOverviewSerializer
|
||||
@@ -592,6 +641,20 @@ class ShoppingListEntryViewSet(viewsets.ModelViewSet):
|
||||
queryset = ShoppingListEntry.objects
|
||||
serializer_class = ShoppingListEntrySerializer
|
||||
permission_classes = [CustomIsOwner | CustomIsShared]
|
||||
query_params = [
|
||||
QueryParam(name='id',
|
||||
description=_('Returns the shopping list entry with a primary key of id. Multiple values allowed.'),
|
||||
qtype='int'),
|
||||
QueryParam(
|
||||
name='checked',
|
||||
description=_(
|
||||
'Filter shopping list entries on checked. [''true'', ''false'', ''both'', ''<b>recent</b>'']<br> - ''recent'' includes unchecked items and recently completed items.')
|
||||
),
|
||||
QueryParam(name='supermarket',
|
||||
description=_('Returns the shopping list entries sorted by supermarket category order.'),
|
||||
qtype='int'),
|
||||
]
|
||||
schema = QueryParamAutoSchema()
|
||||
|
||||
def get_queryset(self):
|
||||
return self.queryset.filter(
|
||||
@@ -632,7 +695,7 @@ class ViewLogViewSet(viewsets.ModelViewSet):
|
||||
class CookLogViewSet(viewsets.ModelViewSet):
|
||||
queryset = CookLog.objects
|
||||
serializer_class = CookLogSerializer
|
||||
permission_classes = [CustomIsOwner] # CustomIsShared? since ratings are in the cooklog?
|
||||
permission_classes = [CustomIsOwner]
|
||||
pagination_class = DefaultPagination
|
||||
|
||||
def get_queryset(self):
|
||||
@@ -720,7 +783,8 @@ def get_recipe_file(request, recipe_id):
|
||||
@group_required('user')
|
||||
def sync_all(request):
|
||||
if request.space.demo or settings.HOSTED:
|
||||
messages.add_message(request, messages.ERROR, _('This feature is not yet available in the hosted version of tandoor!'))
|
||||
messages.add_message(request, messages.ERROR,
|
||||
_('This feature is not yet available in the hosted version of tandoor!'))
|
||||
return redirect('index')
|
||||
|
||||
monitors = Sync.objects.filter(active=True).filter(space=request.user.userpreference.space)
|
||||
@@ -757,7 +821,8 @@ def share_link(request, pk):
|
||||
if request.space.allow_sharing:
|
||||
recipe = get_object_or_404(Recipe, pk=pk, space=request.space)
|
||||
link = ShareLink.objects.create(recipe=recipe, created_by=request.user, space=request.space)
|
||||
return JsonResponse({'pk': pk, 'share': link.uuid, 'link': request.build_absolute_uri(reverse('view_recipe', args=[pk, link.uuid]))})
|
||||
return JsonResponse({'pk': pk, 'share': link.uuid,
|
||||
'link': request.build_absolute_uri(reverse('view_recipe', args=[pk, link.uuid]))})
|
||||
else:
|
||||
return JsonResponse({'error': 'sharing_disabled'}, status=403)
|
||||
|
||||
@@ -918,7 +983,7 @@ def ingredient_from_string(request):
|
||||
|
||||
@group_required('user')
|
||||
def get_facets(request):
|
||||
key = request.GET['hash']
|
||||
key = request.GET.get('hash', None)
|
||||
|
||||
return JsonResponse(
|
||||
{
|
||||
|
||||
@@ -191,13 +191,12 @@ def import_url(request):
|
||||
|
||||
ingredient.save()
|
||||
step.ingredients.add(ingredient)
|
||||
print(ingredient)
|
||||
|
||||
if 'image' in data and data['image'] != '' and data['image'] is not None:
|
||||
try:
|
||||
response = requests.get(data['image'])
|
||||
|
||||
img, filetype = handle_image(request, BytesIO(response.content))
|
||||
img, filetype = handle_image(request, File(BytesIO(response.content), name='image'))
|
||||
recipe.image = File(
|
||||
img, name=f'{uuid.uuid4()}_{recipe.pk}{filetype}'
|
||||
)
|
||||
|
||||
@@ -11,6 +11,7 @@ from django.utils.translation import gettext as _
|
||||
from cookbook.forms import ExportForm, ImportForm, ImportExportBase
|
||||
from cookbook.helper.permission_helper import group_required
|
||||
from cookbook.integration.cookbookapp import CookBookApp
|
||||
from cookbook.integration.copymethat import CopyMeThat
|
||||
from cookbook.integration.pepperplate import Pepperplate
|
||||
from cookbook.integration.cheftap import ChefTap
|
||||
from cookbook.integration.chowdown import Chowdown
|
||||
@@ -65,6 +66,8 @@ def get_integration(request, export_type):
|
||||
return Plantoeat(request, export_type)
|
||||
if export_type == ImportExportBase.COOKBOOKAPP:
|
||||
return CookBookApp(request, export_type)
|
||||
if export_type == ImportExportBase.COPYMETHAT:
|
||||
return CopyMeThat(request, export_type)
|
||||
|
||||
|
||||
@group_required('user')
|
||||
|
||||
@@ -221,6 +221,23 @@ def user_file(request):
|
||||
)
|
||||
|
||||
|
||||
@group_required('user')
|
||||
def step(request):
|
||||
# recipe-param is the name of the parameters used when filtering recipes by this attribute
|
||||
# model-name is the models.js name of the model, probably ALL-CAPS
|
||||
return render(
|
||||
request,
|
||||
'generic/model_template.html',
|
||||
{
|
||||
"title": _("Steps"),
|
||||
"config": {
|
||||
'model': "STEP", # *REQUIRED* name of the model in models.js
|
||||
'recipe_param': 'steps',
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@group_required('user')
|
||||
def shopping_list_new(request):
|
||||
# recipe-param is the name of the parameters used when filtering recipes by this attribute
|
||||
|
||||
@@ -201,7 +201,10 @@ class InviteLinkCreate(GroupRequiredMixin, CreateView):
|
||||
def form_valid(self, form):
|
||||
obj = form.save(commit=False)
|
||||
obj.created_by = self.request.user
|
||||
obj.space = self.request.space
|
||||
|
||||
# verify given space is actually owned by the user creating the link
|
||||
if obj.space.created_by != self.request.user:
|
||||
obj.space = self.request.space
|
||||
obj.save()
|
||||
if obj.email:
|
||||
try:
|
||||
|
||||
@@ -13,7 +13,7 @@ from django.contrib.auth.password_validation import validate_password
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db.models import Avg, Q, Sum
|
||||
from django.http import HttpResponseRedirect, JsonResponse
|
||||
from django.shortcuts import get_object_or_404, render, redirect
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse, reverse_lazy
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext as _
|
||||
@@ -22,16 +22,15 @@ from django_tables2 import RequestConfig
|
||||
from rest_framework.authtoken.models import Token
|
||||
|
||||
from cookbook.filters import RecipeFilter
|
||||
from cookbook.forms import (CommentForm, Recipe, User,
|
||||
UserCreateForm, UserNameForm, UserPreference,
|
||||
UserPreferenceForm, SpaceJoinForm, SpaceCreateForm,
|
||||
SearchPreferenceForm)
|
||||
from cookbook.helper.permission_helper import group_required, share_link_valid, has_group_permission
|
||||
from cookbook.models import (Comment, CookLog, InviteLink, MealPlan,
|
||||
ViewLog, ShoppingList, Space, Keyword, RecipeImport, Unit,
|
||||
Food, UserFile, ShareLink, SearchPreference, SearchFields)
|
||||
from cookbook.tables import (CookLogTable, RecipeTable, RecipeTableSmall,
|
||||
ViewLogTable, InviteLinkTable)
|
||||
from cookbook.forms import (CommentForm, Recipe, SearchPreferenceForm, SpaceCreateForm,
|
||||
SpaceJoinForm, User, UserCreateForm, UserNameForm, UserPreference,
|
||||
UserPreferenceForm)
|
||||
from cookbook.helper.permission_helper import group_required, has_group_permission, share_link_valid
|
||||
from cookbook.models import (Comment, CookLog, Food, InviteLink, Keyword, MealPlan, RecipeImport,
|
||||
SearchFields, SearchPreference, ShareLink, ShoppingList, Space, Unit,
|
||||
UserFile, ViewLog)
|
||||
from cookbook.tables import (CookLogTable, InviteLinkTable, RecipeTable, RecipeTableSmall,
|
||||
ViewLogTable)
|
||||
from cookbook.views.data import Object
|
||||
from recipes.version import BUILD_REF, VERSION_NUMBER
|
||||
|
||||
@@ -331,10 +330,10 @@ def user_settings(request):
|
||||
if not sp:
|
||||
sp = SearchPreferenceForm(user=request.user)
|
||||
fields_searched = (
|
||||
len(search_form.cleaned_data['icontains'])
|
||||
+ len(search_form.cleaned_data['istartswith'])
|
||||
+ len(search_form.cleaned_data['trigram'])
|
||||
+ len(search_form.cleaned_data['fulltext'])
|
||||
len(search_form.cleaned_data['icontains'])
|
||||
+ len(search_form.cleaned_data['istartswith'])
|
||||
+ len(search_form.cleaned_data['trigram'])
|
||||
+ len(search_form.cleaned_data['fulltext'])
|
||||
)
|
||||
if fields_searched == 0:
|
||||
search_form.add_error(None, _('You must select at least one field to search!'))
|
||||
@@ -382,7 +381,7 @@ def user_settings(request):
|
||||
if up:
|
||||
preference_form = UserPreferenceForm(instance=up, space=request.space)
|
||||
else:
|
||||
preference_form = UserPreferenceForm( space=request.space)
|
||||
preference_form = UserPreferenceForm(space=request.space)
|
||||
|
||||
fields_searched = len(sp.icontains.all()) + len(sp.istartswith.all()) + len(sp.trigram.all()) + len(
|
||||
sp.fulltext.all())
|
||||
|
||||
@@ -59,6 +59,8 @@ folder of the GitHub repository.
|
||||
|
||||
In order to contribute to the documentation you can fork the repository and edit the markdown files in the browser.
|
||||
|
||||
Now install mkdocs and dependencies: `pip install mkdocs-material mkdocs-include-markdown-plugin`.
|
||||
|
||||
If you want to test the documentation locally run `mkdocs serve` from the project root.
|
||||
|
||||
## Contribute Translations
|
||||
|
||||
11
docs/faq.md
11
docs/faq.md
@@ -37,4 +37,13 @@ There is only one installation of the Dropbox system, but it handles multiple us
|
||||
For Tandoor that means all people that work together on one recipe collection can be in one space.
|
||||
If you want to host the collection of your friends family or your neighbor you can create a separate space for them (trough the admin interface).
|
||||
|
||||
Sharing between spaces is currently not possible but is planned for future releases.
|
||||
Sharing between spaces is currently not possible but is planned for future releases.
|
||||
|
||||
## Create Admin user / reset passwords
|
||||
To create a superuser or reset a lost password if access to the container is lost you need to
|
||||
|
||||
1. execute into the container using `docker-compose exec web_recipes sh`
|
||||
2. activate the virtual environment `source venv/bin/activate`
|
||||
3. run `python manage.py createsuperuser` and follow the steps shown.
|
||||
|
||||
To change a password enter `python manage.py changepassword <username>` in step 3.
|
||||
@@ -2,13 +2,13 @@ This application features a very versatile import and export feature in order
|
||||
to offer the best experience possible and allow you to freely choose where your data goes.
|
||||
|
||||
!!! warning "WIP"
|
||||
The Module is relatively new. There is a know issue with [Timeouts](https://github.com/vabene1111/recipes/issues/417) on large exports.
|
||||
The Module is relatively new. There is a known issue with [Timeouts](https://github.com/vabene1111/recipes/issues/417) on large exports.
|
||||
A fix is being developed and will likely be released with the next version.
|
||||
|
||||
The Module is build with maximum flexibility and expandability in mind and allows to easily add new
|
||||
The Module is built with maximum flexibility and expandability in mind and allows to easily add new
|
||||
integrations to allow you to both import and export your recipes into whatever format you desire.
|
||||
|
||||
Feel like there is an important integration missing ? Just take a look at the [integration issues](https://github.com/vabene1111/recipes/issues?q=is%3Aissue+is%3Aopen+label%3Aintegration) or open a new one
|
||||
Feel like there is an important integration missing? Just take a look at the [integration issues](https://github.com/vabene1111/recipes/issues?q=is%3Aissue+is%3Aopen+label%3Aintegration) or open a new one
|
||||
if your favorite one is missing.
|
||||
|
||||
!!! info "Export"
|
||||
@@ -37,11 +37,12 @@ Overview of the capabilities of the different integrations.
|
||||
| OpenEats | ✔️ | ❌ | ⌚ |
|
||||
| Plantoeat | ✔️ | ❌ | ✔ |
|
||||
| CookBookApp | ✔️ | ⌚ | ✔️ |
|
||||
| CopyMeThat | ✔️ | ❌ | ✔️ |
|
||||
|
||||
✔ = implemented, ❌ = not implemented and not possible/planned, ⌚ = not yet implemented
|
||||
|
||||
## Default
|
||||
The default integration is the build in (and preferred) way to import and export recipes.
|
||||
The default integration is the built in (and preferred) way to import and export recipes.
|
||||
It is maintained with new fields added and contains all data to transfer your recipes from one installation to another.
|
||||
|
||||
It is also one of the few recipe formats that is actually structured in a way that allows for
|
||||
@@ -90,7 +91,7 @@ Mealie provides structured data similar to nextcloud.
|
||||
|
||||
To migrate your recipes
|
||||
|
||||
1. Go to you Mealie settings and create a new Backup
|
||||
1. Go to your Mealie settings and create a new Backup
|
||||
2. Download the backup by clicking on it and pressing download (this wasn't working for me, so I had to manually pull it from the server)
|
||||
3. Upload the entire `.zip` file to the importer page and import everything
|
||||
|
||||
@@ -118,7 +119,7 @@ Recipes.zip/
|
||||
```
|
||||
|
||||
## Safron
|
||||
Go to you safron settings page and export your recipes.
|
||||
Go to your safron settings page and export your recipes.
|
||||
Then simply upload the entire `.zip` file to the importer.
|
||||
|
||||
!!! warning "Images"
|
||||
@@ -131,8 +132,8 @@ The `.paprikarecipes` file is basically just a zip with gzipped contents. Simply
|
||||
all your recipes.
|
||||
|
||||
## Pepperplate
|
||||
Pepperplate provides a `.zip` files contain all your recipes as `.txt` files. These files are well-structured and allow
|
||||
the import of all data without loosing anything.
|
||||
Pepperplate provides a `.zip` file containing all of your recipes as `.txt` files. These files are well-structured and allow
|
||||
the import of all data without losing anything.
|
||||
|
||||
Simply export the recipes from Pepperplate and upload the zip to Tandoor. Images are not included in the export and
|
||||
thus cannot be imported.
|
||||
@@ -145,7 +146,7 @@ This format is basically completely unstructured and every export looks differen
|
||||
and leads to suboptimal results. Images are also not supported as they are not included in the export (at least
|
||||
the tests I had).
|
||||
|
||||
Usually the import should recognize all ingredients and put everything else into the instructions. If you import fails
|
||||
Usually the import should recognize all ingredients and put everything else into the instructions. If your import fails
|
||||
or is worse than this feel free to provide me with more example data and I can try to improve the importer.
|
||||
|
||||
As ChefTap cannot import these files anyway there won't be an exporter implemented in Tandoor.
|
||||
@@ -154,7 +155,7 @@ As ChefTap cannot import these files anyway there won't be an exporter implement
|
||||
Meal master can be imported by uploading one or more meal master files.
|
||||
The files should either be `.txt`, `.MMF` or `.MM` files.
|
||||
|
||||
The MealMaster spec allow for many variations. Currently, only the on column format for ingredients is supported.
|
||||
The MealMaster spec allow for many variations. Currently, only the one column format for ingredients is supported.
|
||||
Second line notes to ingredients are currently also not imported as a note but simply put into the instructions.
|
||||
If you have MealMaster recipes that cannot be imported feel free to raise an issue.
|
||||
|
||||
@@ -166,7 +167,7 @@ The generated file can simply be imported into Tandoor.
|
||||
As I only had limited sample data feel free to open an issue if your RezKonv export cannot be imported.
|
||||
|
||||
## Recipekeeper
|
||||
Recipe keeper allows to export a zip file containing recipes and images using its apps.
|
||||
Recipe keeper allows you to export a zip file containing recipes and images using its apps.
|
||||
This zip file can simply be imported into Tandoor.
|
||||
|
||||
## OpenEats
|
||||
@@ -213,8 +214,12 @@ Store the outputted json string in a `.json` file and simply import it using the
|
||||
|
||||
## Plantoeat
|
||||
|
||||
Plan to eat allow to export a text file containing all your recipes. Simply upload that text file to Tandoor to import all recipes
|
||||
Plan to eat allows you to export a text file containing all your recipes. Simply upload that text file to Tandoor to import all recipes
|
||||
|
||||
## CookBookApp
|
||||
|
||||
CookBookApp can export .zip files containing .html files. Upload the entire ZIP to Tandoor to import all included recipes.
|
||||
CookBookApp can export .zip files containing .html files. Upload the entire ZIP to Tandoor to import all included recipes.
|
||||
|
||||
## CopyMeThat
|
||||
|
||||
CopyMeThat can export .zip files containing an `.html` file as well as a folder containing all the images. Upload the entire ZIP to Tandoor to import all included recipes.
|
||||
@@ -51,7 +51,7 @@ There are different versions (tags) released on docker hub.
|
||||
The main, and also recommended, installation option is to install this application using Docker Compose.
|
||||
|
||||
1. Choose your `docker-compose.yml` from the examples below.
|
||||
2. Download the `.env` configuration file with `wget`, then **edit it accordingly**.
|
||||
2. Download the `.env` configuration file with `wget`, then **edit it accordingly** (you NEED to set `SECRET_KEY` and `POSTGRES_PASSWORD`).
|
||||
```shell
|
||||
wget https://raw.githubusercontent.com/vabene1111/recipes/develop/.env.template -O .env
|
||||
```
|
||||
@@ -65,47 +65,9 @@ This configuration exposes the application through an nginx web server on port 8
|
||||
wget https://raw.githubusercontent.com/vabene1111/recipes/develop/docs/install/docker/plain/docker-compose.yml
|
||||
```
|
||||
|
||||
```yaml
|
||||
version: "3"
|
||||
services:
|
||||
db_recipes:
|
||||
restart: always
|
||||
image: postgres:11-alpine
|
||||
volumes:
|
||||
- ./postgresql:/var/lib/postgresql/data
|
||||
env_file:
|
||||
- ./.env
|
||||
|
||||
web_recipes:
|
||||
image: vabene1111/recipes
|
||||
restart: always
|
||||
env_file:
|
||||
- ./.env
|
||||
volumes:
|
||||
- staticfiles:/opt/recipes/staticfiles
|
||||
- nginx_config:/opt/recipes/nginx/conf.d
|
||||
- ./mediafiles:/opt/recipes/mediafiles
|
||||
depends_on:
|
||||
- db_recipes
|
||||
|
||||
nginx_recipes:
|
||||
image: nginx:mainline-alpine
|
||||
restart: always
|
||||
ports:
|
||||
- 80:80
|
||||
env_file:
|
||||
- ./.env
|
||||
depends_on:
|
||||
- web_recipes
|
||||
volumes:
|
||||
- nginx_config:/etc/nginx/conf.d:ro
|
||||
- staticfiles:/static
|
||||
- ./mediafiles:/media
|
||||
|
||||
volumes:
|
||||
nginx_config:
|
||||
staticfiles:
|
||||
```
|
||||
~~~yaml
|
||||
{% include "./docker/plain/docker-compose.yml" %}
|
||||
~~~
|
||||
|
||||
### Reverse Proxy
|
||||
|
||||
@@ -123,62 +85,9 @@ If you use traefik, this configuration is the one for you.
|
||||
wget https://raw.githubusercontent.com/vabene1111/recipes/develop/docs/install/docker/traefik-nginx/docker-compose.yml
|
||||
```
|
||||
|
||||
```yaml
|
||||
version: "3"
|
||||
services:
|
||||
db_recipes:
|
||||
restart: always
|
||||
image: postgres:11-alpine
|
||||
volumes:
|
||||
- ./postgresql:/var/lib/postgresql/data
|
||||
env_file:
|
||||
- ./.env
|
||||
networks:
|
||||
- default
|
||||
|
||||
web_recipes:
|
||||
image: vabene1111/recipes
|
||||
restart: always
|
||||
env_file:
|
||||
- ./.env
|
||||
volumes:
|
||||
- staticfiles:/opt/recipes/staticfiles
|
||||
- nginx_config:/opt/recipes/nginx/conf.d
|
||||
- ./mediafiles:/opt/recipes/mediafiles
|
||||
depends_on:
|
||||
- db_recipes
|
||||
networks:
|
||||
- default
|
||||
|
||||
nginx_recipes:
|
||||
image: nginx:mainline-alpine
|
||||
restart: always
|
||||
env_file:
|
||||
- ./.env
|
||||
volumes:
|
||||
- nginx_config:/etc/nginx/conf.d:ro
|
||||
- staticfiles:/static
|
||||
- ./mediafiles:/media
|
||||
labels: # traefik example labels
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.recipes.rule=Host(`recipes.mydomain.com`, `recipes.myotherdomain.com`)"
|
||||
- "traefik.http.routers.recipes.entrypoints=web_secure" # your https endpoint
|
||||
- "traefik.http.routers.recipes.tls.certresolver=le_resolver" # your cert resolver
|
||||
depends_on:
|
||||
- web_recipes
|
||||
networks:
|
||||
- default
|
||||
- traefik
|
||||
|
||||
networks:
|
||||
default:
|
||||
traefik: # This is you external traefik network
|
||||
external: true
|
||||
|
||||
volumes:
|
||||
nginx_config:
|
||||
staticfiles:
|
||||
```
|
||||
~~~yaml
|
||||
{% include "./docker/traefik-nginx/docker-compose.yml" %}
|
||||
~~~
|
||||
|
||||
#### nginx-proxy
|
||||
|
||||
@@ -198,58 +107,31 @@ LETSENCRYPT_EMAIL=
|
||||
wget https://raw.githubusercontent.com/vabene1111/recipes/develop/docs/install/docker/nginx-proxy/docker-compose.yml
|
||||
```
|
||||
|
||||
```yaml
|
||||
version: "3"
|
||||
services:
|
||||
db_recipes:
|
||||
restart: always
|
||||
image: postgres:11-alpine
|
||||
volumes:
|
||||
- ./postgresql:/var/lib/postgresql/data
|
||||
env_file:
|
||||
- ./.env
|
||||
networks:
|
||||
- default
|
||||
~~~yaml
|
||||
{% include "./docker/nginx-proxy/docker-compose.yml" %}
|
||||
~~~
|
||||
|
||||
web_recipes:
|
||||
image: vabene1111/recipes
|
||||
restart: always
|
||||
env_file:
|
||||
- ./.env
|
||||
volumes:
|
||||
- staticfiles:/opt/recipes/staticfiles
|
||||
- nginx_config:/opt/recipes/nginx/conf.d
|
||||
- ./mediafiles:/opt/recipes/mediafiles
|
||||
depends_on:
|
||||
- db_recipes
|
||||
networks:
|
||||
- default
|
||||
#### Nginx Swag by LinuxServer
|
||||
[This container](https://github.com/linuxserver/docker-swag) is an all in one solution created by LinuxServer.io.
|
||||
|
||||
nginx_recipes:
|
||||
image: nginx:mainline-alpine
|
||||
restart: always
|
||||
env_file:
|
||||
- ./.env
|
||||
depends_on:
|
||||
- web_recipes
|
||||
volumes:
|
||||
- nginx_config:/etc/nginx/conf.d:ro
|
||||
- staticfiles:/static
|
||||
- ./mediafiles:/media
|
||||
networks:
|
||||
- default
|
||||
- nginx-proxy
|
||||
It contains templates for popular apps, including Tandoor Recipes, so you don't have to manually configure nginx and discard the template provided in Tandoor repo. Tandoor config is called `recipes.subdomain.conf.sample` which you can adapt for your instance.
|
||||
|
||||
networks:
|
||||
default:
|
||||
nginx-proxy:
|
||||
external:
|
||||
name: nginx-proxy
|
||||
If you're running Swag on the default port, you'll just need to change the container name to yours.
|
||||
|
||||
volumes:
|
||||
nginx_config:
|
||||
staticfiles:
|
||||
```
|
||||
If your running Swag on a custom port, some headers must be changed:
|
||||
|
||||
- Create a copy of `proxy.conf`
|
||||
- Replace `proxy_set_header X-Forwarded-Host $host;` and `proxy_set_header Host $host;` to
|
||||
- `proxy_set_header X-Forwarded-Host $http_host;` and `proxy_set_header Host $http_host;`
|
||||
- Update `recipes.subdomain.conf` to use the new file
|
||||
- Restart the linuxserver/swag container and Recipes will work correctly
|
||||
|
||||
More information [here](https://github.com/TandoorRecipes/recipes/issues/959#issuecomment-962648627).
|
||||
|
||||
|
||||
In both cases, also make sure to mount `/media/` in your swag container to point to your Tandoor Recipes Media directory.
|
||||
|
||||
Please refer to the [appropriate documentation](https://github.com/linuxserver/docker-swag#usage) for the container setup.
|
||||
|
||||
## Additional Information
|
||||
|
||||
|
||||
@@ -4,12 +4,14 @@ metadata:
|
||||
labels:
|
||||
app: recipes
|
||||
name: recipes-nginx-config
|
||||
namespace: default
|
||||
data:
|
||||
nginx-config: |-
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
http {
|
||||
include mime.types;
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
@@ -24,10 +26,5 @@ data:
|
||||
location /media/ {
|
||||
alias /media/;
|
||||
}
|
||||
# pass requests for dynamic content to gunicorn
|
||||
location / {
|
||||
proxy_set_header Host $host;
|
||||
proxy_pass http://localhost:8080;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
13
docs/install/k8s/15-secrets.yaml
Normal file
13
docs/install/k8s/15-secrets.yaml
Normal file
@@ -0,0 +1,13 @@
|
||||
kind: Secret
|
||||
apiVersion: v1
|
||||
metadata:
|
||||
name: recipes
|
||||
namespace: default
|
||||
type: Opaque
|
||||
data:
|
||||
# echo -n 'db-password' | base64
|
||||
postgresql-password: ZGItcGFzc3dvcmQ=
|
||||
# echo -n 'postgres-user-password' | base64
|
||||
postgresql-postgres-password: cG9zdGdyZXMtdXNlci1wYXNzd29yZA==
|
||||
# echo -n 'secret-key' | sha256sum | awk '{ printf $1 }' | base64
|
||||
secret-key: ODVkYmUxNWQ3NWVmOTMwOGM3YWUwZjMzYzdhMzI0Y2M2ZjRiZjUxOWEyZWQyZjMwMjdiZDMzYzE0MGE0ZjlhYQ==
|
||||
5
docs/install/k8s/20-service-account.yml
Normal file
5
docs/install/k8s/20-service-account.yml
Normal file
@@ -0,0 +1,5 @@
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: recipes
|
||||
namespace: default
|
||||
@@ -1,50 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolume
|
||||
metadata:
|
||||
name: recipes-db
|
||||
labels:
|
||||
app: recipes
|
||||
type: local
|
||||
tier: db
|
||||
spec:
|
||||
storageClassName: manual
|
||||
capacity:
|
||||
storage: 1Gi
|
||||
accessModes:
|
||||
- ReadWriteMany
|
||||
hostPath:
|
||||
path: "/data/recipes/db"
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolume
|
||||
metadata:
|
||||
name: recipes-media
|
||||
labels:
|
||||
app: recipes
|
||||
type: local
|
||||
tier: media
|
||||
spec:
|
||||
storageClassName: manual
|
||||
capacity:
|
||||
storage: 1Gi
|
||||
accessModes:
|
||||
- ReadWriteMany
|
||||
hostPath:
|
||||
path: "/data/recipes/media"
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolume
|
||||
metadata:
|
||||
name: recipes-static
|
||||
labels:
|
||||
app: recipes
|
||||
type: local
|
||||
tier: static
|
||||
spec:
|
||||
storageClassName: manual
|
||||
capacity:
|
||||
storage: 1Gi
|
||||
accessModes:
|
||||
- ReadWriteMany
|
||||
hostPath:
|
||||
path: "/data/recipes/static"
|
||||
@@ -1,34 +1,13 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: recipes-db
|
||||
labels:
|
||||
app: recipes
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
tier: db
|
||||
storageClassName: manual
|
||||
accessModes:
|
||||
- ReadWriteMany
|
||||
resources:
|
||||
requests:
|
||||
storage: 1Gi
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: recipes-media
|
||||
namespace: default
|
||||
labels:
|
||||
app: recipes
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
tier: media
|
||||
app: recipes
|
||||
storageClassName: manual
|
||||
accessModes:
|
||||
- ReadWriteMany
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 1Gi
|
||||
@@ -37,16 +16,12 @@ apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: recipes-static
|
||||
namespace: default
|
||||
labels:
|
||||
app: recipes
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
tier: static
|
||||
app: recipes
|
||||
storageClassName: manual
|
||||
accessModes:
|
||||
- ReadWriteMany
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 1Gi
|
||||
|
||||
142
docs/install/k8s/40-sts-postgresql.yaml
Normal file
142
docs/install/k8s/40-sts-postgresql.yaml
Normal file
@@ -0,0 +1,142 @@
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
labels:
|
||||
app: recipes
|
||||
tier: database
|
||||
name: recipes-postgresql
|
||||
namespace: default
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: recipes
|
||||
serviceName: recipes-postgresql
|
||||
updateStrategy:
|
||||
type: RollingUpdate
|
||||
template:
|
||||
metadata:
|
||||
annotations:
|
||||
backup.velero.io/backup-volumes: data
|
||||
labels:
|
||||
app: recipes
|
||||
tier: database
|
||||
name: recipes-postgresql
|
||||
namespace: default
|
||||
spec:
|
||||
restartPolicy: Always
|
||||
securityContext:
|
||||
fsGroup: 999
|
||||
serviceAccount: recipes
|
||||
serviceAccountName: recipes
|
||||
terminationGracePeriodSeconds: 30
|
||||
containers:
|
||||
- name: recipes-db
|
||||
env:
|
||||
- name: BITNAMI_DEBUG
|
||||
value: "false"
|
||||
- name: POSTGRESQL_PORT_NUMBER
|
||||
value: "5432"
|
||||
- name: POSTGRESQL_VOLUME_DIR
|
||||
value: /bitnami/postgresql
|
||||
- name: PGDATA
|
||||
value: /bitnami/postgresql/data
|
||||
- name: POSTGRES_USER
|
||||
value: recipes
|
||||
- name: POSTGRES_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: recipes
|
||||
key: postgresql-password
|
||||
- name: POSTGRESQL_POSTGRES_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: recipes
|
||||
key: postgresql-postgres-password
|
||||
- name: POSTGRES_DB
|
||||
value: recipes
|
||||
image: docker.io/bitnami/postgresql:11.5.0-debian-9-r60
|
||||
imagePullPolicy: IfNotPresent
|
||||
livenessProbe:
|
||||
exec:
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- exec pg_isready -U "postgres" -d "wiki" -h 127.0.0.1 -p 5432
|
||||
failureThreshold: 6
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 10
|
||||
successThreshold: 1
|
||||
timeoutSeconds: 5
|
||||
ports:
|
||||
- containerPort: 5432
|
||||
name: postgresql
|
||||
protocol: TCP
|
||||
readinessProbe:
|
||||
exec:
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- -e
|
||||
- |
|
||||
pg_isready -U "postgres" -d "wiki" -h 127.0.0.1 -p 5432
|
||||
[ -f /opt/bitnami/postgresql/tmp/.initialized ]
|
||||
failureThreshold: 6
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
successThreshold: 1
|
||||
timeoutSeconds: 5
|
||||
resources:
|
||||
requests:
|
||||
cpu: 250m
|
||||
memory: 256Mi
|
||||
securityContext:
|
||||
runAsUser: 1001
|
||||
terminationMessagePath: /dev/termination-log
|
||||
terminationMessagePolicy: File
|
||||
volumeMounts:
|
||||
- mountPath: /bitnami/postgresql
|
||||
name: data
|
||||
dnsPolicy: ClusterFirst
|
||||
initContainers:
|
||||
- command:
|
||||
- sh
|
||||
- -c
|
||||
- |
|
||||
mkdir -p /bitnami/postgresql/data
|
||||
chmod 700 /bitnami/postgresql/data
|
||||
find /bitnami/postgresql -mindepth 0 -maxdepth 1 -not -name ".snapshot" -not -name "lost+found" | \
|
||||
xargs chown -R 1001:1001
|
||||
image: docker.io/bitnami/minideb:stretch
|
||||
imagePullPolicy: Always
|
||||
name: init-chmod-data
|
||||
resources:
|
||||
requests:
|
||||
cpu: 250m
|
||||
memory: 256Mi
|
||||
securityContext:
|
||||
runAsUser: 0
|
||||
volumeMounts:
|
||||
- mountPath: /bitnami/postgresql
|
||||
name: data
|
||||
restartPolicy: Always
|
||||
securityContext:
|
||||
fsGroup: 1001
|
||||
serviceAccount: recipes
|
||||
serviceAccountName: recipes
|
||||
terminationGracePeriodSeconds: 30
|
||||
updateStrategy:
|
||||
type: RollingUpdate
|
||||
volumeClaimTemplates:
|
||||
- apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: data
|
||||
namespace: default
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: 2Gi
|
||||
volumeMode: Filesystem
|
||||
19
docs/install/k8s/45-service-db.yaml
Normal file
19
docs/install/k8s/45-service-db.yaml
Normal file
@@ -0,0 +1,19 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
labels:
|
||||
app: recipes
|
||||
tier: database
|
||||
name: recipes-postgresql
|
||||
namespace: default
|
||||
spec:
|
||||
ports:
|
||||
- name: postgresql
|
||||
port: 5432
|
||||
protocol: TCP
|
||||
targetPort: postgresql
|
||||
selector:
|
||||
app: recipes
|
||||
tier: database
|
||||
sessionAffinity: None
|
||||
type: ClusterIP
|
||||
@@ -2,6 +2,7 @@ apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: recipes
|
||||
namespace: default
|
||||
labels:
|
||||
app: recipes
|
||||
environment: production
|
||||
@@ -9,17 +10,78 @@ metadata:
|
||||
spec:
|
||||
replicas: 1
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
app: recipes
|
||||
environment: production
|
||||
template:
|
||||
metadata:
|
||||
annotations:
|
||||
backup.velero.io/backup-volumes: media,static
|
||||
labels:
|
||||
app: recipes
|
||||
tier: frontend
|
||||
environment: production
|
||||
spec:
|
||||
restartPolicy: Always
|
||||
serviceAccount: recipes
|
||||
serviceAccountName: recipes
|
||||
initContainers:
|
||||
- name: init-chmod-data
|
||||
env:
|
||||
- name: SECRET_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: recipes
|
||||
key: secret-key
|
||||
- name: DB_ENGINE
|
||||
value: django.db.backends.postgresql_psycopg2
|
||||
- name: POSTGRES_HOST
|
||||
value: recipes-postgresql
|
||||
- name: POSTGRES_PORT
|
||||
value: "5432"
|
||||
- name: POSTGRES_USER
|
||||
value: postgres
|
||||
- name: POSTGRES_DB
|
||||
value: recipes
|
||||
- name: POSTGRES_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: recipes
|
||||
key: postgresql-postgres-password
|
||||
image: vabene1111/recipes:1.0.1
|
||||
imagePullPolicy: Always
|
||||
resources:
|
||||
requests:
|
||||
cpu: 250m
|
||||
memory: 64Mi
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- |
|
||||
set -e
|
||||
source venv/bin/activate
|
||||
echo "Updating database"
|
||||
python manage.py migrate
|
||||
python manage.py collectstatic_js_reverse
|
||||
python manage.py collectstatic --noinput
|
||||
echo "Setting media file attributes"
|
||||
chown -R 65534:65534 /opt/recipes/mediafiles
|
||||
find /opt/recipes/mediafiles -type d | xargs -r chmod 755
|
||||
find /opt/recipes/mediafiles -type f | xargs -r chmod 644
|
||||
echo "Done"
|
||||
securityContext:
|
||||
runAsUser: 0
|
||||
volumeMounts:
|
||||
- mountPath: /opt/recipes/mediafiles
|
||||
name: media
|
||||
# mount as subPath due to lost+found on ext4 pvc
|
||||
subPath: files
|
||||
- mountPath: /opt/recipes/staticfiles
|
||||
name: static
|
||||
# mount as subPath due to lost+found on ext4 pvc
|
||||
subPath: files
|
||||
containers:
|
||||
- name: recipes-nginx
|
||||
image: nginx:latest
|
||||
@@ -28,69 +90,94 @@ spec:
|
||||
- containerPort: 80
|
||||
protocol: TCP
|
||||
name: http
|
||||
- containerPort: 8080
|
||||
protocol: TCP
|
||||
name: gunicorn
|
||||
resources:
|
||||
requests:
|
||||
cpu: 250m
|
||||
memory: 64Mi
|
||||
volumeMounts:
|
||||
- mountPath: '/media'
|
||||
- mountPath: /media
|
||||
name: media
|
||||
- mountPath: '/static'
|
||||
# mount as subPath due to lost+found on ext4 pvc
|
||||
subPath: files
|
||||
- mountPath: /static
|
||||
name: static
|
||||
# mount as subPath due to lost+found on ext4 pvc
|
||||
subPath: files
|
||||
- name: nginx-config
|
||||
mountPath: /etc/nginx/nginx.conf
|
||||
subPath: nginx-config
|
||||
readOnly: true
|
||||
- name: recipes
|
||||
image: 'vabene1111/recipes:latest'
|
||||
image: vabene1111/recipes:1.0.1
|
||||
imagePullPolicy: IfNotPresent
|
||||
command:
|
||||
- /opt/recipes/venv/bin/gunicorn
|
||||
- -b
|
||||
- :8080
|
||||
- --access-logfile
|
||||
- "-"
|
||||
- --error-logfile
|
||||
- "-"
|
||||
- --log-level
|
||||
- INFO
|
||||
- recipes.wsgi
|
||||
livenessProbe:
|
||||
failureThreshold: 3
|
||||
httpGet:
|
||||
path: /
|
||||
port: 8080
|
||||
scheme: HTTP
|
||||
periodSeconds: 30
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /
|
||||
port: 8080
|
||||
scheme: HTTP
|
||||
periodSeconds: 30
|
||||
resources:
|
||||
requests:
|
||||
cpu: 250m
|
||||
memory: 64Mi
|
||||
volumeMounts:
|
||||
- mountPath: '/opt/recipes/mediafiles'
|
||||
- mountPath: /opt/recipes/mediafiles
|
||||
name: media
|
||||
- mountPath: '/opt/recipes/staticfiles'
|
||||
# mount as subPath due to lost+found on ext4 pvc
|
||||
subPath: files
|
||||
- mountPath: /opt/recipes/staticfiles
|
||||
name: static
|
||||
# mount as subPath due to lost+found on ext4 pvc
|
||||
subPath: files
|
||||
env:
|
||||
- name: DEBUG
|
||||
value: "0"
|
||||
- name: ALLOWED_HOSTS
|
||||
value: '*'
|
||||
- name: SECRET_KEY
|
||||
value: # CHANGEME
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: recipes
|
||||
key: secret-key
|
||||
- name: DB_ENGINE
|
||||
value: django.db.backends.postgresql_psycopg2
|
||||
- name: POSTGRES_HOST
|
||||
value: localhost
|
||||
value: recipes-postgresql
|
||||
- name: POSTGRES_PORT
|
||||
value: "5432"
|
||||
- name: POSTGRES_USER
|
||||
value: recipes
|
||||
value: postgres
|
||||
- name: POSTGRES_DB
|
||||
value: recipes
|
||||
- name: POSTGRES_PASSWORD
|
||||
value: # CHANGEME
|
||||
- name: recipes-db
|
||||
image: 'postgres:latest'
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
- containerPort: 5432
|
||||
volumeMounts:
|
||||
- mountPath: '/var/lib/postgresql/data'
|
||||
name: database
|
||||
env:
|
||||
- name: POSTGRES_USER
|
||||
value: recipes
|
||||
- name: POSTGRES_DB
|
||||
value: recipes
|
||||
- name: POSTGRES_PASSWORD
|
||||
value: # CHANGEME
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: recipes
|
||||
key: postgresql-postgres-password
|
||||
securityContext:
|
||||
runAsUser: 65534
|
||||
volumes:
|
||||
- name: database
|
||||
persistentVolumeClaim:
|
||||
claimName: recipes-db
|
||||
- name: media
|
||||
persistentVolumeClaim:
|
||||
claimName: recipes-media
|
||||
|
||||
@@ -2,14 +2,21 @@ apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: recipes
|
||||
namespace: default
|
||||
labels:
|
||||
app: recipes
|
||||
tier: frontend
|
||||
spec:
|
||||
selector:
|
||||
app: recipes
|
||||
tier: frontend
|
||||
environment: production
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: http
|
||||
name: http
|
||||
protocol: TCP
|
||||
- port: 8080
|
||||
targetPort: gunicorn
|
||||
name: gunicorn
|
||||
protocol: TCP
|
||||
|
||||
38
docs/install/k8s/70-ingress.yaml
Normal file
38
docs/install/k8s/70-ingress.yaml
Normal file
@@ -0,0 +1,38 @@
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
annotations:
|
||||
#cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||
#kubernetes.io/ingress.class: nginx
|
||||
name: recipes
|
||||
namespace: default
|
||||
spec:
|
||||
rules:
|
||||
- host: recipes.local
|
||||
http:
|
||||
paths:
|
||||
- backend:
|
||||
service:
|
||||
name: recipes
|
||||
port:
|
||||
number: 8080
|
||||
path: /
|
||||
pathType: Prefix
|
||||
- backend:
|
||||
service:
|
||||
name: recipes
|
||||
port:
|
||||
number: 80
|
||||
path: /media
|
||||
pathType: Prefix
|
||||
- backend:
|
||||
service:
|
||||
name: recipes
|
||||
port:
|
||||
number: 80
|
||||
path: /static
|
||||
pathType: Prefix
|
||||
#tls:
|
||||
#- hosts:
|
||||
# - recipes.local
|
||||
# secretName: recipes-local-tls
|
||||
@@ -1,31 +1,98 @@
|
||||
!!! info "Community Contributed"
|
||||
This guide was contributed by the community and is neither officially supported, nor updated or tested.
|
||||
**!!! info "Community Contributed" This guide was contributed by the community and is neither officially supported, nor updated or tested.**
|
||||
|
||||
This is a basic kubernetes setup.
|
||||
Please note that this does not necessarily follow Kubernetes best practices and should only used as a
|
||||
basis to build your own setup from!
|
||||
# K8s Setup
|
||||
|
||||
All files con be found here in the Github Repo:
|
||||
[docs/install/k8s](https://github.com/vabene1111/recipes/tree/develop/docs/install/k8s)
|
||||
This is a setup which should be sufficent for production use. Be sure to replace the default secrets!
|
||||
|
||||
## Important notes
|
||||
# Files
|
||||
|
||||
State (database, static files and media files) is handled via `PersistentVolumes`.
|
||||
## 10-configmap.yaml
|
||||
|
||||
Note that you will most likely have to change the `PersistentVolumes` in `30-pv.yaml`. The current setup is only usable for a single-node cluster because it uses local storage on the kubernetes worker nodes under `/data/recipes/`. It should just serve as an example.
|
||||
The nginx config map. This is loaded as nginx.conf in the nginx sidecar to configure nginx to deliver static content.
|
||||
|
||||
Currently, the deployment in `50-deployment.yaml` just pulls the `latest` tag of all containers. In a production setup, you should set this to a fixed version!
|
||||
## 15-secrets.yaml
|
||||
|
||||
See env variables tagged with `CHANGEME` in `50-deployment.yaml` and make sure to change those! A better setup would use kubernetes secrets but this is not implemented yet.
|
||||
The secrets **replace them!!** This file is only here for a quick start. Be aware that changing secrets after installation will be messy and is not documented here. **You should set new secrets before the installation.** As you are reading this document **before** the installation ;-)
|
||||
|
||||
## Updates
|
||||
Create your own postgresql passwords and the secret key for the django app
|
||||
|
||||
These manifests are not tested against new versions.
|
||||
see also [Managing Secrets using kubectl](https://kubernetes.io/docs/tasks/configmap-secret/managing-secret-using-kubectl/)
|
||||
|
||||
## Apply the manifets
|
||||
**Replace** `db-password`, `postgres-user-password` and `secret-key` **with something - well - secret :-)**
|
||||
|
||||
To apply the manifest with `kubectl`, use the following command:
|
||||
~~~
|
||||
echo -n 'db-password' > ./db-password.txt
|
||||
echo -n 'postgres-user-password' > ./postgres-password.txt
|
||||
echo -n 'secret-key' | sha256sum | awk '{ printf $1 }' > ./secret-key.txt
|
||||
~~~
|
||||
|
||||
```
|
||||
Delete the default secrets file `15-secrets.yaml` and generate the K8s secret from your files.
|
||||
|
||||
~~~
|
||||
kubectl create secret generic recipes \
|
||||
--from-file=postgresql-password=./db-password.txt \
|
||||
--from-file=postgresql-postgres-password=./postgres-password.txt \
|
||||
--from-file=secret-key=./secret-key.txt
|
||||
~~~
|
||||
|
||||
## 20-service-account.yml
|
||||
|
||||
Creating service account `recipes` for deployment and stateful set.
|
||||
|
||||
## 30-pvc.yaml
|
||||
|
||||
The creation of the persistent volume claims for media and static content. May you want to increase the size. This expects to have a storage class installed.
|
||||
|
||||
## 40-sts-postgresql.yaml
|
||||
|
||||
The PostgreSQL stateful set, based on a bitnami image. It runs a init container as root to do the preparations. The postgres container itsef runs as a lower privileged user. The recipes app uses the database super user (postgres) as the recipies app is doing some db migrations on startup, which needs super user privileges.
|
||||
|
||||
## 45-service-db.yaml
|
||||
|
||||
Creating the database service.
|
||||
|
||||
## 50-deployment.yaml
|
||||
|
||||
The deployment first fires up a init container to do the database migrations and file modifications. This init container runs as root. The init conainer runs part of the [boot.sh](https://github.com/TandoorRecipes/recipes/blob/develop/boot.sh) script from the `vabene1111/recipes` image.
|
||||
|
||||
The deployment then runs two containers, the recipes-nginx and the recipes container which runs the gunicorn app. The nginx container gets it's nginx.conf via config map to deliver static content `/static` and `/media`. The guincorn container gets it's secret key and the database password from the secret `recipes`. `gunicorn` runs as user `nobody`.
|
||||
|
||||
## 60-service.yaml
|
||||
|
||||
Creating the app service.
|
||||
|
||||
## 70-ingress.yaml
|
||||
|
||||
Setting up the ingress for the recipes service. Requests for static content `/static` and `/media` are send to the nginx container, everything else to gunicorn. TLS setup via cert-manager is prepared. You have to **change the host** from `recipes.local` to your specific domain.
|
||||
|
||||
# Conclusion
|
||||
|
||||
All in all:
|
||||
|
||||
- The database is set up as a stateful set.
|
||||
- The database container runs as a low privileged user.
|
||||
- Database and application use secrets.
|
||||
- The application also runs as a low privileged user.
|
||||
- nginx runs as root but forks children with a low privileged user.
|
||||
- There's an ingress rule to access the application from outside.
|
||||
|
||||
I tried the setup with [kind](https://kind.sigs.k8s.io/) and it runs well on my local cluster.
|
||||
|
||||
There is a warning, when you check your system as super user:
|
||||
|
||||
**Media Serving Warning**
|
||||
Serving media files directly using gunicorn/python is not recommend! Please follow the steps described here to update your installation.
|
||||
|
||||
I don't know how this check works, but this warning is simply wrong! ;-) Media and static files are routed by ingress to the nginx container - I promise :-)
|
||||
|
||||
# Updates
|
||||
|
||||
These manifests are tested against Release 1.0.1. Newer versions may not work without changes.
|
||||
|
||||
# Apply the manifets
|
||||
|
||||
To apply the manifest with kubectl, use the following command:
|
||||
|
||||
~~~
|
||||
kubectl apply -f ./docs/k8s/
|
||||
```
|
||||
~~~
|
||||
|
||||
@@ -60,7 +60,41 @@ Creating recipes_web_recipes_1 ... done
|
||||
- Browse to 192.168.1.1:2000 or whatever your IP and port are
|
||||
- While the containers are starting and doing whatever they need to do, you might still get HTTP errors e.g. 500 or 502. Just be patient and try again in a moment
|
||||
|
||||
5. Additional SSL Setup
|
||||
5. Firewall
|
||||
You need to set up firewall rules in order for the recipes_web container to be able to connect to the recipes_db container.
|
||||
|
||||
- Control Panel -> Security -> Firewall -> Edit Rules -> Create
|
||||
- Ports: All
|
||||
- Source IP: Specific IP -> Select -> Subnet
|
||||
- insert docker network ip (can be found in the docker application, network tab)
|
||||
- Example: IP address: 172.18.0.0 and Subnet mask/Prefix length: 255.255.255.0
|
||||
- Action: Allow
|
||||
- Save and make sure it's above the deny rules
|
||||
|
||||
6. Additional SSL Setup
|
||||
Easiest way is to do it via Reverse Proxy
|
||||
- Control Panel -> Login Portal (renamed Since DSM 7, previously Application Portal) -> Advanced -> Reverse Proxy
|
||||
- Create
|
||||
- insert name
|
||||
- Source:
|
||||
- Protocol: HTTPS
|
||||
- Hostname: URL if you acces from outside, otherwise ip in network
|
||||
- Port: The port you want to access, has to be a different one that the one in the docker-compose file
|
||||
- HSTS can be enabled
|
||||
- Destination:
|
||||
- Protocol: HTTP
|
||||
- Hostname: localhost
|
||||
- Port: port in docker-compose file
|
||||
- Click on Custom Header and press Create -> Websocket
|
||||
- Save
|
||||
- Control Panel -> Security -> Firewall -> Edit Rules -> Create
|
||||
- Ports: Select form a list of build-in applications -> Select -> You find your Reverse Proxy, enable it
|
||||
- Source IP: Depends, All allows access from outside, i use specific to only connect in my network
|
||||
- Action: Allow
|
||||
- Save and make sure it's above the deny rules
|
||||
|
||||
[Deprecated, Note: ssl Path changed for DSM 7]
|
||||
6.1 Additional SSL Setup
|
||||
- create foler `ssl` inside `nginx` folder
|
||||
- download your ssl certificate from `security` tab in dsm `control panel`
|
||||
- or create a task in `task manager` because Synology will update the certificate every few months
|
||||
|
||||
@@ -21,6 +21,9 @@ markdown_extensions:
|
||||
- pymdownx.highlight
|
||||
- pymdownx.superfences
|
||||
|
||||
plugins:
|
||||
- include-markdown
|
||||
|
||||
nav:
|
||||
- Home: 'index.md'
|
||||
- Installation:
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -56,6 +56,7 @@ CORS_ORIGIN_ALLOW_ALL = True
|
||||
|
||||
LOGIN_REDIRECT_URL = "index"
|
||||
LOGOUT_REDIRECT_URL = "index"
|
||||
ACCOUNT_LOGOUT_REDIRECT_URL = "index"
|
||||
|
||||
SESSION_EXPIRE_AT_BROWSER_CLOSE = False
|
||||
SESSION_COOKIE_AGE = 365 * 60 * 24 * 60
|
||||
@@ -151,6 +152,7 @@ MIDDLEWARE = [
|
||||
]
|
||||
|
||||
SORT_TREE_BY_NAME = bool(int(os.getenv('SORT_TREE_BY_NAME', False)))
|
||||
DISABLE_TREE_FIX_STARTUP = bool(int(os.getenv('DISABLE_TREE_FIX_STARTUP', False)))
|
||||
|
||||
if bool(int(os.getenv('SQL_DEBUG', False))):
|
||||
MIDDLEWARE += ('recipes.middleware.SqlPrintingMiddleware',)
|
||||
@@ -162,12 +164,12 @@ if ENABLE_METRICS:
|
||||
AUTHENTICATION_BACKENDS = []
|
||||
|
||||
# LDAP
|
||||
LDAP_AUTH=bool(os.getenv('LDAP_AUTH', False))
|
||||
LDAP_AUTH = bool(os.getenv('LDAP_AUTH', False))
|
||||
if LDAP_AUTH:
|
||||
import ldap
|
||||
from django_auth_ldap.config import LDAPSearch
|
||||
AUTHENTICATION_BACKENDS.append('django_auth_ldap.backend.LDAPBackend')
|
||||
AUTH_LDAP_SERVER_URI = os.getenv('AUTH_LDAP_SERVER_URI')
|
||||
AUTH_LDAP_SERVER_URI = os.getenv('AUTH_LDAP_SERVER_URI')
|
||||
AUTH_LDAP_BIND_DN = os.getenv('AUTH_LDAP_BIND_DN')
|
||||
AUTH_LDAP_BIND_PASSWORD = os.getenv('AUTH_LDAP_BIND_PASSWORD')
|
||||
AUTH_LDAP_USER_SEARCH = LDAPSearch(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Django==3.2.9
|
||||
cryptography==35.0.0
|
||||
Django==3.2.10
|
||||
cryptography==36.0.0
|
||||
django-annoying==0.10.6
|
||||
django-autocomplete-light==3.8.2
|
||||
django-cleanup==5.2.0
|
||||
@@ -11,13 +11,13 @@ drf-writable-nested==0.6.3
|
||||
bleach==4.1.0
|
||||
bleach-allowlist==1.0.3
|
||||
gunicorn==20.1.0
|
||||
lxml==4.6.3
|
||||
Markdown==3.3.4
|
||||
lxml==4.6.5
|
||||
Markdown==3.3.6
|
||||
Pillow==8.4.0
|
||||
psycopg2-binary==2.9.1
|
||||
python-dotenv==0.19.1
|
||||
psycopg2-binary==2.9.2
|
||||
python-dotenv==0.19.2
|
||||
requests==2.26.0
|
||||
simplejson==3.17.5
|
||||
simplejson==3.17.6
|
||||
six==1.16.0
|
||||
webdavclient3==3.14.6
|
||||
whitenoise==5.3.0
|
||||
@@ -25,20 +25,20 @@ icalendar==4.0.9
|
||||
pyyaml==6.0
|
||||
uritemplate==4.1.1
|
||||
beautifulsoup4==4.10.0
|
||||
microdata==0.7.1
|
||||
Jinja2==3.0.2
|
||||
microdata==0.7.2
|
||||
Jinja2==3.0.3
|
||||
django-webpack-loader==1.4.1
|
||||
django-js-reverse==0.9.1
|
||||
django-allauth==0.45.0
|
||||
recipe-scrapers==13.5.0
|
||||
django-allauth==0.46.0
|
||||
recipe-scrapers==13.7.0
|
||||
django-scopes==1.2.0
|
||||
pytest==6.2.5
|
||||
pytest-django==4.4.0
|
||||
pytest-django==4.5.1
|
||||
django-treebeard==4.5.1
|
||||
django-cors-headers==3.10.0
|
||||
django-storages==1.12.3
|
||||
boto3==1.19.7
|
||||
boto3==1.20.19
|
||||
django-prometheus==2.1.0
|
||||
django-hCaptcha==0.1.0
|
||||
python-ldap==3.3.1
|
||||
python-ldap==3.4.0
|
||||
django-auth-ldap==3.0.0
|
||||
164
vue/package.json
164
vue/package.json
@@ -1,87 +1,87 @@
|
||||
{
|
||||
"name": "vue",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"lint": "vue-cli-service lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/eslint-parser": "^7.16.0",
|
||||
"@kangc/v-md-editor": "^1.7.7",
|
||||
"@kevinfaguiar/vue-twemoji-picker": "^5.7.4",
|
||||
"@popperjs/core": "^2.10.1",
|
||||
"@riophae/vue-treeselect": "^0.4.0",
|
||||
"axios": "^0.21.4",
|
||||
"bootstrap-vue": "^2.21.2",
|
||||
"core-js": "^3.19.0",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.29.1",
|
||||
"prismjs": "^1.25.0",
|
||||
"vue": "^2.6.14",
|
||||
"vue-class-component": "^7.2.3",
|
||||
"vue-click-outside": "^1.1.0",
|
||||
"vue-clickaway": "^2.2.2",
|
||||
"vue-cookies": "^1.7.4",
|
||||
"vue-i18n": "^8.26.5",
|
||||
"vue-infinite-loading": "^2.4.5",
|
||||
"vue-multiselect": "^2.1.6",
|
||||
"vue-property-decorator": "^9.1.2",
|
||||
"vue-simple-calendar": "^5.0.1",
|
||||
"vue-template-compiler": "^2.6.14",
|
||||
"vue2-touch-events": "^3.2.2",
|
||||
"vuedraggable": "^2.24.3",
|
||||
"vuex": "^3.6.0",
|
||||
"workbox-webpack-plugin": "^6.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@kazupon/vue-i18n-loader": "^0.5.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.33.0",
|
||||
"@typescript-eslint/parser": "^4.32.0",
|
||||
"@vue/cli-plugin-babel": "~4.5.13",
|
||||
"@vue/cli-plugin-eslint": "~4.5.15",
|
||||
"@vue/cli-plugin-pwa": "~4.5.13",
|
||||
"@vue/cli-plugin-typescript": "^4.5.15",
|
||||
"@vue/cli-service": "~4.5.13",
|
||||
"@vue/compiler-sfc": "^3.2.20",
|
||||
"@vue/eslint-config-typescript": "^7.0.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"eslint": "^7.28.0",
|
||||
"eslint-plugin-vue": "^8.0.3",
|
||||
"typescript": "~4.4.4",
|
||||
"vue-cli-plugin-i18n": "^2.1.1",
|
||||
"webpack-bundle-tracker": "1.4.0",
|
||||
"workbox-expiration": "^6.3.0",
|
||||
"workbox-navigation-preload": "^6.0.2",
|
||||
"workbox-precaching": "^6.3.0",
|
||||
"workbox-routing": "^6.3.0",
|
||||
"workbox-strategies": "^6.2.4"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
"env": {
|
||||
"node": true
|
||||
"name": "vue",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "vue-cli-service build",
|
||||
"lint": "vue-cli-service lint"
|
||||
},
|
||||
"extends": [
|
||||
"plugin:vue/vue3-essential",
|
||||
"eslint:recommended",
|
||||
"@vue/typescript"
|
||||
"dependencies": {
|
||||
"@babel/eslint-parser": "^7.16.0",
|
||||
"@kangc/v-md-editor": "^1.7.7",
|
||||
"@kevinfaguiar/vue-twemoji-picker": "^5.7.4",
|
||||
"@popperjs/core": "^2.10.1",
|
||||
"@riophae/vue-treeselect": "^0.4.0",
|
||||
"axios": "^0.24.0",
|
||||
"bootstrap-vue": "^2.21.2",
|
||||
"core-js": "^3.19.0",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.29.1",
|
||||
"prismjs": "^1.25.0",
|
||||
"vue": "^2.6.14",
|
||||
"vue-class-component": "^7.2.3",
|
||||
"vue-click-outside": "^1.1.0",
|
||||
"vue-clickaway": "^2.2.2",
|
||||
"vue-cookies": "^1.7.4",
|
||||
"vue-i18n": "^8.26.5",
|
||||
"vue-infinite-loading": "^2.4.5",
|
||||
"vue-multiselect": "^2.1.6",
|
||||
"vue-property-decorator": "^9.1.2",
|
||||
"vue-simple-calendar": "^5.0.1",
|
||||
"vue-template-compiler": "^2.6.14",
|
||||
"vue2-touch-events": "^3.2.2",
|
||||
"vuedraggable": "^2.24.3",
|
||||
"vuex": "^3.6.0",
|
||||
"workbox-webpack-plugin": "^6.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@kazupon/vue-i18n-loader": "^0.5.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.33.0",
|
||||
"@typescript-eslint/parser": "^4.32.0",
|
||||
"@vue/cli-plugin-babel": "~4.5.13",
|
||||
"@vue/cli-plugin-eslint": "~4.5.15",
|
||||
"@vue/cli-plugin-pwa": "~4.5.13",
|
||||
"@vue/cli-plugin-typescript": "^4.5.15",
|
||||
"@vue/cli-service": "~4.5.13",
|
||||
"@vue/compiler-sfc": "^3.2.20",
|
||||
"@vue/eslint-config-typescript": "^9.1.0",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"eslint": "^7.28.0",
|
||||
"eslint-plugin-vue": "^8.0.3",
|
||||
"typescript": "~4.5.2",
|
||||
"vue-cli-plugin-i18n": "^2.1.1",
|
||||
"webpack-bundle-tracker": "1.4.0",
|
||||
"workbox-expiration": "^6.3.0",
|
||||
"workbox-navigation-preload": "^6.0.2",
|
||||
"workbox-precaching": "^6.3.0",
|
||||
"workbox-routing": "^6.3.0",
|
||||
"workbox-strategies": "^6.2.4"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"root": true,
|
||||
"env": {
|
||||
"node": true
|
||||
},
|
||||
"extends": [
|
||||
"plugin:vue/vue3-essential",
|
||||
"eslint:recommended",
|
||||
"@vue/typescript"
|
||||
],
|
||||
"parserOptions": {
|
||||
"parser": "@typescript-eslint/parser"
|
||||
},
|
||||
"rules": {
|
||||
"no-unused-vars": "off"
|
||||
}
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
"not dead"
|
||||
],
|
||||
"parserOptions": {
|
||||
"parser": "@typescript-eslint/parser"
|
||||
},
|
||||
"rules": {
|
||||
"no-unused-vars": "off"
|
||||
"resolutions": {
|
||||
"@vue/cli-plugin-pwa/workbox-webpack-plugin": "^5.1.3",
|
||||
"coa": "2.0.2"
|
||||
}
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
"not dead"
|
||||
],
|
||||
"resolutions": {
|
||||
"@vue/cli-plugin-pwa/workbox-webpack-plugin": "^5.1.3",
|
||||
"coa": "2.0.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,22 +136,22 @@
|
||||
<ContextMenu ref="menu">
|
||||
<template #menu="{ contextData }">
|
||||
<ContextMenuItem @click="$refs.menu.close();openEntryEdit(contextData.originalItem.entry)">
|
||||
<a class="dropdown-item p-2" href="#"><i class="fas fa-pen"></i> {{ $t("Edit") }}</a>
|
||||
<a class="dropdown-item p-2" href="javascript:void(0)"><i class="fas fa-pen"></i> {{ $t("Edit") }}</a>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem @click="$refs.menu.close();moveEntryLeft(contextData)">
|
||||
<a class="dropdown-item p-2" href="#"><i class="fas fa-arrow-left"></i> {{ $t("Move") }}</a>
|
||||
<a class="dropdown-item p-2" href="javascript:void(0)"><i class="fas fa-arrow-left"></i> {{ $t("Move") }}</a>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem @click="$refs.menu.close();moveEntryRight(contextData)">
|
||||
<a class="dropdown-item p-2" href="#"><i class="fas fa-arrow-right"></i> {{ $t("Move") }}</a>
|
||||
<a class="dropdown-item p-2" href="javascript:void(0)"><i class="fas fa-arrow-right"></i> {{ $t("Move") }}</a>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem @click="$refs.menu.close();createEntry(contextData.originalItem.entry)">
|
||||
<a class="dropdown-item p-2" href="#"><i class="fas fa-copy"></i> {{ $t("Clone") }}</a>
|
||||
<a class="dropdown-item p-2" href="javascript:void(0)"><i class="fas fa-copy"></i> {{ $t("Clone") }}</a>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem @click="$refs.menu.close();addToShopping(contextData)">
|
||||
<a class="dropdown-item p-2" href="#"><i class="fas fa-shopping-cart"></i> {{ $t("Add_to_Shopping") }}</a>
|
||||
<a class="dropdown-item p-2" href="javascript:void(0)"><i class="fas fa-shopping-cart"></i> {{ $t("Add_to_Shopping") }}</a>
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem @click="$refs.menu.close();deleteEntry(contextData)">
|
||||
<a class="dropdown-item p-2 text-danger" href="#"><i class="fas fa-trash"></i> {{ $t("Delete") }}</a>
|
||||
<a class="dropdown-item p-2 text-danger" href="javascript:void(0)"><i class="fas fa-trash"></i> {{ $t("Delete") }}</a>
|
||||
</ContextMenuItem>
|
||||
</template>
|
||||
</ContextMenu>
|
||||
@@ -513,11 +513,17 @@ export default {
|
||||
return entry.id === id
|
||||
})[0]
|
||||
},
|
||||
moveEntry(null_object, target_date) {
|
||||
moveEntry(null_object, target_date, drag_event) {
|
||||
this.plan_entries.forEach((entry) => {
|
||||
if (entry.id === this.dragged_item.id) {
|
||||
entry.date = target_date
|
||||
this.saveEntry(entry)
|
||||
if (drag_event.ctrlKey) {
|
||||
let new_entry = Object.assign({}, entry)
|
||||
new_entry.date = target_date
|
||||
this.createEntry(new_entry)
|
||||
} else {
|
||||
entry.date = target_date
|
||||
this.saveEntry(entry)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
@@ -1,479 +1,477 @@
|
||||
<template>
|
||||
<div id="app" style="margin-bottom: 4vh" v-if="this_model">
|
||||
<generic-modal-form v-if="this_model"
|
||||
:model="this_model"
|
||||
:action="this_action"
|
||||
:item1="this_item"
|
||||
:item2="this_target"
|
||||
:show="show_modal"
|
||||
@finish-action="finishAction"/>
|
||||
<div id="app" style="margin-bottom: 4vh" v-if="this_model">
|
||||
<generic-modal-form v-if="this_model" :model="this_model" :action="this_action" :item1="this_item" :item2="this_target" :show="show_modal" @finish-action="finishAction" />
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-2 d-none d-md-block"></div>
|
||||
<div class="col-xl-8 col-12">
|
||||
<div class="container-fluid d-flex flex-column flex-grow-1">
|
||||
<!-- dynamically loaded header components -->
|
||||
<div class="row" v-if="header_component_name !== ''">
|
||||
<div class="col-md-12">
|
||||
<component :is="headerComponent"></component>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-2 d-none d-md-block">
|
||||
</div>
|
||||
<div class="col-xl-8 col-12">
|
||||
<div class="container-fluid d-flex flex-column flex-grow-1">
|
||||
<div class="row">
|
||||
<div class="col-md-9" style="margin-top: 1vh">
|
||||
<h3>
|
||||
<!-- <span><b-button variant="link" size="sm" class="text-dark shadow-none"><i class="fas fa-chevron-down"></i></b-button></span> -->
|
||||
<model-menu />
|
||||
<span>{{ this.this_model.name }}</span>
|
||||
<span v-if="this_model.name !== 'Step'"
|
||||
><b-button variant="link" @click="startAction({ action: 'new' })"><i class="fas fa-plus-circle fa-2x"></i></b-button></span
|
||||
><!-- TODO add proper field to model config to determine if create should be available or not -->
|
||||
</h3>
|
||||
</div>
|
||||
<div class="col-md-3" style="position: relative; margin-top: 1vh">
|
||||
<b-form-checkbox
|
||||
v-model="show_split"
|
||||
name="check-button"
|
||||
v-if="paginated"
|
||||
class="shadow-none"
|
||||
style="position: relative; top: 50%; transform: translateY(-50%)"
|
||||
switch
|
||||
>
|
||||
{{ $t("show_split_screen") }}
|
||||
</b-form-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- dynamically loaded header components -->
|
||||
<div class="row" v-if="header_component_name !== ''">
|
||||
<div class="col-md-12">
|
||||
<component :is="headerComponent"></component>
|
||||
<div class="row">
|
||||
<div class="col" :class="{ 'col-md-6': show_split }">
|
||||
<!-- model isn't paginated and loads in one API call -->
|
||||
<div v-if="!paginated">
|
||||
<generic-horizontal-card
|
||||
v-for="i in items_left"
|
||||
v-bind:key="i.id"
|
||||
:item="i"
|
||||
:model="this_model"
|
||||
@item-action="startAction($event, 'left')"
|
||||
@finish-action="finishAction"
|
||||
/>
|
||||
</div>
|
||||
<!-- model is paginated and needs managed -->
|
||||
<generic-infinite-cards v-if="paginated" :card_counts="left_counts" :scroll="show_split" @search="getItems($event, 'left')" @reset="resetList('left')">
|
||||
<template v-slot:cards>
|
||||
<generic-horizontal-card
|
||||
v-for="i in items_left"
|
||||
v-bind:key="i.id"
|
||||
:item="i"
|
||||
:model="this_model"
|
||||
@item-action="startAction($event, 'left')"
|
||||
@finish-action="finishAction"
|
||||
/>
|
||||
</template>
|
||||
</generic-infinite-cards>
|
||||
</div>
|
||||
<div class="col col-md-6" v-if="show_split">
|
||||
<generic-infinite-cards
|
||||
v-if="this_model"
|
||||
:card_counts="right_counts"
|
||||
:scroll="show_split"
|
||||
@search="getItems($event, 'right')"
|
||||
@reset="resetList('right')"
|
||||
>
|
||||
<template v-slot:cards>
|
||||
<generic-horizontal-card
|
||||
v-for="i in items_right"
|
||||
v-bind:key="i.id"
|
||||
:item="i"
|
||||
:model="this_model"
|
||||
@item-action="startAction($event, 'right')"
|
||||
@finish-action="finishAction"
|
||||
/>
|
||||
</template>
|
||||
</generic-infinite-cards>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-9" style="margin-top: 1vh">
|
||||
<h3>
|
||||
<!-- <span><b-button variant="link" size="sm" class="text-dark shadow-none"><i class="fas fa-chevron-down"></i></b-button></span> -->
|
||||
<model-menu/>
|
||||
<span>{{ this.this_model.name }}</span>
|
||||
<span><b-button variant="link" @click="startAction({'action':'new'})"><i
|
||||
class="fas fa-plus-circle fa-2x"></i></b-button></span>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="col-md-3" style="position: relative; margin-top: 1vh">
|
||||
<b-form-checkbox v-model="show_split" name="check-button" v-if="paginated"
|
||||
class="shadow-none"
|
||||
style="position:relative;top: 50%; transform: translateY(-50%);" switch>
|
||||
{{ $t('show_split_screen') }}
|
||||
</b-form-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col" :class="{'col-md-6' : show_split}">
|
||||
<!-- model isn't paginated and loads in one API call -->
|
||||
<div v-if="!paginated">
|
||||
<generic-horizontal-card v-for="i in items_left" v-bind:key="i.id"
|
||||
:item=i
|
||||
:model="this_model"
|
||||
@item-action="startAction($event, 'left')"
|
||||
@finish-action="finishAction"/>
|
||||
</div>
|
||||
<!-- model is paginated and needs managed -->
|
||||
<generic-infinite-cards v-if="paginated"
|
||||
:card_counts="left_counts"
|
||||
:scroll="show_split"
|
||||
@search="getItems($event, 'left')"
|
||||
@reset="resetList('left')">
|
||||
<template v-slot:cards>
|
||||
<generic-horizontal-card
|
||||
v-for="i in items_left" v-bind:key="i.id"
|
||||
:item=i
|
||||
:model="this_model"
|
||||
@item-action="startAction($event, 'left')"
|
||||
@finish-action="finishAction"/>
|
||||
</template>
|
||||
</generic-infinite-cards>
|
||||
</div>
|
||||
<div class="col col-md-6" v-if="show_split">
|
||||
<generic-infinite-cards v-if="this_model"
|
||||
:card_counts="right_counts"
|
||||
:scroll="show_split"
|
||||
@search="getItems($event, 'right')"
|
||||
@reset="resetList('right')">
|
||||
<template v-slot:cards>
|
||||
<generic-horizontal-card
|
||||
v-for="i in items_right" v-bind:key="i.id"
|
||||
:item=i
|
||||
:model="this_model"
|
||||
@item-action="startAction($event, 'right')"
|
||||
@finish-action="finishAction"/>
|
||||
</template>
|
||||
</generic-infinite-cards>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Vue from "vue"
|
||||
import { BootstrapVue } from "bootstrap-vue"
|
||||
|
||||
import Vue from 'vue'
|
||||
import {BootstrapVue} from 'bootstrap-vue'
|
||||
import "bootstrap-vue/dist/bootstrap-vue.css"
|
||||
|
||||
import 'bootstrap-vue/dist/bootstrap-vue.css'
|
||||
import { CardMixin, ApiMixin, getConfig } from "@/utils/utils"
|
||||
import { StandardToasts, ToastMixin } from "@/utils/utils"
|
||||
|
||||
import {CardMixin, ApiMixin, getConfig} from "@/utils/utils";
|
||||
import {StandardToasts, ToastMixin} from "@/utils/utils";
|
||||
|
||||
import GenericInfiniteCards from "@/components/GenericInfiniteCards";
|
||||
import GenericHorizontalCard from "@/components/GenericHorizontalCard";
|
||||
import GenericModalForm from "@/components/Modals/GenericModalForm";
|
||||
import ModelMenu from "@/components/ModelMenu";
|
||||
import {ApiApiFactory} from "@/utils/openapi/api";
|
||||
import GenericInfiniteCards from "@/components/GenericInfiniteCards"
|
||||
import GenericHorizontalCard from "@/components/GenericHorizontalCard"
|
||||
import GenericModalForm from "@/components/Modals/GenericModalForm"
|
||||
import ModelMenu from "@/components/ModelMenu"
|
||||
import { ApiApiFactory } from "@/utils/openapi/api"
|
||||
//import StorageQuota from "@/components/StorageQuota";
|
||||
|
||||
Vue.use(BootstrapVue)
|
||||
|
||||
export default {
|
||||
// TODO ApiGenerator doesn't capture and share error information - would be nice to share error details when available
|
||||
// or i'm capturing it incorrectly
|
||||
name: 'ModelListView',
|
||||
mixins: [CardMixin, ApiMixin, ToastMixin],
|
||||
components: {
|
||||
GenericHorizontalCard, GenericModalForm, GenericInfiniteCards, ModelMenu,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// this.Models and this.Actions inherited from ApiMixin
|
||||
items_left: [],
|
||||
items_right: [],
|
||||
right_counts: {'max': 9999, 'current': 0},
|
||||
left_counts: {'max': 9999, 'current': 0},
|
||||
this_model: undefined,
|
||||
model_menu: undefined,
|
||||
this_action: undefined,
|
||||
this_recipe_param: undefined,
|
||||
this_item: {},
|
||||
this_target: {},
|
||||
show_modal: false,
|
||||
show_split: false,
|
||||
paginated: false,
|
||||
header_component_name: undefined,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
headerComponent() {
|
||||
// TODO this leads webpack to create one .js file for each component in this folder because at runtime any one of them could be requested
|
||||
// TODO this is not necessarily bad but maybe there are better options to do this
|
||||
return () => import(/* webpackChunkName: "header-component" */ `@/components/${this.header_component_name}`)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// value is passed from lists.py
|
||||
let model_config = JSON.parse(document.getElementById('model_config').textContent)
|
||||
this.this_model = this.Models[model_config?.model]
|
||||
this.this_recipe_param = model_config?.recipe_param
|
||||
this.paginated = this.this_model?.paginated ?? false
|
||||
this.header_component_name = this.this_model?.list?.header_component?.name ?? undefined
|
||||
this.$nextTick(() => {
|
||||
if (!this.paginated) {
|
||||
this.getItems({page:1},'left')
|
||||
}
|
||||
})
|
||||
this.$i18n.locale = window.CUSTOM_LOCALE
|
||||
},
|
||||
methods: {
|
||||
// this.genericAPI inherited from ApiMixin
|
||||
resetList: function (e) {
|
||||
this['items_' + e] = []
|
||||
this[e + '_counts'].max = 9999 + Math.random()
|
||||
this[e + '_counts'].current = 0
|
||||
// TODO ApiGenerator doesn't capture and share error information - would be nice to share error details when available
|
||||
// or i'm capturing it incorrectly
|
||||
name: "ModelListView",
|
||||
mixins: [CardMixin, ApiMixin, ToastMixin],
|
||||
components: {
|
||||
GenericHorizontalCard,
|
||||
GenericModalForm,
|
||||
GenericInfiniteCards,
|
||||
ModelMenu,
|
||||
},
|
||||
startAction: function (e, param) {
|
||||
let source = e?.source ?? {}
|
||||
let target = e?.target ?? undefined
|
||||
this.this_item = source
|
||||
this.this_target = target
|
||||
|
||||
switch (e.action) {
|
||||
case 'delete':
|
||||
this.this_action = this.Actions.DELETE
|
||||
this.show_modal = true
|
||||
break;
|
||||
case 'new':
|
||||
this.this_action = this.Actions.CREATE
|
||||
this.show_modal = true
|
||||
break;
|
||||
case 'edit':
|
||||
this.this_item = e.source
|
||||
this.this_action = this.Actions.UPDATE
|
||||
this.show_modal = true
|
||||
break;
|
||||
case 'move':
|
||||
if (target == null) {
|
||||
this.this_item = e.source
|
||||
this.this_action = this.Actions.MOVE
|
||||
this.show_modal = true
|
||||
} else {
|
||||
this.moveThis(source.id, target.id)
|
||||
}
|
||||
break;
|
||||
case 'merge':
|
||||
if (target == null) {
|
||||
this.this_item = e.source
|
||||
this.this_action = this.Actions.MERGE
|
||||
this.show_modal = true
|
||||
} else {
|
||||
this.mergeThis(e.source, e.target, false)
|
||||
}
|
||||
break;
|
||||
case 'merge-automate':
|
||||
if (target == null) {
|
||||
this.this_item = e.source
|
||||
this.this_action = this.Actions.MERGE
|
||||
this.show_modal = true
|
||||
} else {
|
||||
this.mergeThis(e.source, e.target, true)
|
||||
}
|
||||
break
|
||||
case 'get-children':
|
||||
if (source.show_children) {
|
||||
Vue.set(source, 'show_children', false)
|
||||
} else {
|
||||
this.getChildren(param, source)
|
||||
}
|
||||
break;
|
||||
case 'get-recipes':
|
||||
if (source.show_recipes) {
|
||||
Vue.set(source, 'show_recipes', false)
|
||||
} else {
|
||||
this.getRecipes(param, source)
|
||||
}
|
||||
break;
|
||||
}
|
||||
},
|
||||
finishAction: function (e) {
|
||||
let update = undefined
|
||||
switch (e?.action) {
|
||||
case 'save':
|
||||
this.saveThis(e.form_data)
|
||||
break;
|
||||
}
|
||||
if (e !== 'cancel') {
|
||||
switch (this.this_action) {
|
||||
case this.Actions.DELETE:
|
||||
this.deleteThis(this.this_item.id)
|
||||
break;
|
||||
case this.Actions.CREATE:
|
||||
this.saveThis(e.form_data)
|
||||
break;
|
||||
case this.Actions.UPDATE:
|
||||
update = e.form_data
|
||||
update.id = this.this_item.id
|
||||
this.saveThis(update)
|
||||
break;
|
||||
case this.Actions.MERGE:
|
||||
this.mergeThis(this.this_item, e.form_data.target, false)
|
||||
break;
|
||||
case this.Actions.MOVE:
|
||||
this.moveThis(this.this_item.id, e.form_data.target.id)
|
||||
break;
|
||||
data() {
|
||||
return {
|
||||
// this.Models and this.Actions inherited from ApiMixin
|
||||
items_left: [],
|
||||
items_right: [],
|
||||
right_counts: { max: 9999, current: 0 },
|
||||
left_counts: { max: 9999, current: 0 },
|
||||
this_model: undefined,
|
||||
model_menu: undefined,
|
||||
this_action: undefined,
|
||||
this_recipe_param: undefined,
|
||||
this_item: {},
|
||||
this_target: {},
|
||||
show_modal: false,
|
||||
show_split: false,
|
||||
paginated: false,
|
||||
header_component_name: undefined,
|
||||
}
|
||||
}
|
||||
this.clearState()
|
||||
},
|
||||
getItems: function (params, col) {
|
||||
let column = col || 'left'
|
||||
params.options = {'query':{'extended': 1}} // returns extended values in API response
|
||||
this.genericAPI(this.this_model, this.Actions.LIST, params).then((result) => {
|
||||
let results = result.data?.results ?? result.data
|
||||
|
||||
if (results?.length) {
|
||||
|
||||
// let secondaryRequest = undefined;
|
||||
// if (this['items_' + column]?.length < getConfig(this.this_model, this.Actions.LIST).config.pageSize.default * (params.page - 1)) {
|
||||
// // the item list is smaller than it should be based on the site the user is own
|
||||
// // this happens when an item is deleted (or merged)
|
||||
// // to prevent issues insert the last item of the previous search page before loading the new results
|
||||
// params.page = params.page - 1
|
||||
// secondaryRequest = this.genericAPI(this.this_model, this.Actions.LIST, params).then((result) => {
|
||||
// let prev_page_results = result.data?.results ?? result.data
|
||||
// if (prev_page_results?.length) {
|
||||
// results = [prev_page_results[prev_page_results.length]].concat(results)
|
||||
//
|
||||
// this['items_' + column] = this['items_' + column].concat(results) //TODO duplicate code, find some elegant workaround
|
||||
// this[column + '_counts']['current'] = getConfig(this.this_model, this.Actions.LIST).config.pageSize.default * (params.page - 1) + results.length
|
||||
// this[column + '_counts']['max'] = result.data?.count ?? 0
|
||||
// }
|
||||
// })
|
||||
// } else {
|
||||
//
|
||||
// }
|
||||
|
||||
this['items_' + column] = this['items_' + column].concat(results)
|
||||
this[column + '_counts']['current'] = getConfig(this.this_model, this.Actions.LIST).config.pageSize.default * (params.page - 1) + results.length
|
||||
this[column + '_counts']['max'] = result.data?.count ?? 0
|
||||
|
||||
} else {
|
||||
this[column + '_counts']['max'] = 0
|
||||
this[column + '_counts']['current'] = 0
|
||||
console.log('no data returned')
|
||||
}
|
||||
}).catch((err) => {
|
||||
console.log(err, Object.keys(err))
|
||||
StandardToasts.makeStandardToast(StandardToasts.FAIL_FETCH)
|
||||
})
|
||||
computed: {
|
||||
headerComponent() {
|
||||
// TODO this leads webpack to create one .js file for each component in this folder because at runtime any one of them could be requested
|
||||
// TODO this is not necessarily bad but maybe there are better options to do this
|
||||
return () => import(/* webpackChunkName: "header-component" */ `@/components/${this.header_component_name}`)
|
||||
},
|
||||
},
|
||||
getThis: function (id, callback) {
|
||||
return this.genericAPI(this.this_model, this.Actions.FETCH, {'id': id})
|
||||
},
|
||||
saveThis: function (thisItem) {
|
||||
if (!thisItem?.id) { // if there is no item id assume it's a new item
|
||||
this.genericAPI(this.this_model, this.Actions.CREATE, thisItem).then((result) => {
|
||||
// look for and destroy any existing cards to prevent duplicates in the GET case of get_or_create
|
||||
// then place all new items at the top of the list - could sort instead
|
||||
this.items_left = [result.data].concat(this.destroyCard(result?.data?.id, this.items_left))
|
||||
// this creates a deep copy to make sure that columns stay independent
|
||||
this.items_right = [{...result.data}].concat(this.destroyCard(result?.data?.id, this.items_right))
|
||||
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_CREATE)
|
||||
}).catch((err) => {
|
||||
console.log(err)
|
||||
StandardToasts.makeStandardToast(StandardToasts.FAIL_CREATE)
|
||||
mounted() {
|
||||
// value is passed from lists.py
|
||||
let model_config = JSON.parse(document.getElementById("model_config").textContent)
|
||||
this.this_model = this.Models[model_config?.model]
|
||||
this.this_recipe_param = model_config?.recipe_param
|
||||
this.paginated = this.this_model?.paginated ?? false
|
||||
this.header_component_name = this.this_model?.list?.header_component?.name ?? undefined
|
||||
this.$nextTick(() => {
|
||||
if (!this.paginated) {
|
||||
this.getItems({ page: 1 }, "left")
|
||||
}
|
||||
})
|
||||
} else {
|
||||
this.genericAPI(this.this_model, this.Actions.UPDATE, thisItem).then((result) => {
|
||||
this.refreshThis(thisItem.id)
|
||||
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_UPDATE)
|
||||
}).catch((err) => {
|
||||
console.log(err, err.response)
|
||||
StandardToasts.makeStandardToast(StandardToasts.FAIL_UPDATE)
|
||||
})
|
||||
}
|
||||
this.$i18n.locale = window.CUSTOM_LOCALE
|
||||
},
|
||||
moveThis: function (source_id, target_id) {
|
||||
if (source_id === target_id) {
|
||||
this.makeToast(this.$t('Error'), this.$t('Cannot move item to itself'), 'danger')
|
||||
this.clearState()
|
||||
return
|
||||
}
|
||||
let item = this.findCard(source_id, this.items_left) || this.findCard(source_id, this.items_right)
|
||||
if (source_id === undefined || target_id === undefined || item?.parent == target_id) {
|
||||
this.makeToast(this.$t('Warning'), this.$t('Nothing to do'), 'warning')
|
||||
this.clearState()
|
||||
return
|
||||
}
|
||||
this.genericAPI(this.this_model, this.Actions.MOVE, {'source': source_id, 'target': target_id}).then((result) => {
|
||||
if (target_id === 0) {
|
||||
this.items_left = [item].concat(this.destroyCard(source_id, this.items_left)) // order matters, destroy old card before adding it back in at root
|
||||
this.items_right = [...[item]].concat(this.destroyCard(source_id, this.items_right)) // order matters, destroy old card before adding it back in at root
|
||||
item.parent = null
|
||||
} else {
|
||||
this.items_left = this.destroyCard(source_id, this.items_left)
|
||||
this.items_right = this.destroyCard(source_id, this.items_right)
|
||||
this.refreshThis(target_id)
|
||||
}
|
||||
// TODO make standard toast
|
||||
this.makeToast(this.$t('Success'), 'Succesfully moved resource', 'success')
|
||||
}).catch((err) => {
|
||||
console.log(err)
|
||||
this.makeToast(this.$t('Error'), err.bodyText, 'danger')
|
||||
})
|
||||
methods: {
|
||||
// this.genericAPI inherited from ApiMixin
|
||||
resetList: function (e) {
|
||||
this["items_" + e] = []
|
||||
this[e + "_counts"].max = 9999 + Math.random()
|
||||
this[e + "_counts"].current = 0
|
||||
},
|
||||
startAction: function (e, param) {
|
||||
let source = e?.source ?? {}
|
||||
let target = e?.target ?? undefined
|
||||
this.this_item = source
|
||||
this.this_target = target
|
||||
|
||||
switch (e.action) {
|
||||
case "delete":
|
||||
this.this_action = this.Actions.DELETE
|
||||
this.show_modal = true
|
||||
break
|
||||
case "new":
|
||||
this.this_action = this.Actions.CREATE
|
||||
this.show_modal = true
|
||||
break
|
||||
case "edit":
|
||||
this.this_item = e.source
|
||||
this.this_action = this.Actions.UPDATE
|
||||
this.show_modal = true
|
||||
break
|
||||
case "move":
|
||||
if (target == null) {
|
||||
this.this_item = e.source
|
||||
this.this_action = this.Actions.MOVE
|
||||
this.show_modal = true
|
||||
} else {
|
||||
// this is redundant - function also exists in GenericModal
|
||||
this.moveThis(source.id, target.id)
|
||||
}
|
||||
break
|
||||
case "merge":
|
||||
if (target == null) {
|
||||
this.this_item = e.source
|
||||
this.this_action = this.Actions.MERGE
|
||||
this.show_modal = true
|
||||
} else {
|
||||
// this is redundant - function also exists in GenericModal
|
||||
this.mergeThis(e.source, e.target, false)
|
||||
}
|
||||
break
|
||||
case "merge-automate":
|
||||
if (target == null) {
|
||||
this.this_item = e.source
|
||||
this.this_action = this.Actions.MERGE
|
||||
this.this_item.automate = true
|
||||
this.show_modal = true
|
||||
} else {
|
||||
// this is redundant - function also exists in GenericModal
|
||||
this.mergeThis(e.source, e.target, true)
|
||||
}
|
||||
break
|
||||
case "get-children":
|
||||
if (source.show_children) {
|
||||
Vue.set(source, "show_children", false)
|
||||
} else {
|
||||
this.getChildren(param, source)
|
||||
}
|
||||
break
|
||||
case "get-recipes":
|
||||
if (source.show_recipes) {
|
||||
Vue.set(source, "show_recipes", false)
|
||||
} else {
|
||||
this.getRecipes(param, source)
|
||||
}
|
||||
break
|
||||
}
|
||||
},
|
||||
finishAction: function (e) {
|
||||
switch (e?.action) {
|
||||
case "save":
|
||||
this.saveThis(e.form_data)
|
||||
break
|
||||
}
|
||||
if (e !== "cancel") {
|
||||
switch (this.this_action) {
|
||||
case this.Actions.DELETE:
|
||||
console.log("delete")
|
||||
this.deleteThis(this.this_item.id)
|
||||
break
|
||||
case this.Actions.CREATE:
|
||||
this.saveThis(e.item)
|
||||
break
|
||||
case this.Actions.UPDATE:
|
||||
this.updateThis(this.this_item)
|
||||
break
|
||||
case this.Actions.MERGE:
|
||||
this.mergeUpdateItem(this.this_item.id, e.target)
|
||||
break
|
||||
case this.Actions.MOVE:
|
||||
this.moveUpdateItem(this.this_item.id, e.target)
|
||||
break
|
||||
}
|
||||
}
|
||||
this.clearState()
|
||||
},
|
||||
getItems: function (params, col) {
|
||||
let column = col || "left"
|
||||
params.options = { query: { extended: 1 } } // returns extended values in API response
|
||||
this.genericAPI(this.this_model, this.Actions.LIST, params)
|
||||
.then((result) => {
|
||||
let results = result.data?.results ?? result.data
|
||||
|
||||
if (results?.length) {
|
||||
// let secondaryRequest = undefined;
|
||||
// if (this['items_' + column]?.length < getConfig(this.this_model, this.Actions.LIST).config.pageSize.default * (params.page - 1)) {
|
||||
// // the item list is smaller than it should be based on the site the user is own
|
||||
// // this happens when an item is deleted (or merged)
|
||||
// // to prevent issues insert the last item of the previous search page before loading the new results
|
||||
// params.page = params.page - 1
|
||||
// secondaryRequest = this.genericAPI(this.this_model, this.Actions.LIST, params).then((result) => {
|
||||
// let prev_page_results = result.data?.results ?? result.data
|
||||
// if (prev_page_results?.length) {
|
||||
// results = [prev_page_results[prev_page_results.length]].concat(results)
|
||||
//
|
||||
// this['items_' + column] = this['items_' + column].concat(results) //TODO duplicate code, find some elegant workaround
|
||||
// this[column + '_counts']['current'] = getConfig(this.this_model, this.Actions.LIST).config.pageSize.default * (params.page - 1) + results.length
|
||||
// this[column + '_counts']['max'] = result.data?.count ?? 0
|
||||
// }
|
||||
// })
|
||||
// } else {
|
||||
//
|
||||
// }
|
||||
|
||||
this["items_" + column] = this["items_" + column].concat(results)
|
||||
this[column + "_counts"]["current"] = getConfig(this.this_model, this.Actions.LIST).config.pageSize.default * (params.page - 1) + results.length
|
||||
this[column + "_counts"]["max"] = result.data?.count ?? 0
|
||||
} else {
|
||||
this[column + "_counts"]["max"] = 0
|
||||
this[column + "_counts"]["current"] = 0
|
||||
console.log("no data returned")
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err, Object.keys(err))
|
||||
StandardToasts.makeStandardToast(StandardToasts.FAIL_FETCH)
|
||||
})
|
||||
},
|
||||
getThis: function (id, callback) {
|
||||
return this.genericAPI(this.this_model, this.Actions.FETCH, { id: id })
|
||||
},
|
||||
saveThis: function (item) {
|
||||
// look for and destroy any existing cards to prevent duplicates in the GET case of get_or_create
|
||||
// then place all new items at the top of the list - could sort instead
|
||||
this.items_left = [item].concat(this.destroyCard(item?.id, this.items_left))
|
||||
// this creates a deep copy to make sure that columns stay independent
|
||||
this.items_right = [{ ...item }].concat(this.destroyCard(item?.id, this.items_right))
|
||||
},
|
||||
updateThis: function (item) {
|
||||
this.refreshThis(item.id)
|
||||
},
|
||||
moveThis: function (source_id, target_id) {
|
||||
// TODO: this function is almost 100% duplicated in GenericModalForm and only exists to enable drag and drop
|
||||
if (source_id === target_id) {
|
||||
this.makeToast(this.$t("Error"), this.$t("err_move_self"), "danger")
|
||||
this.clearState()
|
||||
return
|
||||
}
|
||||
let item = this.findCard(source_id, this.items_left) || this.findCard(source_id, this.items_right)
|
||||
if (source_id === undefined || target_id === undefined || item?.parent == target_id) {
|
||||
this.makeToast(this.$t("Warning"), this.$t("nothing"), "warning")
|
||||
this.clearState()
|
||||
return
|
||||
}
|
||||
this.genericAPI(this.this_model, this.Actions.MOVE, { source: source_id, target: target_id })
|
||||
.then((result) => {
|
||||
this.moveUpdateItem(source_id, target_id)
|
||||
// TODO make standard toast
|
||||
this.makeToast(this.$t("Success"), "Succesfully moved resource", "success")
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err)
|
||||
this.makeToast(this.$t("Error"), err.bodyText, "danger")
|
||||
})
|
||||
},
|
||||
moveUpdateItem: function (source_id, target_id) {
|
||||
let item = this.findCard(source_id, this.items_left) || this.findCard(source_id, this.items_right)
|
||||
if (target_id === 0) {
|
||||
this.items_left = [item].concat(this.destroyCard(source_id, this.items_left)) // order matters, destroy old card before adding it back in at root
|
||||
this.items_right = [...[item]].concat(this.destroyCard(source_id, this.items_right)) // order matters, destroy old card before adding it back in at root
|
||||
item.parent = null
|
||||
} else {
|
||||
this.items_left = this.destroyCard(source_id, this.items_left)
|
||||
this.items_right = this.destroyCard(source_id, this.items_right)
|
||||
this.refreshThis(target_id)
|
||||
}
|
||||
},
|
||||
mergeThis: function (source, target, automate) {
|
||||
// TODO: this function is almost 100% duplicated in GenericModalForm and only exists to enable drag and drop
|
||||
let source_id = source.id
|
||||
let target_id = target.id
|
||||
if (source_id === target_id) {
|
||||
this.makeToast(this.$t("Error"), this.$t("err_merge_self"), "danger")
|
||||
this.clearState()
|
||||
return
|
||||
}
|
||||
if (!source_id || !target_id) {
|
||||
this.makeToast(this.$t("Warning"), this.$t("nothing"), "warning")
|
||||
this.clearState()
|
||||
return
|
||||
}
|
||||
this.genericAPI(this.this_model, this.Actions.MERGE, {
|
||||
source: source_id,
|
||||
target: target_id,
|
||||
})
|
||||
.then((result) => {
|
||||
this.mergeUpdateItem(source_id, target_id)
|
||||
// TODO make standard toast
|
||||
this.makeToast(this.$t("Success"), "Succesfully merged resource", "success")
|
||||
})
|
||||
.catch((err) => {
|
||||
//TODO error checking not working with OpenAPI methods
|
||||
console.log("Error", err)
|
||||
this.makeToast(this.$t("Error"), err.bodyText, "danger")
|
||||
})
|
||||
|
||||
if (automate) {
|
||||
let apiClient = new ApiApiFactory()
|
||||
|
||||
let automation = {
|
||||
name: `Merge ${source.name} with ${target.name}`,
|
||||
param_1: source.name,
|
||||
param_2: target.name,
|
||||
}
|
||||
|
||||
if (this.this_model === this.Models.FOOD) {
|
||||
automation.type = "FOOD_ALIAS"
|
||||
}
|
||||
if (this.this_model === this.Models.UNIT) {
|
||||
automation.type = "UNIT_ALIAS"
|
||||
}
|
||||
if (this.this_model === this.Models.KEYWORD) {
|
||||
automation.type = "KEYWORD_ALIAS"
|
||||
}
|
||||
|
||||
apiClient.createAutomation(automation)
|
||||
}
|
||||
},
|
||||
mergeUpdateItem: function (source, target, automate) {
|
||||
this.items_left = this.destroyCard(source, this.items_left)
|
||||
this.items_right = this.destroyCard(source, this.items_right)
|
||||
this.refreshThis(target)
|
||||
},
|
||||
getChildren: function (col, item) {
|
||||
let parent = {}
|
||||
let params = {
|
||||
root: item.id,
|
||||
pageSize: 200,
|
||||
query: { extended: 1 },
|
||||
options: { query: { extended: 1 } },
|
||||
}
|
||||
this.genericAPI(this.this_model, this.Actions.LIST, params)
|
||||
.then((result) => {
|
||||
parent = this.findCard(item.id, this["items_" + col])
|
||||
if (parent) {
|
||||
Vue.set(parent, "children", result.data.results)
|
||||
Vue.set(parent, "show_children", true)
|
||||
Vue.set(parent, "show_recipes", false)
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err)
|
||||
this.makeToast(this.$t("Error"), err.bodyText, "danger")
|
||||
})
|
||||
},
|
||||
getRecipes: function (col, item) {
|
||||
let parent = {}
|
||||
// TODO: make this generic
|
||||
let params = { pageSize: 50, random: true }
|
||||
params[this.this_recipe_param] = item.id
|
||||
console.log("RECIPE PARAM", this.this_recipe_param, params, item.id)
|
||||
this.genericAPI(this.Models.RECIPE, this.Actions.LIST, params)
|
||||
.then((result) => {
|
||||
parent = this.findCard(item.id, this["items_" + col])
|
||||
if (parent) {
|
||||
Vue.set(parent, "recipes", result.data.results)
|
||||
Vue.set(parent, "show_recipes", true)
|
||||
Vue.set(parent, "show_children", false)
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err)
|
||||
this.makeToast(this.$t("Error"), err.bodyText, "danger")
|
||||
})
|
||||
},
|
||||
refreshThis: function (id) {
|
||||
this.getThis(id).then((result) => {
|
||||
this.refreshCard(result.data, this.items_left)
|
||||
this.refreshCard({ ...result.data }, this.items_right)
|
||||
})
|
||||
},
|
||||
deleteThis: function (id) {
|
||||
this.items_left = this.destroyCard(id, this.items_left)
|
||||
this.items_right = this.destroyCard(id, this.items_right)
|
||||
},
|
||||
clearState: function () {
|
||||
this.show_modal = false
|
||||
this.this_action = undefined
|
||||
this.this_item = undefined
|
||||
this.this_target = undefined
|
||||
},
|
||||
},
|
||||
mergeThis: function (source, target, automate) {
|
||||
let source_id = source.id
|
||||
let target_id = target.id
|
||||
if (source_id === target_id) {
|
||||
this.makeToast(this.$t('Error'), this.$t('Cannot merge item with itself'), 'danger')
|
||||
this.clearState()
|
||||
return
|
||||
}
|
||||
if (!source_id || !target_id) {
|
||||
this.makeToast(this.$t('Warning'), this.$t('Nothing to do'), 'warning')
|
||||
this.clearState()
|
||||
return
|
||||
}
|
||||
this.genericAPI(this.this_model, this.Actions.MERGE, {
|
||||
'source': source_id,
|
||||
'target': target_id
|
||||
}).then((result) => {
|
||||
this.items_left = this.destroyCard(source_id, this.items_left)
|
||||
this.items_right = this.destroyCard(source_id, this.items_right)
|
||||
this.refreshThis(target_id)
|
||||
// TODO make standard toast
|
||||
this.makeToast(this.$t('Success'), 'Succesfully merged resource', 'success')
|
||||
}).catch((err) => {
|
||||
//TODO error checking not working with OpenAPI methods
|
||||
console.log('Error', err)
|
||||
this.makeToast(this.$t('Error'), err.bodyText, 'danger')
|
||||
})
|
||||
|
||||
if (automate) {
|
||||
let apiClient = new ApiApiFactory()
|
||||
|
||||
let automation = {
|
||||
name: `Merge ${source.name} with ${target.name}`,
|
||||
param_1: source.name,
|
||||
param_2: target.name
|
||||
}
|
||||
|
||||
if (this.this_model === this.Models.FOOD) {
|
||||
automation.type = 'FOOD_ALIAS'
|
||||
}
|
||||
if (this.this_model === this.Models.UNIT) {
|
||||
automation.type = 'UNIT_ALIAS'
|
||||
}
|
||||
if (this.this_model === this.Models.KEYWORD) {
|
||||
automation.type = 'KEYWORD_ALIAS'
|
||||
}
|
||||
|
||||
apiClient.createAutomation(automation)
|
||||
}
|
||||
|
||||
},
|
||||
getChildren: function (col, item) {
|
||||
let parent = {}
|
||||
let params = {
|
||||
'root': item.id,
|
||||
'pageSize': 200,
|
||||
'query': {'extended': 1},
|
||||
'options': {'query':{'extended': 1}}
|
||||
}
|
||||
this.genericAPI(this.this_model, this.Actions.LIST, params).then((result) => {
|
||||
parent = this.findCard(item.id, this['items_' + col])
|
||||
if (parent) {
|
||||
Vue.set(parent, 'children', result.data.results)
|
||||
Vue.set(parent, 'show_children', true)
|
||||
Vue.set(parent, 'show_recipes', false)
|
||||
}
|
||||
}).catch((err) => {
|
||||
console.log(err)
|
||||
this.makeToast(this.$t('Error'), err.bodyText, 'danger')
|
||||
})
|
||||
},
|
||||
getRecipes: function (col, item) {
|
||||
let parent = {}
|
||||
// TODO: make this generic
|
||||
let params = {'pageSize': 50}
|
||||
params[this.this_recipe_param] = item.id
|
||||
|
||||
this.genericAPI(this.Models.RECIPE, this.Actions.LIST, params).then((result) => {
|
||||
parent = this.findCard(item.id, this['items_' + col])
|
||||
if (parent) {
|
||||
Vue.set(parent, 'recipes', result.data.results)
|
||||
Vue.set(parent, 'show_recipes', true)
|
||||
Vue.set(parent, 'show_children', false)
|
||||
}
|
||||
|
||||
}).catch((err) => {
|
||||
console.log(err)
|
||||
this.makeToast(this.$t('Error'), err.bodyText, 'danger')
|
||||
})
|
||||
},
|
||||
refreshThis: function (id) {
|
||||
this.getThis(id).then(result => {
|
||||
this.refreshCard(result.data, this.items_left)
|
||||
this.refreshCard({...result.data}, this.items_right)
|
||||
})
|
||||
},
|
||||
deleteThis: function (id) {
|
||||
this.genericAPI(this.this_model, this.Actions.DELETE, {'id': id}).then((result) => {
|
||||
this.items_left = this.destroyCard(id, this.items_left)
|
||||
this.items_right = this.destroyCard(id, this.items_right)
|
||||
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_DELETE)
|
||||
}).catch((err) => {
|
||||
console.log(err)
|
||||
StandardToasts.makeStandardToast(StandardToasts.FAIL_DELETE)
|
||||
})
|
||||
},
|
||||
clearState: function () {
|
||||
this.show_modal = false
|
||||
this.this_action = undefined
|
||||
this.this_item = undefined
|
||||
this.this_target = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style src="vue-multiselect/dist/vue-multiselect.min.css"></style>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
||||
<style></style>
|
||||
|
||||
@@ -49,13 +49,13 @@
|
||||
|
||||
<div class="col-md-6 mt-1">
|
||||
<label for="id_name"> {{ $t('Preparation') }} {{ $t('Time') }} ({{ $t('min') }})</label>
|
||||
<input class="form-control" id="id_prep_time" v-model="recipe.working_time">
|
||||
<input class="form-control" id="id_prep_time" v-model="recipe.working_time" type="number">
|
||||
<br/>
|
||||
<label for="id_name"> {{ $t('Waiting') }} {{ $t('Time') }} ({{ $t('min') }})</label>
|
||||
<input class="form-control" id="id_wait_time" v-model="recipe.waiting_time">
|
||||
<input class="form-control" id="id_wait_time" v-model="recipe.waiting_time" type="number">
|
||||
<br/>
|
||||
<label for="id_name"> {{ $t('Servings') }}</label>
|
||||
<input class="form-control" id="id_servings" v-model="recipe.servings">
|
||||
<input class="form-control" id="id_servings" v-model="recipe.servings" type="number">
|
||||
<br/>
|
||||
<label for="id_name"> {{ $t('Servings') }} {{ $t('Text') }}</label>
|
||||
<input class="form-control" id="id_servings_text" v-model="recipe.servings_text" maxlength="32">
|
||||
@@ -343,7 +343,7 @@
|
||||
</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 }">
|
||||
<input class="form-control"
|
||||
<input class="form-control" maxlength="256"
|
||||
v-model="ingredient.note"
|
||||
v-bind:placeholder="$t('Note')"
|
||||
v-on:keydown.tab="event => {if(step.ingredients.indexOf(ingredient) === (step.ingredients.length -1)){event.preventDefault();addIngredient(step)}}">
|
||||
@@ -623,6 +623,11 @@ export default {
|
||||
this.sortIngredients(s)
|
||||
}
|
||||
|
||||
if (this.recipe.waiting_time === '' || isNaN(this.recipe.waiting_time)){ this.recipe.waiting_time = 0}
|
||||
if (this.recipe.working_time === ''|| isNaN(this.recipe.working_time)){ this.recipe.working_time = 0}
|
||||
if (this.recipe.servings === ''|| isNaN(this.recipe.servings)){ this.recipe.servings = 0}
|
||||
|
||||
|
||||
apiFactory.updateRecipe(this.recipe_id, this.recipe,
|
||||
{}).then((response) => {
|
||||
console.log(response)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -88,23 +88,21 @@
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<table class="table table-sm">
|
||||
<!-- eslint-disable vue/no-v-for-template-key-on-child -->
|
||||
<template v-for="s in recipe.steps" >
|
||||
<template v-if="s.show_as_header && s.name !== '' && s.ingredients.length > 0">
|
||||
<b v-bind:key="s.id">{{s.name}}</b>
|
||||
</template>
|
||||
<template v-for="i in s.ingredients">
|
||||
<ingredient-component :ingredient="i" :ingredient_factor="ingredient_factor" :key="i.id"
|
||||
@checked-state-changed="updateIngredientCheckedState"></ingredient-component>
|
||||
</template>
|
||||
<template v-for="s in recipe.steps" v-bind:key="s.id">
|
||||
<div class="row" >
|
||||
<div class="col-md-12">
|
||||
<template v-if="s.show_as_header && s.name !== '' && s.ingredients.length > 0">
|
||||
<b v-bind:key="s.id">{{s.name}}</b>
|
||||
</template>
|
||||
<!-- eslint-enable vue/no-v-for-template-key-on-child -->
|
||||
</table>
|
||||
<table class="table table-sm">
|
||||
<template v-for="i in s.ingredients" :key="i.id">
|
||||
<ingredient-component :ingredient="i" :ingredient_factor="ingredient_factor"
|
||||
@checked-state-changed="updateIngredientCheckedState"></ingredient-component>
|
||||
</template>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<b-card-body class="p-4">
|
||||
<ol style="max-height: 60vh;overflow-y:auto;-webkit-overflow-scrolling: touch;" class="mb-1">
|
||||
<li v-for="(recipe, index) in recipes" v-bind:key="index" v-on:click="$emit('switchRecipe', index)">
|
||||
<a href="#">{{ recipe.recipe_content.name }} <recipe-rating :recipe="recipe"></recipe-rating> </a>
|
||||
<a href="javascript:void(0)">{{ recipe.recipe_content.name }} <recipe-rating :recipe="recipe"></recipe-rating> </a>
|
||||
</li>
|
||||
</ol>
|
||||
<b-card-text v-if="recipes.length === 0">
|
||||
|
||||
@@ -116,6 +116,8 @@ export default {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style src="vue-multiselect/dist/vue-multiselect.min.css"></style>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
|
||||
@@ -1,44 +1,45 @@
|
||||
<template>
|
||||
<div v-if="itemList">
|
||||
<span :key="k.id" v-for="k in itemList" class="pl-1">
|
||||
<b-badge pill :variant="color">{{thisLabel(k)}}</b-badge>
|
||||
</span>
|
||||
<span :key="k.id" v-for="k in itemList" class="pl-1">
|
||||
<b-badge pill :variant="color">{{ thisLabel(k) }}</b-badge>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'GenericPill',
|
||||
props: {
|
||||
item_list: {required: true, type: Array},
|
||||
label: {type: String, default: 'name'},
|
||||
color: {type: String, default: 'light'}
|
||||
},
|
||||
computed: {
|
||||
itemList: function() {
|
||||
if(Array.isArray(this.item_list)) {
|
||||
return this.item_list
|
||||
} else if (!this.item_list?.id) {
|
||||
return false
|
||||
} else {
|
||||
return [this.item_list]
|
||||
}
|
||||
name: "GenericPill",
|
||||
props: {
|
||||
item_list: {
|
||||
type: Array,
|
||||
default() {
|
||||
return []
|
||||
},
|
||||
},
|
||||
label: { type: String, default: "name" },
|
||||
color: { type: String, default: "light" },
|
||||
},
|
||||
computed: {
|
||||
itemList: function () {
|
||||
if (Array.isArray(this.item_list)) {
|
||||
return this.item_list
|
||||
} else if (!this.item_list?.id) {
|
||||
return false
|
||||
} else {
|
||||
return [this.item_list]
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {},
|
||||
methods: {
|
||||
thisLabel: function (item) {
|
||||
let fields = this.label.split("::")
|
||||
let value = item
|
||||
fields.forEach((x) => {
|
||||
value = value[x]
|
||||
})
|
||||
return value
|
||||
},
|
||||
},
|
||||
|
||||
},
|
||||
mounted() {
|
||||
|
||||
},
|
||||
methods: {
|
||||
thisLabel: function (item) {
|
||||
let fields = this.label.split('::')
|
||||
let value = item
|
||||
fields.forEach(x => {
|
||||
value = value[x]
|
||||
});
|
||||
return value
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<b-modal :id="modal_id" size="lg" :title="modal_title" hide-footer aria-label="">
|
||||
<b-modal :id="modal_id" size="lg" :title="modal_title" hide-footer aria-label="" @show="showModal">
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
<div class="row">
|
||||
@@ -60,6 +60,18 @@
|
||||
:placeholder="$t('Servings')"></b-form-input>
|
||||
</b-input-group>
|
||||
<small tabindex="-1" class="form-text text-muted">{{ $t("Servings") }}</small>
|
||||
<b-form-group class="mt-3">
|
||||
<generic-multiselect required
|
||||
@change="entryEditing.shared = $event.val" parent_variable="entryEditing.shared"
|
||||
:label="'username'"
|
||||
:model="Models.USER_NAME"
|
||||
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
|
||||
v-bind:placeholder="$t('Share')" :limit="10"
|
||||
:multiple="true"
|
||||
:initial_selection="entryEditing.shared"
|
||||
></generic-multiselect>
|
||||
<small tabindex="-1" class="form-text text-muted">{{ $t("Share") }}</small>
|
||||
</b-form-group>
|
||||
</div>
|
||||
<div class="col-lg-6 d-none d-lg-block d-xl-block">
|
||||
<recipe-card :recipe="entryEditing.recipe" v-if="entryEditing.recipe != null"></recipe-card>
|
||||
@@ -103,7 +115,7 @@ export default {
|
||||
allow_delete: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
}
|
||||
},
|
||||
mixins: [ApiMixin],
|
||||
components: {
|
||||
@@ -114,7 +126,8 @@ export default {
|
||||
return {
|
||||
entryEditing: {},
|
||||
missing_recipe: false,
|
||||
missing_meal_type: false
|
||||
missing_meal_type: false,
|
||||
default_plan_share: []
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -126,6 +139,15 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
showModal() {
|
||||
let apiClient = new ApiApiFactory()
|
||||
|
||||
apiClient.listUserPreferences().then(result => {
|
||||
if (this.entry.id === -1) {
|
||||
this.entryEditing.shared = result.data[0].plan_share
|
||||
}
|
||||
})
|
||||
},
|
||||
editEntry() {
|
||||
this.missing_meal_type = false
|
||||
this.missing_recipe = false
|
||||
@@ -155,6 +177,13 @@ export default {
|
||||
this.entryEditing.meal_type = null;
|
||||
}
|
||||
},
|
||||
selectShared(event) {
|
||||
if (event.val != null) {
|
||||
this.entryEditing.shared = event.val;
|
||||
} else {
|
||||
this.entryEditing.meal_type = null;
|
||||
}
|
||||
},
|
||||
createMealType(event) {
|
||||
if (event != "") {
|
||||
let apiClient = new ApiApiFactory()
|
||||
|
||||
@@ -1,143 +1,250 @@
|
||||
<template>
|
||||
<div>
|
||||
<b-modal :id="'modal_'+id" @hidden="cancelAction">
|
||||
<template v-slot:modal-title><h4>{{ form.title }}</h4></template>
|
||||
<div v-for="(f, i) in form.fields" v-bind:key=i>
|
||||
<p v-if="f.type=='instruction'">{{ f.label }}</p>
|
||||
<!-- this lookup is single selection -->
|
||||
<lookup-input v-if="f.type=='lookup'"
|
||||
:form="f"
|
||||
:model="listModel(f.list)"
|
||||
@change="storeValue"/> <!-- TODO add ability to create new items associated with lookup -->
|
||||
<!-- TODO: add multi-selection input list -->
|
||||
<checkbox-input v-if="f.type=='checkbox'"
|
||||
:label="f.label"
|
||||
:value="f.value"
|
||||
:field="f.field"/>
|
||||
<text-input v-if="f.type=='text'"
|
||||
:label="f.label"
|
||||
:value="f.value"
|
||||
:field="f.field"
|
||||
:placeholder="f.placeholder"/>
|
||||
<choice-input v-if="f.type=='choice'"
|
||||
:label="f.label"
|
||||
:value="f.value"
|
||||
:field="f.field"
|
||||
:options="f.options"
|
||||
:placeholder="f.placeholder"/>
|
||||
<emoji-input v-if="f.type=='emoji'"
|
||||
:label="f.label"
|
||||
:value="f.value"
|
||||
:field="f.field"
|
||||
@change="storeValue"/>
|
||||
<file-input v-if="f.type=='file'"
|
||||
:label="f.label"
|
||||
:value="f.value"
|
||||
:field="f.field"
|
||||
@change="storeValue"/>
|
||||
</div>
|
||||
<div>
|
||||
<b-modal :id="'modal_' + id" @hidden="cancelAction">
|
||||
<template v-slot:modal-title
|
||||
><h4>{{ form.title }}</h4></template
|
||||
>
|
||||
<div v-for="(f, i) in form.fields" v-bind:key="i">
|
||||
<p v-if="f.type == 'instruction'">{{ f.label }}</p>
|
||||
<!-- this lookup is single selection -->
|
||||
<lookup-input v-if="f.type == 'lookup'" :form="f" :model="listModel(f.list)" @change="storeValue" />
|
||||
<!-- TODO add ability to create new items associated with lookup -->
|
||||
<!-- TODO: add multi-selection input list -->
|
||||
<checkbox-input v-if="f.type == 'checkbox'" :label="f.label" :value="f.value" :field="f.field" />
|
||||
<text-input v-if="f.type == 'text'" :label="f.label" :value="f.value" :field="f.field" :placeholder="f.placeholder" />
|
||||
<choice-input v-if="f.type == 'choice'" :label="f.label" :value="f.value" :field="f.field" :options="f.options" :placeholder="f.placeholder" />
|
||||
<emoji-input v-if="f.type == 'emoji'" :label="f.label" :value="f.value" :field="f.field" @change="storeValue" />
|
||||
<file-input v-if="f.type == 'file'" :label="f.label" :value="f.value" :field="f.field" @change="storeValue" />
|
||||
</div>
|
||||
|
||||
<template v-slot:modal-footer>
|
||||
<b-button class="float-right mx-1" variant="secondary" v-on:click="cancelAction">{{ $t('Cancel') }}</b-button>
|
||||
<b-button class="float-right mx-1" variant="primary" v-on:click="doAction">{{ form.ok_label }}</b-button>
|
||||
</template>
|
||||
</b-modal>
|
||||
</div>
|
||||
<template v-slot:modal-footer>
|
||||
<b-button class="float-right mx-1" variant="secondary" v-on:click="cancelAction">{{ $t("Cancel") }}</b-button>
|
||||
<b-button class="float-right mx-1" variant="primary" v-on:click="doAction">{{ form.ok_label }}</b-button>
|
||||
</template>
|
||||
</b-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Vue from 'vue'
|
||||
import {BootstrapVue} from 'bootstrap-vue'
|
||||
import {getForm} from "@/utils/utils";
|
||||
import Vue from "vue"
|
||||
import { BootstrapVue } from "bootstrap-vue"
|
||||
import { getForm } from "@/utils/utils"
|
||||
|
||||
Vue.use(BootstrapVue)
|
||||
|
||||
import {Models} from "@/utils/models";
|
||||
import CheckboxInput from "@/components/Modals/CheckboxInput";
|
||||
import LookupInput from "@/components/Modals/LookupInput";
|
||||
import TextInput from "@/components/Modals/TextInput";
|
||||
import EmojiInput from "@/components/Modals/EmojiInput";
|
||||
import ChoiceInput from "@/components/Modals/ChoiceInput";
|
||||
import FileInput from "@/components/Modals/FileInput";
|
||||
import { ApiApiFactory } from "@/utils/openapi/api"
|
||||
import { ApiMixin, StandardToasts, ToastMixin } from "@/utils/utils"
|
||||
import CheckboxInput from "@/components/Modals/CheckboxInput"
|
||||
import LookupInput from "@/components/Modals/LookupInput"
|
||||
import TextInput from "@/components/Modals/TextInput"
|
||||
import EmojiInput from "@/components/Modals/EmojiInput"
|
||||
import ChoiceInput from "@/components/Modals/ChoiceInput"
|
||||
import FileInput from "@/components/Modals/FileInput"
|
||||
|
||||
export default {
|
||||
name: 'GenericModalForm',
|
||||
components: {FileInput, CheckboxInput, LookupInput, TextInput, EmojiInput, ChoiceInput},
|
||||
props: {
|
||||
model: {required: true, type: Object},
|
||||
action: {required: true, type: Object},
|
||||
item1: {
|
||||
type: Object, default() {
|
||||
return undefined
|
||||
}
|
||||
name: "GenericModalForm",
|
||||
components: { FileInput, CheckboxInput, LookupInput, TextInput, EmojiInput, ChoiceInput },
|
||||
mixins: [ApiMixin, ToastMixin],
|
||||
props: {
|
||||
model: { required: true, type: Object },
|
||||
action: { type: Object },
|
||||
item1: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
},
|
||||
},
|
||||
item2: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {}
|
||||
},
|
||||
},
|
||||
show: { required: true, type: Boolean, default: false },
|
||||
},
|
||||
item2: {
|
||||
type: Object, default() {
|
||||
return undefined
|
||||
}
|
||||
},
|
||||
show: {required: true, type: Boolean, default: false},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
id: undefined,
|
||||
form_data: {},
|
||||
form: {},
|
||||
dirty: false,
|
||||
special_handling: false
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.id = Math.random()
|
||||
this.$root.$on('change', this.storeValue); // boostrap modal placed at document so have to listen at root of component
|
||||
|
||||
},
|
||||
computed: {
|
||||
buttonLabel() {
|
||||
return this.buttons[this.action].label;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
'show': function () {
|
||||
if (this.show) {
|
||||
this.form = getForm(this.model, this.action, this.item1, this.item2)
|
||||
this.dirty = true
|
||||
this.$bvModal.show('modal_' + this.id)
|
||||
} else {
|
||||
this.$bvModal.hide('modal_' + this.id)
|
||||
this.form_data = {}
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
doAction: function () {
|
||||
this.dirty = false
|
||||
this.$emit('finish-action', {'form_data': this.detectOverride(this.form_data)})
|
||||
},
|
||||
cancelAction: function () {
|
||||
if (this.dirty) {
|
||||
this.dirty = false
|
||||
this.$emit('finish-action', 'cancel')
|
||||
}
|
||||
},
|
||||
storeValue: function (field, value) {
|
||||
this.form_data[field] = value
|
||||
},
|
||||
listModel: function (m) {
|
||||
if (m === 'self') {
|
||||
return this.model
|
||||
} else {
|
||||
return Models[m]
|
||||
}
|
||||
},
|
||||
detectOverride: function (form) {
|
||||
for (const [k, v] of Object.entries(form)) {
|
||||
if (form[k].__override__) {
|
||||
form[k] = form[k].__override__
|
||||
data() {
|
||||
return {
|
||||
id: undefined,
|
||||
form_data: {},
|
||||
form: {},
|
||||
dirty: false,
|
||||
special_handling: false,
|
||||
}
|
||||
}
|
||||
return form
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.id = Math.random()
|
||||
this.$root.$on("change", this.storeValue) // boostrap modal placed at document so have to listen at root of component
|
||||
},
|
||||
computed: {
|
||||
buttonLabel() {
|
||||
return this.buttons[this.action].label
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
show: function () {
|
||||
if (this.show) {
|
||||
this.form = getForm(this.model, this.action, this.item1, this.item2)
|
||||
this.dirty = true
|
||||
this.$bvModal.show("modal_" + this.id)
|
||||
} else {
|
||||
this.$bvModal.hide("modal_" + this.id)
|
||||
this.form_data = {}
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
doAction: function () {
|
||||
this.dirty = false
|
||||
switch (this.action) {
|
||||
case this.Actions.DELETE:
|
||||
this.delete()
|
||||
break
|
||||
case this.Actions.CREATE:
|
||||
this.save()
|
||||
break
|
||||
case this.Actions.UPDATE:
|
||||
this.form_data.id = this.item1.id
|
||||
this.save()
|
||||
break
|
||||
case this.Actions.MERGE:
|
||||
this.merge(this.item1, this.form_data.target.id, this.item1?.automate ?? false)
|
||||
break
|
||||
case this.Actions.MOVE:
|
||||
this.move(this.item1.id, this.form_data.target.id)
|
||||
break
|
||||
}
|
||||
},
|
||||
cancelAction: function () {
|
||||
if (this.dirty) {
|
||||
this.dirty = false
|
||||
this.$emit("finish-action", "cancel")
|
||||
}
|
||||
},
|
||||
storeValue: function (field, value) {
|
||||
this.form_data[field] = value
|
||||
},
|
||||
listModel: function (m) {
|
||||
if (m === "self") {
|
||||
return this.model
|
||||
} else {
|
||||
return this.Models[m]
|
||||
}
|
||||
},
|
||||
detectOverride: function (form) {
|
||||
for (const [k, v] of Object.entries(form)) {
|
||||
if (form[k].__override__) {
|
||||
form[k] = form[k].__override__
|
||||
}
|
||||
}
|
||||
return form
|
||||
},
|
||||
delete: function () {
|
||||
this.genericAPI(this.model, this.Actions.DELETE, { id: this.item1.id })
|
||||
.then((result) => {
|
||||
this.$emit("finish-action")
|
||||
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_DELETE)
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err)
|
||||
StandardToasts.makeStandardToast(StandardToasts.FAIL_DELETE)
|
||||
this.$emit("finish-action", "cancel")
|
||||
})
|
||||
},
|
||||
save: function () {
|
||||
if (!this.item1?.id) {
|
||||
// if there is no item id assume it's a new item
|
||||
this.genericAPI(this.model, this.Actions.CREATE, this.form_data)
|
||||
.then((result) => {
|
||||
this.$emit("finish-action", { item: result.data })
|
||||
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_CREATE)
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err)
|
||||
StandardToasts.makeStandardToast(StandardToasts.FAIL_CREATE)
|
||||
this.$emit("finish-action", "cancel")
|
||||
})
|
||||
} else {
|
||||
this.genericAPI(this.model, this.Actions.UPDATE, this.form_data)
|
||||
.then((result) => {
|
||||
this.$emit("finish-action")
|
||||
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_UPDATE)
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err, err.response)
|
||||
StandardToasts.makeStandardToast(StandardToasts.FAIL_UPDATE)
|
||||
this.$emit("finish-action", "cancel")
|
||||
})
|
||||
}
|
||||
},
|
||||
move: function () {
|
||||
if (this.item1.id === this.form_data.target.id) {
|
||||
this.makeToast(this.$t("Error"), this.$t("err_move_self"), "danger")
|
||||
this.$emit("finish-action", "cancel")
|
||||
return
|
||||
}
|
||||
if (this.form_data.target.id === undefined || this.item1?.parent == this.form_data.target.id) {
|
||||
this.makeToast(this.$t("Warning"), this.$t("nothing"), "warning")
|
||||
this.$emit("finish-action", "cancel")
|
||||
return
|
||||
}
|
||||
this.genericAPI(this.model, this.Actions.MOVE, { source: this.item1.id, target: this.form_data.target.id })
|
||||
.then((result) => {
|
||||
this.$emit("finish-action", { target: this.form_data.target.id })
|
||||
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_MOVE)
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err)
|
||||
StandardToasts.makeStandardToast(StandardToasts.FAIL_MOVE)
|
||||
this.$emit("finish-action", "cancel")
|
||||
})
|
||||
},
|
||||
merge: function () {
|
||||
if (this.item1.id === this.form_data.target.id) {
|
||||
this.makeToast(this.$t("Error"), this.$t("err_merge_self"), "danger")
|
||||
this.$emit("finish-action", "cancel")
|
||||
return
|
||||
}
|
||||
if (!this.item1.id || !this.form_data.target.id) {
|
||||
this.makeToast(this.$t("Warning"), this.$t("nothing"), "warning")
|
||||
this.$emit("finish-action", "cancel")
|
||||
return
|
||||
}
|
||||
this.genericAPI(this.model, this.Actions.MERGE, {
|
||||
source: this.item1.id,
|
||||
target: this.form_data.target.id,
|
||||
})
|
||||
.then((result) => {
|
||||
this.$emit("finish-action", { target: this.form_data.target.id })
|
||||
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_MERGE)
|
||||
})
|
||||
.catch((err) => {
|
||||
//TODO error checking not working with OpenAPI methods
|
||||
console.log("Error", err)
|
||||
StandardToasts.makeStandardToast(StandardToasts.FAIL_MERGE)
|
||||
this.$emit("finish-action", "cancel")
|
||||
})
|
||||
|
||||
if (this.item1.automate) {
|
||||
let apiClient = new ApiApiFactory()
|
||||
let automation = {
|
||||
name: `Merge ${this.item1.name} with ${this.form_data.target.name}`,
|
||||
param_1: this.item1.name,
|
||||
param_2: this.form_data.target.name,
|
||||
}
|
||||
|
||||
if (this.model === this.Models.FOOD) {
|
||||
automation.type = "FOOD_ALIAS"
|
||||
}
|
||||
if (this.model === this.Models.UNIT) {
|
||||
automation.type = "UNIT_ALIAS"
|
||||
}
|
||||
if (this.model === this.Models.KEYWORD) {
|
||||
automation.type = "KEYWORD_ALIAS"
|
||||
}
|
||||
|
||||
apiClient.createAutomation(automation)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -1,157 +1,171 @@
|
||||
<template>
|
||||
<div>
|
||||
<b-form-group
|
||||
v-bind:label="form.label"
|
||||
class="mb-3">
|
||||
<generic-multiselect
|
||||
@change="new_value=$event.val"
|
||||
@remove="new_value=undefined"
|
||||
:initial_selection="initialSelection"
|
||||
:model="model"
|
||||
:multiple="useMultiple"
|
||||
:sticky_options="sticky_options"
|
||||
:allow_create="create_new"
|
||||
:create_placeholder="createPlaceholder"
|
||||
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
|
||||
:placeholder="modelName"
|
||||
@new="addNew">
|
||||
</generic-multiselect>
|
||||
<b-form-group class="mb-3">
|
||||
<template #label v-if="show_label">
|
||||
{{ form.label }}
|
||||
</template>
|
||||
<generic-multiselect
|
||||
@change="new_value = $event.val"
|
||||
@remove="new_value = undefined"
|
||||
:initial_selection="initialSelection"
|
||||
:model="model"
|
||||
:multiple="useMultiple"
|
||||
:sticky_options="sticky_options"
|
||||
:allow_create="form.allow_create"
|
||||
:create_placeholder="createPlaceholder"
|
||||
style="flex-grow: 1; flex-shrink: 1; flex-basis: 0"
|
||||
:placeholder="modelName"
|
||||
@new="addNew"
|
||||
>
|
||||
</generic-multiselect>
|
||||
</b-form-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import GenericMultiselect from "@/components/GenericMultiselect";
|
||||
import {StandardToasts, ApiMixin} from "@/utils/utils";
|
||||
import GenericMultiselect from "@/components/GenericMultiselect"
|
||||
import { StandardToasts, ApiMixin } from "@/utils/utils"
|
||||
|
||||
export default {
|
||||
name: 'LookupInput',
|
||||
components: {GenericMultiselect},
|
||||
mixins: [ApiMixin],
|
||||
props: {
|
||||
form: {type: Object, default () {return undefined}},
|
||||
model: {type: Object, default () {return undefined}},
|
||||
|
||||
// TODO: include create_new and create_text props and associated functionality to create objects for drop down
|
||||
// see 'tagging' here: https://vue-multiselect.js.org/#sub-tagging
|
||||
// perfect world would have it trigger a new modal associated with the associated item model
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
new_value: undefined,
|
||||
field: undefined,
|
||||
label: undefined,
|
||||
sticky_options: undefined,
|
||||
first_run: true
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.new_value = this.form?.value
|
||||
this.field = this.form?.field ?? 'You Forgot To Set Field Name'
|
||||
this.label = this.form?.label ?? ''
|
||||
this.sticky_options = this.form?.sticky_options ?? []
|
||||
},
|
||||
computed: {
|
||||
modelName() {
|
||||
return this?.model?.name ?? this.$t('Search')
|
||||
name: "LookupInput",
|
||||
components: { GenericMultiselect },
|
||||
mixins: [ApiMixin],
|
||||
props: {
|
||||
form: {
|
||||
type: Object,
|
||||
default() {
|
||||
return undefined
|
||||
},
|
||||
},
|
||||
model: {
|
||||
type: Object,
|
||||
default() {
|
||||
return undefined
|
||||
},
|
||||
},
|
||||
show_label: { type: Boolean, default: true },
|
||||
},
|
||||
useMultiple() {
|
||||
return this.form?.multiple || this.form?.ordered || false
|
||||
},
|
||||
initialSelection() {
|
||||
let this_value = this.form.value
|
||||
let arrayValues = undefined
|
||||
// multiselect is expect to get an array of objects - make sure it gets one
|
||||
if (Array.isArray(this_value)) {
|
||||
arrayValues = this_value
|
||||
} else if (!this_value) {
|
||||
arrayValues = []
|
||||
} else if (typeof(this_value) === 'object') {
|
||||
arrayValues = [this_value]
|
||||
} else {
|
||||
arrayValues = [{'id': -1, 'name': this_value}]
|
||||
}
|
||||
|
||||
if (this.form?.ordered && this.first_run) {
|
||||
return this.flattenItems(arrayValues)
|
||||
} else {
|
||||
return arrayValues
|
||||
}
|
||||
},
|
||||
createPlaceholder() {
|
||||
return this.$t('Create_New_' + this?.model?.name)
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'new_value': function () {
|
||||
let x = this?.new_value
|
||||
// pass the unflattened attributes that can be restored when ready to save/update
|
||||
if (this.form?.ordered) {
|
||||
x['__override__'] = this.unflattenItem(this?.new_value)
|
||||
}
|
||||
this.$root.$emit('change', this.form.field, x)
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
addNew: function(e) {
|
||||
// if create a new item requires more than 1 parameter or the field 'name' is insufficient this will need reworked
|
||||
// in a perfect world this would trigger a new modal and allow editing all fields
|
||||
this.genericAPI(this.model, this.Actions.CREATE, {'name': e}).then((result) => {
|
||||
this.new_value = result.data
|
||||
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_CREATE)
|
||||
}).catch((err) => {
|
||||
console.log(err)
|
||||
StandardToasts.makeStandardToast(StandardToasts.FAIL_CREATE)
|
||||
})
|
||||
},
|
||||
// ordered lookups have nested attributes that need flattened attributes to drive lookup
|
||||
flattenItems: function(itemlist) {
|
||||
let flat_items = []
|
||||
let item = undefined
|
||||
let label = this.form.list_label.split('::')
|
||||
itemlist.forEach(x => {
|
||||
item = {}
|
||||
for (const [k, v] of Object.entries(x)) {
|
||||
if (k == label[0]) {
|
||||
item['id'] = v.id
|
||||
item[label[1]] = v[label[1]]
|
||||
} else {
|
||||
item[this.form.field + '__' + k] = v
|
||||
}
|
||||
data() {
|
||||
return {
|
||||
new_value: undefined,
|
||||
field: undefined,
|
||||
label: undefined,
|
||||
sticky_options: undefined,
|
||||
first_run: true,
|
||||
}
|
||||
flat_items.push(item)
|
||||
});
|
||||
this.first_run = false
|
||||
return flat_items
|
||||
},
|
||||
unflattenItem: function(itemList) {
|
||||
let unflat_items = []
|
||||
let item = undefined
|
||||
let this_label = undefined
|
||||
let label = this.form.list_label.split('::')
|
||||
let order = 0
|
||||
itemList.forEach(x => {
|
||||
item = {}
|
||||
item[label[0]] = {}
|
||||
for (const [k, v] of Object.entries(x)) {
|
||||
switch(k) {
|
||||
case 'id':
|
||||
item[label[0]]['id'] = v
|
||||
break;
|
||||
case label[1]:
|
||||
item[label[0]][label[1]] = v
|
||||
break;
|
||||
default:
|
||||
this_label = k.replace(this.form.field + '__', '')
|
||||
}
|
||||
|
||||
}
|
||||
item['order'] = order
|
||||
order++
|
||||
unflat_items.push(item)
|
||||
});
|
||||
return unflat_items
|
||||
}
|
||||
}
|
||||
mounted() {
|
||||
this.new_value = this.form?.value
|
||||
this.field = this.form?.field ?? "You Forgot To Set Field Name"
|
||||
this.label = this.form?.label ?? ""
|
||||
this.sticky_options = this.form?.sticky_options ?? []
|
||||
},
|
||||
computed: {
|
||||
modelName() {
|
||||
return this?.model?.name ?? this.$t("Search")
|
||||
},
|
||||
useMultiple() {
|
||||
return this.form?.multiple || this.form?.ordered || false
|
||||
},
|
||||
initialSelection() {
|
||||
let this_value = this.new_value
|
||||
let arrayValues = undefined
|
||||
// multiselect is expect to get an array of objects - make sure it gets one
|
||||
if (Array.isArray(this_value)) {
|
||||
arrayValues = this_value
|
||||
} else if (!this_value) {
|
||||
arrayValues = []
|
||||
} else if (typeof this_value === "object") {
|
||||
arrayValues = [this_value]
|
||||
} else {
|
||||
arrayValues = [{ id: -1, name: this_value }]
|
||||
}
|
||||
|
||||
if (this.form?.ordered && this.first_run) {
|
||||
return this.flattenItems(arrayValues)
|
||||
} else {
|
||||
return arrayValues
|
||||
}
|
||||
},
|
||||
createPlaceholder() {
|
||||
return this.$t("Create_New_" + this?.model?.name)
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
"form.value": function (newVal, oldVal) {
|
||||
this.new_value = newVal
|
||||
},
|
||||
new_value: function () {
|
||||
let x = this?.new_value
|
||||
// pass the unflattened attributes that can be restored when ready to save/update
|
||||
if (this.form?.ordered) {
|
||||
x["__override__"] = this.unflattenItem(this?.new_value)
|
||||
}
|
||||
this.$root.$emit("change", this.form.field, x)
|
||||
this.$emit("change", x)
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
addNew: function (e) {
|
||||
// if create a new item requires more than 1 parameter or the field 'name' is insufficient this will need reworked
|
||||
// in a perfect world this would trigger a new modal and allow editing all fields
|
||||
this.genericAPI(this.model, this.Actions.CREATE, { name: e })
|
||||
.then((result) => {
|
||||
this.new_value = result.data
|
||||
StandardToasts.makeStandardToast(StandardToasts.SUCCESS_CREATE)
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err)
|
||||
StandardToasts.makeStandardToast(StandardToasts.FAIL_CREATE)
|
||||
})
|
||||
},
|
||||
// ordered lookups have nested attributes that need flattened attributes to drive lookup
|
||||
flattenItems: function (itemlist) {
|
||||
let flat_items = []
|
||||
let item = undefined
|
||||
let label = this.form.list_label.split("::")
|
||||
itemlist.forEach((x) => {
|
||||
item = {}
|
||||
for (const [k, v] of Object.entries(x)) {
|
||||
if (k == label[0]) {
|
||||
item["id"] = v.id
|
||||
item[label[1]] = v[label[1]]
|
||||
} else {
|
||||
item[this.form.field + "__" + k] = v
|
||||
}
|
||||
}
|
||||
flat_items.push(item)
|
||||
})
|
||||
this.first_run = false
|
||||
return flat_items
|
||||
},
|
||||
unflattenItem: function (itemList) {
|
||||
let unflat_items = []
|
||||
let item = undefined
|
||||
let this_label = undefined
|
||||
let label = this.form.list_label.split("::")
|
||||
let order = 0
|
||||
itemList.forEach((x) => {
|
||||
item = {}
|
||||
item[label[0]] = {}
|
||||
for (const [k, v] of Object.entries(x)) {
|
||||
switch (k) {
|
||||
case "id":
|
||||
item[label[0]]["id"] = v
|
||||
break
|
||||
case label[1]:
|
||||
item[label[0]][label[1]] = v
|
||||
break
|
||||
default:
|
||||
this_label = k.replace(this.form.field + "__", "")
|
||||
}
|
||||
}
|
||||
item["order"] = order
|
||||
order++
|
||||
unflat_items.push(item)
|
||||
})
|
||||
return unflat_items
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -34,6 +34,10 @@
|
||||
<i class="fas fa-file fa-fw"></i> {{ Models['USERFILE'].name }}
|
||||
</b-dropdown-item>
|
||||
|
||||
<b-dropdown-item :href="resolveDjangoUrl('list_step')">
|
||||
<i class="fas fa-puzzle-piece fa-fw"></i>{{ Models['STEP'].name }}
|
||||
</b-dropdown-item>
|
||||
|
||||
</b-dropdown>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div>
|
||||
|
||||
<div class="dropdown d-print-none">
|
||||
<a class="btn shadow-none" href="#" role="button" id="dropdownMenuLink"
|
||||
<a class="btn shadow-none" href="javascript:void(0);" role="button" id="dropdownMenuLink"
|
||||
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<i class="fas fa-ellipsis-v fa-lg"></i>
|
||||
</a>
|
||||
@@ -15,7 +15,7 @@
|
||||
<a class="dropdown-item" :href="resolveDjangoUrl('edit_convert_recipe', recipe.id)" v-if="!recipe.internal"><i
|
||||
class="fas fa-exchange-alt fa-fw"></i> {{ $t('convert_internal') }}</a>
|
||||
|
||||
<a href="#">
|
||||
<a href="javascript:void(0);">
|
||||
<button class="dropdown-item" @click="$bvModal.show(`id_modal_add_book_${modal_id}`)">
|
||||
<i class="fas fa-bookmark fa-fw"></i> {{ $t('Manage_Books') }}
|
||||
</button>
|
||||
@@ -26,17 +26,17 @@
|
||||
<i class="fas fa-shopping-cart fa-fw"></i> {{ $t('Add_to_Shopping') }}
|
||||
</a>
|
||||
|
||||
<a class="dropdown-item" @click="createMealPlan" href="#"><i
|
||||
<a class="dropdown-item" @click="createMealPlan" href="javascript:void(0);"><i
|
||||
class="fas fa-calendar fa-fw"></i> {{ $t('Add_to_Plan') }}
|
||||
</a>
|
||||
|
||||
<a href="#">
|
||||
<a href="javascript:void(0);">
|
||||
<button class="dropdown-item" @click="$bvModal.show(`id_modal_cook_log_${modal_id}`)"><i
|
||||
class="fas fa-clipboard-list fa-fw"></i> {{ $t('Log_Cooking') }}
|
||||
</button>
|
||||
</a>
|
||||
|
||||
<a href="#">
|
||||
<a href="javascript:void(0);">
|
||||
<button class="dropdown-item" onclick="window.print()"><i
|
||||
class="fas fa-print fa-fw"></i> {{ $t('Print') }}
|
||||
</button>
|
||||
@@ -45,7 +45,7 @@
|
||||
<a class="dropdown-item" :href="resolveDjangoUrl('view_export') + '?r=' + recipe.id" target="_blank"
|
||||
rel="noopener noreferrer"><i class="fas fa-file-export fa-fw"></i> {{ $t('Export') }}</a>
|
||||
|
||||
<a href="#">
|
||||
<a href="javascript:void(0);">
|
||||
<button class="dropdown-item" @click="createShareLink()" v-if="recipe.internal"><i
|
||||
class="fas fa-share-alt fa-fw"></i> {{ $t('Share') }}
|
||||
</button>
|
||||
|
||||
@@ -112,8 +112,8 @@
|
||||
<a :href="resolveDjangoUrl('view_recipe',step.step_recipe_data.id)">{{ step.step_recipe_data.name }}</a>
|
||||
</h2>
|
||||
<div v-for="(sub_step, index) in step.step_recipe_data.steps" v-bind:key="`substep_${sub_step.id}`">
|
||||
<Step :recipe="step.step_recipe_data" :step="sub_step" :ingredient_factor="ingredient_factor" :index="index"
|
||||
:start_time="start_time" :force_ingredients="true"></Step>
|
||||
<step-component :recipe="step.step_recipe_data" :step="sub_step" :ingredient_factor="ingredient_factor" :index="index"
|
||||
:start_time="start_time" :force_ingredients="true"></step-component>
|
||||
</div>
|
||||
</div>
|
||||
</b-collapse>
|
||||
|
||||
@@ -169,7 +169,7 @@
|
||||
"Week": "Woche",
|
||||
"Month": "Monat",
|
||||
"Year": "Jahr",
|
||||
"Drag_Here_To_Delete": "Ziehen zum löschen",
|
||||
"Drag_Here_To_Delete": "Ziehen zum Löschen",
|
||||
"Select_File": "Datei auswählen",
|
||||
"Image": "Bild",
|
||||
"Planner": "Planer",
|
||||
|
||||
@@ -1,210 +1,218 @@
|
||||
{
|
||||
"warning_feature_beta": "This feature is currently in a BETA (testing) state. Please expect bugs and possibly breaking changes in the future (possibly loosing feature related data) when using this feature.",
|
||||
"err_fetching_resource": "There was an error fetching a resource!",
|
||||
"err_creating_resource": "There was an error creating a resource!",
|
||||
"err_updating_resource": "There was an error updating a resource!",
|
||||
"err_deleting_resource": "There was an error deleting a resource!",
|
||||
"success_fetching_resource": "Successfully fetched a resource!",
|
||||
"success_creating_resource": "Successfully created a resource!",
|
||||
"success_updating_resource": "Successfully updated a resource!",
|
||||
"success_deleting_resource": "Successfully deleted a resource!",
|
||||
"file_upload_disabled": "File upload is not enabled for your space.",
|
||||
"step_time_minutes": "Step time in minutes",
|
||||
"confirm_delete": "Are you sure you want to delete this {object}?",
|
||||
"import_running": "Import running, please wait!",
|
||||
"all_fields_optional": "All fields are optional and can be left empty.",
|
||||
"convert_internal": "Convert to internal recipe",
|
||||
"show_only_internal": "Show only internal recipes",
|
||||
"show_split_screen": "Split View",
|
||||
"Log_Recipe_Cooking": "Log Recipe Cooking",
|
||||
"External_Recipe_Image": "External Recipe Image",
|
||||
"Add_to_Shopping": "Add to Shopping",
|
||||
"Add_to_Plan": "Add to Plan",
|
||||
"Step_start_time": "Step start time",
|
||||
"Sort_by_new": "Sort by new",
|
||||
"Table_of_Contents": "Table of Contents",
|
||||
"Recipes_per_page": "Recipes per Page",
|
||||
"Show_as_header": "Show as header",
|
||||
"Hide_as_header": "Hide as header",
|
||||
"Add_nutrition_recipe": "Add nutrition to recipe",
|
||||
"Remove_nutrition_recipe": "Delete nutrition from recipe",
|
||||
"Copy_template_reference": "Copy template reference",
|
||||
"Save_and_View": "Save & View",
|
||||
"Manage_Books": "Manage Books",
|
||||
"Meal_Plan": "Meal Plan",
|
||||
"Select_Book": "Select Book",
|
||||
"Select_File": "Select File",
|
||||
"Recipe_Image": "Recipe Image",
|
||||
"Import_finished": "Import finished",
|
||||
"View_Recipes": "View Recipes",
|
||||
"Log_Cooking": "Log Cooking",
|
||||
"New_Recipe": "New Recipe",
|
||||
"Url_Import": "Url Import",
|
||||
"Reset_Search": "Reset Search",
|
||||
"Recently_Viewed": "Recently Viewed",
|
||||
"Load_More": "Load More",
|
||||
"New_Keyword": "New Keyword",
|
||||
"Delete_Keyword": "Delete Keyword",
|
||||
"Edit_Keyword": "Edit Keyword",
|
||||
"Edit_Recipe": "Edit Recipe",
|
||||
"Move_Keyword": "Move Keyword",
|
||||
"Merge_Keyword": "Merge Keyword",
|
||||
"Hide_Keywords": "Hide Keyword",
|
||||
"Hide_Recipes": "Hide Recipes",
|
||||
"Move_Up": "Move up",
|
||||
"Move_Down": "Move down",
|
||||
"Step_Name": "Step Name",
|
||||
"Step_Type": "Step Type",
|
||||
"Make_header": "Make_Header",
|
||||
"Make_Ingredient": "Make_Ingredient",
|
||||
"Enable_Amount": "Enable Amount",
|
||||
"Disable_Amount": "Disable Amount",
|
||||
"Add_Step": "Add Step",
|
||||
"Keywords": "Keywords",
|
||||
"Books": "Books",
|
||||
"Proteins": "Proteins",
|
||||
"Fats": "Fats",
|
||||
"Carbohydrates": "Carbohydrates",
|
||||
"Calories": "Calories",
|
||||
"Energy": "Energy",
|
||||
"Nutrition": "Nutrition",
|
||||
"Date": "Date",
|
||||
"Share": "Share",
|
||||
"Automation": "Automation",
|
||||
"Parameter": "Parameter",
|
||||
"Export": "Export",
|
||||
"Copy": "Copy",
|
||||
"Rating": "Rating",
|
||||
"Close": "Close",
|
||||
"Cancel": "Cancel",
|
||||
"Link": "Link",
|
||||
"Add": "Add",
|
||||
"New": "New",
|
||||
"Note": "Note",
|
||||
"Success": "Success",
|
||||
"Failure": "Failure",
|
||||
"Ingredients": "Ingredients",
|
||||
"Supermarket": "Supermarket",
|
||||
"Categories": "Categories",
|
||||
"Category": "Category",
|
||||
"Selected": "Selected",
|
||||
"min": "min",
|
||||
"Servings": "Servings",
|
||||
"Waiting": "Waiting",
|
||||
"Preparation": "Preparation",
|
||||
"External": "External",
|
||||
"Size": "Size",
|
||||
"Files": "Files",
|
||||
"File": "File",
|
||||
"Edit": "Edit",
|
||||
"Image": "Image",
|
||||
"Delete": "Delete",
|
||||
"Open": "Open",
|
||||
"Ok": "Open",
|
||||
"Save": "Save",
|
||||
"Step": "Step",
|
||||
"Search": "Search",
|
||||
"Import": "Import",
|
||||
"Print": "Print",
|
||||
"Settings": "Settings",
|
||||
"or": "or",
|
||||
"and": "and",
|
||||
"Information": "Information",
|
||||
"Download": "Download",
|
||||
"Create": "Create",
|
||||
"Advanced Search Settings": "Advanced Search Settings",
|
||||
"View": "View",
|
||||
"Recipes": "Recipes",
|
||||
"Move": "Move",
|
||||
"Merge": "Merge",
|
||||
"Parent": "Parent",
|
||||
"delete_confirmation": "Are you sure that you want to delete {source}?",
|
||||
"move_confirmation": "Move <i>{child}</i> to parent <i>{parent}</i>",
|
||||
"merge_confirmation": "Replace <i>{source}</i> with <i>{target}</i>",
|
||||
"create_rule": "and create automation",
|
||||
"move_selection": "Select a parent {type} to move {source} to.",
|
||||
"merge_selection": "Replace all occurrences of {source} with the selected {type}.",
|
||||
"Root": "Root",
|
||||
"Ignore_Shopping": "Ignore Shopping",
|
||||
"Shopping_Category": "Shopping Category",
|
||||
"Edit_Food": "Edit Food",
|
||||
"Move_Food": "Move Food",
|
||||
"New_Food": "New Food",
|
||||
"Hide_Food": "Hide Food",
|
||||
"Food_Alias": "Food Alias",
|
||||
"Unit_Alias": "Unit Alias",
|
||||
"Keyword_Alias": "Keyword Alias",
|
||||
"Delete_Food": "Delete Food",
|
||||
"No_ID": "ID not found, cannot delete.",
|
||||
"Meal_Plan_Days": "Future meal plans",
|
||||
"merge_title": "Merge {type}",
|
||||
"move_title": "Move {type}",
|
||||
"Food": "Food",
|
||||
"Recipe_Book": "Recipe Book",
|
||||
"del_confirmation_tree": "Are you sure that you want to delete {source} and all of it's children?",
|
||||
"delete_title": "Delete {type}",
|
||||
"create_title": "New {type}",
|
||||
"edit_title": "Edit {type}",
|
||||
"Name": "Name",
|
||||
"Type": "Type",
|
||||
"Description": "Description",
|
||||
"Recipe": "Recipe",
|
||||
"tree_root": "Root of Tree",
|
||||
"Icon": "Icon",
|
||||
"Unit": "Unit",
|
||||
"No_Results": "No Results",
|
||||
"New_Unit": "New Unit",
|
||||
"Create_New_Shopping Category": "Create New Shopping Category",
|
||||
"Create_New_Food": "Add New Food",
|
||||
"Create_New_Keyword": "Add New Keyword",
|
||||
"Create_New_Unit": "Add New Unit",
|
||||
"Create_New_Meal_Type": "Add New Meal Type",
|
||||
"and_up": "& Up",
|
||||
"Instructions": "Instructions",
|
||||
"Unrated": "Unrated",
|
||||
"Automate": "Automate",
|
||||
"Empty": "Empty",
|
||||
"Key_Ctrl": "Ctrl",
|
||||
"Key_Shift": "Shift",
|
||||
"Time": "Time",
|
||||
"Text": "Text",
|
||||
"Shopping_list": "Shopping List",
|
||||
"Create_Meal_Plan_Entry": "Create meal plan entry",
|
||||
"Edit_Meal_Plan_Entry": "Edit meal plan entry",
|
||||
"Title": "Title",
|
||||
"Week": "Week",
|
||||
"Month": "Month",
|
||||
"Year": "Year",
|
||||
"Planner": "Planner",
|
||||
"Planner_Settings": "Planner settings",
|
||||
"Period": "Period",
|
||||
"Plan_Period_To_Show": "Show weeks, months or years",
|
||||
"Periods": "Periods",
|
||||
"Plan_Show_How_Many_Periods": "How many periods to show",
|
||||
"Starting_Day": "Starting day of the week",
|
||||
"Meal_Types": "Meal types",
|
||||
"Meal_Type": "Meal type",
|
||||
"Clone": "Clone",
|
||||
"Drag_Here_To_Delete": "Drag here to delete",
|
||||
"Meal_Type_Required": "Meal type is required",
|
||||
"Title_or_Recipe_Required": "Title or recipe selection required",
|
||||
"Color": "Color",
|
||||
"New_Meal_Type": "New Meal type",
|
||||
"Week_Numbers": "Week numbers",
|
||||
"Show_Week_Numbers": "Show week numbers ?",
|
||||
"Export_As_ICal": "Export current period to iCal format",
|
||||
"Export_To_ICal": "Export .ics",
|
||||
"Cannot_Add_Notes_To_Shopping": "Notes cannot be added to the shopping list",
|
||||
"Added_To_Shopping_List": "Added to shopping list",
|
||||
"Shopping_List_Empty": "Your shopping list is currently empty, you can add items via the context menu of a meal plan entry (right click on the card or left click the menu icon)",
|
||||
"Next_Period": "Next Period",
|
||||
"Previous_Period": "Previous Period",
|
||||
"Current_Period": "Current Period",
|
||||
"Next_Day": "Next Day",
|
||||
"Previous_Day": "Previous Day",
|
||||
"Coming_Soon": "Coming-Soon",
|
||||
"Auto_Planner": "Auto-Planner",
|
||||
"New_Cookbook": "New cookbook",
|
||||
"Hide_Keyword": "Hide keywords",
|
||||
"Clear": "Clear"
|
||||
"warning_feature_beta": "This feature is currently in a BETA (testing) state. Please expect bugs and possibly breaking changes in the future (possibly loosing feature related data) when using this feature.",
|
||||
"err_fetching_resource": "There was an error fetching a resource!",
|
||||
"err_creating_resource": "There was an error creating a resource!",
|
||||
"err_updating_resource": "There was an error updating a resource!",
|
||||
"err_deleting_resource": "There was an error deleting a resource!",
|
||||
"err_moving_resource": "There was an error moving a resource!",
|
||||
"err_merging_resource": "There was an error merging a resource!",
|
||||
"success_fetching_resource": "Successfully fetched a resource!",
|
||||
"success_creating_resource": "Successfully created a resource!",
|
||||
"success_updating_resource": "Successfully updated a resource!",
|
||||
"success_deleting_resource": "Successfully deleted a resource!",
|
||||
"success_moving_resource": "Successfully moved a resource!",
|
||||
"success_merging_resource": "Successfully merged a resource!",
|
||||
"file_upload_disabled": "File upload is not enabled for your space.",
|
||||
"step_time_minutes": "Step time in minutes",
|
||||
"confirm_delete": "Are you sure you want to delete this {object}?",
|
||||
"import_running": "Import running, please wait!",
|
||||
"all_fields_optional": "All fields are optional and can be left empty.",
|
||||
"convert_internal": "Convert to internal recipe",
|
||||
"show_only_internal": "Show only internal recipes",
|
||||
"show_split_screen": "Split View",
|
||||
"Log_Recipe_Cooking": "Log Recipe Cooking",
|
||||
"External_Recipe_Image": "External Recipe Image",
|
||||
"Add_to_Shopping": "Add to Shopping",
|
||||
"Add_to_Plan": "Add to Plan",
|
||||
"Step_start_time": "Step start time",
|
||||
"Sort_by_new": "Sort by new",
|
||||
"Table_of_Contents": "Table of Contents",
|
||||
"Recipes_per_page": "Recipes per Page",
|
||||
"Show_as_header": "Show as header",
|
||||
"Hide_as_header": "Hide as header",
|
||||
"Add_nutrition_recipe": "Add nutrition to recipe",
|
||||
"Remove_nutrition_recipe": "Delete nutrition from recipe",
|
||||
"Copy_template_reference": "Copy template reference",
|
||||
"Save_and_View": "Save & View",
|
||||
"Manage_Books": "Manage Books",
|
||||
"Meal_Plan": "Meal Plan",
|
||||
"Select_Book": "Select Book",
|
||||
"Select_File": "Select File",
|
||||
"Recipe_Image": "Recipe Image",
|
||||
"Import_finished": "Import finished",
|
||||
"View_Recipes": "View Recipes",
|
||||
"Log_Cooking": "Log Cooking",
|
||||
"New_Recipe": "New Recipe",
|
||||
"Url_Import": "Url Import",
|
||||
"Reset_Search": "Reset Search",
|
||||
"Recently_Viewed": "Recently Viewed",
|
||||
"Load_More": "Load More",
|
||||
"New_Keyword": "New Keyword",
|
||||
"Delete_Keyword": "Delete Keyword",
|
||||
"Edit_Keyword": "Edit Keyword",
|
||||
"Edit_Recipe": "Edit Recipe",
|
||||
"Move_Keyword": "Move Keyword",
|
||||
"Merge_Keyword": "Merge Keyword",
|
||||
"Hide_Keywords": "Hide Keyword",
|
||||
"Hide_Recipes": "Hide Recipes",
|
||||
"Move_Up": "Move up",
|
||||
"Move_Down": "Move down",
|
||||
"Step_Name": "Step Name",
|
||||
"Step_Type": "Step Type",
|
||||
"Make_header": "Make_Header",
|
||||
"Make_Ingredient": "Make_Ingredient",
|
||||
"Enable_Amount": "Enable Amount",
|
||||
"Disable_Amount": "Disable Amount",
|
||||
"Add_Step": "Add Step",
|
||||
"Keywords": "Keywords",
|
||||
"Books": "Books",
|
||||
"Proteins": "Proteins",
|
||||
"Fats": "Fats",
|
||||
"Carbohydrates": "Carbohydrates",
|
||||
"Calories": "Calories",
|
||||
"Energy": "Energy",
|
||||
"Nutrition": "Nutrition",
|
||||
"Date": "Date",
|
||||
"Share": "Share",
|
||||
"Automation": "Automation",
|
||||
"Parameter": "Parameter",
|
||||
"Export": "Export",
|
||||
"Copy": "Copy",
|
||||
"Rating": "Rating",
|
||||
"Close": "Close",
|
||||
"Cancel": "Cancel",
|
||||
"Link": "Link",
|
||||
"Add": "Add",
|
||||
"New": "New",
|
||||
"Note": "Note",
|
||||
"Success": "Success",
|
||||
"Failure": "Failure",
|
||||
"Ingredients": "Ingredients",
|
||||
"Supermarket": "Supermarket",
|
||||
"Categories": "Categories",
|
||||
"Category": "Category",
|
||||
"Selected": "Selected",
|
||||
"min": "min",
|
||||
"Servings": "Servings",
|
||||
"Waiting": "Waiting",
|
||||
"Preparation": "Preparation",
|
||||
"External": "External",
|
||||
"Size": "Size",
|
||||
"Files": "Files",
|
||||
"File": "File",
|
||||
"Edit": "Edit",
|
||||
"Image": "Image",
|
||||
"Delete": "Delete",
|
||||
"Open": "Open",
|
||||
"Ok": "Open",
|
||||
"Save": "Save",
|
||||
"Step": "Step",
|
||||
"Search": "Search",
|
||||
"Import": "Import",
|
||||
"Print": "Print",
|
||||
"Settings": "Settings",
|
||||
"or": "or",
|
||||
"and": "and",
|
||||
"Information": "Information",
|
||||
"Download": "Download",
|
||||
"Create": "Create",
|
||||
"Advanced Search Settings": "Advanced Search Settings",
|
||||
"View": "View",
|
||||
"Recipes": "Recipes",
|
||||
"Move": "Move",
|
||||
"Merge": "Merge",
|
||||
"Parent": "Parent",
|
||||
"delete_confirmation": "Are you sure that you want to delete {source}?",
|
||||
"move_confirmation": "Move <i>{child}</i> to parent <i>{parent}</i>",
|
||||
"merge_confirmation": "Replace <i>{source}</i> with <i>{target}</i>",
|
||||
"create_rule": "and create automation",
|
||||
"move_selection": "Select a parent {type} to move {source} to.",
|
||||
"merge_selection": "Replace all occurrences of {source} with the selected {type}.",
|
||||
"Root": "Root",
|
||||
"Ignore_Shopping": "Ignore Shopping",
|
||||
"Shopping_Category": "Shopping Category",
|
||||
"Edit_Food": "Edit Food",
|
||||
"Move_Food": "Move Food",
|
||||
"New_Food": "New Food",
|
||||
"Hide_Food": "Hide Food",
|
||||
"Food_Alias": "Food Alias",
|
||||
"Unit_Alias": "Unit Alias",
|
||||
"Keyword_Alias": "Keyword Alias",
|
||||
"Delete_Food": "Delete Food",
|
||||
"No_ID": "ID not found, cannot delete.",
|
||||
"Meal_Plan_Days": "Future meal plans",
|
||||
"merge_title": "Merge {type}",
|
||||
"move_title": "Move {type}",
|
||||
"Food": "Food",
|
||||
"Recipe_Book": "Recipe Book",
|
||||
"del_confirmation_tree": "Are you sure that you want to delete {source} and all of it's children?",
|
||||
"delete_title": "Delete {type}",
|
||||
"create_title": "New {type}",
|
||||
"edit_title": "Edit {type}",
|
||||
"Name": "Name",
|
||||
"Type": "Type",
|
||||
"Description": "Description",
|
||||
"Recipe": "Recipe",
|
||||
"tree_root": "Root of Tree",
|
||||
"Icon": "Icon",
|
||||
"Unit": "Unit",
|
||||
"No_Results": "No Results",
|
||||
"New_Unit": "New Unit",
|
||||
"Create_New_Shopping Category": "Create New Shopping Category",
|
||||
"Create_New_Food": "Add New Food",
|
||||
"Create_New_Keyword": "Add New Keyword",
|
||||
"Create_New_Unit": "Add New Unit",
|
||||
"Create_New_Meal_Type": "Add New Meal Type",
|
||||
"and_up": "& Up",
|
||||
"Instructions": "Instructions",
|
||||
"Unrated": "Unrated",
|
||||
"Automate": "Automate",
|
||||
"Empty": "Empty",
|
||||
"Key_Ctrl": "Ctrl",
|
||||
"Key_Shift": "Shift",
|
||||
"Time": "Time",
|
||||
"Text": "Text",
|
||||
"Shopping_list": "Shopping List",
|
||||
"Create_Meal_Plan_Entry": "Create meal plan entry",
|
||||
"Edit_Meal_Plan_Entry": "Edit meal plan entry",
|
||||
"Title": "Title",
|
||||
"Week": "Week",
|
||||
"Month": "Month",
|
||||
"Year": "Year",
|
||||
"Planner": "Planner",
|
||||
"Planner_Settings": "Planner settings",
|
||||
"Period": "Period",
|
||||
"Plan_Period_To_Show": "Show weeks, months or years",
|
||||
"Periods": "Periods",
|
||||
"Plan_Show_How_Many_Periods": "How many periods to show",
|
||||
"Starting_Day": "Starting day of the week",
|
||||
"Meal_Types": "Meal types",
|
||||
"Meal_Type": "Meal type",
|
||||
"Clone": "Clone",
|
||||
"Drag_Here_To_Delete": "Drag here to delete",
|
||||
"Meal_Type_Required": "Meal type is required",
|
||||
"Title_or_Recipe_Required": "Title or recipe selection required",
|
||||
"Color": "Color",
|
||||
"New_Meal_Type": "New Meal type",
|
||||
"Week_Numbers": "Week numbers",
|
||||
"Show_Week_Numbers": "Show week numbers ?",
|
||||
"Export_As_ICal": "Export current period to iCal format",
|
||||
"Export_To_ICal": "Export .ics",
|
||||
"Cannot_Add_Notes_To_Shopping": "Notes cannot be added to the shopping list",
|
||||
"Added_To_Shopping_List": "Added to shopping list",
|
||||
"Shopping_List_Empty": "Your shopping list is currently empty, you can add items via the context menu of a meal plan entry (right click on the card or left click the menu icon)",
|
||||
"Next_Period": "Next Period",
|
||||
"Previous_Period": "Previous Period",
|
||||
"Current_Period": "Current Period",
|
||||
"Next_Day": "Next Day",
|
||||
"Previous_Day": "Previous Day",
|
||||
"Coming_Soon": "Coming-Soon",
|
||||
"Auto_Planner": "Auto-Planner",
|
||||
"New_Cookbook": "New cookbook",
|
||||
"Hide_Keyword": "Hide keywords",
|
||||
"Clear": "Clear",
|
||||
"err_move_self": "Cannot move item to itself",
|
||||
"nothing": "Nothing to do",
|
||||
"err_merge_self": "Cannot merge item with itself",
|
||||
"show_sql": "Show SQL"
|
||||
}
|
||||
|
||||
@@ -190,5 +190,7 @@
|
||||
"Hide_as_header": "Cacher comme entête",
|
||||
"Copy_template_reference": "Copier le modèle de référence",
|
||||
"Edit_Recipe": "Modifier une Recette",
|
||||
"Move_Up": "Monter"
|
||||
"Move_Up": "Monter",
|
||||
"Time": "Temps",
|
||||
"Coming_Soon": "Bientôt disponible"
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"err_fetching_resource": "Si è verificato un errore nel recupero della risorsa!",
|
||||
"err_creating_resource": "Si è verificato un errore durante la creazione di una risorsa!",
|
||||
"err_updating_resource": "Si è verificato un errore nell'aggiornamento della risorsa!",
|
||||
"err_deleting_resource": "Si è verificato un errore nella cancellazione della risorsa!",
|
||||
"err_updating_resource": "Si è verificato un errore durante l'aggiornamento della risorsa!",
|
||||
"err_deleting_resource": "Si è verificato un errore durante la cancellazione della risorsa!",
|
||||
"success_fetching_resource": "Risorsa recuperata con successo!",
|
||||
"success_creating_resource": "Risorsa creata con successo!",
|
||||
"success_updating_resource": "Risorsa aggiornata con successo!",
|
||||
@@ -29,7 +29,7 @@
|
||||
"New_Recipe": "Nuova Ricetta",
|
||||
"Url_Import": "Importa da URL",
|
||||
"Reset_Search": "Ripristina Ricerca",
|
||||
"Recently_Viewed": "Visualizzati di recente",
|
||||
"Recently_Viewed": "Visualizzato di recente",
|
||||
"Load_More": "Carica di più",
|
||||
"New_Keyword": "Nuova parola chiave",
|
||||
"Delete_Keyword": "Elimina parola chiave",
|
||||
@@ -44,7 +44,7 @@
|
||||
"Fats": "Grassi",
|
||||
"Carbohydrates": "Carboidrati",
|
||||
"Calories": "Calorie",
|
||||
"Energy": "",
|
||||
"Energy": "Energia",
|
||||
"Nutrition": "Nutrienti",
|
||||
"Date": "Data",
|
||||
"Share": "Condividi",
|
||||
@@ -125,7 +125,7 @@
|
||||
"Disable_Amount": "Disabilita Quantità",
|
||||
"Key_Ctrl": "Ctrl",
|
||||
"No_Results": "Nessun risultato",
|
||||
"Create_New_Shopping Category": "Crea nuova categoria di spesa",
|
||||
"Create_New_Shopping Category": "Crea nuova categoria della spesa",
|
||||
"Create_New_Keyword": "Aggiungi nuova parola chiave",
|
||||
"and_up": "& Su",
|
||||
"step_time_minutes": "Tempo dello step in minuti",
|
||||
@@ -149,7 +149,7 @@
|
||||
"Create_New_Unit": "Aggiungi nuova unità",
|
||||
"Instructions": "Istruzioni",
|
||||
"Time": "Tempo",
|
||||
"Shopping_Category": "Categoria di spesa",
|
||||
"Shopping_Category": "Categoria Spesa",
|
||||
"Meal_Plan_Days": "Piani alimentari futuri",
|
||||
"tree_root": "Radice dell'albero",
|
||||
"Automation": "Automazione",
|
||||
@@ -202,5 +202,11 @@
|
||||
"Next_Day": "Giorno successivo",
|
||||
"Previous_Day": "Giorno precedente",
|
||||
"Add_nutrition_recipe": "Aggiungi nutrienti alla ricetta",
|
||||
"Remove_nutrition_recipe": "Elimina nutrienti dalla ricetta"
|
||||
"Remove_nutrition_recipe": "Elimina nutrienti dalla ricetta",
|
||||
"Coming_Soon": "In-Arrivo",
|
||||
"Auto_Planner": "Pianificazione automatica",
|
||||
"New_Cookbook": "Nuovo libro di ricette",
|
||||
"Hide_Keyword": "Nascondi parole chiave",
|
||||
"Clear": "Pulisci",
|
||||
"Shopping_List_Empty": "La tua lista della spesa è vuota, puoi aggiungere elementi dal menù contestuale di una voce nel piano alimentare (clicca con il tasto destro sulla scheda o clicca con il tasto sinistro sull'icona del menù)"
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"Fats": "Vetten",
|
||||
"Carbohydrates": "Koolhydraten",
|
||||
"Calories": "Calorieën",
|
||||
"Energy": "",
|
||||
"Energy": "Energie",
|
||||
"Nutrition": "Voedingswaarde",
|
||||
"Date": "Datum",
|
||||
"Share": "Deel",
|
||||
@@ -112,8 +112,8 @@
|
||||
"Step_Type": "Stap Type",
|
||||
"Make_Header": "Maak_Koptekst",
|
||||
"Make_Ingredient": "Maak_Ingrediënt",
|
||||
"Enable_Amount": "Schakel Hoeveelheid in",
|
||||
"Disable_Amount": "Schakel Hoeveelheid uit",
|
||||
"Enable_Amount": "Schakel hoeveelheid in",
|
||||
"Disable_Amount": "Schakel hoeveelheid uit",
|
||||
"Add_Step": "Voeg Stap toe",
|
||||
"Note": "Notitie",
|
||||
"delete_confirmation": "Weet je zeker dat je {source} wil verwijderen?",
|
||||
@@ -171,7 +171,7 @@
|
||||
"Title": "Titel",
|
||||
"Week": "Week",
|
||||
"Month": "Maand",
|
||||
"Make_header": "Maak_Koptekst",
|
||||
"Make_header": "Maak dit de koptekst",
|
||||
"Color": "Kleur",
|
||||
"New_Meal_Type": "Nieuw Maaltype",
|
||||
"Image": "Afbeelding",
|
||||
@@ -204,5 +204,10 @@
|
||||
"Previous_Day": "Vorige dag",
|
||||
"Cannot_Add_Notes_To_Shopping": "Notities kunnen niet aan de boodschappenlijst toegevoegd worden",
|
||||
"Remove_nutrition_recipe": "Verwijder voedingswaarde van recept",
|
||||
"Add_nutrition_recipe": "Voeg voedingswaarde toe aan recept"
|
||||
"Add_nutrition_recipe": "Voeg voedingswaarde toe aan recept",
|
||||
"Coming_Soon": "Binnenkort beschikbaar",
|
||||
"Auto_Planner": "Autoplanner",
|
||||
"New_Cookbook": "Nieuw kookboek",
|
||||
"Hide_Keyword": "Verberg etiketten",
|
||||
"Clear": "Maak leeg"
|
||||
}
|
||||
|
||||
210
vue/src/locales/ro.json
Normal file
210
vue/src/locales/ro.json
Normal file
@@ -0,0 +1,210 @@
|
||||
{
|
||||
"warning_feature_beta": "Momentan această funcționalitate este în fază de testare (BETA). Vă rugăm să vă așteptați la erori și, eventual, modificări de întrerupere în viitor atunci când utilizați această caracteristică (cu posibila pierdere a datelor legate de funcționalitate).",
|
||||
"err_fetching_resource": "A apărut o eroare la apelarea unei resurse!",
|
||||
"err_creating_resource": "A apărut o eroare la crearea unei resurse!",
|
||||
"err_updating_resource": "A apărut o eroare la actualizarea unei resurse!",
|
||||
"err_deleting_resource": "A apărut o eroare la ștergerea unei resurse!",
|
||||
"success_fetching_resource": "Apelare cu succes a unei resurse!",
|
||||
"success_creating_resource": "Creare cu succes a unei resurse!",
|
||||
"success_updating_resource": "Actualizare cu succes a unei resurse!",
|
||||
"success_deleting_resource": "Ștergere cu succes a unei resurse!",
|
||||
"file_upload_disabled": "Încărcarea fișierelor nu este activată pentru spațiul dvs.",
|
||||
"step_time_minutes": "Timpul pasului în minute",
|
||||
"confirm_delete": "Sunteți sigur că vreți să ștergeți acest {object}?",
|
||||
"import_running": "Import în desfășurare, așteptați!",
|
||||
"all_fields_optional": "Toate câmpurile sunt opționale și pot fi lăsate necompletate.",
|
||||
"convert_internal": "Transformați în rețetă internă",
|
||||
"show_only_internal": "Arătați doar rețetele interne",
|
||||
"show_split_screen": "Vedere divizată",
|
||||
"Log_Recipe_Cooking": "",
|
||||
"External_Recipe_Image": "",
|
||||
"Add_to_Shopping": "",
|
||||
"Add_to_Plan": "",
|
||||
"Step_start_time": "",
|
||||
"Sort_by_new": "",
|
||||
"Table_of_Contents": "",
|
||||
"Recipes_per_page": "",
|
||||
"Show_as_header": "",
|
||||
"Hide_as_header": "",
|
||||
"Add_nutrition_recipe": "",
|
||||
"Remove_nutrition_recipe": "",
|
||||
"Copy_template_reference": "",
|
||||
"Save_and_View": "",
|
||||
"Manage_Books": "",
|
||||
"Meal_Plan": "",
|
||||
"Select_Book": "",
|
||||
"Select_File": "",
|
||||
"Recipe_Image": "",
|
||||
"Import_finished": "",
|
||||
"View_Recipes": "",
|
||||
"Log_Cooking": "",
|
||||
"New_Recipe": "",
|
||||
"Url_Import": "",
|
||||
"Reset_Search": "",
|
||||
"Recently_Viewed": "",
|
||||
"Load_More": "",
|
||||
"New_Keyword": "",
|
||||
"Delete_Keyword": "",
|
||||
"Edit_Keyword": "",
|
||||
"Edit_Recipe": "",
|
||||
"Move_Keyword": "",
|
||||
"Merge_Keyword": "",
|
||||
"Hide_Keywords": "",
|
||||
"Hide_Recipes": "",
|
||||
"Move_Up": "",
|
||||
"Move_Down": "",
|
||||
"Step_Name": "",
|
||||
"Step_Type": "",
|
||||
"Make_header": "",
|
||||
"Make_Ingredient": "",
|
||||
"Enable_Amount": "",
|
||||
"Disable_Amount": "",
|
||||
"Add_Step": "",
|
||||
"Keywords": "",
|
||||
"Books": "",
|
||||
"Proteins": "",
|
||||
"Fats": "",
|
||||
"Carbohydrates": "",
|
||||
"Calories": "",
|
||||
"Energy": "",
|
||||
"Nutrition": "",
|
||||
"Date": "",
|
||||
"Share": "",
|
||||
"Automation": "",
|
||||
"Parameter": "",
|
||||
"Export": "",
|
||||
"Copy": "",
|
||||
"Rating": "",
|
||||
"Close": "",
|
||||
"Cancel": "",
|
||||
"Link": "",
|
||||
"Add": "",
|
||||
"New": "",
|
||||
"Note": "",
|
||||
"Success": "",
|
||||
"Failure": "",
|
||||
"Ingredients": "",
|
||||
"Supermarket": "",
|
||||
"Categories": "",
|
||||
"Category": "",
|
||||
"Selected": "",
|
||||
"min": "",
|
||||
"Servings": "",
|
||||
"Waiting": "",
|
||||
"Preparation": "",
|
||||
"External": "",
|
||||
"Size": "",
|
||||
"Files": "",
|
||||
"File": "",
|
||||
"Edit": "",
|
||||
"Image": "",
|
||||
"Delete": "",
|
||||
"Open": "",
|
||||
"Ok": "",
|
||||
"Save": "",
|
||||
"Step": "",
|
||||
"Search": "",
|
||||
"Import": "",
|
||||
"Print": "",
|
||||
"Settings": "",
|
||||
"or": "",
|
||||
"and": "",
|
||||
"Information": "",
|
||||
"Download": "",
|
||||
"Create": "",
|
||||
"Advanced Search Settings": "",
|
||||
"View": "",
|
||||
"Recipes": "",
|
||||
"Move": "",
|
||||
"Merge": "",
|
||||
"Parent": "",
|
||||
"delete_confirmation": "",
|
||||
"move_confirmation": "",
|
||||
"merge_confirmation": "",
|
||||
"create_rule": "",
|
||||
"move_selection": "",
|
||||
"merge_selection": "",
|
||||
"Root": "",
|
||||
"Ignore_Shopping": "",
|
||||
"Shopping_Category": "",
|
||||
"Edit_Food": "",
|
||||
"Move_Food": "",
|
||||
"New_Food": "",
|
||||
"Hide_Food": "",
|
||||
"Food_Alias": "",
|
||||
"Unit_Alias": "",
|
||||
"Keyword_Alias": "",
|
||||
"Delete_Food": "",
|
||||
"No_ID": "",
|
||||
"Meal_Plan_Days": "",
|
||||
"merge_title": "",
|
||||
"move_title": "",
|
||||
"Food": "",
|
||||
"Recipe_Book": "",
|
||||
"del_confirmation_tree": "",
|
||||
"delete_title": "",
|
||||
"create_title": "",
|
||||
"edit_title": "",
|
||||
"Name": "",
|
||||
"Type": "",
|
||||
"Description": "",
|
||||
"Recipe": "",
|
||||
"tree_root": "",
|
||||
"Icon": "",
|
||||
"Unit": "",
|
||||
"No_Results": "",
|
||||
"New_Unit": "",
|
||||
"Create_New_Shopping Category": "",
|
||||
"Create_New_Food": "",
|
||||
"Create_New_Keyword": "",
|
||||
"Create_New_Unit": "",
|
||||
"Create_New_Meal_Type": "",
|
||||
"and_up": "",
|
||||
"Instructions": "",
|
||||
"Unrated": "",
|
||||
"Automate": "",
|
||||
"Empty": "",
|
||||
"Key_Ctrl": "",
|
||||
"Key_Shift": "",
|
||||
"Time": "",
|
||||
"Text": "",
|
||||
"Shopping_list": "",
|
||||
"Create_Meal_Plan_Entry": "",
|
||||
"Edit_Meal_Plan_Entry": "",
|
||||
"Title": "",
|
||||
"Week": "",
|
||||
"Month": "",
|
||||
"Year": "",
|
||||
"Planner": "",
|
||||
"Planner_Settings": "",
|
||||
"Period": "",
|
||||
"Plan_Period_To_Show": "",
|
||||
"Periods": "",
|
||||
"Plan_Show_How_Many_Periods": "",
|
||||
"Starting_Day": "",
|
||||
"Meal_Types": "",
|
||||
"Meal_Type": "",
|
||||
"Clone": "",
|
||||
"Drag_Here_To_Delete": "",
|
||||
"Meal_Type_Required": "",
|
||||
"Title_or_Recipe_Required": "",
|
||||
"Color": "",
|
||||
"New_Meal_Type": "",
|
||||
"Week_Numbers": "",
|
||||
"Show_Week_Numbers": "",
|
||||
"Export_As_ICal": "",
|
||||
"Export_To_ICal": "",
|
||||
"Cannot_Add_Notes_To_Shopping": "",
|
||||
"Added_To_Shopping_List": "",
|
||||
"Shopping_List_Empty": "",
|
||||
"Next_Period": "",
|
||||
"Previous_Period": "",
|
||||
"Current_Period": "",
|
||||
"Next_Day": "",
|
||||
"Previous_Day": "",
|
||||
"Coming_Soon": "",
|
||||
"Auto_Planner": "",
|
||||
"New_Cookbook": "",
|
||||
"Hide_Keyword": "",
|
||||
"Clear": ""
|
||||
}
|
||||
210
vue/src/locales/sl.json
Normal file
210
vue/src/locales/sl.json
Normal file
@@ -0,0 +1,210 @@
|
||||
{
|
||||
"warning_feature_beta": "Ta funkcija je trenutno v stanju BETA (testiranje). Pri uporabi te funkcije pričakujte napake in morebitne prelomne spremembe v prihodnosti (morda izgubite podatke, povezane s to funkcijo).",
|
||||
"err_fetching_resource": "",
|
||||
"err_creating_resource": "",
|
||||
"err_updating_resource": "",
|
||||
"err_deleting_resource": "",
|
||||
"success_fetching_resource": "",
|
||||
"success_creating_resource": "",
|
||||
"success_updating_resource": "",
|
||||
"success_deleting_resource": "",
|
||||
"file_upload_disabled": "",
|
||||
"step_time_minutes": "",
|
||||
"confirm_delete": "",
|
||||
"import_running": "",
|
||||
"all_fields_optional": "",
|
||||
"convert_internal": "",
|
||||
"show_only_internal": "",
|
||||
"show_split_screen": "",
|
||||
"Log_Recipe_Cooking": "",
|
||||
"External_Recipe_Image": "",
|
||||
"Add_to_Shopping": "",
|
||||
"Add_to_Plan": "",
|
||||
"Step_start_time": "",
|
||||
"Sort_by_new": "",
|
||||
"Table_of_Contents": "",
|
||||
"Recipes_per_page": "",
|
||||
"Show_as_header": "",
|
||||
"Hide_as_header": "",
|
||||
"Add_nutrition_recipe": "",
|
||||
"Remove_nutrition_recipe": "",
|
||||
"Copy_template_reference": "",
|
||||
"Save_and_View": "",
|
||||
"Manage_Books": "",
|
||||
"Meal_Plan": "",
|
||||
"Select_Book": "",
|
||||
"Select_File": "",
|
||||
"Recipe_Image": "",
|
||||
"Import_finished": "",
|
||||
"View_Recipes": "",
|
||||
"Log_Cooking": "",
|
||||
"New_Recipe": "Nov Recept",
|
||||
"Url_Import": "",
|
||||
"Reset_Search": "",
|
||||
"Recently_Viewed": "",
|
||||
"Load_More": "",
|
||||
"New_Keyword": "",
|
||||
"Delete_Keyword": "",
|
||||
"Edit_Keyword": "",
|
||||
"Edit_Recipe": "Uredi Recept",
|
||||
"Move_Keyword": "",
|
||||
"Merge_Keyword": "",
|
||||
"Hide_Keywords": "",
|
||||
"Hide_Recipes": "",
|
||||
"Move_Up": "",
|
||||
"Move_Down": "",
|
||||
"Step_Name": "",
|
||||
"Step_Type": "",
|
||||
"Make_header": "",
|
||||
"Make_Ingredient": "",
|
||||
"Enable_Amount": "",
|
||||
"Disable_Amount": "",
|
||||
"Add_Step": "",
|
||||
"Keywords": "",
|
||||
"Books": "Knjige",
|
||||
"Proteins": "",
|
||||
"Fats": "",
|
||||
"Carbohydrates": "",
|
||||
"Calories": "",
|
||||
"Energy": "",
|
||||
"Nutrition": "",
|
||||
"Date": "Datum",
|
||||
"Share": "Deli",
|
||||
"Automation": "",
|
||||
"Parameter": "",
|
||||
"Export": "",
|
||||
"Copy": "",
|
||||
"Rating": "",
|
||||
"Close": "",
|
||||
"Cancel": "",
|
||||
"Link": "",
|
||||
"Add": "",
|
||||
"New": "",
|
||||
"Note": "",
|
||||
"Success": "",
|
||||
"Failure": "",
|
||||
"Ingredients": "",
|
||||
"Supermarket": "",
|
||||
"Categories": "",
|
||||
"Category": "",
|
||||
"Selected": "",
|
||||
"min": "",
|
||||
"Servings": "",
|
||||
"Waiting": "",
|
||||
"Preparation": "",
|
||||
"External": "",
|
||||
"Size": "",
|
||||
"Files": "",
|
||||
"File": "",
|
||||
"Edit": "",
|
||||
"Image": "",
|
||||
"Delete": "Izbriši",
|
||||
"Open": "Odpri",
|
||||
"Ok": "Odpri",
|
||||
"Save": "Shrani",
|
||||
"Step": "",
|
||||
"Search": "Iskanje",
|
||||
"Import": "Uvozi",
|
||||
"Print": "Natisni",
|
||||
"Settings": "",
|
||||
"or": "",
|
||||
"and": "",
|
||||
"Information": "",
|
||||
"Download": "Prenesi",
|
||||
"Create": "",
|
||||
"Advanced Search Settings": "",
|
||||
"View": "",
|
||||
"Recipes": "Recepti",
|
||||
"Move": "",
|
||||
"Merge": "",
|
||||
"Parent": "",
|
||||
"delete_confirmation": "",
|
||||
"move_confirmation": "",
|
||||
"merge_confirmation": "",
|
||||
"create_rule": "",
|
||||
"move_selection": "",
|
||||
"merge_selection": "",
|
||||
"Root": "",
|
||||
"Ignore_Shopping": "",
|
||||
"Shopping_Category": "",
|
||||
"Edit_Food": "",
|
||||
"Move_Food": "",
|
||||
"New_Food": "",
|
||||
"Hide_Food": "",
|
||||
"Food_Alias": "",
|
||||
"Unit_Alias": "",
|
||||
"Keyword_Alias": "",
|
||||
"Delete_Food": "",
|
||||
"No_ID": "",
|
||||
"Meal_Plan_Days": "",
|
||||
"merge_title": "",
|
||||
"move_title": "",
|
||||
"Food": "Hrana",
|
||||
"Recipe_Book": "",
|
||||
"del_confirmation_tree": "",
|
||||
"delete_title": "",
|
||||
"create_title": "",
|
||||
"edit_title": "",
|
||||
"Name": "",
|
||||
"Type": "",
|
||||
"Description": "",
|
||||
"Recipe": "",
|
||||
"tree_root": "",
|
||||
"Icon": "",
|
||||
"Unit": "",
|
||||
"No_Results": "",
|
||||
"New_Unit": "",
|
||||
"Create_New_Shopping Category": "",
|
||||
"Create_New_Food": "Dodaj Novo Hrano",
|
||||
"Create_New_Keyword": "",
|
||||
"Create_New_Unit": "",
|
||||
"Create_New_Meal_Type": "",
|
||||
"and_up": "",
|
||||
"Instructions": "",
|
||||
"Unrated": "",
|
||||
"Automate": "",
|
||||
"Empty": "",
|
||||
"Key_Ctrl": "",
|
||||
"Key_Shift": "",
|
||||
"Time": "",
|
||||
"Text": "",
|
||||
"Shopping_list": "Nakupovalni Seznam",
|
||||
"Create_Meal_Plan_Entry": "",
|
||||
"Edit_Meal_Plan_Entry": "",
|
||||
"Title": "",
|
||||
"Week": "Teden",
|
||||
"Month": "Mesec",
|
||||
"Year": "Leto",
|
||||
"Planner": "",
|
||||
"Planner_Settings": "",
|
||||
"Period": "",
|
||||
"Plan_Period_To_Show": "",
|
||||
"Periods": "",
|
||||
"Plan_Show_How_Many_Periods": "",
|
||||
"Starting_Day": "",
|
||||
"Meal_Types": "",
|
||||
"Meal_Type": "",
|
||||
"Clone": "",
|
||||
"Drag_Here_To_Delete": "",
|
||||
"Meal_Type_Required": "",
|
||||
"Title_or_Recipe_Required": "",
|
||||
"Color": "Barva",
|
||||
"New_Meal_Type": "",
|
||||
"Week_Numbers": "",
|
||||
"Show_Week_Numbers": "",
|
||||
"Export_As_ICal": "",
|
||||
"Export_To_ICal": "",
|
||||
"Cannot_Add_Notes_To_Shopping": "",
|
||||
"Added_To_Shopping_List": "",
|
||||
"Shopping_List_Empty": "",
|
||||
"Next_Period": "",
|
||||
"Previous_Period": "",
|
||||
"Current_Period": "",
|
||||
"Next_Day": "Naslednji Dan",
|
||||
"Previous_Day": "Prejšnji Dan",
|
||||
"Coming_Soon": "",
|
||||
"Auto_Planner": "",
|
||||
"New_Cookbook": "",
|
||||
"Hide_Keyword": "",
|
||||
"Clear": ""
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -215,12 +215,6 @@ export interface Food {
|
||||
* @memberof Food
|
||||
*/
|
||||
supermarket_category?: FoodSupermarketCategory | null;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof Food
|
||||
*/
|
||||
image?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
@@ -233,12 +227,6 @@ export interface Food {
|
||||
* @memberof Food
|
||||
*/
|
||||
numchild?: number;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof Food
|
||||
*/
|
||||
numrecipe?: string;
|
||||
}
|
||||
/**
|
||||
*
|
||||
@@ -387,12 +375,6 @@ export interface ImportLogKeyword {
|
||||
* @memberof ImportLogKeyword
|
||||
*/
|
||||
description?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ImportLogKeyword
|
||||
*/
|
||||
image?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
@@ -405,12 +387,6 @@ export interface ImportLogKeyword {
|
||||
* @memberof ImportLogKeyword
|
||||
*/
|
||||
numchild?: number;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ImportLogKeyword
|
||||
*/
|
||||
numrecipe?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
@@ -444,10 +420,10 @@ export interface Ingredient {
|
||||
food: StepFood | null;
|
||||
/**
|
||||
*
|
||||
* @type {StepUnit}
|
||||
* @type {FoodSupermarketCategory}
|
||||
* @memberof Ingredient
|
||||
*/
|
||||
unit: StepUnit | null;
|
||||
unit: FoodSupermarketCategory | null;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
@@ -629,10 +605,10 @@ export interface InlineResponse2004 {
|
||||
previous?: string | null;
|
||||
/**
|
||||
*
|
||||
* @type {Array<RecipeOverview>}
|
||||
* @type {Array<Step>}
|
||||
* @memberof InlineResponse2004
|
||||
*/
|
||||
results?: Array<RecipeOverview>;
|
||||
results?: Array<Step>;
|
||||
}
|
||||
/**
|
||||
*
|
||||
@@ -660,10 +636,10 @@ export interface InlineResponse2005 {
|
||||
previous?: string | null;
|
||||
/**
|
||||
*
|
||||
* @type {Array<ViewLog>}
|
||||
* @type {Array<RecipeOverview>}
|
||||
* @memberof InlineResponse2005
|
||||
*/
|
||||
results?: Array<ViewLog>;
|
||||
results?: Array<RecipeOverview>;
|
||||
}
|
||||
/**
|
||||
*
|
||||
@@ -691,10 +667,10 @@ export interface InlineResponse2006 {
|
||||
previous?: string | null;
|
||||
/**
|
||||
*
|
||||
* @type {Array<CookLog>}
|
||||
* @type {Array<ViewLog>}
|
||||
* @memberof InlineResponse2006
|
||||
*/
|
||||
results?: Array<CookLog>;
|
||||
results?: Array<ViewLog>;
|
||||
}
|
||||
/**
|
||||
*
|
||||
@@ -722,10 +698,10 @@ export interface InlineResponse2007 {
|
||||
previous?: string | null;
|
||||
/**
|
||||
*
|
||||
* @type {Array<SupermarketCategoryRelation>}
|
||||
* @type {Array<CookLog>}
|
||||
* @memberof InlineResponse2007
|
||||
*/
|
||||
results?: Array<SupermarketCategoryRelation>;
|
||||
results?: Array<CookLog>;
|
||||
}
|
||||
/**
|
||||
*
|
||||
@@ -753,9 +729,40 @@ export interface InlineResponse2008 {
|
||||
previous?: string | null;
|
||||
/**
|
||||
*
|
||||
* @type {Array<ImportLog>}
|
||||
* @type {Array<SupermarketCategoryRelation>}
|
||||
* @memberof InlineResponse2008
|
||||
*/
|
||||
results?: Array<SupermarketCategoryRelation>;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface InlineResponse2009
|
||||
*/
|
||||
export interface InlineResponse2009 {
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof InlineResponse2009
|
||||
*/
|
||||
count?: number;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof InlineResponse2009
|
||||
*/
|
||||
next?: string | null;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof InlineResponse2009
|
||||
*/
|
||||
previous?: string | null;
|
||||
/**
|
||||
*
|
||||
* @type {Array<ImportLog>}
|
||||
* @memberof InlineResponse2009
|
||||
*/
|
||||
results?: Array<ImportLog>;
|
||||
}
|
||||
/**
|
||||
@@ -794,12 +801,6 @@ export interface Keyword {
|
||||
* @memberof Keyword
|
||||
*/
|
||||
description?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof Keyword
|
||||
*/
|
||||
image?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
@@ -812,12 +813,6 @@ export interface Keyword {
|
||||
* @memberof Keyword
|
||||
*/
|
||||
numchild?: number;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof Keyword
|
||||
*/
|
||||
numrecipe?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
@@ -881,10 +876,10 @@ export interface MealPlan {
|
||||
date: string;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @type {MealPlanMealType}
|
||||
* @memberof MealPlan
|
||||
*/
|
||||
meal_type: number;
|
||||
meal_type: MealPlanMealType;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
@@ -910,6 +905,55 @@ export interface MealPlan {
|
||||
*/
|
||||
meal_type_name?: string;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface MealPlanMealType
|
||||
*/
|
||||
export interface MealPlanMealType {
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof MealPlanMealType
|
||||
*/
|
||||
id?: number;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof MealPlanMealType
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof MealPlanMealType
|
||||
*/
|
||||
order?: number;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof MealPlanMealType
|
||||
*/
|
||||
icon?: string | null;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof MealPlanMealType
|
||||
*/
|
||||
color?: string | null;
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
* @memberof MealPlanMealType
|
||||
*/
|
||||
_default?: boolean;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof MealPlanMealType
|
||||
*/
|
||||
created_by?: string;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
@@ -1037,6 +1081,24 @@ export interface MealType {
|
||||
* @memberof MealType
|
||||
*/
|
||||
order?: number;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof MealType
|
||||
*/
|
||||
icon?: string | null;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof MealType
|
||||
*/
|
||||
color?: string | null;
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
* @memberof MealType
|
||||
*/
|
||||
_default?: boolean;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
@@ -1288,12 +1350,6 @@ export interface RecipeKeywords {
|
||||
* @memberof RecipeKeywords
|
||||
*/
|
||||
description?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof RecipeKeywords
|
||||
*/
|
||||
image?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
@@ -1306,12 +1362,6 @@ export interface RecipeKeywords {
|
||||
* @memberof RecipeKeywords
|
||||
*/
|
||||
numchild?: number;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof RecipeKeywords
|
||||
*/
|
||||
numrecipe?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
@@ -1574,6 +1624,12 @@ export interface RecipeSteps {
|
||||
* @memberof RecipeSteps
|
||||
*/
|
||||
step_recipe_data?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof RecipeSteps
|
||||
*/
|
||||
numrecipe?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1680,10 +1736,10 @@ export interface ShoppingListEntries {
|
||||
food: StepFood | null;
|
||||
/**
|
||||
*
|
||||
* @type {StepUnit}
|
||||
* @type {FoodSupermarketCategory}
|
||||
* @memberof ShoppingListEntries
|
||||
*/
|
||||
unit?: StepUnit | null;
|
||||
unit?: FoodSupermarketCategory | null;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
@@ -1729,10 +1785,10 @@ export interface ShoppingListEntry {
|
||||
food: StepFood | null;
|
||||
/**
|
||||
*
|
||||
* @type {StepUnit}
|
||||
* @type {FoodSupermarketCategory}
|
||||
* @memberof ShoppingListEntry
|
||||
*/
|
||||
unit?: StepUnit | null;
|
||||
unit?: FoodSupermarketCategory | null;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
@@ -2004,6 +2060,12 @@ export interface Step {
|
||||
* @memberof Step
|
||||
*/
|
||||
step_recipe_data?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof Step
|
||||
*/
|
||||
numrecipe?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2084,12 +2146,6 @@ export interface StepFood {
|
||||
* @memberof StepFood
|
||||
*/
|
||||
supermarket_category?: FoodSupermarketCategory | null;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof StepFood
|
||||
*/
|
||||
image?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
@@ -2102,12 +2158,6 @@ export interface StepFood {
|
||||
* @memberof StepFood
|
||||
*/
|
||||
numchild?: number;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof StepFood
|
||||
*/
|
||||
numrecipe?: string;
|
||||
}
|
||||
/**
|
||||
*
|
||||
@@ -2129,10 +2179,10 @@ export interface StepIngredients {
|
||||
food: StepFood | null;
|
||||
/**
|
||||
*
|
||||
* @type {StepUnit}
|
||||
* @type {FoodSupermarketCategory}
|
||||
* @memberof StepIngredients
|
||||
*/
|
||||
unit: StepUnit | null;
|
||||
unit: FoodSupermarketCategory | null;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
@@ -2164,43 +2214,6 @@ export interface StepIngredients {
|
||||
*/
|
||||
no_amount?: boolean;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface StepUnit
|
||||
*/
|
||||
export interface StepUnit {
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof StepUnit
|
||||
*/
|
||||
id?: number;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof StepUnit
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof StepUnit
|
||||
*/
|
||||
description?: string | null;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof StepUnit
|
||||
*/
|
||||
numrecipe?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof StepUnit
|
||||
*/
|
||||
image?: string;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
@@ -2458,18 +2471,6 @@ export interface Unit {
|
||||
* @memberof Unit
|
||||
*/
|
||||
description?: string | null;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof Unit
|
||||
*/
|
||||
numrecipe?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof Unit
|
||||
*/
|
||||
image?: string;
|
||||
}
|
||||
/**
|
||||
*
|
||||
@@ -4782,6 +4783,7 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration)
|
||||
* @param {number} [units] Id of unit a recipe should have.
|
||||
* @param {number} [rating] Id of unit a recipe should have.
|
||||
* @param {string} [books] Id of book a recipe should have. For multiple repeat parameter.
|
||||
* @param {string} [steps] Id of a step a recipe should have. For multiple repeat parameter.
|
||||
* @param {string} [keywordsOr] If recipe should have all (AND) or any (OR) of the provided keywords.
|
||||
* @param {string} [foodsOr] If recipe should have all (AND) or any (OR) any of the provided foods.
|
||||
* @param {string} [booksOr] If recipe should be in all (AND) or any (OR) any of the provided books.
|
||||
@@ -4793,7 +4795,7 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration)
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
listRecipes: async (query?: string, keywords?: string, foods?: string, units?: number, rating?: number, books?: string, keywordsOr?: string, foodsOr?: string, booksOr?: string, internal?: string, random?: string, _new?: string, page?: number, pageSize?: number, options: any = {}): Promise<RequestArgs> => {
|
||||
listRecipes: async (query?: string, keywords?: string, foods?: string, units?: number, rating?: number, books?: string, steps?: string, keywordsOr?: string, foodsOr?: string, booksOr?: string, internal?: string, random?: string, _new?: string, page?: number, pageSize?: number, options: any = {}): Promise<RequestArgs> => {
|
||||
const localVarPath = `/api/recipe/`;
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
@@ -4830,6 +4832,10 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration)
|
||||
localVarQueryParameter['books'] = books;
|
||||
}
|
||||
|
||||
if (steps !== undefined) {
|
||||
localVarQueryParameter['steps'] = steps;
|
||||
}
|
||||
|
||||
if (keywordsOr !== undefined) {
|
||||
localVarQueryParameter['keywords_or'] = keywordsOr;
|
||||
}
|
||||
@@ -4962,10 +4968,13 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration)
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} [query] Query string matched (fuzzy) against object name.
|
||||
* @param {number} [page] A page number within the paginated result set.
|
||||
* @param {number} [pageSize] Number of results to return per page.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
listSteps: async (options: any = {}): Promise<RequestArgs> => {
|
||||
listSteps: async (query?: string, page?: number, pageSize?: number, options: any = {}): Promise<RequestArgs> => {
|
||||
const localVarPath = `/api/step/`;
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
@@ -4978,6 +4987,18 @@ export const ApiApiAxiosParamCreator = function (configuration?: Configuration)
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
if (query !== undefined) {
|
||||
localVarQueryParameter['query'] = query;
|
||||
}
|
||||
|
||||
if (page !== undefined) {
|
||||
localVarQueryParameter['page'] = page;
|
||||
}
|
||||
|
||||
if (pageSize !== undefined) {
|
||||
localVarQueryParameter['page_size'] = pageSize;
|
||||
}
|
||||
|
||||
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter, options.query);
|
||||
@@ -8892,7 +8913,7 @@ export const ApiApiFp = function(configuration?: Configuration) {
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async listCookLogs(page?: number, pageSize?: number, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<InlineResponse2006>> {
|
||||
async listCookLogs(page?: number, pageSize?: number, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<InlineResponse2007>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.listCookLogs(page, pageSize, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
@@ -8917,7 +8938,7 @@ export const ApiApiFp = function(configuration?: Configuration) {
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async listImportLogs(page?: number, pageSize?: number, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<InlineResponse2008>> {
|
||||
async listImportLogs(page?: number, pageSize?: number, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<InlineResponse2009>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.listImportLogs(page, pageSize, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
@@ -8988,6 +9009,7 @@ export const ApiApiFp = function(configuration?: Configuration) {
|
||||
* @param {number} [units] Id of unit a recipe should have.
|
||||
* @param {number} [rating] Id of unit a recipe should have.
|
||||
* @param {string} [books] Id of book a recipe should have. For multiple repeat parameter.
|
||||
* @param {string} [steps] Id of a step a recipe should have. For multiple repeat parameter.
|
||||
* @param {string} [keywordsOr] If recipe should have all (AND) or any (OR) of the provided keywords.
|
||||
* @param {string} [foodsOr] If recipe should have all (AND) or any (OR) any of the provided foods.
|
||||
* @param {string} [booksOr] If recipe should be in all (AND) or any (OR) any of the provided books.
|
||||
@@ -8999,8 +9021,8 @@ export const ApiApiFp = function(configuration?: Configuration) {
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async listRecipes(query?: string, keywords?: string, foods?: string, units?: number, rating?: number, books?: string, keywordsOr?: string, foodsOr?: string, booksOr?: string, internal?: string, random?: string, _new?: string, page?: number, pageSize?: number, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<InlineResponse2004>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.listRecipes(query, keywords, foods, units, rating, books, keywordsOr, foodsOr, booksOr, internal, random, _new, page, pageSize, options);
|
||||
async listRecipes(query?: string, keywords?: string, foods?: string, units?: number, rating?: number, books?: string, steps?: string, keywordsOr?: string, foodsOr?: string, booksOr?: string, internal?: string, random?: string, _new?: string, page?: number, pageSize?: number, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<InlineResponse2005>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.listRecipes(query, keywords, foods, units, rating, books, steps, keywordsOr, foodsOr, booksOr, internal, random, _new, page, pageSize, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
@@ -9032,11 +9054,14 @@ export const ApiApiFp = function(configuration?: Configuration) {
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} [query] Query string matched (fuzzy) against object name.
|
||||
* @param {number} [page] A page number within the paginated result set.
|
||||
* @param {number} [pageSize] Number of results to return per page.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async listSteps(options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<Step>>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.listSteps(options);
|
||||
async listSteps(query?: string, page?: number, pageSize?: number, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<InlineResponse2004>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.listSteps(query, page, pageSize, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
@@ -9055,7 +9080,7 @@ export const ApiApiFp = function(configuration?: Configuration) {
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async listSupermarketCategoryRelations(page?: number, pageSize?: number, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<InlineResponse2007>> {
|
||||
async listSupermarketCategoryRelations(page?: number, pageSize?: number, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<InlineResponse2008>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.listSupermarketCategoryRelations(page, pageSize, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
@@ -9143,7 +9168,7 @@ export const ApiApiFp = function(configuration?: Configuration) {
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async listViewLogs(page?: number, pageSize?: number, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<InlineResponse2005>> {
|
||||
async listViewLogs(page?: number, pageSize?: number, options?: any): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<InlineResponse2006>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.listViewLogs(page, pageSize, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
@@ -10529,7 +10554,7 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?:
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
listCookLogs(page?: number, pageSize?: number, options?: any): AxiosPromise<InlineResponse2006> {
|
||||
listCookLogs(page?: number, pageSize?: number, options?: any): AxiosPromise<InlineResponse2007> {
|
||||
return localVarFp.listCookLogs(page, pageSize, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
@@ -10552,7 +10577,7 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?:
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
listImportLogs(page?: number, pageSize?: number, options?: any): AxiosPromise<InlineResponse2008> {
|
||||
listImportLogs(page?: number, pageSize?: number, options?: any): AxiosPromise<InlineResponse2009> {
|
||||
return localVarFp.listImportLogs(page, pageSize, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
@@ -10616,6 +10641,7 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?:
|
||||
* @param {number} [units] Id of unit a recipe should have.
|
||||
* @param {number} [rating] Id of unit a recipe should have.
|
||||
* @param {string} [books] Id of book a recipe should have. For multiple repeat parameter.
|
||||
* @param {string} [steps] Id of a step a recipe should have. For multiple repeat parameter.
|
||||
* @param {string} [keywordsOr] If recipe should have all (AND) or any (OR) of the provided keywords.
|
||||
* @param {string} [foodsOr] If recipe should have all (AND) or any (OR) any of the provided foods.
|
||||
* @param {string} [booksOr] If recipe should be in all (AND) or any (OR) any of the provided books.
|
||||
@@ -10627,8 +10653,8 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?:
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
listRecipes(query?: string, keywords?: string, foods?: string, units?: number, rating?: number, books?: string, keywordsOr?: string, foodsOr?: string, booksOr?: string, internal?: string, random?: string, _new?: string, page?: number, pageSize?: number, options?: any): AxiosPromise<InlineResponse2004> {
|
||||
return localVarFp.listRecipes(query, keywords, foods, units, rating, books, keywordsOr, foodsOr, booksOr, internal, random, _new, page, pageSize, options).then((request) => request(axios, basePath));
|
||||
listRecipes(query?: string, keywords?: string, foods?: string, units?: number, rating?: number, books?: string, steps?: string, keywordsOr?: string, foodsOr?: string, booksOr?: string, internal?: string, random?: string, _new?: string, page?: number, pageSize?: number, options?: any): AxiosPromise<InlineResponse2005> {
|
||||
return localVarFp.listRecipes(query, keywords, foods, units, rating, books, steps, keywordsOr, foodsOr, booksOr, internal, random, _new, page, pageSize, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
@@ -10656,11 +10682,14 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?:
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} [query] Query string matched (fuzzy) against object name.
|
||||
* @param {number} [page] A page number within the paginated result set.
|
||||
* @param {number} [pageSize] Number of results to return per page.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
listSteps(options?: any): AxiosPromise<Array<Step>> {
|
||||
return localVarFp.listSteps(options).then((request) => request(axios, basePath));
|
||||
listSteps(query?: string, page?: number, pageSize?: number, options?: any): AxiosPromise<InlineResponse2004> {
|
||||
return localVarFp.listSteps(query, page, pageSize, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
@@ -10677,7 +10706,7 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?:
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
listSupermarketCategoryRelations(page?: number, pageSize?: number, options?: any): AxiosPromise<InlineResponse2007> {
|
||||
listSupermarketCategoryRelations(page?: number, pageSize?: number, options?: any): AxiosPromise<InlineResponse2008> {
|
||||
return localVarFp.listSupermarketCategoryRelations(page, pageSize, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
@@ -10756,7 +10785,7 @@ export const ApiApiFactory = function (configuration?: Configuration, basePath?:
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
listViewLogs(page?: number, pageSize?: number, options?: any): AxiosPromise<InlineResponse2005> {
|
||||
listViewLogs(page?: number, pageSize?: number, options?: any): AxiosPromise<InlineResponse2006> {
|
||||
return localVarFp.listViewLogs(page, pageSize, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
@@ -12270,6 +12299,7 @@ export class ApiApi extends BaseAPI {
|
||||
* @param {number} [units] Id of unit a recipe should have.
|
||||
* @param {number} [rating] Id of unit a recipe should have.
|
||||
* @param {string} [books] Id of book a recipe should have. For multiple repeat parameter.
|
||||
* @param {string} [steps] Id of a step a recipe should have. For multiple repeat parameter.
|
||||
* @param {string} [keywordsOr] If recipe should have all (AND) or any (OR) of the provided keywords.
|
||||
* @param {string} [foodsOr] If recipe should have all (AND) or any (OR) any of the provided foods.
|
||||
* @param {string} [booksOr] If recipe should be in all (AND) or any (OR) any of the provided books.
|
||||
@@ -12282,8 +12312,8 @@ export class ApiApi extends BaseAPI {
|
||||
* @throws {RequiredError}
|
||||
* @memberof ApiApi
|
||||
*/
|
||||
public listRecipes(query?: string, keywords?: string, foods?: string, units?: number, rating?: number, books?: string, keywordsOr?: string, foodsOr?: string, booksOr?: string, internal?: string, random?: string, _new?: string, page?: number, pageSize?: number, options?: any) {
|
||||
return ApiApiFp(this.configuration).listRecipes(query, keywords, foods, units, rating, books, keywordsOr, foodsOr, booksOr, internal, random, _new, page, pageSize, options).then((request) => request(this.axios, this.basePath));
|
||||
public listRecipes(query?: string, keywords?: string, foods?: string, units?: number, rating?: number, books?: string, steps?: string, keywordsOr?: string, foodsOr?: string, booksOr?: string, internal?: string, random?: string, _new?: string, page?: number, pageSize?: number, options?: any) {
|
||||
return ApiApiFp(this.configuration).listRecipes(query, keywords, foods, units, rating, books, steps, keywordsOr, foodsOr, booksOr, internal, random, _new, page, pageSize, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -12318,12 +12348,15 @@ export class ApiApi extends BaseAPI {
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} [query] Query string matched (fuzzy) against object name.
|
||||
* @param {number} [page] A page number within the paginated result set.
|
||||
* @param {number} [pageSize] Number of results to return per page.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof ApiApi
|
||||
*/
|
||||
public listSteps(options?: any) {
|
||||
return ApiApiFp(this.configuration).listSteps(options).then((request) => request(this.axios, this.basePath));
|
||||
public listSteps(query?: string, page?: number, pageSize?: number, options?: any) {
|
||||
return ApiApiFp(this.configuration).listSteps(query, page, pageSize, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,15 +1,26 @@
|
||||
/*
|
||||
* Utility functions to call bootstrap toasts
|
||||
* */
|
||||
import {BToast} from 'bootstrap-vue'
|
||||
import i18n from "@/i18n";
|
||||
* Utility functions to call bootstrap toasts
|
||||
* */
|
||||
import i18n from "@/i18n"
|
||||
import { frac } from "@/utils/fractions"
|
||||
/*
|
||||
* Utility functions to use OpenAPIs generically
|
||||
* */
|
||||
import { ApiApiFactory } from "@/utils/openapi/api.ts"
|
||||
import axios from "axios"
|
||||
import { BToast } from "bootstrap-vue"
|
||||
// /*
|
||||
// * Utility functions to use manipulate nested components
|
||||
// * */
|
||||
import Vue from "vue"
|
||||
import { Actions, Models } from "./models"
|
||||
|
||||
export const ToastMixin = {
|
||||
methods: {
|
||||
makeToast: function (title, message, variant = null) {
|
||||
return makeToast(title, message, variant)
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export function makeToast(title, message, variant = null) {
|
||||
@@ -17,57 +28,71 @@ export function makeToast(title, message, variant = null) {
|
||||
toaster.$bvToast.toast(message, {
|
||||
title: title,
|
||||
variant: variant,
|
||||
toaster: 'b-toaster-bottom-right',
|
||||
solid: true
|
||||
toaster: "b-toaster-bottom-right",
|
||||
solid: true,
|
||||
})
|
||||
}
|
||||
|
||||
export class StandardToasts {
|
||||
static SUCCESS_CREATE = 'SUCCESS_CREATE'
|
||||
static SUCCESS_FETCH = 'SUCCESS_FETCH'
|
||||
static SUCCESS_UPDATE = 'SUCCESS_UPDATE'
|
||||
static SUCCESS_DELETE = 'SUCCESS_DELETE'
|
||||
static SUCCESS_CREATE = "SUCCESS_CREATE"
|
||||
static SUCCESS_FETCH = "SUCCESS_FETCH"
|
||||
static SUCCESS_UPDATE = "SUCCESS_UPDATE"
|
||||
static SUCCESS_DELETE = "SUCCESS_DELETE"
|
||||
static SUCCESS_MOVE = "SUCCESS_MOVE"
|
||||
static SUCCESS_MERGE = "SUCCESS_MERGE"
|
||||
|
||||
static FAIL_CREATE = 'FAIL_CREATE'
|
||||
static FAIL_FETCH = 'FAIL_FETCH'
|
||||
static FAIL_UPDATE = 'FAIL_UPDATE'
|
||||
static FAIL_DELETE = 'FAIL_DELETE'
|
||||
static FAIL_CREATE = "FAIL_CREATE"
|
||||
static FAIL_FETCH = "FAIL_FETCH"
|
||||
static FAIL_UPDATE = "FAIL_UPDATE"
|
||||
static FAIL_DELETE = "FAIL_DELETE"
|
||||
static FAIL_MOVE = "FAIL_MOVE"
|
||||
static FAIL_MERGE = "FAIL_MERGE"
|
||||
|
||||
static makeStandardToast(toast) {
|
||||
static makeStandardToast(toast, err_details = undefined) {
|
||||
switch (toast) {
|
||||
case StandardToasts.SUCCESS_CREATE:
|
||||
makeToast(i18n.tc('Success'), i18n.tc('success_creating_resource'), 'success')
|
||||
break;
|
||||
makeToast(i18n.tc("Success"), i18n.tc("success_creating_resource"), "success")
|
||||
break
|
||||
case StandardToasts.SUCCESS_FETCH:
|
||||
makeToast(i18n.tc('Success'), i18n.tc('success_fetching_resource'), 'success')
|
||||
break;
|
||||
makeToast(i18n.tc("Success"), i18n.tc("success_fetching_resource"), "success")
|
||||
break
|
||||
case StandardToasts.SUCCESS_UPDATE:
|
||||
makeToast(i18n.tc('Success'), i18n.tc('success_updating_resource'), 'success')
|
||||
break;
|
||||
makeToast(i18n.tc("Success"), i18n.tc("success_updating_resource"), "success")
|
||||
break
|
||||
case StandardToasts.SUCCESS_DELETE:
|
||||
makeToast(i18n.tc('Success'), i18n.tc('success_deleting_resource'), 'success')
|
||||
break;
|
||||
makeToast(i18n.tc("Success"), i18n.tc("success_deleting_resource"), "success")
|
||||
break
|
||||
case StandardToasts.SUCCESS_MOVE:
|
||||
makeToast(i18n.tc("Success"), i18n.tc("success_moving_resource"), "success")
|
||||
break
|
||||
case StandardToasts.SUCCESS_MERGE:
|
||||
makeToast(i18n.tc("Success"), i18n.tc("success_merging_resource"), "success")
|
||||
break
|
||||
case StandardToasts.FAIL_CREATE:
|
||||
makeToast(i18n.tc('Failure'), i18n.tc('err_creating_resource'), 'danger')
|
||||
break;
|
||||
makeToast(i18n.tc("Failure"), i18n.tc("err_creating_resource"), "danger")
|
||||
break
|
||||
case StandardToasts.FAIL_FETCH:
|
||||
makeToast(i18n.tc('Failure'), i18n.tc('err_fetching_resource'), 'danger')
|
||||
break;
|
||||
makeToast(i18n.tc("Failure"), i18n.tc("err_fetching_resource"), "danger")
|
||||
break
|
||||
case StandardToasts.FAIL_UPDATE:
|
||||
makeToast(i18n.tc('Failure'), i18n.tc('err_updating_resource'), 'danger')
|
||||
break;
|
||||
makeToast(i18n.tc("Failure"), i18n.tc("err_updating_resource"), "danger")
|
||||
break
|
||||
case StandardToasts.FAIL_DELETE:
|
||||
makeToast(i18n.tc('Failure'), i18n.tc('err_deleting_resource'), 'danger')
|
||||
break;
|
||||
|
||||
makeToast(i18n.tc("Failure"), i18n.tc("err_deleting_resource"), "danger")
|
||||
break
|
||||
case StandardToasts.FAIL_MOVE:
|
||||
makeToast(i18n.tc("Failure"), i18n.tc("err_moving_resource") + (err_details ? "\n" + err_details : ""), "danger")
|
||||
break
|
||||
case StandardToasts.FAIL_MERGE:
|
||||
makeToast(i18n.tc("Failure"), i18n.tc("err_merging_resource") + (err_details ? "\n" + err_details : ""), "danger")
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Utility functions to use djangos gettext
|
||||
* */
|
||||
* Utility functions to use djangos gettext
|
||||
* */
|
||||
|
||||
export const GettextMixin = {
|
||||
methods: {
|
||||
@@ -77,8 +102,8 @@ export const GettextMixin = {
|
||||
*/
|
||||
_: function (param) {
|
||||
return djangoGettext(param)
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export function djangoGettext(param) {
|
||||
@@ -86,8 +111,8 @@ export function djangoGettext(param) {
|
||||
}
|
||||
|
||||
/*
|
||||
* Utility function to use djangos named urls
|
||||
* */
|
||||
* Utility function to use djangos named urls
|
||||
* */
|
||||
|
||||
// uses https://github.com/ierror/django-js-reverse#use-the-urls-in-javascript
|
||||
export const ResolveUrlMixin = {
|
||||
@@ -99,50 +124,48 @@ export const ResolveUrlMixin = {
|
||||
*/
|
||||
resolveDjangoUrl: function (url, params = null) {
|
||||
return resolveDjangoUrl(url, params)
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export function resolveDjangoUrl(url, params = null) {
|
||||
if (params == null) {
|
||||
return window.Urls[url]()
|
||||
} else if (typeof(params) != "object") {
|
||||
} else if (typeof params != "object") {
|
||||
return window.Urls[url](params)
|
||||
} else if (typeof(params) == "object") {
|
||||
} else if (typeof params == "object") {
|
||||
if (params.length === 1) {
|
||||
return window.Urls[url](params)
|
||||
} else if (params.length === 2) {
|
||||
return window.Urls[url](params[0],params[1])
|
||||
return window.Urls[url](params[0], params[1])
|
||||
} else if (params.length === 3) {
|
||||
return window.Urls[url](params[0],params[1],params[2])
|
||||
return window.Urls[url](params[0], params[1], params[2])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* other utilities
|
||||
* */
|
||||
* other utilities
|
||||
* */
|
||||
|
||||
export function getUserPreference(pref) {
|
||||
if(window.USER_PREF === undefined) {
|
||||
return undefined;
|
||||
if (window.USER_PREF === undefined) {
|
||||
return undefined
|
||||
}
|
||||
return window.USER_PREF[pref]
|
||||
}
|
||||
|
||||
import {frac} from "@/utils/fractions";
|
||||
|
||||
export function calculateAmount(amount, factor) {
|
||||
if (getUserPreference('use_fractions')) {
|
||||
let return_string = ''
|
||||
let fraction = frac((amount * factor), 10, true)
|
||||
if (getUserPreference("use_fractions")) {
|
||||
let return_string = ""
|
||||
let fraction = frac(amount * factor, 10, true)
|
||||
|
||||
if (fraction[0] > 0) {
|
||||
return_string += fraction[0]
|
||||
}
|
||||
|
||||
if (fraction[1] > 0) {
|
||||
return_string += ` <sup>${(fraction[1])}</sup>⁄<sub>${(fraction[2])}</sub>`
|
||||
return_string += ` <sup>${fraction[1]}</sup>⁄<sub>${fraction[2]}</sub>`
|
||||
}
|
||||
|
||||
return return_string
|
||||
@@ -152,23 +175,23 @@ export function calculateAmount(amount, factor) {
|
||||
}
|
||||
|
||||
export function roundDecimals(num) {
|
||||
let decimals = ((getUserPreference('user_fractions')) ? getUserPreference('user_fractions') : 2);
|
||||
return +(Math.round(num + `e+${decimals}`) + `e-${decimals}`);
|
||||
let decimals = getUserPreference("user_fractions") ? getUserPreference("user_fractions") : 2
|
||||
return +(Math.round(num + `e+${decimals}`) + `e-${decimals}`)
|
||||
}
|
||||
|
||||
const KILOJOULES_PER_CALORIE = 4.18
|
||||
|
||||
export function calculateEnergy(amount, factor) {
|
||||
if (getUserPreference('use_kj')) {
|
||||
if (getUserPreference("use_kj")) {
|
||||
let joules = amount * KILOJOULES_PER_CALORIE
|
||||
return calculateAmount(joules, factor) + ' kJ'
|
||||
return calculateAmount(joules, factor) + " kJ"
|
||||
} else {
|
||||
return calculateAmount(amount, factor) + ' kcal'
|
||||
return calculateAmount(amount, factor) + " kcal"
|
||||
}
|
||||
}
|
||||
|
||||
export function convertEnergyToCalories(amount) {
|
||||
if (getUserPreference('use_kj')) {
|
||||
if (getUserPreference("use_kj")) {
|
||||
return amount / KILOJOULES_PER_CALORIE
|
||||
} else {
|
||||
return amount
|
||||
@@ -176,33 +199,25 @@ export function convertEnergyToCalories(amount) {
|
||||
}
|
||||
|
||||
export function energyHeading() {
|
||||
if (getUserPreference('use_kj')) {
|
||||
return 'Energy'
|
||||
if (getUserPreference("use_kj")) {
|
||||
return "Energy"
|
||||
} else {
|
||||
return 'Calories'
|
||||
return "Calories"
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Utility functions to use OpenAPIs generically
|
||||
* */
|
||||
import {ApiApiFactory} from "@/utils/openapi/api.ts";
|
||||
|
||||
import axios from "axios";
|
||||
axios.defaults.xsrfCookieName = 'csrftoken'
|
||||
axios.defaults.xsrfCookieName = "csrftoken"
|
||||
axios.defaults.xsrfHeaderName = "X-CSRFTOKEN"
|
||||
import { Actions, Models } from './models';
|
||||
import {RequestArgs} from "@/utils/openapi/base";
|
||||
|
||||
export const ApiMixin = {
|
||||
data() {
|
||||
return {
|
||||
Models: Models,
|
||||
Actions: Actions
|
||||
Actions: Actions,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
genericAPI: function(model, action, options) {
|
||||
genericAPI: function (model, action, options) {
|
||||
let setup = getConfig(model, action)
|
||||
if (setup?.config?.function) {
|
||||
return specialCases[setup.config.function](action, options, setup)
|
||||
@@ -212,10 +227,10 @@ export const ApiMixin = {
|
||||
let apiClient = new ApiApiFactory()
|
||||
return apiClient[func](...parameters)
|
||||
},
|
||||
genericGetAPI: function(url, options) {
|
||||
return axios.get(this.resolveDjangoUrl(url), {'params':options, 'emulateJSON': true})
|
||||
}
|
||||
}
|
||||
genericGetAPI: function (url, options) {
|
||||
return axios.get(this.resolveDjangoUrl(url), { params: options, emulateJSON: true })
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// /*
|
||||
@@ -223,37 +238,37 @@ export const ApiMixin = {
|
||||
// * */
|
||||
function formatParam(config, value, options) {
|
||||
if (config) {
|
||||
for (const [k, v] of Object.entries(config)) {
|
||||
switch(k) {
|
||||
case 'type':
|
||||
switch(v) {
|
||||
case 'string':
|
||||
for (const [k, v] of Object.entries(config)) {
|
||||
switch (k) {
|
||||
case "type":
|
||||
switch (v) {
|
||||
case "string":
|
||||
if (Array.isArray(value)) {
|
||||
let tmpValue = []
|
||||
value.forEach(x => tmpValue.push(String(x)))
|
||||
value.forEach((x) => tmpValue.push(String(x)))
|
||||
value = tmpValue
|
||||
} else if (value !== undefined) {
|
||||
value = String(value)
|
||||
}
|
||||
break;
|
||||
case 'integer':
|
||||
break
|
||||
case "integer":
|
||||
if (Array.isArray(value)) {
|
||||
let tmpValue = []
|
||||
value.forEach(x => tmpValue.push(parseInt(x)))
|
||||
value.forEach((x) => tmpValue.push(parseInt(x)))
|
||||
value = tmpValue
|
||||
} else if (value !== undefined) {
|
||||
value = parseInt(value)
|
||||
}
|
||||
break;
|
||||
break
|
||||
}
|
||||
break;
|
||||
case 'function':
|
||||
break
|
||||
case "function":
|
||||
// needs wrapped in a promise and wait for the called function to complete before moving on
|
||||
specialCases[v](value, options)
|
||||
break;
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
function buildParams(options, setup) {
|
||||
@@ -280,60 +295,56 @@ function buildParams(options, setup) {
|
||||
this_value = getDefault(config?.[item], options)
|
||||
}
|
||||
parameters.push(this_value)
|
||||
});
|
||||
})
|
||||
return parameters
|
||||
}
|
||||
function getDefault(config, options) {
|
||||
let value = undefined
|
||||
value = config?.default ?? undefined
|
||||
if (typeof(value) === 'object') {
|
||||
if (typeof value === "object") {
|
||||
let condition = false
|
||||
switch(value.function) {
|
||||
switch (value.function) {
|
||||
// CONDITIONAL case requires 4 keys:
|
||||
// - check: which other OPTIONS key to check against
|
||||
// - operator: what type of operation to perform
|
||||
// - true: what value to assign when true
|
||||
// - false: what value to assign when false
|
||||
case 'CONDITIONAL':
|
||||
switch(value.operator) {
|
||||
case 'not_exist':
|
||||
condition = (
|
||||
(!options?.[value.check] ?? undefined)
|
||||
|| options?.[value.check]?.length == 0
|
||||
)
|
||||
case "CONDITIONAL":
|
||||
switch (value.operator) {
|
||||
case "not_exist":
|
||||
condition = (!options?.[value.check] ?? undefined) || options?.[value.check]?.length == 0
|
||||
if (condition) {
|
||||
value = value.true
|
||||
} else {
|
||||
value = value.false
|
||||
}
|
||||
break;
|
||||
break
|
||||
}
|
||||
break;
|
||||
break
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
export function getConfig(model, action) {
|
||||
|
||||
let f = action.function
|
||||
// if not defined partialUpdate will use params from create
|
||||
if (f === 'partialUpdate' && !model?.[f]?.params) {
|
||||
model[f] = {'params': [...['id'], ...model.create.params]}
|
||||
if (f === "partialUpdate" && !model?.[f]?.params) {
|
||||
model[f] = { params: [...["id"], ...model.create.params] }
|
||||
}
|
||||
|
||||
|
||||
let config = {
|
||||
'name': model.name,
|
||||
'apiName': model.apiName,
|
||||
name: model.name,
|
||||
apiName: model.apiName,
|
||||
}
|
||||
// spread operator merges dictionaries - last item in list takes precedence
|
||||
config = {...config, ...action, ...model.model_type?.[f], ...model?.[f]}
|
||||
config = { ...config, ...action, ...model.model_type?.[f], ...model?.[f] }
|
||||
// nested dictionaries are not merged - so merge again on any nested keys
|
||||
config.config = {...action?.config, ...model.model_type?.[f]?.config, ...model?.[f]?.config}
|
||||
config.config = { ...action?.config, ...model.model_type?.[f]?.config, ...model?.[f]?.config }
|
||||
// look in partialUpdate again if necessary
|
||||
if (f === 'partialUpdate' && Object.keys(config.config).length === 0) {
|
||||
config.config = {...model.model_type?.create?.config, ...model?.create?.config}
|
||||
if (f === "partialUpdate" && Object.keys(config.config).length === 0) {
|
||||
config.config = { ...model.model_type?.create?.config, ...model?.create?.config }
|
||||
}
|
||||
config['function'] = f + config.apiName + (config?.suffix ?? '') // parens are required to force optional chaining to evaluate before concat
|
||||
config["function"] = f + config.apiName + (config?.suffix ?? "") // parens are required to force optional chaining to evaluate before concat
|
||||
return config
|
||||
}
|
||||
|
||||
@@ -342,181 +353,175 @@ export function getConfig(model, action) {
|
||||
// * */
|
||||
export function getForm(model, action, item1, item2) {
|
||||
let f = action.function
|
||||
let config = {...action?.form, ...model.model_type?.[f]?.form, ...model?.[f]?.form}
|
||||
// if not defined partialUpdate will use form from create
|
||||
if (f === 'partialUpdate' && Object.keys(config).length == 0) {
|
||||
config = {...Actions.CREATE?.form, ...model.model_type?.['create']?.form, ...model?.['create']?.form}
|
||||
config['title'] = {...action?.form_title, ...model.model_type?.[f]?.form_title, ...model?.[f]?.form_title}
|
||||
let config = { ...action?.form, ...model.model_type?.[f]?.form, ...model?.[f]?.form }
|
||||
// if not defined partialUpdate will use form from create
|
||||
if (f === "partialUpdate" && Object.keys(config).length == 0) {
|
||||
config = { ...Actions.CREATE?.form, ...model.model_type?.["create"]?.form, ...model?.["create"]?.form }
|
||||
config["title"] = { ...action?.form_title, ...model.model_type?.[f]?.form_title, ...model?.[f]?.form_title }
|
||||
}
|
||||
let form = {'fields': []}
|
||||
let value = ''
|
||||
let form = { fields: [] }
|
||||
let value = ""
|
||||
for (const [k, v] of Object.entries(config)) {
|
||||
if (v?.function){
|
||||
switch(v.function) {
|
||||
case 'translate':
|
||||
if (v?.function) {
|
||||
switch (v.function) {
|
||||
case "translate":
|
||||
value = formTranslate(v, model, item1, item2)
|
||||
}
|
||||
} else {
|
||||
value = v
|
||||
}
|
||||
if (value?.form_field) {
|
||||
value['value'] = item1?.[value?.field] ?? undefined
|
||||
form.fields.push(
|
||||
{
|
||||
...value,
|
||||
...{
|
||||
'label': formTranslate(value?.label, model, item1, item2),
|
||||
'placeholder': formTranslate(value?.placeholder, model, item1, item2)
|
||||
}
|
||||
}
|
||||
)
|
||||
value["value"] = item1?.[value?.field] ?? undefined
|
||||
form.fields.push({
|
||||
...value,
|
||||
...{
|
||||
label: formTranslate(value?.label, model, item1, item2),
|
||||
placeholder: formTranslate(value?.placeholder, model, item1, item2),
|
||||
},
|
||||
})
|
||||
} else {
|
||||
form[k] = value
|
||||
}
|
||||
}
|
||||
return form
|
||||
|
||||
}
|
||||
function formTranslate(translate, model, item1, item2) {
|
||||
if (typeof(translate) !== 'object') {return translate}
|
||||
if (typeof translate !== "object") {
|
||||
return translate
|
||||
}
|
||||
let phrase = translate.phrase
|
||||
let options = {}
|
||||
let obj = undefined
|
||||
translate?.params.forEach(function (x, index) {
|
||||
switch(x.from){
|
||||
case 'item1':
|
||||
switch (x.from) {
|
||||
case "item1":
|
||||
obj = item1
|
||||
break;
|
||||
case 'item2':
|
||||
break
|
||||
case "item2":
|
||||
obj = item2
|
||||
break;
|
||||
case 'model':
|
||||
break
|
||||
case "model":
|
||||
obj = model
|
||||
}
|
||||
options[x.token] = obj[x.attribute]
|
||||
})
|
||||
return i18n.t(phrase, options)
|
||||
|
||||
}
|
||||
|
||||
// /*
|
||||
// * Utility functions to use manipulate nested components
|
||||
// * */
|
||||
import Vue from 'vue'
|
||||
export const CardMixin = {
|
||||
methods: {
|
||||
findCard: function(id, card_list){
|
||||
findCard: function (id, card_list) {
|
||||
let card_length = card_list?.length ?? 0
|
||||
if (card_length == 0) {
|
||||
return false
|
||||
return false
|
||||
}
|
||||
let cards = card_list.filter(obj => obj.id == id)
|
||||
let cards = card_list.filter((obj) => obj.id == id)
|
||||
if (cards.length == 1) {
|
||||
return cards[0]
|
||||
return cards[0]
|
||||
} else if (cards.length == 0) {
|
||||
for (const c of card_list.filter(x => x.show_children == true)) {
|
||||
cards = this.findCard(id, c.children)
|
||||
if (cards) {
|
||||
return cards
|
||||
for (const c of card_list.filter((x) => x.show_children == true)) {
|
||||
cards = this.findCard(id, c.children)
|
||||
if (cards) {
|
||||
return cards
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log('something terrible happened')
|
||||
console.log("something terrible happened")
|
||||
}
|
||||
},
|
||||
destroyCard: function(id, card_list) {
|
||||
destroyCard: function (id, card_list) {
|
||||
let card = this.findCard(id, card_list)
|
||||
let p_id = card?.parent ?? undefined
|
||||
|
||||
|
||||
if (p_id) {
|
||||
let parent = this.findCard(p_id, card_list)
|
||||
if (parent){
|
||||
Vue.set(parent, 'numchild', parent.numchild - 1)
|
||||
if (parent) {
|
||||
Vue.set(parent, "numchild", parent.numchild - 1)
|
||||
if (parent.show_children) {
|
||||
let idx = parent.children.indexOf(parent.children.find(x => x.id === id))
|
||||
let idx = parent.children.indexOf(parent.children.find((x) => x.id === id))
|
||||
Vue.delete(parent.children, idx)
|
||||
}
|
||||
}
|
||||
}
|
||||
return card_list.filter(x => x.id != id)
|
||||
},
|
||||
refreshCard: function(obj, card_list){
|
||||
return card_list.filter((x) => x.id != id)
|
||||
},
|
||||
refreshCard: function (obj, card_list) {
|
||||
let target = {}
|
||||
let idx = undefined
|
||||
target = this.findCard(obj.id, card_list)
|
||||
|
||||
|
||||
if (target) {
|
||||
idx = card_list.indexOf(card_list.find(x => x.id === target.id))
|
||||
idx = card_list.indexOf(card_list.find((x) => x.id === target.id))
|
||||
Vue.set(card_list, idx, obj)
|
||||
}
|
||||
if (target?.parent) {
|
||||
let parent = this.findCard(target.parent, card_list)
|
||||
if (parent) {
|
||||
if (parent.show_children){
|
||||
idx = parent.children.indexOf(parent.children.find(x => x.id === target.id))
|
||||
if (parent.show_children) {
|
||||
idx = parent.children.indexOf(parent.children.find((x) => x.id === target.id))
|
||||
Vue.set(parent.children, idx, obj)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
const specialCases = {
|
||||
// the supermarket API requires chaining promises together, instead of trying to make
|
||||
// this use case generic just treat it as a unique use case
|
||||
SupermarketWithCategories: function(action, options, setup) {
|
||||
SupermarketWithCategories: function (action, options, setup) {
|
||||
let API = undefined
|
||||
let GenericAPI = ApiMixin.methods.genericAPI
|
||||
let params = []
|
||||
if (action.function === 'partialUpdate') {
|
||||
if (action.function === "partialUpdate") {
|
||||
API = GenericAPI
|
||||
params = [Models.SUPERMARKET, Actions.FETCH, {'id': options.id}]
|
||||
|
||||
} else if (action.function === 'create') {
|
||||
params = [Models.SUPERMARKET, Actions.FETCH, { id: options.id }]
|
||||
} else if (action.function === "create") {
|
||||
API = new ApiApiFactory()[setup.function]
|
||||
params = buildParams(options, setup)
|
||||
}
|
||||
|
||||
return API(...params).then((result) => {
|
||||
// either get the supermarket or create the supermarket (but without the category relations)
|
||||
return result.data
|
||||
}).then((result) => {
|
||||
// delete, update or change all of the category/relations
|
||||
let id = result.id
|
||||
let existing_categories = result.category_to_supermarket
|
||||
let updated_categories = options.category_to_supermarket
|
||||
|
||||
let promises = []
|
||||
// if the 'category.name' key does not exist on the updated_categories, the categories were not updated
|
||||
if (updated_categories?.[0]?.category?.name) {
|
||||
// list of category relationship ids that are not part of the updated supermarket
|
||||
let removed_categories = existing_categories.filter(x => !updated_categories.map(x => x.category.id).includes(x.category.id))
|
||||
let added_categories = updated_categories.filter(x => !existing_categories.map(x => x.category.id).includes(x.category.id))
|
||||
let changed_categories = updated_categories.filter(x => existing_categories.map(x => x.category.id).includes(x.category.id))
|
||||
|
||||
removed_categories.forEach(x => {
|
||||
promises.push(GenericAPI(Models.SHOPPING_CATEGORY_RELATION, Actions.DELETE, {'id': x.id}))
|
||||
})
|
||||
let item = {'supermarket': id}
|
||||
added_categories.forEach(x => {
|
||||
item.order = x.order
|
||||
item.category = {'id': x.category.id, 'name': x.category.name}
|
||||
promises.push(GenericAPI(Models.SHOPPING_CATEGORY_RELATION, Actions.CREATE, item))
|
||||
})
|
||||
changed_categories.forEach(x => {
|
||||
item.id = x?.id ?? existing_categories.find(y => y.category.id === x.category.id).id;
|
||||
item.order = x.order
|
||||
item.category = {'id': x.category.id, 'name': x.category.name}
|
||||
promises.push(GenericAPI(Models.SHOPPING_CATEGORY_RELATION, Actions.UPDATE, item))
|
||||
})
|
||||
}
|
||||
|
||||
return Promise.all(promises).then(() => {
|
||||
// finally get and return the Supermarket which everything downstream is expecting
|
||||
return GenericAPI(Models.SUPERMARKET, Actions.FETCH, {'id': id})
|
||||
return API(...params)
|
||||
.then((result) => {
|
||||
// either get the supermarket or create the supermarket (but without the category relations)
|
||||
return result.data
|
||||
})
|
||||
})
|
||||
}
|
||||
.then((result) => {
|
||||
// delete, update or change all of the category/relations
|
||||
let id = result.id
|
||||
let existing_categories = result.category_to_supermarket
|
||||
let updated_categories = options.category_to_supermarket
|
||||
|
||||
let promises = []
|
||||
// if the 'category.name' key does not exist on the updated_categories, the categories were not updated
|
||||
if (updated_categories?.[0]?.category?.name) {
|
||||
// list of category relationship ids that are not part of the updated supermarket
|
||||
let removed_categories = existing_categories.filter((x) => !updated_categories.map((x) => x.category.id).includes(x.category.id))
|
||||
let added_categories = updated_categories.filter((x) => !existing_categories.map((x) => x.category.id).includes(x.category.id))
|
||||
let changed_categories = updated_categories.filter((x) => existing_categories.map((x) => x.category.id).includes(x.category.id))
|
||||
|
||||
removed_categories.forEach((x) => {
|
||||
promises.push(GenericAPI(Models.SHOPPING_CATEGORY_RELATION, Actions.DELETE, { id: x.id }))
|
||||
})
|
||||
let item = { supermarket: id }
|
||||
added_categories.forEach((x) => {
|
||||
item.order = x.order
|
||||
item.category = { id: x.category.id, name: x.category.name }
|
||||
promises.push(GenericAPI(Models.SHOPPING_CATEGORY_RELATION, Actions.CREATE, item))
|
||||
})
|
||||
changed_categories.forEach((x) => {
|
||||
item.id = x?.id ?? existing_categories.find((y) => y.category.id === x.category.id).id
|
||||
item.order = x.order
|
||||
item.category = { id: x.category.id, name: x.category.name }
|
||||
promises.push(GenericAPI(Models.SHOPPING_CATEGORY_RELATION, Actions.UPDATE, item))
|
||||
})
|
||||
}
|
||||
|
||||
return Promise.all(promises).then(() => {
|
||||
// finally get and return the Supermarket which everything downstream is expecting
|
||||
return GenericAPI(Models.SUPERMARKET, Actions.FETCH, { id: id })
|
||||
})
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,45 +1,45 @@
|
||||
const BundleTracker = require("webpack-bundle-tracker");
|
||||
const BundleTracker = require("webpack-bundle-tracker")
|
||||
|
||||
const pages = {
|
||||
'recipe_search_view': {
|
||||
entry: './src/apps/RecipeSearchView/main.js',
|
||||
chunks: ['chunk-vendors']
|
||||
recipe_search_view: {
|
||||
entry: "./src/apps/RecipeSearchView/main.js",
|
||||
chunks: ["chunk-vendors"],
|
||||
},
|
||||
'recipe_view': {
|
||||
entry: './src/apps/RecipeView/main.js',
|
||||
chunks: ['chunk-vendors']
|
||||
recipe_view: {
|
||||
entry: "./src/apps/RecipeView/main.js",
|
||||
chunks: ["chunk-vendors"],
|
||||
},
|
||||
'offline_view': {
|
||||
entry: './src/apps/OfflineView/main.js',
|
||||
chunks: ['chunk-vendors']
|
||||
offline_view: {
|
||||
entry: "./src/apps/OfflineView/main.js",
|
||||
chunks: ["chunk-vendors"],
|
||||
},
|
||||
'import_response_view': {
|
||||
entry: './src/apps/ImportResponseView/main.js',
|
||||
chunks: ['chunk-vendors']
|
||||
import_response_view: {
|
||||
entry: "./src/apps/ImportResponseView/main.js",
|
||||
chunks: ["chunk-vendors"],
|
||||
},
|
||||
'supermarket_view': {
|
||||
entry: './src/apps/SupermarketView/main.js',
|
||||
chunks: ['chunk-vendors']
|
||||
supermarket_view: {
|
||||
entry: "./src/apps/SupermarketView/main.js",
|
||||
chunks: ["chunk-vendors"],
|
||||
},
|
||||
'model_list_view': {
|
||||
entry: './src/apps/ModelListView/main.js',
|
||||
chunks: ['chunk-vendors']
|
||||
model_list_view: {
|
||||
entry: "./src/apps/ModelListView/main.js",
|
||||
chunks: ["chunk-vendors"],
|
||||
},
|
||||
'edit_internal_recipe': {
|
||||
entry: './src/apps/RecipeEditView/main.js',
|
||||
chunks: ['chunk-vendors']
|
||||
edit_internal_recipe: {
|
||||
entry: "./src/apps/RecipeEditView/main.js",
|
||||
chunks: ["chunk-vendors"],
|
||||
},
|
||||
'cookbook_view': {
|
||||
entry: './src/apps/CookbookView/main.js',
|
||||
chunks: ['chunk-vendors']
|
||||
cookbook_view: {
|
||||
entry: "./src/apps/CookbookView/main.js",
|
||||
chunks: ["chunk-vendors"],
|
||||
},
|
||||
'meal_plan_view': {
|
||||
entry: './src/apps/MealPlanView/main.js',
|
||||
chunks: ['chunk-vendors']
|
||||
meal_plan_view: {
|
||||
entry: "./src/apps/MealPlanView/main.js",
|
||||
chunks: ["chunk-vendors"],
|
||||
},
|
||||
'checklist_view': {
|
||||
entry: './src/apps/ChecklistView/main.js',
|
||||
chunks: ['chunk-vendors']
|
||||
checklist_view: {
|
||||
entry: "./src/apps/ChecklistView/main.js",
|
||||
chunks: ["chunk-vendors"],
|
||||
},
|
||||
}
|
||||
|
||||
@@ -47,54 +47,51 @@ module.exports = {
|
||||
pages: pages,
|
||||
filenameHashing: false,
|
||||
productionSourceMap: false,
|
||||
publicPath: process.env.NODE_ENV === 'production'
|
||||
? ''
|
||||
: 'http://localhost:8080/',
|
||||
outputDir: '../cookbook/static/vue/',
|
||||
publicPath: process.env.NODE_ENV === "production" ? "" : "http://localhost:8080/",
|
||||
outputDir: "../cookbook/static/vue/",
|
||||
runtimeCompiler: true,
|
||||
pwa: {
|
||||
name: 'Recipes',
|
||||
themeColor: '#4DBA87',
|
||||
msTileColor: '#000000',
|
||||
appleMobileWebAppCapable: 'yes',
|
||||
appleMobileWebAppStatusBarStyle: 'black',
|
||||
name: "Recipes",
|
||||
themeColor: "#4DBA87",
|
||||
msTileColor: "#000000",
|
||||
appleMobileWebAppCapable: "yes",
|
||||
appleMobileWebAppStatusBarStyle: "black",
|
||||
|
||||
|
||||
workboxPluginMode: 'InjectManifest',
|
||||
workboxPluginMode: "InjectManifest",
|
||||
workboxOptions: {
|
||||
swSrc: './src/sw.js',
|
||||
swDest: '../../templates/sw.js',
|
||||
swSrc: "./src/sw.js",
|
||||
swDest: "../../templates/sw.js",
|
||||
manifestTransforms: [
|
||||
originalManifest => {
|
||||
const result = originalManifest.map(entry => new Object({url: 'static/vue/' + entry.url}))
|
||||
return {manifest: result, warnings: []};
|
||||
}
|
||||
(originalManifest) => {
|
||||
const result = originalManifest.map((entry) => new Object({ url: "static/vue/" + entry.url }))
|
||||
return { manifest: result, warnings: [] }
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
},
|
||||
pluginOptions: {
|
||||
i18n: {
|
||||
locale: 'en',
|
||||
fallbackLocale: 'en',
|
||||
localeDir: 'locales',
|
||||
enableInSFC: true
|
||||
}
|
||||
locale: "en",
|
||||
fallbackLocale: "en",
|
||||
localeDir: "locales",
|
||||
enableInSFC: true,
|
||||
},
|
||||
},
|
||||
chainWebpack: config => {
|
||||
|
||||
config.optimization.splitChunks({
|
||||
chainWebpack: (config) => {
|
||||
config.optimization.splitChunks(
|
||||
{
|
||||
cacheGroups: {
|
||||
vendor: {
|
||||
test: /[\\/]node_modules[\\/]/,
|
||||
name: "chunk-vendors",
|
||||
chunks: "all",
|
||||
priority: 1
|
||||
priority: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
// TODO make this conditional on .env DEBUG = FALSE
|
||||
config.optimization.minimize(true)
|
||||
);
|
||||
)
|
||||
|
||||
//TODO somehow remov them as they are also added to the manifest config of the service worker
|
||||
/*
|
||||
@@ -105,19 +102,17 @@ module.exports = {
|
||||
})
|
||||
*/
|
||||
|
||||
config.plugin('BundleTracker').use(BundleTracker, [{relativePath: true, path: '../vue/'}]);
|
||||
config.plugin("BundleTracker").use(BundleTracker, [{ relativePath: true, path: "../vue/" }])
|
||||
|
||||
config.resolve.alias
|
||||
.set('__STATIC__', 'static')
|
||||
config.resolve.alias.set("__STATIC__", "static")
|
||||
|
||||
config.devServer
|
||||
.public('http://localhost:8080')
|
||||
.host('localhost')
|
||||
.public("http://localhost:8080")
|
||||
.host("localhost")
|
||||
.port(8080)
|
||||
.hotOnly(true)
|
||||
.watchOptions({poll: 500})
|
||||
.watchOptions({ poll: 500 })
|
||||
.https(false)
|
||||
.headers({"Access-Control-Allow-Origin": ["*"]})
|
||||
|
||||
}
|
||||
};
|
||||
.headers({ "Access-Control-Allow-Origin": ["*"] })
|
||||
},
|
||||
}
|
||||
|
||||
11237
vue/yarn.lock
Normal file
11237
vue/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user